Merge "STL Refactor, rename swipeSpec to motionSpatialSpec 1/2" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 69892f9b..a60ced5 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -65,13 +65,13 @@
         "android.sdk.flags-aconfig-java",
         "android.security.flags-aconfig-java",
         "android.server.app.flags-aconfig-java",
+        "android.service.appprediction.flags-aconfig-java",
         "android.service.autofill.flags-aconfig-java",
         "android.service.chooser.flags-aconfig-java",
         "android.service.compat.flags-aconfig-java",
         "android.service.controls.flags-aconfig-java",
         "android.service.dreams.flags-aconfig-java",
         "android.service.notification.flags-aconfig-java",
-        "android.service.appprediction.flags-aconfig-java",
         "android.service.quickaccesswallet.flags-aconfig-java",
         "android.service.voice.flags-aconfig-java",
         "android.speech.flags-aconfig-java",
@@ -107,6 +107,7 @@
         "com.android.server.flags.services-aconfig-java",
         "com.android.text.flags-aconfig-java",
         "com.android.window.flags.window-aconfig-java",
+        "configinfra_framework_flags_java_lib",
         "conscrypt_exported_aconfig_flags_lib",
         "device_policy_aconfig_flags_lib",
         "display_flags_lib",
@@ -522,7 +523,10 @@
     package: "android.companion.virtualdevice.flags",
     container: "system",
     exportable: true,
-    srcs: ["core/java/android/companion/virtual/flags/*.aconfig"],
+    srcs: [
+        "core/java/android/companion/virtual/flags/flags.aconfig",
+        "core/java/android/companion/virtual/flags/launched_flags.aconfig",
+    ],
 }
 
 java_aconfig_library {
@@ -547,7 +551,7 @@
     name: "android.companion.virtual.flags-aconfig",
     package: "android.companion.virtual.flags",
     container: "system",
-    srcs: ["core/java/android/companion/virtual/*.aconfig"],
+    srcs: ["core/java/android/companion/virtual/flags/deprecated_flags_do_not_edit.aconfig"],
 }
 
 // InputMethod
@@ -827,8 +831,8 @@
     min_sdk_version: "30",
     apex_available: [
         "//apex_available:platform",
-        "com.android.permission",
         "com.android.nfcservices",
+        "com.android.permission",
     ],
 }
 
@@ -1584,6 +1588,13 @@
 }
 
 java_aconfig_library {
+    name: "android.app.appfunctions.flags-aconfig-java-host",
+    aconfig_declarations: "android.app.appfunctions.flags-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+    host_supported: true,
+}
+
+java_aconfig_library {
     name: "android.app.appfunctions.exported-flags-aconfig-java",
     aconfig_declarations: "android.app.appfunctions.flags-aconfig",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
diff --git a/Android.bp b/Android.bp
index a1f6e30..9d8c8a6 100644
--- a/Android.bp
+++ b/Android.bp
@@ -408,7 +408,6 @@
         "bouncycastle-repackaged-unbundled",
         "com.android.sysprop.foldlockbehavior",
         "com.android.sysprop.view",
-        "configinfra_framework_flags_java_lib",
         "framework-internal-utils",
         "dynamic_instrumentation_manager_aidl-java",
         // If MimeMap ever becomes its own APEX, then this dependency would need to be removed
@@ -537,45 +536,6 @@
     }),
 }
 
-// This is identical to "framework-minus-apex" but with "jarjar_shards" hardcodd.
-// (also "stem" is commented out to avoid a conflict with the "framework-minus-apex")
-// TODO(b/383559945) This module is just for local testing / verification. It's not used
-// by anything. Remove it once we roll out RELEASE_USE_SHARDED_JARJAR_ON_FRAMEWORK_MINUS_APEX.
-java_library {
-    name: "framework-minus-apex_jarjar-sharded",
-    defaults: [
-        "framework-minus-apex-with-libs-defaults",
-        "framework-non-updatable-lint-defaults",
-    ],
-    installable: true,
-    // For backwards compatibility.
-    // stem: "framework",
-    apex_available: ["//apex_available:platform"],
-    visibility: [
-        "//frameworks/base",
-        "//frameworks/base/location",
-        // TODO(b/147128803) remove the below lines
-        "//frameworks/base/apex/blobstore/framework",
-        "//frameworks/base/apex/jobscheduler/framework",
-        "//frameworks/base/packages/Tethering/tests/unit",
-        "//packages/modules/Connectivity/Tethering/tests/unit",
-    ],
-    errorprone: {
-        javacflags: [
-            "-Xep:AndroidFrameworkCompatChange:ERROR",
-            "-Xep:AndroidFrameworkUid:ERROR",
-        ],
-    },
-    lint: {
-        baseline_filename: "lint-baseline.xml",
-        warning_checks: [
-            "FlaggedApi",
-        ],
-    },
-    jarjar_prefix: "com.android.internal.hidden_from_bootclasspath",
-    jarjar_shards: "10",
-}
-
 java_library {
     name: "framework-minus-apex-intdefs",
     defaults: ["framework-minus-apex-with-libs-defaults"],
diff --git a/apct-tests/perftests/aconfig/src/android/os/flagging/AconfigPackagePerfTest.java b/apct-tests/perftests/aconfig/src/android/os/flagging/AconfigPackagePerfTest.java
index df6e3c8..e790874 100644
--- a/apct-tests/perftests/aconfig/src/android/os/flagging/AconfigPackagePerfTest.java
+++ b/apct-tests/perftests/aconfig/src/android/os/flagging/AconfigPackagePerfTest.java
@@ -43,7 +43,7 @@
 
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
 
-    @Parameterized.Parameters(name = "isPlatform={0}")
+    @Parameterized.Parameters(name = "isPlatform_{0}")
     public static Collection<Object[]> data() {
         return Arrays.asList(new Object[][] {{false}, {true}});
     }
@@ -60,10 +60,9 @@
         }
     }
 
-    @Parameterized.Parameter(0)
-
     // if this variable is true, then the test query flags from system/product/vendor
     // if this variable is false, then the test query flags from updatable partitions
+    @Parameterized.Parameter(0)
     public boolean mIsPlatform;
 
     @Test
diff --git a/apct-tests/perftests/core/src/android/app/OverlayManagerPerfTest.java b/apct-tests/perftests/core/src/android/app/OverlayManagerPerfTest.java
index a12121f..5d39ccc 100644
--- a/apct-tests/perftests/core/src/android/app/OverlayManagerPerfTest.java
+++ b/apct-tests/perftests/core/src/android/app/OverlayManagerPerfTest.java
@@ -20,7 +20,6 @@
 
 import android.content.Context;
 import android.content.om.OverlayManager;
-import android.os.UserHandle;
 import android.perftests.utils.BenchmarkState;
 import android.perftests.utils.PerfStatusReporter;
 import android.perftests.utils.TestPackageInstaller;
@@ -127,7 +126,7 @@
     private void assertSetEnabled(boolean enabled, Context context, Stream<String> packagesStream) {
         final var overlayPackages = packagesStream.toList();
         overlayPackages.forEach(
-                name -> sOverlayManager.setEnabled(name, enabled, UserHandle.SYSTEM));
+                name -> sOverlayManager.setEnabled(name, enabled, context.getUser()));
 
         // Wait for the overlay changes to propagate
         final var endTime = System.nanoTime() + TimeUnit.SECONDS.toNanos(20);
@@ -174,7 +173,7 @@
             // Disable the overlay and remove the idmap for the next iteration of the test
             state.pauseTiming();
             assertSetEnabled(false, sContext, packageName);
-            sOverlayManager.invalidateCachesForOverlay(packageName, UserHandle.SYSTEM);
+            sOverlayManager.invalidateCachesForOverlay(packageName, sContext.getUser());
             state.resumeTiming();
         }
     }
@@ -189,7 +188,7 @@
             // Disable the overlay and remove the idmap for the next iteration of the test
             state.pauseTiming();
             assertSetEnabled(false, sContext, packageName);
-            sOverlayManager.invalidateCachesForOverlay(packageName, UserHandle.SYSTEM);
+            sOverlayManager.invalidateCachesForOverlay(packageName, sContext.getUser());
             state.resumeTiming();
         }
     }
diff --git a/apct-tests/perftests/core/src/android/content/pm/SystemFeaturesPerfTest.java b/apct-tests/perftests/core/src/android/content/pm/SystemFeaturesPerfTest.java
new file mode 100644
index 0000000..43f5453
--- /dev/null
+++ b/apct-tests/perftests/core/src/android/content/pm/SystemFeaturesPerfTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm;
+
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.pm.RoSystemFeatures;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class SystemFeaturesPerfTest {
+    // As each query is relatively cheap, add an inner iteration loop to reduce execution noise.
+    private static final int NUM_ITERATIONS = 10;
+
+    @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+    @Test
+    public void hasSystemFeature_PackageManager() {
+        final PackageManager pm =
+                InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageManager();
+        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            for (int i = 0; i < NUM_ITERATIONS; ++i) {
+                pm.hasSystemFeature(PackageManager.FEATURE_WATCH);
+                pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK);
+                pm.hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS);
+                pm.hasSystemFeature(PackageManager.FEATURE_AUTOFILL);
+                pm.hasSystemFeature("com.android.custom.feature.1");
+                pm.hasSystemFeature("foo");
+                pm.hasSystemFeature("");
+            }
+        }
+    }
+
+    @Test
+    public void hasSystemFeature_SystemFeaturesCache() {
+        final PackageManager pm =
+                InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageManager();
+        final SystemFeaturesCache cache =
+                new SystemFeaturesCache(Arrays.asList(pm.getSystemAvailableFeatures()));
+        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            for (int i = 0; i < NUM_ITERATIONS; ++i) {
+                cache.maybeHasFeature(PackageManager.FEATURE_WATCH, 0);
+                cache.maybeHasFeature(PackageManager.FEATURE_LEANBACK, 0);
+                cache.maybeHasFeature(PackageManager.FEATURE_IPSEC_TUNNELS, 0);
+                cache.maybeHasFeature(PackageManager.FEATURE_AUTOFILL, 0);
+                cache.maybeHasFeature("com.android.custom.feature.1", 0);
+                cache.maybeHasFeature("foo", 0);
+                cache.maybeHasFeature("", 0);
+            }
+        }
+    }
+
+    @Test
+    public void hasSystemFeature_RoSystemFeatures() {
+        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            for (int i = 0; i < NUM_ITERATIONS; ++i) {
+                RoSystemFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 0);
+                RoSystemFeatures.maybeHasFeature(PackageManager.FEATURE_LEANBACK, 0);
+                RoSystemFeatures.maybeHasFeature(PackageManager.FEATURE_IPSEC_TUNNELS, 0);
+                RoSystemFeatures.maybeHasFeature(PackageManager.FEATURE_AUTOFILL, 0);
+                RoSystemFeatures.maybeHasFeature("com.android.custom.feature.1", 0);
+                RoSystemFeatures.maybeHasFeature("foo", 0);
+                RoSystemFeatures.maybeHasFeature("", 0);
+            }
+        }
+    }
+}
diff --git a/apct-tests/perftests/windowmanager/src/android/wm/InTaskTransitionTest.java b/apct-tests/perftests/windowmanager/src/android/wm/InTaskTransitionTest.java
index 2d2cf1c8..b04d08f 100644
--- a/apct-tests/perftests/windowmanager/src/android/wm/InTaskTransitionTest.java
+++ b/apct-tests/perftests/windowmanager/src/android/wm/InTaskTransitionTest.java
@@ -34,11 +34,20 @@
 import org.junit.Rule;
 import org.junit.Test;
 
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+
 /** Measure the performance of warm launch activity in the same task. */
 public class InTaskTransitionTest extends WindowManagerPerfTestBase
         implements RemoteCallback.OnResultListener {
 
     private static final long TIMEOUT_MS = 5000;
+    private static final String LOG_SEPARATOR = "LOG_SEPARATOR";
 
     @Rule
     public final PerfManualStatusReporter mPerfStatusReporter = new PerfManualStatusReporter();
@@ -62,6 +71,7 @@
 
         final ManualBenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         long measuredTimeNs = 0;
+        long firstStartTime = 0;
 
         boolean readerStarted = false;
         while (state.keepRunning(measuredTimeNs)) {
@@ -70,6 +80,10 @@
                 readerStarted = true;
             }
             final long startTime = SystemClock.elapsedRealtimeNanos();
+            if (readerStarted && firstStartTime == 0) {
+                firstStartTime = startTime;
+                executeShellCommand("log -t " + LOG_SEPARATOR + " " + firstStartTime);
+            }
             activity.startActivity(next);
             synchronized (mMetricsReader) {
                 try {
@@ -89,6 +103,7 @@
                 state.addExtraResult("windowsDrawnDelayMs", metrics.mWindowsDrawnDelayMs);
             }
         }
+        addExtraTransitionInfo(firstStartTime, state);
     }
 
     @Override
@@ -99,6 +114,46 @@
         }
     }
 
+    private void addExtraTransitionInfo(long startTime, ManualBenchmarkState state) {
+        final ProcessBuilder pb = new ProcessBuilder("sh");
+        final String startLine = String.valueOf(startTime);
+        final String commitTimeStr = " commit=";
+        boolean foundStartLine = false;
+        try {
+            final Process process = pb.start();
+            final InputStream in = process.getInputStream();
+            final PrintWriter out = new PrintWriter(new BufferedWriter(
+                    new OutputStreamWriter(process.getOutputStream())), true /* autoFlush */);
+            out.println("logcat -v brief -d *:S WindowManager:V " + LOG_SEPARATOR + ":I"
+                    + " | grep -e 'Finish Transition' -e " + LOG_SEPARATOR);
+            out.println("exit");
+
+            String line;
+            try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
+                while ((line = reader.readLine()) != null) {
+                    if (!foundStartLine) {
+                        if (line.contains(startLine)) {
+                            foundStartLine = true;
+                        }
+                        continue;
+                    }
+                    final int strPos = line.indexOf(commitTimeStr);
+                    if (strPos < 0) {
+                        continue;
+                    }
+                    final int endPos = line.indexOf("ms", strPos);
+                    if (endPos > strPos) {
+                        final int commitDelayMs = Math.round(Float.parseFloat(
+                                line.substring(strPos + commitTimeStr.length(), endPos)));
+                        state.addExtraResult("commitDelayMs", commitDelayMs);
+                    }
+                }
+            }
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
     /** The test activity runs on a different process to trigger metrics logs. */
     public static class TestActivity extends Activity implements Runnable {
         static final String CALLBACK = "callback";
diff --git a/apex/jobscheduler/framework/aconfig/job.aconfig b/apex/jobscheduler/framework/aconfig/job.aconfig
index 8b1a40c..a0dfd19 100644
--- a/apex/jobscheduler/framework/aconfig/job.aconfig
+++ b/apex/jobscheduler/framework/aconfig/job.aconfig
@@ -17,14 +17,6 @@
 }
 
 flag {
-    name: "backup_jobs_exemption"
-    is_exported: true
-    namespace: "backstage_power"
-    description: "Introduce a new RUN_BACKUP_JOBS permission and exemption logic allowing for longer running jobs for apps whose primary purpose is to backup or sync content."
-    bug: "318731461"
-}
-
-flag {
    name: "handle_abandoned_jobs"
    namespace: "backstage_power"
    description: "Detect, report and take action on jobs that maybe abandoned by the app without calling jobFinished."
diff --git a/boot/preloaded-classes b/boot/preloaded-classes
index b83bd4e..9926aef 100644
--- a/boot/preloaded-classes
+++ b/boot/preloaded-classes
@@ -6470,6 +6470,7 @@
 android.os.connectivity.WifiBatteryStats$1
 android.os.connectivity.WifiBatteryStats
 android.os.flagging.AconfigPackage
+android.os.flagging.PlatformAconfigPackage
 android.os.health.HealthKeys$Constant
 android.os.health.HealthKeys$Constants
 android.os.health.HealthKeys$SortedIntArray
diff --git a/config/preloaded-classes b/config/preloaded-classes
index e53c78f..bdd95f8 100644
--- a/config/preloaded-classes
+++ b/config/preloaded-classes
@@ -6474,6 +6474,7 @@
 android.os.connectivity.WifiBatteryStats$1
 android.os.connectivity.WifiBatteryStats
 android.os.flagging.AconfigPackage
+android.os.flagging.PlatformAconfigPackage
 android.os.health.HealthKeys$Constant
 android.os.health.HealthKeys$Constants
 android.os.health.HealthKeys$SortedIntArray
diff --git a/config/preloaded-classes-denylist b/config/preloaded-classes-denylist
index e3e929c..a6a1d16 100644
--- a/config/preloaded-classes-denylist
+++ b/config/preloaded-classes-denylist
@@ -1,5 +1,4 @@
 android.content.AsyncTaskLoader$LoadTask
-android.media.MediaCodecInfo$CodecCapabilities$FeatureList
 android.net.ConnectivityThread$Singleton
 android.os.FileObserver
 android.os.NullVibrator
diff --git a/core/api/current.txt b/core/api/current.txt
index 59eb31a..b7f7a7f 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -8905,7 +8905,7 @@
   @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public abstract class AppFunctionService extends android.app.Service {
     ctor public AppFunctionService();
     method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
-    method @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<android.app.appfunctions.ExecuteAppFunctionResponse,android.app.appfunctions.AppFunctionException>);
+    method @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.content.pm.SigningInfo, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<android.app.appfunctions.ExecuteAppFunctionResponse,android.app.appfunctions.AppFunctionException>);
     field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
   }
 
@@ -8947,18 +8947,19 @@
     method public android.content.ClipData getClipData();
     method public android.os.Bundle getExtras();
     method public android.content.Intent getIntent();
+    method @FlaggedApi("com.android.window.flags.enable_desktop_windowing_app_to_web_education") @Nullable public android.net.Uri getSessionTransferUri();
     method public String getStructuredData();
     method public android.net.Uri getWebUri();
     method public boolean isAppProvidedIntent();
     method public boolean isAppProvidedWebUri();
     method public void setClipData(android.content.ClipData);
     method public void setIntent(android.content.Intent);
+    method @FlaggedApi("com.android.window.flags.enable_desktop_windowing_app_to_web_education") public void setSessionTransferUri(@Nullable android.net.Uri);
     method public void setStructuredData(String);
     method public void setWebUri(android.net.Uri);
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.app.assist.AssistContent> CREATOR;
     field @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public static final String EXTRA_APP_FUNCTION_DATA = "android.app.assist.extra.APP_FUNCTION_DATA";
-    field @FlaggedApi("com.android.window.flags.enable_desktop_windowing_app_to_web_education") public static final String EXTRA_SESSION_TRANSFER_WEB_URI = "android.app.assist.extra.SESSION_TRANSFER_WEB_URI";
   }
 
   public class AssistStructure implements android.os.Parcelable {
@@ -9190,8 +9191,9 @@
 package android.app.jank {
 
   @FlaggedApi("android.app.jank.detailed_app_jank_metrics_api") public final class AppJankStats {
-    ctor public AppJankStats(int, @NonNull String, @Nullable String, @Nullable String, long, long, @NonNull android.app.jank.RelativeFrameTimeHistogram);
+    ctor public AppJankStats(int, @NonNull String, @Nullable String, @Nullable String, @Nullable String, long, long, @NonNull android.app.jank.RelativeFrameTimeHistogram);
     method public long getJankyFrameCount();
+    method @Nullable public String getNavigationComponent();
     method @NonNull public android.app.jank.RelativeFrameTimeHistogram getRelativeFrameTimeHistogram();
     method public long getTotalFrameCount();
     method public int getUid();
@@ -16894,6 +16896,7 @@
     method public android.graphics.Paint.FontMetricsInt getFontMetricsInt();
     method @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") public void getFontMetricsIntForLocale(@NonNull android.graphics.Paint.FontMetricsInt);
     method public float getFontSpacing();
+    method @FlaggedApi("com.android.text.flags.typeface_redesign_readonly") @Nullable public String getFontVariationOverride();
     method public String getFontVariationSettings();
     method public int getHinting();
     method public float getLetterSpacing();
@@ -16972,6 +16975,7 @@
     method public void setFilterBitmap(boolean);
     method public void setFlags(int);
     method public void setFontFeatureSettings(String);
+    method @FlaggedApi("com.android.text.flags.typeface_redesign_readonly") public boolean setFontVariationOverride(@Nullable String);
     method public boolean setFontVariationSettings(String);
     method public void setHinting(int);
     method public void setLetterSpacing(float);
@@ -33657,16 +33661,23 @@
   }
 
   @FlaggedApi("android.os.cpu_gpu_headrooms") public final class CpuHeadroomParams {
-    ctor public CpuHeadroomParams();
     method public int getCalculationType();
-    method @IntRange(from=0x32, to=0x2710) public long getCalculationWindowMillis();
-    method public void setCalculationType(int);
-    method public void setCalculationWindowMillis(@IntRange(from=0x32, to=0x2710) int);
-    method public void setTids(@NonNull int...);
+    method public long getCalculationWindowMillis();
+    method @NonNull public int[] getTids();
+    method @NonNull public android.os.CpuHeadroomParams.Builder toBuilder();
     field public static final int CPU_HEADROOM_CALCULATION_TYPE_AVERAGE = 1; // 0x1
     field public static final int CPU_HEADROOM_CALCULATION_TYPE_MIN = 0; // 0x0
   }
 
+  public static final class CpuHeadroomParams.Builder {
+    ctor public CpuHeadroomParams.Builder();
+    ctor public CpuHeadroomParams.Builder(@NonNull android.os.CpuHeadroomParams);
+    method @NonNull public android.os.CpuHeadroomParams build();
+    method @NonNull public android.os.CpuHeadroomParams.Builder setCalculationType(int);
+    method @NonNull public android.os.CpuHeadroomParams.Builder setCalculationWindowMillis(@IntRange(from=1) int);
+    method @NonNull public android.os.CpuHeadroomParams.Builder setTids(@NonNull int...);
+  }
+
   public final class CpuUsageInfo implements android.os.Parcelable {
     method public int describeContents();
     method public long getActive();
@@ -33915,13 +33926,20 @@
   }
 
   @FlaggedApi("android.os.cpu_gpu_headrooms") public final class GpuHeadroomParams {
-    ctor public GpuHeadroomParams();
     method public int getCalculationType();
-    method @IntRange(from=0x32, to=0x2710) public int getCalculationWindowMillis();
-    method public void setCalculationType(int);
-    method public void setCalculationWindowMillis(@IntRange(from=0x32, to=0x2710) int);
+    method public int getCalculationWindowMillis();
     field public static final int GPU_HEADROOM_CALCULATION_TYPE_AVERAGE = 1; // 0x1
     field public static final int GPU_HEADROOM_CALCULATION_TYPE_MIN = 0; // 0x0
+    field public static final int GPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MAX = 10000; // 0x2710
+    field public static final int GPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MIN = 50; // 0x32
+  }
+
+  public static final class GpuHeadroomParams.Builder {
+    ctor public GpuHeadroomParams.Builder();
+    ctor public GpuHeadroomParams.Builder(@NonNull android.os.GpuHeadroomParams);
+    method @NonNull public android.os.GpuHeadroomParams build();
+    method @NonNull public android.os.GpuHeadroomParams.Builder setCalculationType(int);
+    method @NonNull public android.os.GpuHeadroomParams.Builder setCalculationWindowMillis(@IntRange(from=1) int);
   }
 
   public class Handler {
@@ -35187,9 +35205,12 @@
 
   public class SystemHealthManager {
     method @FlaggedApi("android.os.cpu_gpu_headrooms") @FloatRange(from=0.0f, to=100.0f) public float getCpuHeadroom(@Nullable android.os.CpuHeadroomParams);
+    method @FlaggedApi("android.os.cpu_gpu_headrooms") @NonNull public android.util.Pair<java.lang.Integer,java.lang.Integer> getCpuHeadroomCalculationWindowRange();
     method @FlaggedApi("android.os.cpu_gpu_headrooms") public long getCpuHeadroomMinIntervalMillis();
     method @FlaggedApi("android.os.cpu_gpu_headrooms") @FloatRange(from=0.0f, to=100.0f) public float getGpuHeadroom(@Nullable android.os.GpuHeadroomParams);
+    method @FlaggedApi("android.os.cpu_gpu_headrooms") @NonNull public android.util.Pair<java.lang.Integer,java.lang.Integer> getGpuHeadroomCalculationWindowRange();
     method @FlaggedApi("android.os.cpu_gpu_headrooms") public long getGpuHeadroomMinIntervalMillis();
+    method @FlaggedApi("android.os.cpu_gpu_headrooms") @IntRange(from=1) public int getMaxCpuHeadroomTidsSize();
     method @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public void getPowerMonitorReadings(@NonNull java.util.List<android.os.PowerMonitor>, @Nullable java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.os.PowerMonitorReadings,java.lang.RuntimeException>);
     method @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public void getSupportedPowerMonitors(@Nullable java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.util.List<android.os.PowerMonitor>>);
     method public android.os.health.HealthStats takeMyUidSnapshot();
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index f82aecb..93f3119 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -581,7 +581,7 @@
 package android.accounts {
 
   public class AccountManager {
-    method @FlaggedApi("android.app.admin.flags.split_create_managed_profile_enabled") @NonNull @RequiresPermission(anyOf={android.Manifest.permission.COPY_ACCOUNTS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) public android.accounts.AccountManagerFuture<java.lang.Boolean> copyAccountToUser(@NonNull android.accounts.Account, @NonNull android.os.UserHandle, @NonNull android.os.UserHandle, @Nullable android.accounts.AccountManagerCallback<java.lang.Boolean>, @Nullable android.os.Handler);
+    method @FlaggedApi("android.app.admin.flags.split_create_managed_profile_enabled") @NonNull @RequiresPermission(anyOf={android.Manifest.permission.COPY_ACCOUNTS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) public android.accounts.AccountManagerFuture<java.lang.Boolean> copyAccountToUser(@NonNull android.accounts.Account, @NonNull android.os.UserHandle, @NonNull android.os.UserHandle, @Nullable android.os.Handler, @Nullable android.accounts.AccountManagerCallback<java.lang.Boolean>);
     method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public android.accounts.AccountManagerFuture<android.os.Bundle> finishSessionAsUser(android.os.Bundle, android.app.Activity, android.os.UserHandle, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler);
   }
 
@@ -1290,7 +1290,6 @@
 
   public class WallpaperManager {
     method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public void clearWallpaper(int, int);
-    method @FlaggedApi("android.app.customization_packs_apis") @NonNull @RequiresPermission(android.Manifest.permission.READ_WALLPAPER_INTERNAL) public android.util.SparseArray<android.graphics.Rect> getBitmapCrops(int);
     method @FlaggedApi("android.app.customization_packs_apis") public static int getOrientation(@NonNull android.graphics.Point);
     method @FloatRange(from=0.0f, to=1.0f) @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT) public float getWallpaperDimAmount();
     method @FlaggedApi("android.app.customization_packs_apis") @Nullable public android.os.ParcelFileDescriptor getWallpaperFile(int, boolean);
@@ -8095,16 +8094,16 @@
     method @Deprecated @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public void deleteModel(java.util.UUID);
     method public int getDetectionServiceOperationsTimeout();
     method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public android.media.soundtrigger.SoundTriggerManager.Model getModel(java.util.UUID);
-    method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int getModelState(@NonNull java.util.UUID);
+    method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) @WorkerThread public int getModelState(@NonNull java.util.UUID);
     method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public android.hardware.soundtrigger.SoundTrigger.ModuleProperties getModuleProperties();
     method @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int getParameter(@NonNull java.util.UUID, int);
-    method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public boolean isRecognitionActive(@NonNull java.util.UUID);
-    method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int loadSoundModel(@NonNull android.hardware.soundtrigger.SoundTrigger.SoundModel);
+    method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) @WorkerThread public boolean isRecognitionActive(@NonNull java.util.UUID);
+    method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) @WorkerThread public int loadSoundModel(@NonNull android.hardware.soundtrigger.SoundTrigger.SoundModel);
     method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public android.hardware.soundtrigger.SoundTrigger.ModelParamRange queryParameter(@Nullable java.util.UUID, int);
     method @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int setParameter(@Nullable java.util.UUID, int, int);
-    method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int startRecognition(@NonNull java.util.UUID, @Nullable android.os.Bundle, @NonNull android.content.ComponentName, @NonNull android.hardware.soundtrigger.SoundTrigger.RecognitionConfig);
-    method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int stopRecognition(@NonNull java.util.UUID);
-    method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int unloadSoundModel(@NonNull java.util.UUID);
+    method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) @WorkerThread public int startRecognition(@NonNull java.util.UUID, @Nullable android.os.Bundle, @NonNull android.content.ComponentName, @NonNull android.hardware.soundtrigger.SoundTrigger.RecognitionConfig);
+    method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) @WorkerThread public int stopRecognition(@NonNull java.util.UUID);
+    method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) @WorkerThread public int unloadSoundModel(@NonNull java.util.UUID);
     method @Deprecated @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public void updateModel(android.media.soundtrigger.SoundTriggerManager.Model);
   }
 
@@ -15045,6 +15044,7 @@
     method public int getCellularIdentifier();
     method public int getNasProtocolMessage();
     method @NonNull public String getPlmn();
+    method @FlaggedApi("com.android.internal.telephony.flags.vendor_specific_cellular_identifier_disclosure_indications") public boolean isBenign();
     method public boolean isEmergency();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field public static final int CELLULAR_IDENTIFIER_IMEI = 2; // 0x2
@@ -15062,6 +15062,8 @@
     field public static final int NAS_PROTOCOL_MESSAGE_IMSI_DETACH_INDICATION = 11; // 0xb
     field public static final int NAS_PROTOCOL_MESSAGE_LOCATION_UPDATE_REQUEST = 5; // 0x5
     field public static final int NAS_PROTOCOL_MESSAGE_REGISTRATION_REQUEST = 7; // 0x7
+    field @FlaggedApi("com.android.internal.telephony.flags.vendor_specific_cellular_identifier_disclosure_indications") public static final int NAS_PROTOCOL_MESSAGE_THREAT_IDENTIFIER_FALSE = 12; // 0xc
+    field @FlaggedApi("com.android.internal.telephony.flags.vendor_specific_cellular_identifier_disclosure_indications") public static final int NAS_PROTOCOL_MESSAGE_THREAT_IDENTIFIER_TRUE = 13; // 0xd
     field public static final int NAS_PROTOCOL_MESSAGE_TRACKING_AREA_UPDATE_REQUEST = 4; // 0x4
     field public static final int NAS_PROTOCOL_MESSAGE_UNKNOWN = 0; // 0x0
   }
@@ -18568,13 +18570,13 @@
 
   @FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") public final class SatelliteManager {
     method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void addAttachRestrictionForCarrier(int, int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
-    method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionSatellite(@NonNull java.util.List<android.telephony.satellite.SatelliteSubscriberInfo>, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>);
+    method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionSatellite(@NonNull java.util.List<android.telephony.satellite.SatelliteSubscriberInfo>, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telephony.satellite.SatelliteManager.SatelliteException>);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionService(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
     method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.Set<java.lang.Integer> getAttachRestrictionReasonsForCarrier(int);
     method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int[] getSatelliteDisallowedReasons();
     method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.List<java.lang.String> getSatellitePlmnsForCarrier(int);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void pollPendingDatagrams(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
-    method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void provisionSatellite(@NonNull java.util.List<android.telephony.satellite.SatelliteSubscriberInfo>, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>);
+    method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void provisionSatellite(@NonNull java.util.List<android.telephony.satellite.SatelliteSubscriberInfo>, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telephony.satellite.SatelliteManager.SatelliteException>);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void provisionService(@NonNull String, @NonNull byte[], @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForCapabilitiesChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteCapabilitiesCallback);
     method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForCommunicationAccessStateChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteCommunicationAccessStateCallback);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index ed8042d..a352d9d 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -2,7 +2,7 @@
 package android {
 
   public static final class Manifest.permission {
-    field @FlaggedApi("com.android.server.accessibility.motion_event_observing") public static final String ACCESSIBILITY_MOTION_EVENT_OBSERVING = "android.permission.ACCESSIBILITY_MOTION_EVENT_OBSERVING";
+    field public static final String ACCESSIBILITY_MOTION_EVENT_OBSERVING = "android.permission.ACCESSIBILITY_MOTION_EVENT_OBSERVING";
     field public static final String ACCESS_NOTIFICATIONS = "android.permission.ACCESS_NOTIFICATIONS";
     field public static final String ACTIVITY_EMBEDDING = "android.permission.ACTIVITY_EMBEDDING";
     field public static final String ADJUST_RUNTIME_PERMISSIONS_POLICY = "android.permission.ADJUST_RUNTIME_PERMISSIONS_POLICY";
@@ -536,6 +536,7 @@
     method @Nullable public android.graphics.Bitmap getBitmap();
     method @Nullable public android.graphics.Bitmap getBitmapAsUser(int, boolean, int);
     method @FlaggedApi("com.android.window.flags.multi_crop") @NonNull @RequiresPermission(android.Manifest.permission.READ_WALLPAPER_INTERNAL) public java.util.List<android.graphics.Rect> getBitmapCrops(@NonNull java.util.List<android.graphics.Point>, int, boolean);
+    method @FlaggedApi("android.app.customization_packs_apis") @NonNull @RequiresPermission(android.Manifest.permission.READ_WALLPAPER_INTERNAL) public android.util.SparseArray<android.graphics.Rect> getBitmapCrops(int);
     method @FlaggedApi("com.android.window.flags.multi_crop") @NonNull public java.util.List<android.graphics.Rect> getBitmapCrops(@NonNull android.graphics.Point, @NonNull java.util.List<android.graphics.Point>, @Nullable java.util.Map<android.graphics.Point,android.graphics.Rect>);
     method public boolean isLockscreenLiveWallpaperEnabled();
     method @Nullable public android.graphics.Rect peekBitmapDimensions();
@@ -2113,6 +2114,11 @@
     method public boolean isAidlHal();
   }
 
+  public static final class MediaMuxer.OutputFormat {
+    field public static final int MUXER_OUTPUT_FIRST = 0; // 0x0
+    field public static final int MUXER_OUTPUT_LAST = 4; // 0x4
+  }
+
   public final class MediaRoute2Info implements android.os.Parcelable {
     method @NonNull public String getOriginalId();
   }
@@ -3409,6 +3415,10 @@
     method public void onBindClient(@Nullable android.content.Intent);
   }
 
+  public class TelecomManager {
+    method @FlaggedApi("com.android.server.telecom.flags.voip_call_monitor_refactor") public boolean hasForegroundServiceDelegation(@Nullable android.telecom.PhoneAccountHandle);
+  }
+
 }
 
 package android.telephony {
@@ -4532,7 +4542,6 @@
     field public final int displayId;
     field public final boolean isDuplicateTouchToWallpaper;
     field public final boolean isFocusable;
-    field public final boolean isPreventSplitting;
     field public final boolean isTouchable;
     field public final boolean isTrustedOverlay;
     field public final boolean isVisible;
diff --git a/core/api/test-lint-baseline.txt b/core/api/test-lint-baseline.txt
index 349b4ed..fe23517 100644
--- a/core/api/test-lint-baseline.txt
+++ b/core/api/test-lint-baseline.txt
@@ -1977,6 +1977,8 @@
     Documentation mentions 'TODO'
 
 
+UnflaggedApi: android.Manifest.permission#ACCESSIBILITY_MOTION_EVENT_OBSERVING:
+    New API must be flagged with @FlaggedApi: field android.Manifest.permission.ACCESSIBILITY_MOTION_EVENT_OBSERVING
 UnflaggedApi: android.Manifest.permission#MANAGE_REMOTE_AUTH:
     New API must be flagged with @FlaggedApi: field android.Manifest.permission.MANAGE_REMOTE_AUTH
 UnflaggedApi: android.Manifest.permission#RESERVED_FOR_TESTING_SIGNATURE:
@@ -2057,6 +2059,10 @@
     New API must be flagged with @FlaggedApi: method android.media.AudioManager.getFocusFadeOutDurationForTest()
 UnflaggedApi: android.media.AudioManager#getFocusUnmuteDelayAfterFadeOutForTest():
     New API must be flagged with @FlaggedApi: method android.media.AudioManager.getFocusUnmuteDelayAfterFadeOutForTest()
+UnflaggedApi: android.media.MediaMuxer.OutputFormat#MUXER_OUTPUT_FIRST:
+    New API must be flagged with @FlaggedApi: field android.media.MediaMuxer.OutputFormat.MUXER_OUTPUT_FIRST
+UnflaggedApi: android.media.MediaMuxer.OutputFormat#MUXER_OUTPUT_LAST:
+    New API must be flagged with @FlaggedApi: field android.media.MediaMuxer.OutputFormat.MUXER_OUTPUT_LAST
 UnflaggedApi: android.media.RingtoneSelection:
     New API must be flagged with @FlaggedApi: class android.media.RingtoneSelection
 UnflaggedApi: android.media.RingtoneSelection#DEFAULT_SELECTION_URI_STRING:
diff --git a/core/java/android/accounts/AccountManager.java b/core/java/android/accounts/AccountManager.java
index 72450999..ddc1ae2 100644
--- a/core/java/android/accounts/AccountManager.java
+++ b/core/java/android/accounts/AccountManager.java
@@ -30,7 +30,6 @@
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
 import android.annotation.Size;
-import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.annotation.UserHandleAware;
@@ -2019,23 +2018,22 @@
      * @param account the account to copy
      * @param fromUser the user to copy the account from
      * @param toUser the target user
-     * @param callback Callback to invoke when the request completes,
-     *     null for no callback
      * @param handler {@link Handler} identifying the callback thread,
      *     null for the main thread
+     * @param callback Callback to invoke when the request completes,
+     *     null for no callback
      * @return An {@link AccountManagerFuture} which resolves to a Boolean indicated whether it
      * succeeded.
      * @hide
      */
-    @SuppressLint("SamShouldBeLast")
     @NonNull
     @SystemApi
     @RequiresPermission(anyOf = {COPY_ACCOUNTS, INTERACT_ACROSS_USERS_FULL})
     @FlaggedApi(FLAG_SPLIT_CREATE_MANAGED_PROFILE_ENABLED)
     public AccountManagerFuture<Boolean> copyAccountToUser(
             @NonNull final Account account, @NonNull final UserHandle fromUser,
-            @NonNull final UserHandle toUser, @Nullable AccountManagerCallback<Boolean> callback,
-            @Nullable Handler handler) {
+            @NonNull final UserHandle toUser, @Nullable Handler handler,
+            @Nullable AccountManagerCallback<Boolean> callback) {
         if (account == null) throw new IllegalArgumentException("account is null");
         if (toUser == null || fromUser == null) {
             throw new IllegalArgumentException("fromUser and toUser cannot be null");
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 8614bde..4782205 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -1027,9 +1027,6 @@
     /** The autofill client controller. Always access via {@link #getAutofillClientController()}. */
     private AutofillClientController mAutofillClientController;
 
-    /** @hide */
-    boolean mEnterAnimationComplete;
-
     private boolean mIsInMultiWindowMode;
     /** @hide */
     boolean mIsInPictureInPictureMode;
@@ -1273,8 +1270,8 @@
      * Requests to show the “Open in browser” education. “Open in browser” is a feature
      * within the app header that allows users to switch from an app to the web. The feature
      * is made available when an application is opened by a user clicking a link or when a
-     * link is provided by an application. Links can be provided by utilizing
-     * {@link AssistContent#EXTRA_AUTHENTICATING_USER_WEB_URI} or
+     * link is provided by an application. Links can be provided by calling
+     * {@link AssistContent#setSessionTransferUri} or
      * {@link AssistContent#setWebUri}.
      *
      * <p>This method should be utilized when an activity wants to nudge the user to switch
@@ -1287,7 +1284,7 @@
      * disruptive to the user to show the education and when it is optimal to switch the user to a
      * browser session. Before requesting to show the education, developers should assert that they
      * have set a link that can be used by the "Open in browser" feature through either
-     * {@link AssistContent#EXTRA_AUTHENTICATING_USER_WEB_URI} or
+     * {@link AssistContent#setSessionTransferUri} or
      * {@link AssistContent#setWebUri} so that users are navigated to a relevant page if they choose
      * to switch to the browser. If a URI is not set using either method, "Open in browser" will
      * utilize a generic link if available which will direct users to the homepage of the site
@@ -1296,7 +1293,7 @@
      * the user will not be provided with the option to switch to the browser and the education will
      * not be shown if requested.
      *
-     * @see android.app.assist.AssistContent#EXTRA_SESSION_TRANSFER_WEB_URI
+     * @see android.app.assist.AssistContent#setSessionTransferUri
      */
     @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION)
     public final void requestOpenInBrowserEducation() {
@@ -2898,7 +2895,6 @@
         mCalled = true;
 
         getAutofillClientController().onActivityStopped(mIntent, mChangingConfigurations);
-        mEnterAnimationComplete = false;
 
         notifyVoiceInteractionManagerServiceActivityEvent(
                 VoiceInteractionSession.VOICE_INTERACTION_ACTIVITY_EVENT_STOP);
@@ -8594,8 +8590,6 @@
      * @hide
      */
     public void dispatchEnterAnimationComplete() {
-        mEnterAnimationComplete = true;
-        mInstrumentation.onEnterAnimationComplete();
         onEnterAnimationComplete();
         if (getWindow() != null && getWindow().getDecorView() != null) {
             View decorView = getWindow().getDecorView();
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index 999db18..6151b8e 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -142,6 +142,15 @@
             String processName, String abiOverride, int uid, Runnable crashHandler);
 
     /**
+     * Called when a user is being deleted. This can happen during normal device usage
+     * or just at startup, when partially removed users are purged. Any state persisted by the
+     * ActivityManager should be purged now.
+     *
+     * @param userId The user being cleaned up.
+     */
+    public abstract void onUserRemoving(@UserIdInt int userId);
+
+    /**
      * Called when a user has been deleted. This can happen during normal device usage
      * or just at startup, when partially removed users are purged. Any state persisted by the
      * ActivityManager should be purged now.
diff --git a/core/java/android/app/IUserSwitchObserver.aidl b/core/java/android/app/IUserSwitchObserver.aidl
index 1ff7a17..d71ee7c 100644
--- a/core/java/android/app/IUserSwitchObserver.aidl
+++ b/core/java/android/app/IUserSwitchObserver.aidl
@@ -19,10 +19,10 @@
 import android.os.IRemoteCallback;
 
 /** {@hide} */
-interface IUserSwitchObserver {
-    void onBeforeUserSwitching(int newUserId);
-    oneway void onUserSwitching(int newUserId, IRemoteCallback reply);
-    oneway void onUserSwitchComplete(int newUserId);
-    oneway void onForegroundProfileSwitch(int newProfileId);
-    oneway void onLockedBootComplete(int newUserId);
+oneway interface IUserSwitchObserver {
+    void onBeforeUserSwitching(int newUserId, IRemoteCallback reply);
+    void onUserSwitching(int newUserId, IRemoteCallback reply);
+    void onUserSwitchComplete(int newUserId);
+    void onForegroundProfileSwitch(int newProfileId);
+    void onLockedBootComplete(int newUserId);
 }
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index 7eacaac..b611acf 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -59,7 +59,6 @@
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
-import android.view.SurfaceControl;
 import android.view.ViewConfiguration;
 import android.view.Window;
 import android.view.WindowManagerGlobal;
@@ -137,7 +136,6 @@
     private PerformanceCollector mPerformanceCollector;
     private Bundle mPerfMetrics = new Bundle();
     private UiAutomation mUiAutomation;
-    private final Object mAnimationCompleteLock = new Object();
 
     @RavenwoodKeep
     public Instrumentation() {
@@ -455,31 +453,6 @@
         idler.waitForIdle();
     }
 
-    private void waitForEnterAnimationComplete(Activity activity) {
-        synchronized (mAnimationCompleteLock) {
-            long timeout = 5000;
-            try {
-                // We need to check that this specified Activity completed the animation, not just
-                // any Activity. If it was another Activity, then decrease the timeout by how long
-                // it's already waited and wait for the thread to wakeup again.
-                while (timeout > 0 && !activity.mEnterAnimationComplete) {
-                    long startTime = System.currentTimeMillis();
-                    mAnimationCompleteLock.wait(timeout);
-                    long totalTime = System.currentTimeMillis() - startTime;
-                    timeout -= totalTime;
-                }
-            } catch (InterruptedException e) {
-            }
-        }
-    }
-
-    /** @hide */
-    public void onEnterAnimationComplete() {
-        synchronized (mAnimationCompleteLock) {
-            mAnimationCompleteLock.notifyAll();
-        }
-    }
-
     /**
      * Execute a call on the application's main thread, blocking until it is
      * complete.  Useful for doing things that are not thread-safe, such as
@@ -640,13 +613,14 @@
             activity = aw.activity;
         }
 
-        // Do not call this method within mSync, lest it could block the main thread.
-        waitForEnterAnimationComplete(activity);
-
-        // Apply an empty transaction to ensure SF has a chance to update before
-        // the Activity is ready (b/138263890).
-        try (SurfaceControl.Transaction t = new SurfaceControl.Transaction()) {
-            t.apply(true);
+        // Typically, callers expect that the launched activity can receive input events after this
+        // method returns, so wait until a stable state, i.e. animation is finished and input info
+        // is updated.
+        try {
+            WindowManagerGlobal.getWindowManagerService()
+                    .syncInputTransactions(true /* waitForAnimations */);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
         }
         return activity;
     }
diff --git a/core/java/android/app/KeyguardManager.java b/core/java/android/app/KeyguardManager.java
index 67f7bee..b5ac4e7 100644
--- a/core/java/android/app/KeyguardManager.java
+++ b/core/java/android/app/KeyguardManager.java
@@ -70,7 +70,6 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.nio.charset.Charset;
-import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.Executor;
@@ -1064,7 +1063,7 @@
             Log.e(TAG, "Save lock exception", e);
             success = false;
         } finally {
-            Arrays.fill(password, (byte) 0);
+            LockPatternUtils.zeroize(password);
         }
         return success;
     }
diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java
index 3d85ea6..ffd235f 100644
--- a/core/java/android/app/LoadedApk.java
+++ b/core/java/android/app/LoadedApk.java
@@ -1129,6 +1129,10 @@
 
     @UnsupportedAppUsage
     public ClassLoader getClassLoader() {
+        ClassLoader ret = mClassLoader;
+        if (ret != null) {
+            return ret;
+        }
         synchronized (mLock) {
             if (mClassLoader == null) {
                 createOrUpdateClassLoaderLocked(null /*addedPaths*/);
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 24594ab..c2ce7d5 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -774,8 +774,9 @@
 
     /**
      * Bit to be bitwise-ored into the {@link #flags} field that should be
-     * set by the system if this notification is a promoted ongoing notification, either via a
-     * user setting or allowlist.
+     * set by the system if this notification is a promoted ongoing notification, both because it
+     * {@link #hasPromotableCharacteristics()} and the user has not disabled the feature for this
+     * app.
      *
      * Applications cannot set this flag directly, but the posting app and
      * {@link android.service.notification.NotificationListenerService} can read it.
@@ -1967,6 +1968,13 @@
         @SystemApi
         public static final int SEMANTIC_ACTION_CONVERSATION_IS_PHISHING = 12;
 
+        /**
+         * {@link #extras} key to a boolean defining if this action requires special visual
+         * treatment.
+         * @hide
+         */
+        public static final String EXTRA_IS_MAGIC = "android.extra.IS_MAGIC";
+
         private final Bundle mExtras;
         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
         private Icon mIcon;
@@ -5855,7 +5863,9 @@
                 return null;
             }
             final int size = mContext.getResources().getDimensionPixelSize(
-                    R.dimen.notification_badge_size);
+                    Flags.notificationsRedesignTemplates()
+                            ? R.dimen.notification_2025_badge_size
+                            : R.dimen.notification_badge_size);
             Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
             Canvas canvas = new Canvas(bitmap);
             badge.setBounds(0, 0, size, size);
@@ -5981,6 +5991,15 @@
             }
             setHeaderlessVerticalMargins(contentView, p, hasSecondLine);
 
+            // Update margins to leave space for the top line (but not for headerless views like
+            // HUNS, which use a different layout that already accounts for that).
+            if (Flags.notificationsRedesignTemplates() && !p.mHeaderless) {
+                int margin = getContentMarginTop(mContext,
+                        R.dimen.notification_2025_content_margin_top);
+                contentView.setViewLayoutMargin(R.id.notification_main_column,
+                        RemoteViews.MARGIN_TOP, margin, TypedValue.COMPLEX_UNIT_PX);
+            }
+
             return contentView;
         }
 
@@ -6204,7 +6223,7 @@
             int textColor = Colors.flattenAlpha(getPrimaryTextColor(p), pillColor);
             contentView.setInt(R.id.expand_button, "setDefaultTextColor", textColor);
             contentView.setInt(R.id.expand_button, "setDefaultPillColor", pillColor);
-            // Use different highlighted colors for e.g. unopened groups
+            // Use different highlighted colors for conversations' unread count
             if (p.mHighlightExpander) {
                 pillColor = Colors.flattenAlpha(
                         getColors(p).getTertiaryFixedDimAccentColor(), bgColor);
@@ -6453,16 +6472,6 @@
             big.setColorStateList(R.id.snooze_button, "setImageTintList", actionColor);
             big.setColorStateList(R.id.bubble_button, "setImageTintList", actionColor);
 
-            // Update margins to leave space for the top line (but not for HUNs, which use a
-            // different layout that already accounts for that).
-            if (Flags.notificationsRedesignTemplates()
-                    && p.mViewType != StandardTemplateParams.VIEW_TYPE_HEADS_UP) {
-                int margin = getContentMarginTop(mContext,
-                        R.dimen.notification_2025_content_margin_top);
-                big.setViewLayoutMargin(R.id.notification_main_column, RemoteViews.MARGIN_TOP,
-                        margin, TypedValue.COMPLEX_UNIT_PX);
-            }
-
             boolean validRemoteInput = false;
 
             // In the UI, contextual actions appear separately from the standard actions, so we
@@ -6804,8 +6813,6 @@
         public RemoteViews makeNotificationGroupHeader() {
             return makeNotificationHeader(mParams.reset()
                     .viewType(StandardTemplateParams.VIEW_TYPE_GROUP_HEADER)
-                    // Highlight group expander until the group is first opened
-                    .highlightExpander(Flags.notificationsRedesignTemplates())
                     .fillTextsFrom(this));
         }
 
@@ -6981,14 +6988,12 @@
          * @param useRegularSubtext uses the normal subtext set if there is one available. Otherwise
          *                          a new subtext is created consisting of the content of the
          *                          notification.
-         * @param highlightExpander whether the expander should use the highlighted colors
          * @hide
          */
-        public RemoteViews makeLowPriorityContentView(boolean useRegularSubtext,
-                boolean highlightExpander) {
+        public RemoteViews makeLowPriorityContentView(boolean useRegularSubtext) {
             StandardTemplateParams p = mParams.reset()
                     .viewType(StandardTemplateParams.VIEW_TYPE_MINIMIZED)
-                    .highlightExpander(highlightExpander)
+                    .highlightExpander(false)
                     .fillTextsFrom(this);
             if (!useRegularSubtext || TextUtils.isEmpty(p.mSubText)) {
                 p.summaryText(createSummaryText());
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 08bd854..aede8aa 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -17,6 +17,7 @@
 package android.app;
 
 import static android.Manifest.permission.POST_NOTIFICATIONS;
+import static android.app.NotificationChannel.DEFAULT_CHANNEL_ID;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.service.notification.Flags.notificationClassification;
 
@@ -50,6 +51,7 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.os.IpcDataCache;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.RemoteException;
@@ -71,6 +73,8 @@
 import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.time.InstantSource;
@@ -1202,12 +1206,20 @@
      * package (see {@link Context#createPackageContext(String, int)}).</p>
      */
     public NotificationChannel getNotificationChannel(String channelId) {
-        INotificationManager service = service();
-        try {
-            return service.getNotificationChannel(mContext.getOpPackageName(),
-                    mContext.getUserId(), mContext.getPackageName(), channelId);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+        if (Flags.nmBinderPerfCacheChannels()) {
+            return getChannelFromList(channelId,
+                    mNotificationChannelListCache.query(new NotificationChannelQuery(
+                            mContext.getOpPackageName(),
+                            mContext.getPackageName(),
+                            mContext.getUserId())));
+        } else {
+            INotificationManager service = service();
+            try {
+                return service.getNotificationChannel(mContext.getOpPackageName(),
+                        mContext.getUserId(), mContext.getPackageName(), channelId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
         }
     }
 
@@ -1222,13 +1234,21 @@
      */
     public @Nullable NotificationChannel getNotificationChannel(@NonNull String channelId,
             @NonNull String conversationId) {
-        INotificationManager service = service();
-        try {
-            return service.getConversationNotificationChannel(mContext.getOpPackageName(),
-                    mContext.getUserId(), mContext.getPackageName(), channelId, true,
-                    conversationId);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+        if (Flags.nmBinderPerfCacheChannels()) {
+            return getConversationChannelFromList(channelId, conversationId,
+                    mNotificationChannelListCache.query(new NotificationChannelQuery(
+                            mContext.getOpPackageName(),
+                            mContext.getPackageName(),
+                            mContext.getUserId())));
+        } else {
+            INotificationManager service = service();
+            try {
+                return service.getConversationNotificationChannel(mContext.getOpPackageName(),
+                        mContext.getUserId(), mContext.getPackageName(), channelId, true,
+                        conversationId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
         }
     }
 
@@ -1241,15 +1261,62 @@
      * {@link Context#createPackageContext(String, int)}).</p>
      */
     public List<NotificationChannel> getNotificationChannels() {
-        INotificationManager service = service();
-        try {
-            return service.getNotificationChannels(mContext.getOpPackageName(),
-                    mContext.getPackageName(), mContext.getUserId()).getList();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+        if (Flags.nmBinderPerfCacheChannels()) {
+            return mNotificationChannelListCache.query(new NotificationChannelQuery(
+               mContext.getOpPackageName(),
+               mContext.getPackageName(),
+               mContext.getUserId()));
+        } else {
+            INotificationManager service = service();
+            try {
+                return service.getNotificationChannels(mContext.getOpPackageName(),
+                        mContext.getPackageName(), mContext.getUserId()).getList();
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
         }
     }
 
+    // channel list assumed to be associated with the appropriate package & user id already.
+    private static NotificationChannel getChannelFromList(String channelId,
+            List<NotificationChannel> channels) {
+        if (channels == null) {
+            return null;
+        }
+        if (channelId == null) {
+            channelId = DEFAULT_CHANNEL_ID;
+        }
+        for (NotificationChannel channel : channels) {
+            if (channelId.equals(channel.getId())) {
+                return channel;
+            }
+        }
+        return null;
+    }
+
+    private static NotificationChannel getConversationChannelFromList(String channelId,
+            String conversationId, List<NotificationChannel> channels) {
+        if (channels == null) {
+            return null;
+        }
+        if (channelId == null) {
+            channelId = DEFAULT_CHANNEL_ID;
+        }
+        if (conversationId == null) {
+            return getChannelFromList(channelId, channels);
+        }
+        NotificationChannel parent = null;
+        for (NotificationChannel channel : channels) {
+            if (conversationId.equals(channel.getConversationId())
+                    && channelId.equals(channel.getParentChannelId())) {
+                return channel;
+            } else if (channelId.equals(channel.getId())) {
+                parent = channel;
+            }
+        }
+        return parent;
+    }
+
     /**
      * Deletes the given notification channel.
      *
@@ -1328,6 +1395,71 @@
         }
     }
 
+    private static final String NOTIFICATION_CHANNEL_CACHE_API = "getNotificationChannel";
+    private static final String NOTIFICATION_CHANNEL_LIST_CACHE_NAME = "getNotificationChannels";
+    private static final int NOTIFICATION_CHANNEL_CACHE_SIZE = 10;
+
+    private final IpcDataCache.QueryHandler<NotificationChannelQuery, List<NotificationChannel>>
+            mNotificationChannelListQueryHandler = new IpcDataCache.QueryHandler<>() {
+                @Override
+                public List<NotificationChannel> apply(NotificationChannelQuery query) {
+                    INotificationManager service = service();
+                    try {
+                        return service.getNotificationChannels(query.callingPkg,
+                                query.targetPkg, query.userId).getList();
+                    } catch (RemoteException e) {
+                        throw e.rethrowFromSystemServer();
+                    }
+                }
+
+                @Override
+                public boolean shouldBypassCache(@NonNull NotificationChannelQuery query) {
+                    // Other locations should also not be querying the cache in the first place if
+                    // the flag is not enabled, but this is an extra precaution.
+                    if (!Flags.nmBinderPerfCacheChannels()) {
+                        Log.wtf(TAG,
+                                "shouldBypassCache called when nm_binder_perf_cache_channels off");
+                        return true;
+                    }
+                    return false;
+                }
+            };
+
+    private final IpcDataCache<NotificationChannelQuery, List<NotificationChannel>>
+            mNotificationChannelListCache =
+            new IpcDataCache<>(NOTIFICATION_CHANNEL_CACHE_SIZE, IpcDataCache.MODULE_SYSTEM,
+                    NOTIFICATION_CHANNEL_CACHE_API, NOTIFICATION_CHANNEL_LIST_CACHE_NAME,
+                    mNotificationChannelListQueryHandler);
+
+    private record NotificationChannelQuery(
+            String callingPkg,
+            String targetPkg,
+            int userId) {}
+
+    /**
+     * @hide
+     */
+    public static void invalidateNotificationChannelCache() {
+        if (Flags.nmBinderPerfCacheChannels()) {
+            IpcDataCache.invalidateCache(IpcDataCache.MODULE_SYSTEM,
+                    NOTIFICATION_CHANNEL_CACHE_API);
+        } else {
+            // if we are here, we have failed to flag something
+            Log.wtf(TAG, "invalidateNotificationChannelCache called without flag");
+        }
+    }
+
+    /**
+     * For testing only: running tests with a cache requires marking the cache's property for
+     * testing, as test APIs otherwise cannot invalidate the cache. This must be called after
+     * calling PropertyInvalidatedCache.setTestMode(true).
+     * @hide
+     */
+    @VisibleForTesting
+    public void setChannelCacheToTestMode() {
+        mNotificationChannelListCache.testPropertyName();
+    }
+
     /**
      * @hide
      */
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index 5567c08..e7e9b00 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -36,6 +36,7 @@
 import android.util.Log;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
+import android.util.SystemPropertySetter;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -157,21 +158,6 @@
     public static final String MODULE_TELEPHONY = "telephony";
 
     /**
-     * Constants that affect retries when the process is unable to write the property.
-     * The first constant is the number of times the process will attempt to set the
-     * property.  The second constant is the delay between attempts.
-     */
-
-    /**
-     * Wait 200ms between retry attempts and the retry limit is 5.  That gives a total possible
-     * delay of 1s, which should be less than ANR timeouts.  The goal is to have the system crash
-     * because the property could not be set (which is a condition that is easily recognized) and
-     * not crash because of an ANR (which can be confusing to debug).
-     */
-    private static final int PROPERTY_FAILURE_RETRY_DELAY_MILLIS = 200;
-    private static final int PROPERTY_FAILURE_RETRY_LIMIT = 5;
-
-    /**
      * Construct a system property that matches the rules described above.  The module is
      * one of the permitted values above.  The API is a string that is a legal Java simple
      * identifier.  The api is modified to conform to the system property style guide by
@@ -958,37 +944,8 @@
          */
         @Override
         void setNonceInternal(long value) {
-            // Failing to set the nonce is a fatal error.  Failures setting a system property have
-            // been reported; given that the failure is probably transient, this function includes
-            // a retry.
             final String str = Long.toString(value);
-            RuntimeException failure = null;
-            for (int attempt = 0; attempt < PROPERTY_FAILURE_RETRY_LIMIT; attempt++) {
-                try {
-                    SystemProperties.set(mName, str);
-                    if (attempt > 0) {
-                        // This log is not guarded.  Based on known bug reports, it should
-                        // occur once a week or less.  The purpose of the log message is to
-                        // identify the retries as a source of delay that might be otherwise
-                        // be attributed to the cache itself.
-                        Log.w(TAG, "Nonce set after " + attempt + " tries");
-                    }
-                    return;
-                } catch (RuntimeException e) {
-                    if (failure == null) {
-                        failure = e;
-                    }
-                    try {
-                        Thread.sleep(PROPERTY_FAILURE_RETRY_DELAY_MILLIS);
-                    } catch (InterruptedException x) {
-                        // Ignore this exception.  The desired delay is only approximate and
-                        // there is no issue if the sleep sometimes terminates early.
-                    }
-                }
-            }
-            // This point is reached only if SystemProperties.set() fails at least once.
-            // Rethrow the first exception that was received.
-            throw failure;
+            SystemPropertySetter.setWithRetry(mName, str);
         }
     }
 
diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java
index 2e6f3e1..5754984 100644
--- a/core/java/android/app/UiModeManager.java
+++ b/core/java/android/app/UiModeManager.java
@@ -753,7 +753,7 @@
      * <p>
      * The mode can be one of:
      * <ul>
-     *   <li><em>{@link #MODE_NIGHT_NO}<em> sets the device into
+     *   <li><em>{@link #MODE_NIGHT_NO}</em> sets the device into
      *       {@code notnight} mode</li>
      *   <li><em>{@link #MODE_NIGHT_YES}</em> sets the device into
      *       {@code night} mode</li>
@@ -889,7 +889,7 @@
      * <p>
      * The mode can be one of:
      * <ul>
-     *   <li><em>{@link #MODE_NIGHT_NO}<em> sets the device into
+     *   <li><em>{@link #MODE_NIGHT_NO}</em> sets the device into
      *       {@code notnight} mode</li>
      *   <li><em>{@link #MODE_NIGHT_YES}</em> sets the device into
      *       {@code night} mode</li>
diff --git a/core/java/android/app/UserSwitchObserver.java b/core/java/android/app/UserSwitchObserver.java
index 727799a1..1664cfb 100644
--- a/core/java/android/app/UserSwitchObserver.java
+++ b/core/java/android/app/UserSwitchObserver.java
@@ -30,7 +30,11 @@
     }
 
     @Override
-    public void onBeforeUserSwitching(int newUserId) throws RemoteException {}
+    public void onBeforeUserSwitching(int newUserId, IRemoteCallback reply) throws RemoteException {
+        if (reply != null) {
+            reply.sendResult(null);
+        }
+    }
 
     @Override
     public void onUserSwitching(int newUserId, IRemoteCallback reply) throws RemoteException {
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index 360376d..73ecc71 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -1690,7 +1690,7 @@
      * @hide
      */
     @FlaggedApi(FLAG_CUSTOMIZATION_PACKS_APIS)
-    @SystemApi
+    @TestApi
     @RequiresPermission(READ_WALLPAPER_INTERNAL)
     @NonNull
     public SparseArray<Rect> getBitmapCrops(@SetWallpaperFlags int which) {
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index a2fddb0..c504521 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -4354,20 +4354,24 @@
     }
 
     /**
-     * Indicates that app functions are not controlled by policy.
+     * Indicates that {@link android.app.appfunctions.AppFunctionManager} is not controlled by
+     * policy.
      *
      * <p>If no admin set this policy, it means appfunctions are enabled.
      */
     @FlaggedApi(android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER)
     public static final int APP_FUNCTIONS_NOT_CONTROLLED_BY_POLICY = 0;
 
-    /** Indicates that app functions are controlled and disabled by a policy. */
+    /** Indicates that {@link android.app.appfunctions.AppFunctionManager} is controlled and
+     * disabled by policy, i.e. no apps in the current user are allowed to expose app functions.
+     */
     @FlaggedApi(android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER)
     public static final int APP_FUNCTIONS_DISABLED = 1;
 
     /**
-     * Indicates that app functions are controlled and disabled by a policy for cross profile
-     * interactions only.
+     * Indicates that {@link android.app.appfunctions.AppFunctionManager} is controlled and
+     * disabled by a policy for cross profile interactions only, i.e. app functions exposed by apps
+     * in the current user can only be invoked within the same user.
      *
      * <p>This is different from {@link #APP_FUNCTIONS_DISABLED} in that it only disables cross
      * profile interactions (even if the caller has permissions required to interact across users).
@@ -4388,7 +4392,9 @@
     public @interface AppFunctionsPolicy {}
 
     /**
-     * Sets the app functions policy which controls app functions operations on the device.
+     * Sets the {@link android.app.appfunctions.AppFunctionManager} policy which controls app
+     * functions operations on the device. An app function is a piece of functionality that apps
+     * expose to the system for cross-app orchestration.
      *
      * <p>This function can only be called by a device owner, a profile owner or holders of the
      * permission {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_APP_FUNCTIONS}.
@@ -4414,7 +4420,7 @@
     }
 
     /**
-     * Returns the current app functions policy.
+     * Returns the current {@link android.app.appfunctions.AppFunctionManager} policy.
      *
      * <p>The returned policy will be the current resolved policy rather than the policy set by the
      * calling admin.
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index af035cb..75589fa 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -160,16 +160,6 @@
 }
 
 flag {
-  name: "fix_race_condition_in_tie_profile_lock"
-  namespace: "enterprise"
-  description: "Fix race condition in tieProfileLockIfNecessary()"
-  bug: "355905501"
-  metadata {
-    purpose: PURPOSE_BUGFIX
-  }
-}
-
-flag {
   name: "quiet_mode_credential_bug_fix"
   namespace: "enterprise"
   description: "Guards a bugfix that ends the credential input flow if the managed user has not stopped."
diff --git a/core/java/android/app/appfunctions/AppFunctionService.java b/core/java/android/app/appfunctions/AppFunctionService.java
index d86f1d8..8e48b4e 100644
--- a/core/java/android/app/appfunctions/AppFunctionService.java
+++ b/core/java/android/app/appfunctions/AppFunctionService.java
@@ -28,6 +28,7 @@
 import android.app.Service;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.SigningInfo;
 import android.os.Binder;
 import android.os.CancellationSignal;
 import android.os.IBinder;
@@ -78,10 +79,10 @@
         void perform(
                 @NonNull ExecuteAppFunctionRequest request,
                 @NonNull String callingPackage,
+                @NonNull SigningInfo callingPackageSigningInfo,
                 @NonNull CancellationSignal cancellationSignal,
                 @NonNull
-                        OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException>
-                                callback);
+                        OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> callback);
     }
 
     /** @hide */
@@ -93,6 +94,7 @@
             public void executeAppFunction(
                     @NonNull ExecuteAppFunctionRequest request,
                     @NonNull String callingPackage,
+                    @NonNull SigningInfo callingPackageSigningInfo,
                     @NonNull ICancellationCallback cancellationCallback,
                     @NonNull IExecuteAppFunctionCallback callback) {
                 if (context.checkCallingPermission(BIND_APP_FUNCTION_SERVICE)
@@ -105,6 +107,7 @@
                     onExecuteFunction.perform(
                             request,
                             callingPackage,
+                            callingPackageSigningInfo,
                             buildCancellationSignal(cancellationCallback),
                             new OutcomeReceiver<>() {
                                 @Override
@@ -154,15 +157,17 @@
     /**
      * Called by the system to execute a specific app function.
      *
-     * <p>This method is triggered when the system requests your AppFunctionService to handle a
-     * particular function you have registered and made available.
+     * <p>This method is the entry point for handling all app function requests in an app. When the
+     * system needs your AppFunctionService to perform a function, it will invoke this method.
      *
-     * <p>To ensure proper routing of function requests, assign a unique identifier to each
-     * function. This identifier doesn't need to be globally unique, but it must be unique within
-     * your app. For example, a function to order food could be identified as "orderFood". In most
-     * cases this identifier should come from the ID automatically generated by the AppFunctions
-     * SDK. You can determine the specific function to invoke by calling {@link
-     * ExecuteAppFunctionRequest#getFunctionIdentifier()}.
+     * <p>Each function you've registered is identified by a unique identifier. This identifier
+     * doesn't need to be globally unique, but it must be unique within your app. For example, a
+     * function to order food could be identified as "orderFood". In most cases, this identifier is
+     * automatically generated by the AppFunctions SDK.
+     *
+     * <p>You can determine which function to execute by calling {@link
+     * ExecuteAppFunctionRequest#getFunctionIdentifier()}. This allows your service to route the
+     * incoming request to the appropriate logic for handling the specific function.
      *
      * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker
      * thread and dispatch the result with the given callback. You should always report back the
@@ -173,6 +178,8 @@
      *
      * @param request The function execution request.
      * @param callingPackage The package name of the app that is requesting the execution.
+     * @param callingPackageSigningInfo The signing information of the app that is requesting the
+     *     execution.
      * @param cancellationSignal A signal to cancel the execution.
      * @param callback A callback to report back the result or error.
      */
@@ -180,10 +187,9 @@
     public abstract void onExecuteFunction(
             @NonNull ExecuteAppFunctionRequest request,
             @NonNull String callingPackage,
+            @NonNull SigningInfo callingPackageSigningInfo,
             @NonNull CancellationSignal cancellationSignal,
-            @NonNull
-                    OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException>
-                            callback);
+            @NonNull OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> callback);
 
     /**
      * Returns result codes from throwable.
diff --git a/core/java/android/app/appfunctions/IAppFunctionService.aidl b/core/java/android/app/appfunctions/IAppFunctionService.aidl
index bf935d2..78bcb7f 100644
--- a/core/java/android/app/appfunctions/IAppFunctionService.aidl
+++ b/core/java/android/app/appfunctions/IAppFunctionService.aidl
@@ -35,12 +35,15 @@
      *
      * @param request  the function execution request.
      * @param callingPackage The package name of the app that is requesting the execution.
+     * @param callingPackageSigningInfo The signing information of the app that is requesting the
+     *      execution.
      * @param cancellationCallback a callback to send back the cancellation transport.
      * @param callback a callback to report back the result.
      */
     void executeAppFunction(
         in ExecuteAppFunctionRequest request,
         in String callingPackage,
+        in android.content.pm.SigningInfo callingPackageSigningInfo,
         in ICancellationCallback cancellationCallback,
         in IExecuteAppFunctionCallback callback
     );
diff --git a/core/java/android/app/assist/AssistContent.java b/core/java/android/app/assist/AssistContent.java
index 3e3ca24..adf8c94 100644
--- a/core/java/android/app/assist/AssistContent.java
+++ b/core/java/android/app/assist/AssistContent.java
@@ -1,6 +1,7 @@
 package android.app.assist;
 
 import android.annotation.FlaggedApi;
+import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ClipData;
 import android.content.Intent;
@@ -30,31 +31,6 @@
     public static final String EXTRA_APP_FUNCTION_DATA =
             "android.app.assist.extra.APP_FUNCTION_DATA";
 
-    /**
-     * This extra can be optionally supplied in the {@link #getExtras} bundle to provide a
-     * {@link Uri} which will be utilized when transitioning a user's session to another surface.
-     *
-     * <p>If provided, instead of using the URI provided in {@link #setWebUri}, the
-     * "Open in browser" feature will use this URI to transition the current session from one
-     * surface to the other. Apps may choose to encode session or user information into this
-     * URI in order to provide a better session transfer experience.
-     *
-     * <p>Unlike {@link #setWebUri}, this URI will not be used for features where the user might
-     * accidentally share it with another user. However, developers should not encode
-     * authentication credentials into this URI, because it will be surfaced in the browser URL
-     * bar and may be copied and shared from there.
-     *
-     * <p>When providing this extra, developers should still continue to provide
-     * {@link #setWebUri} for backwards compatibility with features such as
-     * <a href="https://developer.android.com/guide/components/activities/recents#url-sharing">
-     * recents URL sharing</a> which do not benefit from a session-transfer web URI.
-     *
-     * @see android.app.Activity#requestOpenInBrowserEducation()
-     */
-    @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION)
-    public static final String EXTRA_SESSION_TRANSFER_WEB_URI =
-            "android.app.assist.extra.SESSION_TRANSFER_WEB_URI";
-
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     private boolean mIsAppProvidedIntent = false;
     private boolean mIsAppProvidedWebUri = false;
@@ -66,6 +42,7 @@
     private ClipData mClipData;
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     private Uri mUri;
+    private Uri mSessionTransferUri;
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     private final Bundle mExtras;
 
@@ -200,6 +177,41 @@
     }
 
     /**
+     * This method can be used to provide a {@link Uri} which will be utilized when transitioning a
+     * user's session to another surface.
+     *
+     * <p>If provided, instead of using the URI provided in {@link #setWebUri}, the
+     * "Open in browser" feature will use this URI to transition the current session from one
+     * surface to the other. Apps may choose to encode session or user information into this
+     * URI in order to provide a better session transfer experience. However, while this URI will
+     * only be available to the system and not other applications, developers should not encode
+     * authentication credentials into this URI, because it will be surfaced in the browser URL bar
+     * and may be copied and shared from there.
+     *
+     * <p>When providing this URI, developers should still continue to provide
+     * {@link #setWebUri} for backwards compatibility with features such as
+     * <a href="https://developer.android.com/guide/components/activities/recents#url-sharing">
+     * recents URL sharing</a> which facilitate link sharing with other users and would not benefit
+     * from a session-transfer URI.
+     *
+     * @see android.app.Activity#requestOpenInBrowserEducation()
+     */
+    @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION)
+    public void setSessionTransferUri(@Nullable Uri uri) {
+        mSessionTransferUri = uri;
+    }
+
+    /**
+     * Return the content's session transfer web URI as per
+     * {@link #setSessionTransferUri(android.net.Uri)}, or null if there is none.
+     */
+    @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION)
+    @Nullable
+    public Uri getSessionTransferUri() {
+        return mSessionTransferUri;
+    }
+
+    /**
      * Return Bundle for extra vendor-specific data that can be modified and examined.
      */
     public Bundle getExtras() {
@@ -218,6 +230,9 @@
             mUri = Uri.CREATOR.createFromParcel(in);
         }
         if (in.readInt() != 0) {
+            mSessionTransferUri = Uri.CREATOR.createFromParcel(in);
+        }
+        if (in.readInt() != 0) {
             mStructuredData = in.readString();
         }
         mIsAppProvidedIntent = in.readInt() == 1;
@@ -245,6 +260,12 @@
         } else {
             dest.writeInt(0);
         }
+        if (mSessionTransferUri != null) {
+            dest.writeInt(1);
+            mSessionTransferUri.writeToParcel(dest, flags);
+        } else {
+            dest.writeInt(0);
+        }
         if (mStructuredData != null) {
             dest.writeInt(1);
             dest.writeString(mStructuredData);
diff --git a/core/java/android/app/backup/BackupManagerInternal.java b/core/java/android/app/backup/BackupManagerInternal.java
new file mode 100644
index 0000000..ceb5ae0
--- /dev/null
+++ b/core/java/android/app/backup/BackupManagerInternal.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.annotation.UserIdInt;
+import android.os.IBinder;
+
+/**
+ * Local system service interface for {@link com.android.server.backup.BackupManagerService}.
+ *
+ * @hide Only for use within the system server.
+ */
+public interface BackupManagerInternal {
+
+    /**
+     * Notifies the Backup Manager Service that an agent has become available. This
+     * method is only invoked by the Activity Manager.
+     */
+    void agentConnectedForUser(String packageName, @UserIdInt int userId, IBinder agent);
+
+    /**
+     * Notify the Backup Manager Service that an agent has unexpectedly gone away.
+     * This method is only invoked by the Activity Manager.
+     */
+    void agentDisconnectedForUser(String packageName, @UserIdInt int userId);
+}
diff --git a/core/java/android/app/backup/IBackupManager.aidl b/core/java/android/app/backup/IBackupManager.aidl
index 041c2a7..5d01d72 100644
--- a/core/java/android/app/backup/IBackupManager.aidl
+++ b/core/java/android/app/backup/IBackupManager.aidl
@@ -93,38 +93,6 @@
         IBackupObserver observer);
 
     /**
-     * Notifies the Backup Manager Service that an agent has become available.  This
-     * method is only invoked by the Activity Manager.
-     *
-     * If {@code userId} is different from the calling user id, then the caller must hold the
-     * android.permission.INTERACT_ACROSS_USERS_FULL permission.
-     *
-     * @param userId User id for which an agent has become available.
-     */
-    void agentConnectedForUser(int userId, String packageName, IBinder agent);
-
-    /**
-     * {@link android.app.backup.IBackupManager.agentConnected} for the calling user id.
-     */
-    void agentConnected(String packageName, IBinder agent);
-
-    /**
-     * Notify the Backup Manager Service that an agent has unexpectedly gone away.
-     * This method is only invoked by the Activity Manager.
-     *
-     * If {@code userId} is different from the calling user id, then the caller must hold the
-     * android.permission.INTERACT_ACROSS_USERS_FULL permission.
-     *
-     * @param userId User id for which an agent has unexpectedly gone away.
-     */
-    void agentDisconnectedForUser(int userId, String packageName);
-
-    /**
-     * {@link android.app.backup.IBackupManager.agentDisconnected} for the calling user id.
-     */
-    void agentDisconnected(String packageName);
-
-    /**
      * Notify the Backup Manager Service that an application being installed will
      * need a data-restore pass.  This method is only invoked by the Package Manager.
      *
diff --git a/core/java/android/app/jank/AppJankStats.java b/core/java/android/app/jank/AppJankStats.java
index 6ef6a44..a8ebc383 100644
--- a/core/java/android/app/jank/AppJankStats.java
+++ b/core/java/android/app/jank/AppJankStats.java
@@ -57,6 +57,8 @@
     // Histogram of relative frame times encoded in predetermined buckets.
     private RelativeFrameTimeHistogram mRelativeFrameTimeHistogram;
 
+    // Navigation component associated to this stat.
+    private String mNavigationComponent;
 
     /** Used to indicate no widget category has been set. */
     public static final String WIDGET_CATEGORY_UNSPECIFIED = "unspecified";
@@ -158,6 +160,8 @@
      *
      * @param appUid the Uid of the App that is collecting jank stats.
      * @param widgetId the widget id that frames will be associated to.
+     * @param navigationComponent the intended navigation target within the activity, this could be
+     *                            a navigation destination, screen and/or pane.
      * @param widgetCategory a category used to organize widgets in a structured way that indicates
      *                       they serve a similar purpose or perform related functions. Must be
      *                       prefixed with WIDGET_CATEGORY_ and have a suffix of one of the
@@ -172,14 +176,14 @@
      * @param jankyFrames the total number of janky frames that were counted for this stat.
      * @param relativeFrameTimeHistogram the histogram with predefined buckets. See
      * {@link #getRelativeFrameTimeHistogram()} for details.
-     *
      */
-    public AppJankStats(int appUid, @NonNull String widgetId,
+    public AppJankStats(int appUid, @NonNull String widgetId, @Nullable String navigationComponent,
             @Nullable @WidgetCategory String widgetCategory,
             @Nullable @WidgetState String widgetState, long totalFrames, long jankyFrames,
             @NonNull RelativeFrameTimeHistogram relativeFrameTimeHistogram) {
         mUid = appUid;
         mWidgetId = widgetId;
+        mNavigationComponent = navigationComponent;
         mWidgetCategory = widgetCategory != null ? widgetCategory : WIDGET_CATEGORY_UNSPECIFIED;
         mWidgetState = widgetState != null ? widgetState : WIDGET_STATE_UNSPECIFIED;
         mTotalFrames = totalFrames;
@@ -254,4 +258,13 @@
     public @NonNull RelativeFrameTimeHistogram getRelativeFrameTimeHistogram() {
         return mRelativeFrameTimeHistogram;
     }
+
+    /**
+     * Returns the navigation component if it exists that this stat applies to.
+     *
+     * @return the navigation component if it exists that this stat applies to.
+     */
+    public @Nullable String getNavigationComponent() {
+        return mNavigationComponent;
+    }
 }
diff --git a/core/java/android/app/time/TimeZoneCapabilities.java b/core/java/android/app/time/TimeZoneCapabilities.java
index 4dee159..929f660 100644
--- a/core/java/android/app/time/TimeZoneCapabilities.java
+++ b/core/java/android/app/time/TimeZoneCapabilities.java
@@ -55,7 +55,8 @@
      * The user the capabilities are for. This is used for object equality and debugging but there
      * is no accessor.
      */
-    @NonNull private final UserHandle mUserHandle;
+    @NonNull
+    private final UserHandle mUserHandle;
     private final @CapabilityState int mConfigureAutoDetectionEnabledCapability;
 
     /**
@@ -69,6 +70,7 @@
 
     private final @CapabilityState int mConfigureGeoDetectionEnabledCapability;
     private final @CapabilityState int mSetManualTimeZoneCapability;
+    private final @CapabilityState int mConfigureNotificationsEnabledCapability;
 
     private TimeZoneCapabilities(@NonNull Builder builder) {
         this.mUserHandle = Objects.requireNonNull(builder.mUserHandle);
@@ -78,6 +80,8 @@
         this.mConfigureGeoDetectionEnabledCapability =
                 builder.mConfigureGeoDetectionEnabledCapability;
         this.mSetManualTimeZoneCapability = builder.mSetManualTimeZoneCapability;
+        this.mConfigureNotificationsEnabledCapability =
+                builder.mConfigureNotificationsEnabledCapability;
     }
 
     @NonNull
@@ -88,6 +92,7 @@
                 .setUseLocationEnabled(in.readBoolean())
                 .setConfigureGeoDetectionEnabledCapability(in.readInt())
                 .setSetManualTimeZoneCapability(in.readInt())
+                .setConfigureNotificationsEnabledCapability(in.readInt())
                 .build();
     }
 
@@ -98,6 +103,7 @@
         dest.writeBoolean(mUseLocationEnabled);
         dest.writeInt(mConfigureGeoDetectionEnabledCapability);
         dest.writeInt(mSetManualTimeZoneCapability);
+        dest.writeInt(mConfigureNotificationsEnabledCapability);
     }
 
     /**
@@ -117,8 +123,8 @@
      *
      * Not part of the SDK API because it is intended for use by SettingsUI, which can display
      * text about needing it to be on for location-based time zone detection.
-     * @hide
      *
+     * @hide
      */
     public boolean isUseLocationEnabled() {
         return mUseLocationEnabled;
@@ -148,6 +154,18 @@
     }
 
     /**
+     * Returns the capability state associated with the user's ability to modify the time zone
+     * notification setting. The setting can be updated via {@link
+     * TimeManager#updateTimeZoneConfiguration(TimeZoneConfiguration)}.
+     *
+     * @hide
+     */
+    @CapabilityState
+    public int getConfigureNotificationsEnabledCapability() {
+        return mConfigureNotificationsEnabledCapability;
+    }
+
+    /**
      * Tries to create a new {@link TimeZoneConfiguration} from the {@code config} and the set of
      * {@code requestedChanges}, if {@code this} capabilities allow. The new configuration is
      * returned. If the capabilities do not permit one or more of the requested changes then {@code
@@ -174,6 +192,12 @@
             newConfigBuilder.setGeoDetectionEnabled(requestedChanges.isGeoDetectionEnabled());
         }
 
+        if (requestedChanges.hasIsNotificationsEnabled()) {
+            if (this.getConfigureNotificationsEnabledCapability() < CAPABILITY_NOT_APPLICABLE) {
+                return null;
+            }
+            newConfigBuilder.setNotificationsEnabled(requestedChanges.areNotificationsEnabled());
+        }
         return newConfigBuilder.build();
     }
 
@@ -197,13 +221,16 @@
                 && mUseLocationEnabled == that.mUseLocationEnabled
                 && mConfigureGeoDetectionEnabledCapability
                 == that.mConfigureGeoDetectionEnabledCapability
-                && mSetManualTimeZoneCapability == that.mSetManualTimeZoneCapability;
+                && mSetManualTimeZoneCapability == that.mSetManualTimeZoneCapability
+                && mConfigureNotificationsEnabledCapability
+                == that.mConfigureNotificationsEnabledCapability;
     }
 
     @Override
     public int hashCode() {
         return Objects.hash(mUserHandle, mConfigureAutoDetectionEnabledCapability,
-                mConfigureGeoDetectionEnabledCapability, mSetManualTimeZoneCapability);
+                mConfigureGeoDetectionEnabledCapability, mSetManualTimeZoneCapability,
+                mConfigureNotificationsEnabledCapability);
     }
 
     @Override
@@ -216,6 +243,8 @@
                 + ", mConfigureGeoDetectionEnabledCapability="
                 + mConfigureGeoDetectionEnabledCapability
                 + ", mSetManualTimeZoneCapability=" + mSetManualTimeZoneCapability
+                + ", mConfigureNotificationsEnabledCapability="
+                + mConfigureNotificationsEnabledCapability
                 + '}';
     }
 
@@ -226,11 +255,13 @@
      */
     public static class Builder {
 
-        @NonNull private UserHandle mUserHandle;
+        @NonNull
+        private UserHandle mUserHandle;
         private @CapabilityState int mConfigureAutoDetectionEnabledCapability;
         private Boolean mUseLocationEnabled;
         private @CapabilityState int mConfigureGeoDetectionEnabledCapability;
         private @CapabilityState int mSetManualTimeZoneCapability;
+        private @CapabilityState int mConfigureNotificationsEnabledCapability;
 
         public Builder(@NonNull UserHandle userHandle) {
             mUserHandle = Objects.requireNonNull(userHandle);
@@ -240,12 +271,14 @@
             Objects.requireNonNull(capabilitiesToCopy);
             mUserHandle = capabilitiesToCopy.mUserHandle;
             mConfigureAutoDetectionEnabledCapability =
-                capabilitiesToCopy.mConfigureAutoDetectionEnabledCapability;
+                    capabilitiesToCopy.mConfigureAutoDetectionEnabledCapability;
             mUseLocationEnabled = capabilitiesToCopy.mUseLocationEnabled;
             mConfigureGeoDetectionEnabledCapability =
-                capabilitiesToCopy.mConfigureGeoDetectionEnabledCapability;
+                    capabilitiesToCopy.mConfigureGeoDetectionEnabledCapability;
             mSetManualTimeZoneCapability =
-                capabilitiesToCopy.mSetManualTimeZoneCapability;
+                    capabilitiesToCopy.mSetManualTimeZoneCapability;
+            mConfigureNotificationsEnabledCapability =
+                    capabilitiesToCopy.mConfigureNotificationsEnabledCapability;
         }
 
         /** Sets the value for the "configure automatic time zone detection enabled" capability. */
@@ -274,6 +307,14 @@
             return this;
         }
 
+        /**
+         * Sets the value for the "configure time notifications enabled" capability.
+         */
+        public Builder setConfigureNotificationsEnabledCapability(@CapabilityState int value) {
+            this.mConfigureNotificationsEnabledCapability = value;
+            return this;
+        }
+
         /** Returns the {@link TimeZoneCapabilities}. */
         @NonNull
         public TimeZoneCapabilities build() {
@@ -283,7 +324,9 @@
             verifyCapabilitySet(mConfigureGeoDetectionEnabledCapability,
                     "configureGeoDetectionEnabledCapability");
             verifyCapabilitySet(mSetManualTimeZoneCapability,
-                    "mSetManualTimeZoneCapability");
+                    "setManualTimeZoneCapability");
+            verifyCapabilitySet(mConfigureNotificationsEnabledCapability,
+                    "configureNotificationsEnabledCapability");
             return new TimeZoneCapabilities(this);
         }
 
diff --git a/core/java/android/app/time/TimeZoneConfiguration.java b/core/java/android/app/time/TimeZoneConfiguration.java
index 7403c12..68c090f 100644
--- a/core/java/android/app/time/TimeZoneConfiguration.java
+++ b/core/java/android/app/time/TimeZoneConfiguration.java
@@ -62,7 +62,8 @@
      *
      * @hide
      */
-    @StringDef({ SETTING_AUTO_DETECTION_ENABLED, SETTING_GEO_DETECTION_ENABLED })
+    @StringDef({SETTING_AUTO_DETECTION_ENABLED, SETTING_GEO_DETECTION_ENABLED,
+            SETTING_NOTIFICATIONS_ENABLED})
     @Retention(RetentionPolicy.SOURCE)
     @interface Setting {}
 
@@ -74,6 +75,10 @@
     @Setting
     private static final String SETTING_GEO_DETECTION_ENABLED = "geoDetectionEnabled";
 
+    /** See {@link TimeZoneConfiguration#areNotificationsEnabled()} for details. */
+    @Setting
+    private static final String SETTING_NOTIFICATIONS_ENABLED = "notificationsEnabled";
+
     @NonNull private final Bundle mBundle;
 
     private TimeZoneConfiguration(Builder builder) {
@@ -98,7 +103,8 @@
      */
     public boolean isComplete() {
         return hasIsAutoDetectionEnabled()
-                && hasIsGeoDetectionEnabled();
+                && hasIsGeoDetectionEnabled()
+                && hasIsNotificationsEnabled();
     }
 
     /**
@@ -128,8 +134,7 @@
     /**
      * Returns the value of the {@link #SETTING_GEO_DETECTION_ENABLED} setting. This
      * controls whether the device can use geolocation to determine time zone. This value may only
-     * be used by Android under some circumstances. For example, it is not used when
-     * {@link #isGeoDetectionEnabled()} is {@code false}.
+     * be used by Android under some circumstances.
      *
      * <p>See {@link TimeZoneCapabilities#getConfigureGeoDetectionEnabledCapability()} for how to
      * tell if the setting is meaningful for the current user at this time.
@@ -150,6 +155,32 @@
         return mBundle.containsKey(SETTING_GEO_DETECTION_ENABLED);
     }
 
+    /**
+     * Returns the value of the {@link #SETTING_NOTIFICATIONS_ENABLED} setting. This controls
+     * whether the device can send time and time zone related notifications. This value may only
+     * be used by Android under some circumstances.
+     *
+     * <p>See {@link TimeZoneCapabilities#getConfigureNotificationsEnabledCapability()} ()} for how
+     * to tell if the setting is meaningful for the current user at this time.
+     *
+     * @throws IllegalStateException if the setting is not present
+     *
+     * @hide
+     */
+    public boolean areNotificationsEnabled() {
+        enforceSettingPresent(SETTING_NOTIFICATIONS_ENABLED);
+        return mBundle.getBoolean(SETTING_NOTIFICATIONS_ENABLED);
+    }
+
+    /**
+     * Returns {@code true} if the {@link #areNotificationsEnabled()} setting is present.
+     *
+     * @hide
+     */
+    public boolean hasIsNotificationsEnabled() {
+        return mBundle.containsKey(SETTING_NOTIFICATIONS_ENABLED);
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -244,6 +275,17 @@
             return this;
         }
 
+        /**
+         * Sets the state of the {@link #SETTING_NOTIFICATIONS_ENABLED} setting.         *
+         *
+         * @hide
+         */
+        @NonNull
+        public Builder setNotificationsEnabled(boolean enabled) {
+            this.mBundle.putBoolean(SETTING_NOTIFICATIONS_ENABLED, enabled);
+            return this;
+        }
+
         /** Returns the {@link TimeZoneConfiguration}. */
         @NonNull
         public TimeZoneConfiguration build() {
diff --git a/core/java/android/companion/CompanionDeviceService.java b/core/java/android/companion/CompanionDeviceService.java
index 316d129..971d402 100644
--- a/core/java/android/companion/CompanionDeviceService.java
+++ b/core/java/android/companion/CompanionDeviceService.java
@@ -62,10 +62,11 @@
  *
  * <p>
  * If the companion application has requested observing device presence (see
- * {@link CompanionDeviceManager#startObservingDevicePresence(String)}) the system will
- * <a href="https://developer.android.com/guide/components/bound-services"> bind the service</a>
- * when it detects the device nearby (for BLE devices) or when the device is connected
- * (for Bluetooth devices).
+ * {@link CompanionDeviceManager#stopObservingDevicePresence(ObservingDevicePresenceRequest)})
+ * the system will <a href="https://developer.android.com/guide/components/bound-services">
+ * bind the service</a> when one of the {@link DevicePresenceEvent#EVENT_BLE_APPEARED},
+ * {@link DevicePresenceEvent#EVENT_BT_CONNECTED},
+ * {@link DevicePresenceEvent#EVENT_SELF_MANAGED_APPEARED} event is notified.
  *
  * <p>
  * The system binding {@link CompanionDeviceService} elevates the priority of the process that
@@ -102,15 +103,25 @@
 
     /**
      * An intent action for a service to be bound whenever this app's companion device(s)
-     * are nearby.
+     * are nearby or self-managed device(s) report app appeared.
      *
-     * <p>The app will be kept alive for as long as the device is nearby or companion app reports
-     * appeared.
-     * If the app is not running at the time device gets connected, the app will be woken up.</p>
+     * <p>The app will be kept bound by the system when one of the
+     * {@link DevicePresenceEvent#EVENT_BLE_APPEARED},
+     * {@link DevicePresenceEvent#EVENT_BT_CONNECTED},
+     * {@link DevicePresenceEvent#EVENT_SELF_MANAGED_APPEARED} event is notified.
      *
-     * <p>Shortly after the device goes out of range or the companion app reports disappeared,
-     * the service will be unbound, and the app will be eligible for cleanup, unless any other
-     * user-visible components are running.</p>
+     * If the app is not running when one of the
+     * {@link DevicePresenceEvent#EVENT_BLE_APPEARED},
+     * {@link DevicePresenceEvent#EVENT_BT_CONNECTED},
+     * {@link DevicePresenceEvent#EVENT_SELF_MANAGED_APPEARED} event is notified, the app will be
+     * kept bound by the system.</p>
+     *
+     * <p>Shortly, the service will be unbound if both
+     * {@link DevicePresenceEvent#EVENT_BLE_DISAPPEARED} and
+     * {@link DevicePresenceEvent#EVENT_BT_DISCONNECTED} are notified, or
+     * {@link DevicePresenceEvent#EVENT_SELF_MANAGED_DISAPPEARED} event is notified.
+     * The app will be eligible for cleanup, unless any other user-visible components are
+     * running.</p>
      *
      * If running in background is not essential for the devices that this app can manage,
      * app should avoid declaring this service.</p>
diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java
index 42c7441..311e24b 100644
--- a/core/java/android/companion/virtual/VirtualDeviceInternal.java
+++ b/core/java/android/companion/virtual/VirtualDeviceInternal.java
@@ -83,7 +83,6 @@
 public class VirtualDeviceInternal {
 
     private final Context mContext;
-    private final IVirtualDeviceManager mService;
     private final IVirtualDevice mVirtualDevice;
     private final Object mActivityListenersLock = new Object();
     @GuardedBy("mActivityListenersLock")
@@ -206,7 +205,6 @@
             Context context,
             int associationId,
             VirtualDeviceParams params) throws RemoteException {
-        mService = service;
         mContext = context.getApplicationContext();
         mVirtualDevice = service.createVirtualDevice(
                 new Binder(),
@@ -217,11 +215,7 @@
                 mSoundEffectListener);
     }
 
-    VirtualDeviceInternal(
-            IVirtualDeviceManager service,
-            Context context,
-            IVirtualDevice virtualDevice) {
-        mService = service;
+    VirtualDeviceInternal(Context context, IVirtualDevice virtualDevice) {
         mContext = context.getApplicationContext();
         mVirtualDevice = virtualDevice;
         try {
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index ed2fd99..73ea9f0 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -577,9 +577,8 @@
         }
 
         /** @hide */
-        public VirtualDevice(IVirtualDeviceManager service, Context context,
-                IVirtualDevice virtualDevice) {
-            mVirtualDeviceInternal = new VirtualDeviceInternal(service, context, virtualDevice);
+        public VirtualDevice(Context context, IVirtualDevice virtualDevice) {
+            mVirtualDeviceInternal = new VirtualDeviceInternal(context, virtualDevice);
         }
 
         /**
diff --git a/core/java/android/companion/virtual/flags.aconfig b/core/java/android/companion/virtual/flags/deprecated_flags_do_not_edit.aconfig
similarity index 80%
rename from core/java/android/companion/virtual/flags.aconfig
rename to core/java/android/companion/virtual/flags/deprecated_flags_do_not_edit.aconfig
index 46da4a3..eae5062 100644
--- a/core/java/android/companion/virtual/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/deprecated_flags_do_not_edit.aconfig
@@ -1,24 +1,18 @@
-# Do not add new flags to this file.
+# Do not modify this file.
 #
-# Due to "virtual" keyword in the package name flags
-# added to this file cannot be accessed from C++
-# code.
+# Due to "virtual" keyword in the package name flags added to this file cannot
+# be accessed from C++ code.
 #
 # Use frameworks/base/core/java/android/companion/virtual/flags/flags.aconfig
-# instead.
+# instead for new flags.
+#
+# All of the remaining flags here have been used for API flagging and are
+# therefore exported and should not be deleted.
 
 package: "android.companion.virtual.flags"
 container: "system"
 
 flag {
-  name: "enable_native_vdm"
-  namespace: "virtual_devices"
-  description: "Enable native VDM service"
-  bug: "303535376"
-  is_fixed_read_only: true
-}
-
-flag {
   name: "dynamic_policy"
   is_exported: true
   namespace: "virtual_devices"
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index de01280..84af840 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -1,17 +1,11 @@
+# VirtualDeviceManager flags
 #
-# Copyright (C) 2023 The Android Open Source Project
+# This file contains flags guarding features that are in development.
 #
-# 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.
+# Once a flag is launched or abandoned and there are no more references to it in
+# the codebase, it should be either:
+#  - deleted, or
+#  - moved to launched_flags.aconfig if it was launched and used for API flagging.
 
 package: "android.companion.virtualdevice.flags"
 container: "system"
diff --git a/core/java/android/companion/virtual/flags/launched_flags.aconfig b/core/java/android/companion/virtual/flags/launched_flags.aconfig
new file mode 100644
index 0000000..ee89631
--- /dev/null
+++ b/core/java/android/companion/virtual/flags/launched_flags.aconfig
@@ -0,0 +1,6 @@
+# This file contains the launched VirtualDeviceManager flags from the
+# "android.companion.virtualdevice.flags" package that cannot be deleted because
+# they have been used for API flagging.
+
+package: "android.companion.virtualdevice.flags"
+container: "system"
diff --git a/core/java/android/content/ClipData.java b/core/java/android/content/ClipData.java
index e271cf4..4e292d0 100644
--- a/core/java/android/content/ClipData.java
+++ b/core/java/android/content/ClipData.java
@@ -221,6 +221,12 @@
         // if the data is obtained from {@link #copyForTransferWithActivityInfo}
         private ActivityInfo mActivityInfo;
 
+        private boolean mTokenVerificationEnabled;
+
+        void setTokenVerificationEnabled() {
+            mTokenVerificationEnabled = true;
+        }
+
         /**
          * A builder for a ClipData Item.
          */
@@ -398,7 +404,9 @@
          * Retrieve the raw Intent contained in this Item.
          */
         public Intent getIntent() {
-            Intent.maybeMarkAsMissingCreatorToken(mIntent);
+            if (mTokenVerificationEnabled) {
+                Intent.maybeMarkAsMissingCreatorToken(mIntent);
+            }
             return mIntent;
         }
 
@@ -1353,6 +1361,12 @@
         }
     }
 
+    void setTokenVerificationEnabled() {
+        for (int i = 0; i < mItems.size(); ++i) {
+            mItems.get(i).setTokenVerificationEnabled();
+        }
+    }
+
     @Override
     public int describeContents() {
         return 0;
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 3d75423..01e24d81 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -12394,35 +12394,57 @@
      * @hide
      */
     public void collectExtraIntentKeys() {
+        collectExtraIntentKeys(false);
+    }
+
+    /**
+     * Collects keys in the extra bundle whose value are intents.
+     * With these keys collected on the client side, the system server would only unparcel values
+     * of these keys and create IntentCreatorToken for them.
+     * This method could also be called from the system server side as a catch all safty net in case
+     * these keys are not collected on the client side. In that case, call it with forceUnparcel set
+     * to true since everything is parceled on the system server side.
+     *
+     * @param forceUnparcel if it is true, unparcel everything to determine if an object is an
+     *                      intent. Otherwise, do not unparcel anything.
+     * @hide
+     */
+    public void collectExtraIntentKeys(boolean forceUnparcel) {
         if (preventIntentRedirect()) {
-            collectNestedIntentKeysRecur(new ArraySet<>());
+            collectNestedIntentKeysRecur(new ArraySet<>(), forceUnparcel);
         }
     }
 
-    private void collectNestedIntentKeysRecur(Set<Intent> visited) {
+    private void collectNestedIntentKeysRecur(Set<Intent> visited, boolean forceUnparcel) {
         addExtendedFlags(EXTENDED_FLAG_NESTED_INTENT_KEYS_COLLECTED);
-        if (mExtras != null && !mExtras.isEmpty()) {
+        if (mExtras != null && (forceUnparcel || !mExtras.isParcelled()) && !mExtras.isEmpty()) {
             for (String key : mExtras.keySet()) {
                 Object value;
                 try {
-                    value = mExtras.get(key);
+                    // Do not unparcel any Parcelable objects. It may cause issues for app who would
+                    // change class loader before it reads a parceled value. b/382633789.
+                    // It is okay to not collect a parceled intent since it would have been
+                    // coming from another process and collected by its containing intent already
+                    // in that process.
+                    if (forceUnparcel || !mExtras.isValueParceled(key)) {
+                        value = mExtras.get(key);
+                    } else {
+                        value = null;
+                    }
                 } catch (BadParcelableException e) {
-                    // This could happen when the key points to a LazyValue whose class cannot be
-                    // found by the classLoader - A nested object more than 1 level deeper who is
-                    // of type of a custom class could trigger this situation. In such case, we
-                    // ignore it since it is not an intent. However, it could be a custom type that
-                    // extends from Intent. If such an object is retrieved later in another
-                    // component, then trying to launch such a custom class object will fail unless
-                    // removeLaunchSecurityProtection() is called before it is launched.
+                    // This may still happen if the keys are collected on the system server side, in
+                    // which case, we will try to unparcel everything. If this happens, simply
+                    // ignore it since it is not an intent anyway.
                     value = null;
                 }
                 if (value instanceof Intent intent) {
                     handleNestedIntent(intent, visited, new NestedIntentKey(
-                            NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL, key, 0));
+                                    NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL, key, 0),
+                            forceUnparcel);
                 } else if (value instanceof Parcelable[] parcelables) {
-                    handleParcelableArray(parcelables, key, visited);
+                    handleParcelableArray(parcelables, key, visited, forceUnparcel);
                 } else if (value instanceof ArrayList<?> parcelables) {
-                    handleParcelableList(parcelables, key, visited);
+                    handleParcelableList(parcelables, key, visited, forceUnparcel);
                 }
             }
         }
@@ -12432,13 +12454,15 @@
                 Intent intent = mClipData.getItemAt(i).mIntent;
                 if (intent != null && !visited.contains(intent)) {
                     handleNestedIntent(intent, visited, new NestedIntentKey(
-                            NestedIntentKey.NESTED_INTENT_KEY_TYPE_CLIP_DATA, null, i));
+                                    NestedIntentKey.NESTED_INTENT_KEY_TYPE_CLIP_DATA, null, i),
+                            forceUnparcel);
                 }
             }
         }
     }
 
-    private void handleNestedIntent(Intent intent, Set<Intent> visited, NestedIntentKey key) {
+    private void handleNestedIntent(Intent intent, Set<Intent> visited, NestedIntentKey key,
+            boolean forceUnparcel) {
         if (mCreatorTokenInfo == null) {
             mCreatorTokenInfo = new CreatorTokenInfo();
         }
@@ -12448,24 +12472,28 @@
         mCreatorTokenInfo.mNestedIntentKeys.add(key);
         if (!visited.contains(intent)) {
             visited.add(intent);
-            intent.collectNestedIntentKeysRecur(visited);
+            intent.collectNestedIntentKeysRecur(visited, forceUnparcel);
         }
     }
 
-    private void handleParcelableArray(Parcelable[] parcelables, String key, Set<Intent> visited) {
+    private void handleParcelableArray(Parcelable[] parcelables, String key, Set<Intent> visited,
+            boolean forceUnparcel) {
         for (int i = 0; i < parcelables.length; i++) {
             if (parcelables[i] instanceof Intent intent && !visited.contains(intent)) {
                 handleNestedIntent(intent, visited, new NestedIntentKey(
-                        NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL_ARRAY, key, i));
+                                NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL_ARRAY, key, i),
+                        forceUnparcel);
             }
         }
     }
 
-    private void handleParcelableList(ArrayList<?> parcelables, String key, Set<Intent> visited) {
+    private void handleParcelableList(ArrayList<?> parcelables, String key, Set<Intent> visited,
+            boolean forceUnparcel) {
         for (int i = 0; i < parcelables.size(); i++) {
             if (parcelables.get(i) instanceof Intent intent && !visited.contains(intent)) {
                 handleNestedIntent(intent, visited, new NestedIntentKey(
-                        NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL_LIST, key, i));
+                                NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL_LIST, key, i),
+                        forceUnparcel);
             }
         }
     }
@@ -12478,6 +12506,9 @@
         if (intent.mExtras != null) {
             intent.mExtras.enableTokenVerification();
         }
+        if (intent.mClipData != null) {
+            intent.mClipData.setTokenVerificationEnabled();
+        }
     };
 
     /** @hide */
@@ -12489,6 +12520,9 @@
             // mark trusted creator token present.
             mExtras.enableTokenVerification();
         }
+        if (mClipData != null) {
+            mClipData.setTokenVerificationEnabled();
+        }
     }
 
     /** @hide */
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index c16582f..8c7e93a 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -4649,6 +4649,7 @@
      * the Android Keystore backed by an isolated execution environment. The version indicates
      * which features are implemented in the isolated execution environment:
      * <ul>
+     * <li>400: Inclusion of module information (via tag MODULE_HASH) in the attestation record.
      * <li>300: Ability to include a second IMEI in the ID attestation record, see
      * {@link android.app.admin.DevicePolicyManager#ID_TYPE_IMEI}.
      * <li>200: Hardware support for Curve 25519 (including both Ed25519 signature generation and
@@ -4682,6 +4683,7 @@
      * StrongBox</a>. If this feature has a version, the version number indicates which features are
      * implemented in StrongBox:
      * <ul>
+     * <li>400: Inclusion of module information (via tag MODULE_HASH) in the attestation record.
      * <li>300: Ability to include a second IMEI in the ID attestation record, see
      * {@link android.app.admin.DevicePolicyManager#ID_TYPE_IMEI}.
      * <li>200: No new features for StrongBox (the Android Keystore environment backed by an
diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java
index 0333942..9d11710 100644
--- a/core/java/android/content/pm/RegisteredServicesCache.java
+++ b/core/java/android/content/pm/RegisteredServicesCache.java
@@ -17,6 +17,7 @@
 package android.content.pm;
 
 import android.Manifest;
+import android.annotation.NonNull;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -30,6 +31,7 @@
 import android.os.Handler;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.util.ArrayMap;
 import android.util.AtomicFile;
 import android.util.AttributeSet;
 import android.util.IntArray;
@@ -45,11 +47,11 @@
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
 
-import libcore.io.IoUtils;
-
 import com.google.android.collect.Lists;
 import com.google.android.collect.Maps;
 
+import libcore.io.IoUtils;
+
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
@@ -94,6 +96,9 @@
     @GuardedBy("mServicesLock")
     private final SparseArray<UserServices<V>> mUserServices = new SparseArray<UserServices<V>>(2);
 
+    @GuardedBy("mServicesLock")
+    private final ArrayMap<String, ServiceInfo<V>> mServiceInfoCaches = new ArrayMap<>();
+
     private static class UserServices<V> {
         @GuardedBy("mServicesLock")
         final Map<V, Integer> persistentServices = Maps.newHashMap();
@@ -323,13 +328,16 @@
         public final ComponentName componentName;
         @UnsupportedAppUsage
         public final int uid;
+        public final long lastUpdateTime;
 
         /** @hide */
-        public ServiceInfo(V type, ComponentInfo componentInfo, ComponentName componentName) {
+        public ServiceInfo(V type, ComponentInfo componentInfo, ComponentName componentName,
+                long lastUpdateTime) {
             this.type = type;
             this.componentInfo = componentInfo;
             this.componentName = componentName;
             this.uid = (componentInfo != null) ? componentInfo.applicationInfo.uid : -1;
+            this.lastUpdateTime = lastUpdateTime;
         }
 
         @Override
@@ -490,7 +498,7 @@
         final List<ResolveInfo> resolveInfos = queryIntentServices(userId);
         for (ResolveInfo resolveInfo : resolveInfos) {
             try {
-                ServiceInfo<V> info = parseServiceInfo(resolveInfo);
+                ServiceInfo<V> info = parseServiceInfo(resolveInfo, userId);
                 if (info == null) {
                     Log.w(TAG, "Unable to load service info " + resolveInfo.toString());
                     continue;
@@ -638,13 +646,31 @@
     }
 
     @VisibleForTesting
-    protected ServiceInfo<V> parseServiceInfo(ResolveInfo service)
+    protected ServiceInfo<V> parseServiceInfo(ResolveInfo service, int userId)
             throws XmlPullParserException, IOException {
         android.content.pm.ServiceInfo si = service.serviceInfo;
         ComponentName componentName = new ComponentName(si.packageName, si.name);
 
         PackageManager pm = mContext.getPackageManager();
 
+        // Check if the service has been in the service cache.
+        long lastUpdateTime = -1;
+        if (Flags.optimizeParsingInRegisteredServicesCache()) {
+            try {
+                PackageInfo packageInfo = pm.getPackageInfoAsUser(si.packageName,
+                        PackageManager.MATCH_DIRECT_BOOT_AWARE
+                                | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId);
+                lastUpdateTime = packageInfo.lastUpdateTime;
+
+                ServiceInfo<V> serviceInfo = getServiceInfoFromServiceCache(si, lastUpdateTime);
+                if (serviceInfo != null) {
+                    return serviceInfo;
+                }
+            } catch (NameNotFoundException | SecurityException e) {
+                Slog.d(TAG, "Fail to get the PackageInfo in parseServiceInfo: " + e);
+            }
+        }
+
         XmlResourceParser parser = null;
         try {
             parser = si.loadXmlMetaData(pm, mMetaDataName);
@@ -670,8 +696,13 @@
             if (v == null) {
                 return null;
             }
-            final android.content.pm.ServiceInfo serviceInfo = service.serviceInfo;
-            return new ServiceInfo<V>(v, serviceInfo, componentName);
+            ServiceInfo<V> serviceInfo = new ServiceInfo<V>(v, si, componentName, lastUpdateTime);
+            if (Flags.optimizeParsingInRegisteredServicesCache()) {
+                synchronized (mServicesLock) {
+                    mServiceInfoCaches.put(getServiceCacheKey(si), serviceInfo);
+                }
+            }
+            return serviceInfo;
         } catch (NameNotFoundException e) {
             throw new XmlPullParserException(
                     "Unable to load resources for pacakge " + si.packageName);
@@ -841,4 +872,28 @@
         mContext.unregisterReceiver(mExternalReceiver);
         mContext.unregisterReceiver(mUserRemovedReceiver);
     }
+
+    private static String getServiceCacheKey(@NonNull android.content.pm.ServiceInfo serviceInfo) {
+        StringBuilder sb = new StringBuilder(serviceInfo.packageName);
+        sb.append('-');
+        sb.append(serviceInfo.name);
+        return sb.toString();
+    }
+
+    private ServiceInfo<V> getServiceInfoFromServiceCache(
+            @NonNull android.content.pm.ServiceInfo serviceInfo, long lastUpdateTime) {
+        String serviceCacheKey = getServiceCacheKey(serviceInfo);
+        synchronized (mServicesLock) {
+            ServiceInfo<V> serviceCache = mServiceInfoCaches.get(serviceCacheKey);
+            if (serviceCache == null) {
+                return null;
+            }
+            if (serviceCache.lastUpdateTime == lastUpdateTime) {
+                return serviceCache;
+            }
+            // The service is not latest, remove it from the cache.
+            mServiceInfoCaches.remove(serviceCacheKey);
+            return null;
+        }
+    }
 }
diff --git a/core/java/android/content/pm/SigningInfo.aidl b/core/java/android/content/pm/SigningInfo.aidl
new file mode 100644
index 0000000..bc986d1
--- /dev/null
+++ b/core/java/android/content/pm/SigningInfo.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+package android.content.pm;
+
+parcelable SigningInfo;
\ No newline at end of file
diff --git a/core/java/android/content/pm/SystemFeaturesCache.aidl b/core/java/android/content/pm/SystemFeaturesCache.aidl
new file mode 100644
index 0000000..18c1830
--- /dev/null
+++ b/core/java/android/content/pm/SystemFeaturesCache.aidl
@@ -0,0 +1,19 @@
+/*
+** Copyright 2025, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.content.pm;
+
+parcelable SystemFeaturesCache;
diff --git a/core/java/android/content/pm/SystemFeaturesCache.java b/core/java/android/content/pm/SystemFeaturesCache.java
new file mode 100644
index 0000000..c41a7ab
--- /dev/null
+++ b/core/java/android/content/pm/SystemFeaturesCache.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+/**
+ * A simple cache for SDK-defined system feature versions.
+ *
+ * The dense representation minimizes any per-process memory impact (<1KB). The tradeoff is that
+ * custom, non-SDK defined features are not captured by the cache, for which we can rely on the
+ * usual IPC cache for related queries.
+ *
+ * @hide
+ */
+public final class SystemFeaturesCache implements Parcelable {
+
+    // Sentinel value used for SDK-declared features that are unavailable on the current device.
+    private static final int UNAVAILABLE_FEATURE_VERSION = Integer.MIN_VALUE;
+
+    // An array of versions for SDK-defined features, from [0, PackageManager.SDK_FEATURE_COUNT).
+    @NonNull
+    private final int[] mSdkFeatureVersions;
+
+    /**
+     * Populates the cache from the set of all available {@link FeatureInfo} definitions.
+     *
+     * System features declared in {@link PackageManager} will be entered into the cache based on
+     * availability in this feature set. Other custom system features will be ignored.
+     */
+    public SystemFeaturesCache(@NonNull ArrayMap<String, FeatureInfo> availableFeatures) {
+        this(availableFeatures.values());
+    }
+
+    @VisibleForTesting
+    public SystemFeaturesCache(@NonNull Collection<FeatureInfo> availableFeatures) {
+        // First set all SDK-defined features as unavailable.
+        mSdkFeatureVersions = new int[PackageManager.SDK_FEATURE_COUNT];
+        Arrays.fill(mSdkFeatureVersions, UNAVAILABLE_FEATURE_VERSION);
+
+        // Then populate SDK-defined feature versions from the full set of runtime features.
+        for (FeatureInfo fi : availableFeatures) {
+            int sdkFeatureIndex = PackageManager.maybeGetSdkFeatureIndex(fi.name);
+            if (sdkFeatureIndex >= 0) {
+                mSdkFeatureVersions[sdkFeatureIndex] = fi.version;
+            }
+        }
+    }
+
+    /** Only used by @{code CREATOR.createFromParcel(...)} */
+    private SystemFeaturesCache(@NonNull Parcel parcel) {
+        final int[] featureVersions = parcel.createIntArray();
+        if (featureVersions == null) {
+            throw new IllegalArgumentException(
+                    "Parceled SDK feature versions should never be null");
+        }
+        if (featureVersions.length != PackageManager.SDK_FEATURE_COUNT) {
+            throw new IllegalArgumentException(
+                    String.format(
+                            "Unexpected cached SDK feature count: %d (expected %d)",
+                            featureVersions.length, PackageManager.SDK_FEATURE_COUNT));
+        }
+        mSdkFeatureVersions = featureVersions;
+    }
+
+    /**
+     * @return Whether the given feature is available (for SDK-defined features), otherwise null.
+     */
+    public Boolean maybeHasFeature(@NonNull String featureName, int version) {
+        // Features defined outside of the SDK aren't cached.
+        int sdkFeatureIndex = PackageManager.maybeGetSdkFeatureIndex(featureName);
+        if (sdkFeatureIndex < 0) {
+            return null;
+        }
+
+        // As feature versions can in theory collide with our sentinel value, in the (extremely)
+        // unlikely event that the queried version matches the sentinel value, we can't distinguish
+        // between an unavailable feature and a feature with the defined sentinel value.
+        if (version == UNAVAILABLE_FEATURE_VERSION
+                && mSdkFeatureVersions[sdkFeatureIndex] == UNAVAILABLE_FEATURE_VERSION) {
+            return null;
+        }
+
+        return mSdkFeatureVersions[sdkFeatureIndex] >= version;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel parcel, int flags) {
+        parcel.writeIntArray(mSdkFeatureVersions);
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<SystemFeaturesCache> CREATOR =
+            new Parcelable.Creator<SystemFeaturesCache>() {
+
+                @Override
+                public SystemFeaturesCache createFromParcel(Parcel parcel) {
+                    return new SystemFeaturesCache(parcel);
+                }
+
+                @Override
+                public SystemFeaturesCache[] newArray(int size) {
+                    return new SystemFeaturesCache[size];
+                }
+            };
+}
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 7bba06c..e4b8c90 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -383,3 +383,11 @@
     bug: "334024639"
     description: "Feature flag to check whether a given UID can access a content provider"
 }
+
+flag {
+    name: "optimize_parsing_in_registered_services_cache"
+    namespace: "package_manager_service"
+    description: "Feature flag to optimize RegisteredServicesCache ServiceInfo parsing by using caches."
+    bug: "319137634"
+    is_fixed_read_only: true
+}
diff --git a/core/java/android/content/res/ApkAssets.java b/core/java/android/content/res/ApkAssets.java
index 908999b..0754578 100644
--- a/core/java/android/content/res/ApkAssets.java
+++ b/core/java/android/content/res/ApkAssets.java
@@ -353,7 +353,7 @@
     /** @hide */
     public @NonNull String getDebugName() {
         synchronized (this) {
-            return nativeGetDebugName(mNativePtr);
+            return mNativePtr == 0 ? "<destroyed>" : nativeGetDebugName(mNativePtr);
         }
     }
 
diff --git a/core/java/android/hardware/SystemSensorManager.java b/core/java/android/hardware/SystemSensorManager.java
index 2d3d252..868429c 100644
--- a/core/java/android/hardware/SystemSensorManager.java
+++ b/core/java/android/hardware/SystemSensorManager.java
@@ -16,8 +16,6 @@
 
 package android.hardware;
 
-import static android.companion.virtual.VirtualDeviceManager.ACTION_VIRTUAL_DEVICE_REMOVED;
-import static android.companion.virtual.VirtualDeviceManager.EXTRA_VIRTUAL_DEVICE_ID;
 import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS;
 import static android.content.Context.DEVICE_ID_DEFAULT;
@@ -164,11 +162,7 @@
         // initialize the sensor list
         for (int index = 0;; ++index) {
             Sensor sensor = new Sensor();
-            if (android.companion.virtual.flags.Flags.enableNativeVdm()) {
-                if (!nativeGetDefaultDeviceSensorAtIndex(mNativeInstance, sensor, index)) break;
-            } else {
-                if (!nativeGetSensorAtIndex(mNativeInstance, sensor, index)) break;
-            }
+            if (!nativeGetDefaultDeviceSensorAtIndex(mNativeInstance, sensor, index)) break;
             mFullSensorsList.add(sensor);
             mHandleToSensor.put(sensor.getHandle(), sensor);
         }
@@ -555,11 +549,7 @@
     }
 
     private List<Sensor> createRuntimeSensorListLocked(int deviceId) {
-        if (android.companion.virtual.flags.Flags.vdmPublicApis()) {
-            setupVirtualDeviceListener();
-        } else {
-            setupRuntimeSensorBroadcastReceiver();
-        }
+        setupVirtualDeviceListener();
         List<Sensor> list = new ArrayList<>();
         nativeGetRuntimeSensors(mNativeInstance, deviceId, list);
         mFullRuntimeSensorListByDevice.put(deviceId, list);
@@ -570,35 +560,6 @@
         return list;
     }
 
-    private void setupRuntimeSensorBroadcastReceiver() {
-        if (mRuntimeSensorBroadcastReceiver == null) {
-            mRuntimeSensorBroadcastReceiver = new BroadcastReceiver() {
-                @Override
-                public void onReceive(Context context, Intent intent) {
-                    if (intent.getAction().equals(ACTION_VIRTUAL_DEVICE_REMOVED)) {
-                        synchronized (mFullRuntimeSensorListByDevice) {
-                            final int deviceId = intent.getIntExtra(
-                                    EXTRA_VIRTUAL_DEVICE_ID, DEVICE_ID_DEFAULT);
-                            List<Sensor> removedSensors =
-                                    mFullRuntimeSensorListByDevice.removeReturnOld(deviceId);
-                            if (removedSensors != null) {
-                                for (Sensor s : removedSensors) {
-                                    cleanupSensorConnection(s);
-                                }
-                            }
-                            mRuntimeSensorListByDeviceByType.remove(deviceId);
-                        }
-                    }
-                }
-            };
-
-            IntentFilter filter = new IntentFilter("virtual_device_removed");
-            filter.addAction(ACTION_VIRTUAL_DEVICE_REMOVED);
-            mContext.registerReceiver(mRuntimeSensorBroadcastReceiver, filter,
-                    Context.RECEIVER_NOT_EXPORTED);
-        }
-    }
-
     private void setupVirtualDeviceListener() {
         if (mVirtualDeviceListener != null) {
             return;
diff --git a/core/java/android/hardware/contexthub/HubEndpoint.java b/core/java/android/hardware/contexthub/HubEndpoint.java
index 71702d9..25cdc50 100644
--- a/core/java/android/hardware/contexthub/HubEndpoint.java
+++ b/core/java/android/hardware/contexthub/HubEndpoint.java
@@ -137,6 +137,8 @@
                                                 serviceDescriptor,
                                                 mLifecycleCallback.onSessionOpenRequest(
                                                         initiator, serviceDescriptor)));
+                    } else {
+                        invokeCallbackFinished();
                     }
                 }
 
@@ -163,6 +165,8 @@
                                         + result.getReason());
                         rejectSession(sessionId);
                     }
+
+                    invokeCallbackFinished();
                 }
 
                 private void acceptSession(
@@ -249,7 +253,12 @@
                     activeSession.setOpened();
                     if (mLifecycleCallback != null) {
                         mLifecycleCallbackExecutor.execute(
-                                () -> mLifecycleCallback.onSessionOpened(activeSession));
+                                () -> {
+                                    mLifecycleCallback.onSessionOpened(activeSession);
+                                    invokeCallbackFinished();
+                                });
+                    } else {
+                        invokeCallbackFinished();
                     }
                 }
 
@@ -278,7 +287,10 @@
                                     synchronized (mLock) {
                                         mActiveSessions.remove(sessionId);
                                     }
+                                    invokeCallbackFinished();
                                 });
+                    } else {
+                        invokeCallbackFinished();
                     }
                 }
 
@@ -323,8 +335,17 @@
                                         e.rethrowFromSystemServer();
                                     }
                                 }
+                                invokeCallbackFinished();
                             });
                 }
+
+                private void invokeCallbackFinished() {
+                    try {
+                        mServiceToken.onCallbackFinished();
+                    } catch (RemoteException e) {
+                        e.rethrowFromSystemServer();
+                    }
+                }
             };
 
     /** Binder returned from system service, non-null while registered. */
diff --git a/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl b/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl
index 44f80c8..eb1255c 100644
--- a/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl
+++ b/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl
@@ -94,4 +94,10 @@
      */
     @EnforcePermission("ACCESS_CONTEXT_HUB")
     void sendMessageDeliveryStatus(int sessionId, int messageSeqNumber, byte errorCode);
+
+    /**
+     * Invoked when a callback from IContextHubEndpointCallback finishes.
+     */
+    @EnforcePermission("ACCESS_CONTEXT_HUB")
+    void onCallbackFinished();
 }
diff --git a/core/java/android/hardware/display/DisplayTopology.java b/core/java/android/hardware/display/DisplayTopology.java
index 0e2c05f..1d2f133 100644
--- a/core/java/android/hardware/display/DisplayTopology.java
+++ b/core/java/android/hardware/display/DisplayTopology.java
@@ -679,8 +679,7 @@
     }
 
     /**
-     * Tests whether two brightness float values are within a small enough tolerance
-     * of each other.
+     * Tests whether two float values are within a small enough tolerance of each other.
      * @param a first float to compare
      * @param b second float to compare
      * @return whether the two values are within a small enough tolerance value
diff --git a/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java b/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java
index d84d292..8fbe05c 100644
--- a/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java
+++ b/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java
@@ -121,15 +121,20 @@
 
     /**
      * Returns if sensor type is ultrasonic Udfps
-     * @return true if sensor is ultrasonic Udfps, false otherwise
      */
     public boolean isUltrasonicUdfps() {
         return sensorType == TYPE_UDFPS_ULTRASONIC;
     }
 
     /**
+     * Returns if sensor type is optical Udfps
+     */
+    public boolean isOpticalUdfps() {
+        return sensorType == TYPE_UDFPS_OPTICAL;
+    }
+
+    /**
      * Returns if sensor type is side-FPS
-     * @return true if sensor is side-fps, false otherwise
      */
     public boolean isAnySidefpsType() {
         switch (sensorType) {
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index ed510e4..2bb28a1 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -266,6 +266,11 @@
     @PermissionManuallyEnforced
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
             + "android.Manifest.permission.MANAGE_KEY_GESTURES)")
+    AidlInputGestureData getInputGesture(int userId, in AidlInputGestureData.Trigger trigger);
+
+    @PermissionManuallyEnforced
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+            + "android.Manifest.permission.MANAGE_KEY_GESTURES)")
     int addCustomInputGesture(int userId, in AidlInputGestureData data);
 
     @PermissionManuallyEnforced
diff --git a/core/java/android/hardware/input/InputGestureData.java b/core/java/android/hardware/input/InputGestureData.java
index f41550f..75c652c 100644
--- a/core/java/android/hardware/input/InputGestureData.java
+++ b/core/java/android/hardware/input/InputGestureData.java
@@ -48,27 +48,7 @@
 
     /** Returns the trigger information for this input gesture */
     public Trigger getTrigger() {
-        switch (mInputGestureData.trigger.getTag()) {
-            case AidlInputGestureData.Trigger.Tag.key: {
-                AidlInputGestureData.KeyTrigger trigger = mInputGestureData.trigger.getKey();
-                if (trigger == null) {
-                    throw new RuntimeException("InputGestureData is corrupted, null key trigger!");
-                }
-                return createKeyTrigger(trigger.keycode, trigger.modifierState);
-            }
-            case AidlInputGestureData.Trigger.Tag.touchpadGesture: {
-                AidlInputGestureData.TouchpadGestureTrigger trigger =
-                        mInputGestureData.trigger.getTouchpadGesture();
-                if (trigger == null) {
-                    throw new RuntimeException(
-                            "InputGestureData is corrupted, null touchpad trigger!");
-                }
-                return createTouchpadTrigger(trigger.gestureType);
-            }
-            default:
-                throw new RuntimeException("InputGestureData is corrupted, invalid trigger type!");
-
-        }
+        return createTriggerFromAidlTrigger(mInputGestureData.trigger);
     }
 
     /** Returns the action to perform for this input gesture */
@@ -147,18 +127,7 @@
                         "No app launch data for system action launch application");
             }
             AidlInputGestureData data = new AidlInputGestureData();
-            data.trigger = new AidlInputGestureData.Trigger();
-            if (mTrigger instanceof KeyTrigger keyTrigger) {
-                data.trigger.setKey(new AidlInputGestureData.KeyTrigger());
-                data.trigger.getKey().keycode = keyTrigger.getKeycode();
-                data.trigger.getKey().modifierState = keyTrigger.getModifierState();
-            } else if (mTrigger instanceof TouchpadTrigger touchpadTrigger) {
-                data.trigger.setTouchpadGesture(new AidlInputGestureData.TouchpadGestureTrigger());
-                data.trigger.getTouchpadGesture().gestureType =
-                        touchpadTrigger.getTouchpadGestureType();
-            } else {
-                throw new IllegalArgumentException("Invalid trigger type!");
-            }
+            data.trigger = mTrigger.getAidlTrigger();
             data.gestureType = mKeyGestureType;
             if (mAppLaunchData != null) {
                 if (mAppLaunchData instanceof AppLaunchData.CategoryData categoryData) {
@@ -198,6 +167,7 @@
     }
 
     public interface Trigger {
+        AidlInputGestureData.Trigger getAidlTrigger();
     }
 
     /** Creates a input gesture trigger based on a key press */
@@ -210,85 +180,128 @@
         return new TouchpadTrigger(touchpadGestureType);
     }
 
+    public static Trigger createTriggerFromAidlTrigger(AidlInputGestureData.Trigger aidlTrigger) {
+        switch (aidlTrigger.getTag()) {
+            case AidlInputGestureData.Trigger.Tag.key: {
+                AidlInputGestureData.KeyTrigger trigger = aidlTrigger.getKey();
+                if (trigger == null) {
+                    throw new RuntimeException("aidlTrigger is corrupted, null key trigger!");
+                }
+                return new KeyTrigger(trigger);
+            }
+            case AidlInputGestureData.Trigger.Tag.touchpadGesture: {
+                AidlInputGestureData.TouchpadGestureTrigger trigger =
+                        aidlTrigger.getTouchpadGesture();
+                if (trigger == null) {
+                    throw new RuntimeException(
+                            "aidlTrigger is corrupted, null touchpad trigger!");
+                }
+                return new TouchpadTrigger(trigger);
+            }
+            default:
+                throw new RuntimeException("aidlTrigger is corrupted, invalid trigger type!");
+
+        }
+    }
+
     /** Key based input gesture trigger */
     public static class KeyTrigger implements Trigger {
-        private static final int SHORTCUT_META_MASK =
-                KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON
-                        | KeyEvent.META_SHIFT_ON;
-        private final int mKeycode;
-        private final int mModifierState;
+
+        AidlInputGestureData.KeyTrigger mAidlKeyTrigger;
+
+        private KeyTrigger(@NonNull AidlInputGestureData.KeyTrigger aidlKeyTrigger) {
+            mAidlKeyTrigger = aidlKeyTrigger;
+        }
 
         private KeyTrigger(int keycode, int modifierState) {
             if (keycode <= KeyEvent.KEYCODE_UNKNOWN || keycode > KeyEvent.getMaxKeyCode()) {
                 throw new IllegalArgumentException("Invalid keycode = " + keycode);
             }
-            mKeycode = keycode;
-            mModifierState = modifierState;
+            mAidlKeyTrigger = new AidlInputGestureData.KeyTrigger();
+            mAidlKeyTrigger.keycode = keycode;
+            mAidlKeyTrigger.modifierState = modifierState;
         }
 
         public int getKeycode() {
-            return mKeycode;
+            return mAidlKeyTrigger.keycode;
         }
 
         public int getModifierState() {
-            return mModifierState;
+            return mAidlKeyTrigger.modifierState;
+        }
+
+        public AidlInputGestureData.Trigger getAidlTrigger() {
+            AidlInputGestureData.Trigger trigger = new AidlInputGestureData.Trigger();
+            trigger.setKey(mAidlKeyTrigger);
+            return trigger;
         }
 
         @Override
         public boolean equals(Object o) {
             if (this == o) return true;
             if (!(o instanceof KeyTrigger that)) return false;
-            return mKeycode == that.mKeycode && mModifierState == that.mModifierState;
+            return Objects.equals(mAidlKeyTrigger, that.mAidlKeyTrigger);
         }
 
         @Override
         public int hashCode() {
-            return Objects.hash(mKeycode, mModifierState);
+            return mAidlKeyTrigger.hashCode();
         }
 
         @Override
         public String toString() {
             return "KeyTrigger{" +
-                    "mKeycode=" + KeyEvent.keyCodeToString(mKeycode) +
-                    ", mModifierState=" + mModifierState +
+                    "mKeycode=" + KeyEvent.keyCodeToString(mAidlKeyTrigger.keycode) +
+                    ", mModifierState=" + mAidlKeyTrigger.modifierState +
                     '}';
         }
     }
 
     /** Touchpad based input gesture trigger */
     public static class TouchpadTrigger implements Trigger {
-        private final int mTouchpadGestureType;
+        AidlInputGestureData.TouchpadGestureTrigger mAidlTouchpadTrigger;
+
+        private TouchpadTrigger(
+                @NonNull AidlInputGestureData.TouchpadGestureTrigger aidlTouchpadTrigger) {
+            mAidlTouchpadTrigger = aidlTouchpadTrigger;
+        }
 
         private TouchpadTrigger(int touchpadGestureType) {
             if (touchpadGestureType != TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP) {
                 throw new IllegalArgumentException(
                         "Invalid touchpadGestureType = " + touchpadGestureType);
             }
-            mTouchpadGestureType = touchpadGestureType;
+            mAidlTouchpadTrigger = new AidlInputGestureData.TouchpadGestureTrigger();
+            mAidlTouchpadTrigger.gestureType = touchpadGestureType;
         }
 
         public int getTouchpadGestureType() {
-            return mTouchpadGestureType;
+            return mAidlTouchpadTrigger.gestureType;
+        }
+
+        public AidlInputGestureData.Trigger getAidlTrigger() {
+            AidlInputGestureData.Trigger trigger = new AidlInputGestureData.Trigger();
+            trigger.setTouchpadGesture(mAidlTouchpadTrigger);
+            return trigger;
         }
 
         @Override
         public String toString() {
             return "TouchpadTrigger{" +
-                    "mTouchpadGestureType=" + mTouchpadGestureType +
+                    "mTouchpadGestureType=" + mAidlTouchpadTrigger.gestureType +
                     '}';
         }
 
         @Override
         public boolean equals(Object o) {
             if (this == o) return true;
-            if (o == null || getClass() != o.getClass()) return false;
-            TouchpadTrigger that = (TouchpadTrigger) o;
-            return mTouchpadGestureType == that.mTouchpadGestureType;
+            if (!(o instanceof TouchpadTrigger that)) return false;
+            return Objects.equals(mAidlTouchpadTrigger, that.mAidlTouchpadTrigger);
         }
 
         @Override
         public int hashCode() {
-            return Objects.hashCode(mTouchpadGestureType);
+            return mAidlTouchpadTrigger.hashCode();
         }
     }
 
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index 10224c1..cf41e13 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -1480,6 +1480,30 @@
         mGlobal.unregisterKeyGestureEventHandler(handler);
     }
 
+    /**
+     * Find an input gesture mapped to a particular trigger.
+     *
+     * @param trigger to find the input gesture for
+     * @return input gesture mapped to the provided trigger, {@code null} if none found
+     *
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES)
+    @UserHandleAware
+    @Nullable
+    public InputGestureData getInputGesture(@NonNull InputGestureData.Trigger trigger) {
+        try {
+            AidlInputGestureData result = mIm.getInputGesture(mContext.getUserId(),
+                    trigger.getAidlTrigger());
+            if (result == null) {
+                return null;
+            }
+            return new InputGestureData(result);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     /** Adds a new custom input gesture
      *
      * @param inputGestureData gesture data to add as custom gesture
diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java
index 8da630c..b380e25 100644
--- a/core/java/android/hardware/input/InputSettings.java
+++ b/core/java/android/hardware/input/InputSettings.java
@@ -78,6 +78,24 @@
     public static final int DEFAULT_POINTER_SPEED = 0;
 
     /**
+     * Pointer Speed: The minimum (slowest) mouse scrolling speed (-7).
+     * @hide
+     */
+    public static final int MIN_MOUSE_SCROLLING_SPEED = -7;
+
+    /**
+     * Pointer Speed: The maximum (fastest) mouse scrolling speed (7).
+     * @hide
+     */
+    public static final int MAX_MOUSE_SCROLLING_SPEED = 7;
+
+    /**
+     * Pointer Speed: The default mouse scrolling speed (0).
+     * @hide
+     */
+    public static final int DEFAULT_MOUSE_SCROLLING_SPEED = 0;
+
+    /**
      * Bounce Keys Threshold: The default value of the threshold (500 ms).
      *
      * @hide
@@ -650,6 +668,54 @@
     }
 
     /**
+     * Gets the mouse scrolling speed.
+     *
+     * The returned value only applies when mouse scrolling acceleration is not enabled.
+     *
+     * @param context The application context.
+     * @return The mouse scrolling speed as a value between {@link #MIN_MOUSE_SCROLLING_SPEED} and
+     *         {@link #MAX_MOUSE_SCROLLING_SPEED}, or the default value
+     *         {@link #DEFAULT_MOUSE_SCROLLING_SPEED}.
+     *
+     * @hide
+     */
+    public static int getMouseScrollingSpeed(@NonNull Context context) {
+        if (!isMouseScrollingAccelerationFeatureFlagEnabled()) {
+            return 0;
+        }
+
+        return Settings.System.getIntForUser(context.getContentResolver(),
+                Settings.System.MOUSE_SCROLLING_SPEED, DEFAULT_MOUSE_SCROLLING_SPEED,
+                UserHandle.USER_CURRENT);
+    }
+
+    /**
+     * Sets the mouse scrolling speed, and saves it in the settings.
+     *
+     * The new speed will only apply when mouse scrolling acceleration is not enabled.
+     *
+     * @param context The application context.
+     * @param speed The mouse scrolling speed as a value between {@link #MIN_MOUSE_SCROLLING_SPEED}
+     *              and {@link #MAX_MOUSE_SCROLLING_SPEED}, or the default value
+     *              {@link #DEFAULT_MOUSE_SCROLLING_SPEED}.
+     *
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.WRITE_SETTINGS)
+    public static void setMouseScrollingSpeed(@NonNull Context context, int speed) {
+        if (isMouseScrollingAccelerationEnabled(context)) {
+            return;
+        }
+
+        if (speed < MIN_MOUSE_SCROLLING_SPEED || speed > MAX_MOUSE_SCROLLING_SPEED) {
+            throw new IllegalArgumentException("speed out of range");
+        }
+
+        Settings.System.putIntForUser(context.getContentResolver(),
+                Settings.System.MOUSE_SCROLLING_SPEED, speed, UserHandle.USER_CURRENT);
+    }
+
+    /**
      * Whether mouse vertical scrolling is reversed. This applies only to connected mice.
      *
      * @param context The application context.
diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java
index 66d073f..4025242 100644
--- a/core/java/android/hardware/input/KeyGestureEvent.java
+++ b/core/java/android/hardware/input/KeyGestureEvent.java
@@ -43,6 +43,9 @@
     private static final int LOG_EVENT_UNSPECIFIED =
             FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__UNSPECIFIED;
 
+    // Used as a placeholder to identify if a gesture is reserved for system
+    public static final int KEY_GESTURE_TYPE_SYSTEM_RESERVED = -1;
+
     // These values should not change and values should not be re-used as this data is persisted to
     // long term storage and must be kept backwards compatible.
     public static final int KEY_GESTURE_TYPE_UNSPECIFIED = 0;
@@ -129,6 +132,7 @@
     public static final int KEY_GESTURE_TYPE_MAGNIFICATION_PAN_RIGHT = 79;
     public static final int KEY_GESTURE_TYPE_MAGNIFICATION_PAN_UP = 80;
     public static final int KEY_GESTURE_TYPE_MAGNIFICATION_PAN_DOWN = 81;
+    public static final int KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS = 82;
 
     public static final int FLAG_CANCELLED = 1;
 
@@ -143,6 +147,7 @@
     public static final int ACTION_GESTURE_COMPLETE = 2;
 
     @IntDef(prefix = "KEY_GESTURE_TYPE_", value = {
+            KEY_GESTURE_TYPE_SYSTEM_RESERVED,
             KEY_GESTURE_TYPE_UNSPECIFIED,
             KEY_GESTURE_TYPE_HOME,
             KEY_GESTURE_TYPE_RECENT_APPS,
@@ -225,11 +230,32 @@
             KEY_GESTURE_TYPE_MAGNIFICATION_PAN_RIGHT,
             KEY_GESTURE_TYPE_MAGNIFICATION_PAN_UP,
             KEY_GESTURE_TYPE_MAGNIFICATION_PAN_DOWN,
+            KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface KeyGestureType {
     }
 
+    /**
+     * Returns whether the key gesture type passed as argument is allowed for visible background
+     * users.
+     *
+     * @hide
+     */
+    public static boolean isVisibleBackgrounduserAllowedGesture(int keyGestureType) {
+        switch (keyGestureType) {
+            case KEY_GESTURE_TYPE_SLEEP:
+            case KEY_GESTURE_TYPE_WAKEUP:
+            case KEY_GESTURE_TYPE_LAUNCH_ASSISTANT:
+            case KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT:
+            case KEY_GESTURE_TYPE_VOLUME_MUTE:
+            case KEY_GESTURE_TYPE_RECENT_APPS:
+            case KEY_GESTURE_TYPE_APP_SWITCH:
+                return false;
+        }
+        return true;
+    }
+
     public KeyGestureEvent(@NonNull AidlKeyGestureEvent keyGestureEvent) {
         this.mKeyGestureEvent = keyGestureEvent;
     }
@@ -643,6 +669,8 @@
 
     private static String keyGestureTypeToString(@KeyGestureType int value) {
         switch (value) {
+            case KEY_GESTURE_TYPE_SYSTEM_RESERVED:
+                return "KEY_GESTURE_TYPE_SYSTEM_RESERVED";
             case KEY_GESTURE_TYPE_UNSPECIFIED:
                 return "KEY_GESTURE_TYPE_UNSPECIFIED";
             case KEY_GESTURE_TYPE_HOME:
@@ -807,6 +835,8 @@
                 return "KEY_GESTURE_TYPE_MAGNIFICATION_PAN_UP";
             case KEY_GESTURE_TYPE_MAGNIFICATION_PAN_DOWN:
                 return "KEY_GESTURE_TYPE_MAGNIFICATION_PAN_DOWN";
+            case KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS:
+                return "KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS";
             default:
                 return Integer.toHexString(value);
         }
diff --git a/core/java/android/hardware/location/ContextHubManager.java b/core/java/android/hardware/location/ContextHubManager.java
index 0cd3209..9181bd0 100644
--- a/core/java/android/hardware/location/ContextHubManager.java
+++ b/core/java/android/hardware/location/ContextHubManager.java
@@ -697,6 +697,7 @@
      *
      * @param endpointId Statically generated ID for an endpoint.
      * @return A list of {@link HubDiscoveryInfo} objects that represents the result of discovery.
+     * @throws UnsupportedOperationException If the operation is not supported.
      */
     @FlaggedApi(Flags.FLAG_OFFLOAD_API)
     @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@@ -733,6 +734,7 @@
      *     cannot be null or empty.
      * @return A list of {@link HubDiscoveryInfo} objects that represents the result of discovery.
      * @throws IllegalArgumentException if the serviceDescriptor is empty/null.
+     * @throws UnsupportedOperationException If the operation is not supported.
      */
     @FlaggedApi(Flags.FLAG_OFFLOAD_API)
     @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java
index ecd90e4..1041041 100644
--- a/core/java/android/os/BaseBundle.java
+++ b/core/java/android/os/BaseBundle.java
@@ -386,6 +386,15 @@
     }
 
     /**
+     * return true if the value corresponding to this key is still parceled.
+     * @hide
+     */
+    public boolean isValueParceled(String key) {
+        if (mMap == null) return true;
+        int i = mMap.indexOfKey(key);
+        return (mMap.valueAt(i) instanceof BiFunction<?, ?, ?>);
+    }
+    /**
      * Returns the value for a certain position in the array map for expected return type {@code
      * clazz} (or pass {@code null} for no type check).
      *
diff --git a/core/java/android/os/CpuHeadroomParams.java b/core/java/android/os/CpuHeadroomParams.java
index 072c012..e511976 100644
--- a/core/java/android/os/CpuHeadroomParams.java
+++ b/core/java/android/os/CpuHeadroomParams.java
@@ -28,15 +28,11 @@
 
 /**
  * Headroom request params used by {@link SystemHealthManager#getCpuHeadroom(CpuHeadroomParams)}.
+ *
+ * <p>This class is immutable and one should use the {@link Builder} to build a new instance.
  */
 @FlaggedApi(Flags.FLAG_CPU_GPU_HEADROOMS)
 public final class CpuHeadroomParams {
-    final CpuHeadroomParamsInternal mInternal;
-
-    public CpuHeadroomParams() {
-        mInternal = new CpuHeadroomParamsInternal();
-    }
-
     /** @hide */
     @IntDef(flag = false, prefix = {"CPU_HEADROOM_CALCULATION_TYPE_"}, value = {
             CPU_HEADROOM_CALCULATION_TYPE_MIN, // 0
@@ -47,107 +43,188 @@
     }
 
     /**
-     * Calculates the headroom based on minimum value over a device-defined window.
+     * The headroom calculation type bases on minimum value over a specified window.
      */
     public static final int CPU_HEADROOM_CALCULATION_TYPE_MIN = 0;
 
     /**
-     * Calculates the headroom based on average value over a device-defined window.
+     * The headroom calculation type bases on average value over a specified window.
      */
     public static final int CPU_HEADROOM_CALCULATION_TYPE_AVERAGE = 1;
 
-    private static final int CALCULATION_WINDOW_MILLIS_MIN = 50;
-    private static final int CALCULATION_WINDOW_MILLIS_MAX = 10000;
-    private static final int MAX_TID_COUNT = 5;
+    /** @hide */
+    public final CpuHeadroomParamsInternal mInternal;
+
+    private CpuHeadroomParams() {
+        mInternal = new CpuHeadroomParamsInternal();
+    }
+
+    public static final class Builder {
+        private int mCalculationType = -1;
+        private int mCalculationWindowMillis = -1;
+        private int[] mTids = null;
+
+        public Builder() {
+        }
+
+        /**
+         * Returns a new builder copy with the same values as the params.
+         */
+        public Builder(@NonNull CpuHeadroomParams params) {
+            if (params.mInternal.calculationType >= 0) {
+                mCalculationType = params.mInternal.calculationType;
+            }
+            if (params.mInternal.calculationWindowMillis >= 0) {
+                mCalculationWindowMillis = params.mInternal.calculationWindowMillis;
+            }
+            if (params.mInternal.tids != null) {
+                mTids = Arrays.copyOf(params.mInternal.tids, params.mInternal.tids.length);
+            }
+        }
+
+        /**
+         * Sets the headroom calculation type.
+         * <p>
+         *
+         * @throws IllegalArgumentException if the type is invalid.
+         */
+        @NonNull
+        public Builder setCalculationType(
+                @CpuHeadroomCalculationType int calculationType) {
+            switch (calculationType) {
+                case CPU_HEADROOM_CALCULATION_TYPE_MIN:
+                case CPU_HEADROOM_CALCULATION_TYPE_AVERAGE: {
+                    mCalculationType = calculationType;
+                    return this;
+                }
+            }
+            throw new IllegalArgumentException("Invalid calculation type: " + calculationType);
+        }
+
+        /**
+         * Sets the headroom calculation window size in milliseconds.
+         * <p>
+         *
+         * @param windowMillis the window size in milliseconds ranges from
+         *                     {@link SystemHealthManager#getCpuHeadroomCalculationWindowRange()}.
+         *                     The smaller the window size, the larger fluctuation in the headroom
+         *                     value should be expected. The default value can be retrieved from
+         *                     the {@link CpuHeadroomParams#getCalculationWindowMillis}. The device
+         *                     will try to use the closest feasible window size to this param.
+         * @throws IllegalArgumentException if the window is invalid.
+         */
+        @NonNull
+        public Builder setCalculationWindowMillis(@IntRange(from = 1) int windowMillis) {
+            if (windowMillis <= 0) {
+                throw new IllegalArgumentException("Invalid calculation window: " + windowMillis);
+            }
+            mCalculationWindowMillis = windowMillis;
+            return this;
+        }
+
+        /**
+         * Sets the thread TIDs to track.
+         * <p>
+         * The TIDs should belong to the same of the process that will make the headroom call. And
+         * they should not have different core affinity.
+         * <p>
+         * If not set or set to empty, the headroom will be based on the PID of the process making
+         * the call.
+         *
+         * @param tids non-null list of TIDs, where maximum size can be read from
+         *             {@link SystemHealthManager#getMaxCpuHeadroomTidsSize()}.
+         * @throws IllegalArgumentException if the TID is not positive.
+         */
+        @NonNull
+        public Builder setTids(@NonNull int... tids) {
+            for (int tid : tids) {
+                if (tid <= 0) {
+                    throw new IllegalArgumentException("Invalid TID: " + tid);
+                }
+            }
+            mTids = tids;
+            return this;
+        }
+
+        /**
+         * Builds the {@link CpuHeadroomParams} object.
+         */
+        @NonNull
+        public CpuHeadroomParams build() {
+            CpuHeadroomParams params = new CpuHeadroomParams();
+            if (mCalculationType >= 0) {
+                params.mInternal.calculationType = (byte) mCalculationType;
+            }
+            if (mCalculationWindowMillis >= 0) {
+                params.mInternal.calculationWindowMillis = mCalculationWindowMillis;
+            }
+            if (mTids != null) {
+                params.mInternal.tids = mTids;
+            }
+            return params;
+        }
+    }
 
     /**
-     * Sets the headroom calculation type.
-     * <p>
-     *
-     * @throws IllegalArgumentException if the type is invalid.
+     * Returns a new builder with the same values as this object.
      */
-    public void setCalculationType(@CpuHeadroomCalculationType int calculationType) {
-        switch (calculationType) {
-            case CPU_HEADROOM_CALCULATION_TYPE_MIN:
-            case CPU_HEADROOM_CALCULATION_TYPE_AVERAGE:
-                mInternal.calculationType = (byte) calculationType;
-                return;
-        }
-        throw new IllegalArgumentException("Invalid calculation type: " + calculationType);
+    @NonNull
+    public Builder toBuilder() {
+        return new Builder(this);
     }
 
     /**
      * Gets the headroom calculation type.
-     * Default to {@link #CPU_HEADROOM_CALCULATION_TYPE_MIN} if not set.
+     * <p>
+     * This will return the default value chosen by the device if not set.
      */
     public @CpuHeadroomCalculationType int getCalculationType() {
         @CpuHeadroomCalculationType int validatedType = switch ((int) mInternal.calculationType) {
-            case CPU_HEADROOM_CALCULATION_TYPE_MIN, CPU_HEADROOM_CALCULATION_TYPE_AVERAGE ->
-                    mInternal.calculationType;
+            case CPU_HEADROOM_CALCULATION_TYPE_MIN,
+                 CPU_HEADROOM_CALCULATION_TYPE_AVERAGE -> mInternal.calculationType;
             default -> CPU_HEADROOM_CALCULATION_TYPE_MIN;
         };
         return validatedType;
     }
 
     /**
-     * Sets the headroom calculation window size in milliseconds.
-     * <p>
-     *
-     * @param windowMillis the window size in milliseconds ranges from [50, 10000]. The smaller the
-     *                     window size, the larger fluctuation in the headroom value should be
-     *                     expected. The default value can be retrieved from the
-     *                     {@link #getCalculationWindowMillis}. The device will try to use the
-     *                     closest feasible window size to this param.
-     * @throws IllegalArgumentException if the window size is not in allowed range.
-     */
-    public void setCalculationWindowMillis(
-            @IntRange(from = CALCULATION_WINDOW_MILLIS_MIN, to =
-                    CALCULATION_WINDOW_MILLIS_MAX) int windowMillis) {
-        if (windowMillis < CALCULATION_WINDOW_MILLIS_MIN
-                || windowMillis > CALCULATION_WINDOW_MILLIS_MAX) {
-            throw new IllegalArgumentException("Invalid calculation window: " + windowMillis);
-        }
-        mInternal.calculationWindowMillis = windowMillis;
-    }
-
-    /**
      * Gets the headroom calculation window size in milliseconds.
      * <p>
-     * This will return the default value chosen by the device if the params is not set.
+     * This will return the default value chosen by the device if not set.
      */
-    public @IntRange(from = CALCULATION_WINDOW_MILLIS_MIN, to =
-            CALCULATION_WINDOW_MILLIS_MAX) long getCalculationWindowMillis() {
+    public long getCalculationWindowMillis() {
         return mInternal.calculationWindowMillis;
     }
 
     /**
-     * Sets the thread TIDs to track.
+     * Gets the TIDs to track.
      * <p>
-     * The TIDs should belong to the same of the process that will the headroom call. And they
-     * should not have different core affinity.
-     * <p>
-     * If not set, the headroom will be based on the PID of the process making the call.
-     *
-     * @param tids non-empty list of TIDs, maximum 5.
-     * @throws IllegalArgumentException if the list size is not in allowed range or TID is not
-     *                                  positive.
+     * This will return a copy of the TIDs in the params, or null if the params is not set.
      */
-    public void setTids(@NonNull int... tids) {
-        if (tids.length == 0 || tids.length > MAX_TID_COUNT) {
-            throw new IllegalArgumentException("Invalid number of TIDs: " + tids.length);
-        }
-        for (int tid : tids) {
-            if (tid <= 0) {
-                throw new IllegalArgumentException("Invalid TID: " + tid);
-            }
-        }
-        mInternal.tids = Arrays.copyOf(tids, tids.length);
+    @NonNull
+    public int[] getTids() {
+        return mInternal.tids == null ? null : Arrays.copyOf(mInternal.tids, mInternal.tids.length);
     }
 
-    /**
-     * @hide
-     */
-    public CpuHeadroomParamsInternal getInternal() {
-        return mInternal;
+    @Override
+    public String toString() {
+        return "CpuHeadroomParams{"
+                + "calculationType=" + mInternal.calculationType
+                + ", calculationWindowMillis=" + mInternal.calculationWindowMillis
+                + ", tids=" + Arrays.toString(mInternal.tids)
+                + '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        CpuHeadroomParams that = (CpuHeadroomParams) o;
+        return mInternal.equals(that.mInternal);
+    }
+
+    @Override
+    public int hashCode() {
+        return mInternal.hashCode();
     }
 }
diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java
index 2bc6ab5..d564022 100644
--- a/core/java/android/os/Debug.java
+++ b/core/java/android/os/Debug.java
@@ -2783,4 +2783,12 @@
      */
     public static native boolean logAllocatorStats();
 
+    /**
+     * Return the amount of memory (in kB) allocated by kernel drivers through CMA.
+     * @return a non-negative value or -1 on error.
+     *
+     * @hide
+     */
+    public static native long getKernelCmaUsageKb();
+
 }
diff --git a/core/java/android/os/GpuHeadroomParams.java b/core/java/android/os/GpuHeadroomParams.java
index 126ee8c..5c5e7bb 100644
--- a/core/java/android/os/GpuHeadroomParams.java
+++ b/core/java/android/os/GpuHeadroomParams.java
@@ -19,6 +19,7 @@
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
+import android.annotation.NonNull;
 import android.os.health.SystemHealthManager;
 
 import java.lang.annotation.Retention;
@@ -26,15 +27,11 @@
 
 /**
  * Headroom request params used by {@link SystemHealthManager#getGpuHeadroom(GpuHeadroomParams)}.
+ *
+ * <p>This class is immutable and one should use the {@link Builder} to build a new instance.
  */
 @FlaggedApi(Flags.FLAG_CPU_GPU_HEADROOMS)
 public final class GpuHeadroomParams {
-    final GpuHeadroomParamsInternal mInternal;
-
-    public GpuHeadroomParams() {
-        mInternal = new GpuHeadroomParamsInternal();
-    }
-
     /** @hide */
     @IntDef(flag = false, prefix = {"GPU_HEADROOM_CALCULATION_TYPE_"}, value = {
             GPU_HEADROOM_CALCULATION_TYPE_MIN, // 0
@@ -45,82 +42,153 @@
     }
 
     /**
-     * Calculates the headroom based on minimum value over a device-defined window.
+     * The headroom calculation type bases on minimum value over a specified window.
      */
     public static final int GPU_HEADROOM_CALCULATION_TYPE_MIN = 0;
 
     /**
-     * Calculates the headroom based on average value over a device-defined window.
+     * The headroom calculation type bases on average value over a specified window.
      */
     public static final int GPU_HEADROOM_CALCULATION_TYPE_AVERAGE = 1;
 
-    private static final int CALCULATION_WINDOW_MILLIS_MIN = 50;
-    private static final int CALCULATION_WINDOW_MILLIS_MAX = 10000;
+    /**
+     * The minimum size of the window to compute the headroom over.
+     */
+    public static final int GPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MIN = 50;
 
     /**
-     * Sets the headroom calculation type.
-     * <p>
-     *
-     * @throws IllegalArgumentException if the type is invalid.
+     * The maximum size of the window to compute the headroom over.
      */
-    public void setCalculationType(@GpuHeadroomCalculationType int calculationType) {
-        switch (calculationType) {
-            case GPU_HEADROOM_CALCULATION_TYPE_MIN:
-            case GPU_HEADROOM_CALCULATION_TYPE_AVERAGE:
-                mInternal.calculationType = (byte) calculationType;
-                return;
+    public static final int GPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MAX = 10000;
+
+    /**
+     * @hide
+     */
+    public final GpuHeadroomParamsInternal mInternal;
+
+    /**
+     * @hide
+     */
+    private GpuHeadroomParams() {
+        mInternal = new GpuHeadroomParamsInternal();
+    }
+
+    public static final class Builder {
+        private int mCalculationType = -1;
+        private int mCalculationWindowMillis = -1;
+
+        public Builder() {
         }
-        throw new IllegalArgumentException("Invalid calculation type: " + calculationType);
+
+        /**
+         * Returns a new builder with the same values as this object.
+         */
+        public Builder(@NonNull GpuHeadroomParams params) {
+            if (params.mInternal.calculationType >= 0) {
+                mCalculationType = params.mInternal.calculationType;
+            }
+            if (params.mInternal.calculationWindowMillis >= 0) {
+                mCalculationWindowMillis = params.mInternal.calculationWindowMillis;
+            }
+        }
+
+        /**
+         * Sets the headroom calculation type.
+         * <p>
+         *
+         * @throws IllegalArgumentException if the type is invalid.
+         */
+        @NonNull
+        public Builder setCalculationType(
+                @GpuHeadroomCalculationType int calculationType) {
+            switch (calculationType) {
+                case GPU_HEADROOM_CALCULATION_TYPE_MIN:
+                case GPU_HEADROOM_CALCULATION_TYPE_AVERAGE: {
+                    mCalculationType = calculationType;
+                    return this;
+                }
+            }
+            throw new IllegalArgumentException("Invalid calculation type: " + calculationType);
+        }
+
+        /**
+         * Sets the headroom calculation window size in milliseconds.
+         * <p>
+         *
+         * @param windowMillis the window size in milliseconds ranges from
+         *                     {@link SystemHealthManager#getGpuHeadroomCalculationWindowRange()}.
+         *                     The smaller the window size, the larger fluctuation in the headroom
+         *                     value should be expected. The default value can be retrieved from
+         *                     the {@link GpuHeadroomParams#getCalculationWindowMillis}. The device
+         *                     will try to use the closest feasible window size to this param.
+         * @throws IllegalArgumentException if the window is invalid.
+         */
+        @NonNull
+        public Builder setCalculationWindowMillis(@IntRange(from = 1) int windowMillis) {
+            if (windowMillis <= 0) {
+                throw new IllegalArgumentException("Invalid calculation window: " + windowMillis);
+            }
+            mCalculationWindowMillis = windowMillis;
+            return this;
+        }
+
+        /**
+         * Builds the {@link GpuHeadroomParams} object.
+         */
+        @NonNull
+        public GpuHeadroomParams build() {
+            GpuHeadroomParams params = new GpuHeadroomParams();
+            if (mCalculationType >= 0) {
+                params.mInternal.calculationType = (byte) mCalculationType;
+            }
+            if (mCalculationWindowMillis >= 0) {
+                params.mInternal.calculationWindowMillis = mCalculationWindowMillis;
+            }
+            return params;
+        }
     }
 
     /**
      * Gets the headroom calculation type.
-     * Default to {@link #GPU_HEADROOM_CALCULATION_TYPE_MIN} if the params is not set.
+     * <p>
+     * This will return the default value chosen by the device if not set.
      */
     public @GpuHeadroomCalculationType int getCalculationType() {
         @GpuHeadroomCalculationType int validatedType = switch ((int) mInternal.calculationType) {
-            case GPU_HEADROOM_CALCULATION_TYPE_MIN, GPU_HEADROOM_CALCULATION_TYPE_AVERAGE ->
-                    mInternal.calculationType;
+            case GPU_HEADROOM_CALCULATION_TYPE_MIN,
+                 GPU_HEADROOM_CALCULATION_TYPE_AVERAGE -> mInternal.calculationType;
             default -> GPU_HEADROOM_CALCULATION_TYPE_MIN;
         };
         return validatedType;
     }
 
     /**
-     * Sets the headroom calculation window size in milliseconds.
-     * <p>
-     *
-     * @param windowMillis the window size in milliseconds ranges from [50, 10000]. The smaller the
-     *                     window size, the larger fluctuation in the headroom value should be
-     *                     expected. The default value can be retrieved from the
-     *                     {@link #getCalculationWindowMillis}. The device will try to use the
-     *                     closest feasible window size to this param.
-     * @throws IllegalArgumentException if the window is invalid.
-     */
-    public void setCalculationWindowMillis(
-            @IntRange(from = CALCULATION_WINDOW_MILLIS_MIN, to =
-                    CALCULATION_WINDOW_MILLIS_MAX) int windowMillis) {
-        if (windowMillis < CALCULATION_WINDOW_MILLIS_MIN
-                || windowMillis > CALCULATION_WINDOW_MILLIS_MAX) {
-            throw new IllegalArgumentException("Invalid calculation window: " + windowMillis);
-        }
-        mInternal.calculationWindowMillis = windowMillis;
-    }
-
-    /**
      * Gets the headroom calculation window size in milliseconds.
      * <p>
      * This will return the default value chosen by the device if not set.
      */
-    public @IntRange(from = CALCULATION_WINDOW_MILLIS_MIN, to =
-            CALCULATION_WINDOW_MILLIS_MAX) int getCalculationWindowMillis() {
+    public int getCalculationWindowMillis() {
         return mInternal.calculationWindowMillis;
     }
 
-    /**
-     * @hide
-     */
-    public GpuHeadroomParamsInternal getInternal() {
-        return mInternal;
+    @Override
+    public String toString() {
+        return "GpuHeadroomParams{"
+                + "calculationType=" + mInternal.calculationType
+                + ", calculationWindowMillis=" + mInternal.calculationWindowMillis
+                + '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        GpuHeadroomParams that = (GpuHeadroomParams) o;
+        return mInternal.equals(that.mInternal);
+    }
+
+    @Override
+    public int hashCode() {
+        return mInternal.hashCode();
     }
 }
diff --git a/core/java/android/os/GraphicsEnvironment.java b/core/java/android/os/GraphicsEnvironment.java
index 8f6a508..45a7afa 100644
--- a/core/java/android/os/GraphicsEnvironment.java
+++ b/core/java/android/os/GraphicsEnvironment.java
@@ -78,6 +78,9 @@
     private static final String PROPERTY_GFX_DRIVER_PRERELEASE = "ro.gfx.driver.1";
     private static final String PROPERTY_GFX_DRIVER_BUILD_TIME = "ro.gfx.driver_build_time";
 
+    /// System properties related to EGL
+    private static final String PROPERTY_RO_HARDWARE_EGL = "ro.hardware.egl";
+
     // Metadata flags within the <application> tag in the AndroidManifest.xml file.
     private static final String METADATA_DRIVER_BUILD_TIME =
             "com.android.graphics.driver.build_time";
@@ -504,9 +507,11 @@
 
         final List<ResolveInfo> resolveInfos =
                 pm.queryIntentActivities(intent, PackageManager.MATCH_SYSTEM_ONLY);
-        if (resolveInfos.size() != 1) {
-            Log.v(TAG, "Invalid number of ANGLE packages. Required: 1, Found: "
-                    + resolveInfos.size());
+        if (resolveInfos.isEmpty()) {
+            Log.v(TAG, "No ANGLE packages installed.");
+            return "";
+        } else if (resolveInfos.size() > 1) {
+            Log.v(TAG, "Too many ANGLE packages found: " + resolveInfos.size());
             if (DEBUG) {
                 for (ResolveInfo resolveInfo : resolveInfos) {
                     Log.d(TAG, "Found ANGLE package: " + resolveInfo.activityInfo.packageName);
@@ -516,7 +521,7 @@
         }
 
         // Must be exactly 1 ANGLE PKG found to get here.
-        return resolveInfos.get(0).activityInfo.packageName;
+        return resolveInfos.getFirst().activityInfo.packageName;
     }
 
     /**
@@ -545,10 +550,12 @@
     }
 
     /**
-     * Determine whether ANGLE should be used, attempt to set up from apk first, if ANGLE can be
-     * set up from apk, pass ANGLE details down to the C++ GraphicsEnv class via
-     * GraphicsEnv::setAngleInfo(). If apk setup fails, attempt to set up to use system ANGLE.
-     * Return false if both fail.
+     * If ANGLE is not the system driver, determine whether ANGLE should be used, and if so, pass
+     * down the necessary details to the C++ GraphicsEnv class via GraphicsEnv::setAngleInfo().
+     * <p>
+     * If ANGLE is the system driver or the various flags indicate it should be used, attempt to
+     * set up ANGLE from the APK first, so the updatable libraries are used. If APK setup fails,
+     * attempt to set up the system ANGLE. Return false if both fail.
      *
      * @param context - Context of the application.
      * @param bundle - Bundle of the application.
@@ -559,15 +566,26 @@
      */
     private boolean setupAngle(Context context, Bundle bundle, PackageManager packageManager,
             String packageName) {
-        final String angleChoice = queryAngleChoice(context, bundle, packageName);
-        if (angleChoice.equals(ANGLE_GL_DRIVER_CHOICE_DEFAULT)) {
-            return false;
-        }
-        if (angleChoice.equals(ANGLE_GL_DRIVER_CHOICE_NATIVE)) {
-            nativeSetAngleInfo("", true, packageName, null);
-            return false;
+        final String eglDriverName = SystemProperties.get(PROPERTY_RO_HARDWARE_EGL);
+
+        // The ANGLE choice only makes sense if ANGLE is not the system driver.
+        if (!eglDriverName.equals(ANGLE_DRIVER_NAME)) {
+            final String angleChoice = queryAngleChoice(context, bundle, packageName);
+            if (angleChoice.equals(ANGLE_GL_DRIVER_CHOICE_DEFAULT)) {
+                return false;
+            }
+            if (angleChoice.equals(ANGLE_GL_DRIVER_CHOICE_NATIVE)) {
+                nativeSetAngleInfo("", true, packageName, null);
+                return false;
+            }
         }
 
+        // If we reach here, it means either:
+        // 1. system driver is not ANGLE, but ANGLE is requested.
+        // 2. system driver is ANGLE.
+        // In both cases, setup ANGLE info. We attempt to setup the APK first, so
+        // updated/development libraries are used if the APK is present, falling back to the system
+        // libraries otherwise.
         return setupAngleFromApk(context, bundle, packageManager, packageName)
                 || setupAngleFromSystem(context, bundle, packageName);
     }
@@ -605,7 +623,6 @@
         if (angleInfo == null) {
             anglePkgName = getAnglePackageName(packageManager);
             if (TextUtils.isEmpty(anglePkgName)) {
-                Log.v(TAG, "Failed to find ANGLE package.");
                 return false;
             }
 
@@ -689,11 +706,11 @@
      * @param context
      */
     public void showAngleInUseDialogBox(Context context) {
-        if (!shouldShowAngleInUseDialogBox(context)) {
+        if (!mShouldUseAngle) {
             return;
         }
 
-        if (!mShouldUseAngle) {
+        if (!shouldShowAngleInUseDialogBox(context)) {
             return;
         }
 
diff --git a/core/java/android/os/IHintManager.aidl b/core/java/android/os/IHintManager.aidl
index 5128e91..2432545 100644
--- a/core/java/android/os/IHintManager.aidl
+++ b/core/java/android/os/IHintManager.aidl
@@ -72,6 +72,7 @@
     parcelable HintManagerClientData {
         int powerHalVersion;
         int maxGraphicsPipelineThreads;
+        int maxCpuHeadroomThreads;
         long preferredRateNanos;
         SupportInfo supportInfo;
     }
@@ -88,4 +89,6 @@
      * passing back a bundle of support and configuration information.
      */
     HintManagerClientData registerClient(in IHintManagerClient client);
+
+    HintManagerClientData getClientData();
 }
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index 0879118..4aa7462 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -593,11 +593,11 @@
      */
     public final void recycle() {
         if (mRecycled) {
-            Log.wtf(TAG, "Recycle called on unowned Parcel. (recycle twice?) Here: "
+            String error = "Recycle called on unowned Parcel. (recycle twice?) Here: "
                     + Log.getStackTraceString(new Throwable())
-                    + " Original recycle call (if DEBUG_RECYCLE): ", mStack);
-
-            return;
+                    + " Original recycle call (if DEBUG_RECYCLE): ";
+            Log.wtf(TAG, error, mStack);
+            throw new IllegalStateException(error, mStack);
         }
         mRecycled = true;
 
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index 1801df0..2a5666c 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -615,6 +615,7 @@
             WAKE_REASON_WAKE_KEY,
             WAKE_REASON_WAKE_MOTION,
             WAKE_REASON_HDMI,
+            WAKE_REASON_LID,
             WAKE_REASON_DISPLAY_GROUP_ADDED,
             WAKE_REASON_DISPLAY_GROUP_TURNED_ON,
             WAKE_REASON_UNFOLD_DEVICE,
diff --git a/core/java/android/os/ServiceManager.java b/core/java/android/os/ServiceManager.java
index 9085fe0..a58fea8 100644
--- a/core/java/android/os/ServiceManager.java
+++ b/core/java/android/os/ServiceManager.java
@@ -278,7 +278,7 @@
                 return service;
             } else {
                 return Binder.allowBlocking(
-                        getIServiceManager().checkService(name).getServiceWithMetadata().service);
+                        getIServiceManager().checkService2(name).getServiceWithMetadata().service);
             }
         } catch (RemoteException e) {
             Log.e(TAG, "error in checkService", e);
diff --git a/core/java/android/os/ServiceManagerNative.java b/core/java/android/os/ServiceManagerNative.java
index 7ea521e..a5aa1b3 100644
--- a/core/java/android/os/ServiceManagerNative.java
+++ b/core/java/android/os/ServiceManagerNative.java
@@ -62,16 +62,23 @@
     @UnsupportedAppUsage
     public IBinder getService(String name) throws RemoteException {
         // Same as checkService (old versions of servicemanager had both methods).
-        return checkService(name).getServiceWithMetadata().service;
+        return checkService2(name).getServiceWithMetadata().service;
     }
 
     public Service getService2(String name) throws RemoteException {
         // Same as checkService (old versions of servicemanager had both methods).
-        return checkService(name);
+        return checkService2(name);
     }
 
-    public Service checkService(String name) throws RemoteException {
-        return mServiceManager.checkService(name);
+    // TODO(b/355394904): This function has been deprecated, please use checkService2 instead.
+    @UnsupportedAppUsage
+    public IBinder checkService(String name) throws RemoteException {
+        // Same as checkService (old versions of servicemanager had both methods).
+        return checkService2(name).getServiceWithMetadata().service;
+    }
+
+    public Service checkService2(String name) throws RemoteException {
+        return mServiceManager.checkService2(name);
     }
 
     public void addService(String name, IBinder service, boolean allowIsolated, int dumpPriority)
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index e24f08b..8b83698 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -110,6 +110,14 @@
 }
 
 flag {
+    name: "allow_thermal_hal_skin_forecast"
+    is_exported: true
+    namespace: "game"
+    description: "Enable thermal HAL skin temperature forecast to be used by headroom API"
+    bug: "383211885"
+}
+
+flag {
     name: "allow_thermal_headroom_thresholds"
     is_exported: true
     namespace: "game"
diff --git a/core/java/android/os/health/SystemHealthManager.java b/core/java/android/os/health/SystemHealthManager.java
index cd79e41..a1e9cf2 100644
--- a/core/java/android/os/health/SystemHealthManager.java
+++ b/core/java/android/os/health/SystemHealthManager.java
@@ -18,6 +18,7 @@
 
 import android.annotation.FlaggedApi;
 import android.annotation.FloatRange;
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemService;
@@ -42,6 +43,8 @@
 import android.os.ResultReceiver;
 import android.os.ServiceManager;
 import android.os.SynchronousResultReceiver;
+import android.util.Pair;
+import android.util.Slog;
 
 import com.android.internal.app.IBatteryStats;
 import com.android.server.power.optimization.Flags;
@@ -69,15 +72,41 @@
  * plugged in (e.g. using {@link android.app.job.JobScheduler JobScheduler}), and
  * while that can affect charging rates, it is still preferable to actually draining
  * the battery.
+ * <p>
+ * <b>CPU/GPU Usage</b><br>
+ * CPU/GPU headroom APIs are designed to be best used by applications with consistent and intense
+ * workload such as games to query the remaining capacity headroom over a short period and perform
+ * optimization accordingly. Due to the nature of the fast job scheduling and frequency scaling of
+ * CPU and GPU, the headroom by nature will have "TOCTOU" problem which makes it less suitable for
+ * apps with inconsistent or low workload to take any useful action but simply monitoring. And to
+ * avoid oscillation it's not recommended to adjust workload too frequent (on each polling request)
+ * or too aggressively. As the headroom calculation is more based on reflecting past history usage
+ * than predicting future capacity. Take game as an example, if the API returns CPU headroom of 0 in
+ * one scenario (especially if it's constant across multiple calls), or some value significantly
+ * smaller than other scenarios, then it can reason that the recent performance result is more CPU
+ * bottlenecked. Then reducing the CPU workload intensity can help reserve some headroom to handle
+ * the load variance better, which can result in less frame drops or smooth FPS value. On the other
+ * hand, if the API returns large CPU headroom constantly, the app can be more confident to increase
+ * the workload and expect higher possibility of device meeting its performance expectation.
+ * App can also use thermal APIs to read the current thermal status and headroom first, then poll
+ * the CPU and GPU headroom if the device is (about to) getting thermal throttled. If the CPU/GPU
+ * headrooms provide enough significance such as one valued at 0 while the other at 100, then it can
+ * be used to infer that reducing CPU workload could be more efficient to cool down the device.
+ * There is a caveat that the power controller may scale down the frequency of the CPU and GPU due
+ * to thermal and other reasons, which can result in a higher than usual percentage usage of the
+ * capacity.
  */
 @SystemService(Context.SYSTEM_HEALTH_SERVICE)
 public class SystemHealthManager {
+    private static final String TAG = "SystemHealthManager";
     @NonNull
     private final IBatteryStats mBatteryStats;
     @Nullable
     private final IPowerStatsService mPowerStats;
     @Nullable
     private final IHintManager mHintManager;
+    @Nullable
+    private final IHintManager.HintManagerClientData mHintManagerClientData;
     private List<PowerMonitor> mPowerMonitorsInfo;
     private final Object mPowerMonitorsLock = new Object();
     private static final long TAKE_UID_SNAPSHOT_TIMEOUT_MILLIS = 10_000;
@@ -109,29 +138,72 @@
         mBatteryStats = batteryStats;
         mPowerStats = powerStats;
         mHintManager = hintManager;
+        IHintManager.HintManagerClientData data = null;
+        if (mHintManager != null) {
+            try {
+                data = mHintManager.getClientData();
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to get hint manager client data", e);
+            }
+        }
+        mHintManagerClientData = data;
     }
 
     /**
-     * Provides an estimate of global available CPU headroom.
+     * Provides an estimate of available CPU capacity headroom of the device.
      * <p>
+     * The value can be used by the calling application to determine if the workload was CPU bound
+     * and then take action accordingly to ensure that the workload can be completed smoothly. It
+     * can also be used with the thermal status and headroom to determine if reducing the CPU bound
+     * workload can help reduce the device temperature to avoid thermal throttling.
+     * <p>
+     * If the params are valid, each call will perform at least one synchronous binder transaction
+     * that can take more than 1ms. So it's not recommended to call or wait for this on critical
+     * threads. Some devices may implement this as an on-demand API with lazy initialization, so the
+     * caller should expect higher latency when making the first call (especially with non-default
+     * params) since app starts or after changing params, as the device may need to change its data
+     * collection.
      *
-     * @param  params params to customize the CPU headroom calculation, null to use default params.
-     * @return a single value headroom or a {@code Float.NaN} if it's temporarily unavailable.
-     *         A valid value is ranged from [0, 100], where 0 indicates no more CPU resources can be
-     *         granted.
+     * @param  params params to customize the CPU headroom calculation, or null to use default.
+     * @return a single value headroom or a {@code Float.NaN} if it's temporarily unavailable due to
+     *         server error or not enough user CPU workload.
+     *         Each valid value ranges from [0, 100], where 0 indicates no more cpu resources can be
+     *         granted
      * @throws UnsupportedOperationException if the API is unsupported.
+     * @throws IllegalArgumentException if the params are invalid.
      * @throws SecurityException if the TIDs of the params don't belong to the same process.
      * @throws IllegalStateException if the TIDs of the params don't have the same affinity setting.
      */
     @FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
     public @FloatRange(from = 0f, to = 100f) float getCpuHeadroom(
             @Nullable CpuHeadroomParams params) {
-        if (mHintManager == null) {
+        if (mHintManager == null || mHintManagerClientData == null
+                || !mHintManagerClientData.supportInfo.headroom.isCpuSupported) {
             throw new UnsupportedOperationException();
         }
+        if (params != null) {
+            if (params.mInternal.tids != null && (params.mInternal.tids.length == 0
+                    || params.mInternal.tids.length
+                    > mHintManagerClientData.maxCpuHeadroomThreads)) {
+                throw new IllegalArgumentException(
+                        "Invalid number of TIDs: " + params.mInternal.tids.length);
+            }
+            if (params.mInternal.calculationWindowMillis
+                    < mHintManagerClientData.supportInfo.headroom.cpuMinCalculationWindowMillis
+                    || params.mInternal.calculationWindowMillis
+                    > mHintManagerClientData.supportInfo.headroom.cpuMaxCalculationWindowMillis) {
+                throw new IllegalArgumentException(
+                        "Invalid calculation window: "
+                        + params.mInternal.calculationWindowMillis + ", expect range: ["
+                        + mHintManagerClientData.supportInfo.headroom.cpuMinCalculationWindowMillis
+                        + ", "
+                        + mHintManagerClientData.supportInfo.headroom.cpuMaxCalculationWindowMillis
+                        + "]");
+            }
+        }
         try {
             final CpuHeadroomResult ret = mHintManager.getCpuHeadroom(
-                    params != null ? params.getInternal() : new CpuHeadroomParamsInternal());
+                    params != null ? params.mInternal : new CpuHeadroomParamsInternal());
             if (ret == null || ret.getTag() != CpuHeadroomResult.globalHeadroom) {
                 return Float.NaN;
             }
@@ -141,27 +213,69 @@
         }
     }
 
-
+    /**
+     * Gets the maximum number of TIDs this device supports for getting CPU headroom.
+     * <p>
+     * See {@link CpuHeadroomParams#setTids(int...)}.
+     *
+     * @return the maximum size of TIDs supported
+     * @throws UnsupportedOperationException if the CPU headroom API is unsupported.
+     */
+    @FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
+    public @IntRange(from = 1) int getMaxCpuHeadroomTidsSize() {
+        if (mHintManager == null || mHintManagerClientData == null
+                || !mHintManagerClientData.supportInfo.headroom.isCpuSupported) {
+            throw new UnsupportedOperationException();
+        }
+        return mHintManagerClientData.maxCpuHeadroomThreads;
+    }
 
     /**
-     * Provides an estimate of global available GPU headroom of the device.
+     * Provides an estimate of available GPU capacity headroom of the device.
      * <p>
+     * The value can be used by the calling application to determine if the workload was GPU bound
+     * and then take action accordingly to ensure that the workload can be completed smoothly. It
+     * can also be used with the thermal status and headroom to determine if reducing the GPU bound
+     * workload can help reduce the device temperature to avoid thermal throttling.
+     * <p>
+     * If the params are valid, each call will perform at least one synchronous binder transaction
+     * that can take more than 1ms. So it's not recommended to call or wait for this on critical
+     * threads. Some devices may implement this as an on-demand API with lazy initialization, so the
+     * caller should expect higher latency when making the first call (especially with non-default
+     * params) since app starts or after changing params, as the device may need to change its data
+     * collection.
      *
-     * @param  params params to customize the GPU headroom calculation, null to use default params.
+     * @param  params params to customize the GPU headroom calculation, or null to use default.
      * @return a single value headroom or a {@code Float.NaN} if it's temporarily unavailable.
-     *         A valid value is ranged from [0, 100], where 0 indicates no more GPU resources can be
+     *         Each valid value ranges from [0, 100], where 0 indicates no more cpu resources can be
      *         granted.
      * @throws UnsupportedOperationException if the API is unsupported.
+     * @throws IllegalArgumentException if the params are invalid.
      */
     @FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
     public @FloatRange(from = 0f, to = 100f) float getGpuHeadroom(
             @Nullable GpuHeadroomParams params) {
-        if (mHintManager == null) {
+        if (mHintManager == null || mHintManagerClientData == null
+                || !mHintManagerClientData.supportInfo.headroom.isGpuSupported) {
             throw new UnsupportedOperationException();
         }
+        if (params != null) {
+            if (params.mInternal.calculationWindowMillis
+                    < mHintManagerClientData.supportInfo.headroom.gpuMinCalculationWindowMillis
+                    || params.mInternal.calculationWindowMillis
+                    > mHintManagerClientData.supportInfo.headroom.gpuMaxCalculationWindowMillis) {
+                throw new IllegalArgumentException(
+                        "Invalid calculation window: "
+                        + params.mInternal.calculationWindowMillis + ", expect range: ["
+                        + mHintManagerClientData.supportInfo.headroom.gpuMinCalculationWindowMillis
+                        + ", "
+                        + mHintManagerClientData.supportInfo.headroom.gpuMaxCalculationWindowMillis
+                        + "]");
+            }
+        }
         try {
             final GpuHeadroomResult ret = mHintManager.getGpuHeadroom(
-                    params != null ? params.getInternal() : new GpuHeadroomParamsInternal());
+                    params != null ? params.mInternal : new GpuHeadroomParamsInternal());
             if (ret == null || ret.getTag() != GpuHeadroomResult.globalHeadroom) {
                 return Float.NaN;
             }
@@ -172,7 +286,51 @@
     }
 
     /**
-     * Minimum polling interval for calling {@link #getCpuHeadroom(CpuHeadroomParams)} in
+     * Gets the range of the calculation window size for CPU headroom.
+     * <p>
+     * In API version 36, the range will be a superset of [50, 10000].
+     * <p>
+     * See {@link CpuHeadroomParams#setCalculationWindowMillis(int)}.
+     *
+     * @return the range of the calculation window size supported in milliseconds.
+     * @throws UnsupportedOperationException if the CPU headroom API is unsupported.
+     */
+    @FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
+    @NonNull
+    public Pair<Integer, Integer> getCpuHeadroomCalculationWindowRange() {
+        if (mHintManager == null || mHintManagerClientData == null
+                || !mHintManagerClientData.supportInfo.headroom.isCpuSupported) {
+            throw new UnsupportedOperationException();
+        }
+        return new Pair<>(
+                mHintManagerClientData.supportInfo.headroom.cpuMinCalculationWindowMillis,
+                mHintManagerClientData.supportInfo.headroom.cpuMaxCalculationWindowMillis);
+    }
+
+    /**
+     * Gets the range of the calculation window size for GPU headroom.
+     * <p>
+     * In API version 36, the range will be a superset of [50, 10000].
+     * <p>
+     * See {@link GpuHeadroomParams#setCalculationWindowMillis(int)}.
+     *
+     * @return the range of the calculation window size supported in milliseconds.
+     * @throws UnsupportedOperationException if the GPU headroom API is unsupported.
+     */
+    @FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
+    @NonNull
+    public Pair<Integer, Integer> getGpuHeadroomCalculationWindowRange() {
+        if (mHintManager == null || mHintManagerClientData == null
+                || !mHintManagerClientData.supportInfo.headroom.isGpuSupported) {
+            throw new UnsupportedOperationException();
+        }
+        return new Pair<>(
+                mHintManagerClientData.supportInfo.headroom.gpuMinCalculationWindowMillis,
+                mHintManagerClientData.supportInfo.headroom.gpuMaxCalculationWindowMillis);
+    }
+
+    /**
+     * Gets minimum polling interval for calling {@link #getCpuHeadroom(CpuHeadroomParams)} in
      * milliseconds.
      * <p>
      * The {@link #getCpuHeadroom(CpuHeadroomParams)} API may return cached result if called more
@@ -182,7 +340,8 @@
      */
     @FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
     public long getCpuHeadroomMinIntervalMillis() {
-        if (mHintManager == null) {
+        if (mHintManager == null || mHintManagerClientData == null
+                || !mHintManagerClientData.supportInfo.headroom.isCpuSupported) {
             throw new UnsupportedOperationException();
         }
         try {
@@ -193,7 +352,7 @@
     }
 
     /**
-     * Minimum polling interval for calling {@link #getGpuHeadroom(GpuHeadroomParams)} in
+     * Gets minimum polling interval for calling {@link #getGpuHeadroom(GpuHeadroomParams)} in
      * milliseconds.
      * <p>
      * The {@link #getGpuHeadroom(GpuHeadroomParams)} API may return cached result if called more
@@ -203,7 +362,8 @@
      */
     @FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
     public long getGpuHeadroomMinIntervalMillis() {
-        if (mHintManager == null) {
+        if (mHintManager == null || mHintManagerClientData == null
+                || !mHintManagerClientData.supportInfo.headroom.isGpuSupported) {
             throw new UnsupportedOperationException();
         }
         try {
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index df12a68..baaaa46 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -6372,6 +6372,19 @@
                 "mouse_pointer_acceleration_enabled";
 
         /**
+         * Mouse scrolling speed setting.
+         *
+         * This is an integer value in a range between -7 and +7, so there are 15 possible values.
+         * The setting only applies when mouse scrolling acceleration is not enabled.
+         *   -7 = slowest
+         *    0 = default speed
+         *   +7 = fastest
+         *
+         * @hide
+         */
+        public static final String MOUSE_SCROLLING_SPEED = "mouse_scrolling_speed";
+
+        /**
          * Pointer fill style, specified by
          * {@link android.view.PointerIcon.PointerIconVectorStyleFill} constants.
          *
@@ -6623,6 +6636,7 @@
             PRIVATE_SETTINGS.add(MOUSE_POINTER_ACCELERATION_ENABLED);
             PRIVATE_SETTINGS.add(PREFERRED_REGION);
             PRIVATE_SETTINGS.add(MOUSE_SCROLLING_ACCELERATION);
+            PRIVATE_SETTINGS.add(MOUSE_SCROLLING_SPEED);
         }
 
         /**
@@ -9305,6 +9319,16 @@
                 "accessibility_autoclick_delay";
 
         /**
+         * Integer setting specifying the autoclick cursor area size (the radius of the autoclick
+         * ring indicator) when {@link #ACCESSIBILITY_AUTOCLICK_ENABLED} is set.
+         *
+         * @see #ACCESSIBILITY_AUTOCLICK_ENABLED
+         * @hide
+         */
+        public static final String ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE =
+                "accessibility_autoclick_cursor_area_size";
+
+        /**
          * Whether or not larger size icons are used for the pointer of mouse/trackpad for
          * accessibility.
          * (0 = false, 1 = true)
@@ -10501,6 +10525,15 @@
         public static final String SCREENSAVER_ACTIVATE_ON_SLEEP = "screensaver_activate_on_sleep";
 
         /**
+         * If screensavers are enabled, whether the screensaver should be
+         * automatically launched when the device is stationary and upright.
+         * @hide
+         */
+        @Readable
+        public static final String SCREENSAVER_ACTIVATE_ON_POSTURED =
+                "screensaver_activate_on_postured";
+
+        /**
          * If screensavers are enabled, the default screensaver component.
          * @hide
          */
@@ -12223,49 +12256,6 @@
                 "back_gesture_inset_scale_right";
 
         /**
-         * Indicates whether the trackpad back gesture is enabled.
-         * <p>Type: int (0 for false, 1 for true)
-         *
-         * @hide
-         */
-        public static final String TRACKPAD_GESTURE_BACK_ENABLED = "trackpad_gesture_back_enabled";
-
-        /**
-         * Indicates whether the trackpad home gesture is enabled.
-         * <p>Type: int (0 for false, 1 for true)
-         *
-         * @hide
-         */
-        public static final String TRACKPAD_GESTURE_HOME_ENABLED = "trackpad_gesture_home_enabled";
-
-        /**
-         * Indicates whether the trackpad overview gesture is enabled.
-         * <p>Type: int (0 for false, 1 for true)
-         *
-         * @hide
-         */
-        public static final String TRACKPAD_GESTURE_OVERVIEW_ENABLED =
-                "trackpad_gesture_overview_enabled";
-
-        /**
-         * Indicates whether the trackpad notification gesture is enabled.
-         * <p>Type: int (0 for false, 1 for true)
-         *
-         * @hide
-         */
-        public static final String TRACKPAD_GESTURE_NOTIFICATION_ENABLED =
-                "trackpad_gesture_notification_enabled";
-
-        /**
-         * Indicates whether the trackpad quick switch gesture is enabled.
-         * <p>Type: int (0 for false, 1 for true)
-         *
-         * @hide
-         */
-        public static final String TRACKPAD_GESTURE_QUICK_SWITCH_ENABLED =
-                "trackpad_gesture_quick_switch_enabled";
-
-        /**
          * Current provider of proximity-based sharing services.
          * Default value in @string/config_defaultNearbySharingComponent.
          * No VALIDATOR as this setting will not be backed up.
@@ -13347,6 +13337,16 @@
         public static final String AUTO_TIME_ZONE_EXPLICIT = "auto_time_zone_explicit";
 
         /**
+         * Value to specify if the device should send notifications when {@link #AUTO_TIME_ZONE} is
+         * on and the device's time zone changes.
+         *
+         * <p>1=yes, 0=no.
+         *
+         * @hide
+         */
+        public static final String TIME_ZONE_NOTIFICATIONS = "time_zone_notifications";
+
+        /**
          * URI for the car dock "in" event sound.
          * @hide
          */
@@ -17438,13 +17438,6 @@
 
 
         /**
-         * Whether back preview animations are played when user does a back gesture or presses
-         * the back button.
-         * @hide
-         */
-        public static final String ENABLE_BACK_ANIMATION = "enable_back_animation";
-
-        /**
          * An allow list of packages for which the user has granted the permission to communicate
          * across profiles.
          *
diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig
index ebb6fb4..4a9e945 100644
--- a/core/java/android/security/flags.aconfig
+++ b/core/java/android/security/flags.aconfig
@@ -42,6 +42,16 @@
 }
 
 flag {
+    name: "secure_array_zeroization"
+    namespace: "platform_security"
+    description: "Enable secure array zeroization"
+    bug: "320392352"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "deprecate_fsv_sig"
     namespace: "hardware_backed_security"
     description: "Feature flag for deprecating .fsv_sig"
diff --git a/core/java/android/service/dreams/flags.aconfig b/core/java/android/service/dreams/flags.aconfig
index dfc11dc..d3a230d 100644
--- a/core/java/android/service/dreams/flags.aconfig
+++ b/core/java/android/service/dreams/flags.aconfig
@@ -77,3 +77,10 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "allow_dream_when_postured"
+    namespace: "systemui"
+    description: "Allow dreaming when device is stationary and upright"
+    bug: "383208131"
+}
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index e254bf3..3c53506 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -16,7 +16,6 @@
 
 package android.text;
 
-import static com.android.graphics.hwui.flags.Flags.highContrastTextLuminance;
 import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE;
 import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION;
 import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH;
@@ -77,7 +76,7 @@
     private static final float HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR = 0f;
     private static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_DP = 5f;
     // since we're not using soft light yet, this needs to be much lower than the spec'd 0.8
-    private static final float HIGH_CONTRAST_TEXT_BACKGROUND_ALPHA_PERCENTAGE = 0.5f;
+    private static final float HIGH_CONTRAST_TEXT_BACKGROUND_ALPHA_PERCENTAGE = 0.7f;
 
     /** @hide */
     @IntDef(prefix = { "BREAK_STRATEGY_" }, value = {
@@ -670,15 +669,11 @@
         // High-contrast text mode
         // Determine if the text is black-on-white or white-on-black, so we know what blendmode will
         // give the highest contrast and most realistic text color.
-        // This equation should match the one in libs/hwui/hwui/DrawTextFunctor.h
-        if (highContrastTextLuminance()) {
-            var lab = new double[3];
-            ColorUtils.colorToLAB(color, lab);
-            return lab[0] < 50.0;
-        } else {
-            int channelSum = Color.red(color) + Color.green(color) + Color.blue(color);
-            return channelSum < (128 * 3);
-        }
+        // LINT.IfChange(hct_darken)
+        var lab = new double[3];
+        ColorUtils.colorToLAB(color, lab);
+        return lab[0] < 50.0;
+        // LINT.ThenChange(/libs/hwui/hwui/DrawTextFunctor.h:hct_darken)
     }
 
     private boolean isJustificationRequired(int lineNum) {
diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java
index d8a88b8..d313fc9 100644
--- a/core/java/android/util/FeatureFlagUtils.java
+++ b/core/java/android/util/FeatureFlagUtils.java
@@ -100,13 +100,6 @@
     public static final String SETTINGS_NEW_KEYBOARD_TRACKPAD = "settings_new_keyboard_trackpad";
 
     /**
-     * Enable trackpad gesture settings UI
-     * @hide
-     */
-    public static final String SETTINGS_NEW_KEYBOARD_TRACKPAD_GESTURE =
-            "settings_new_keyboard_trackpad_gesture";
-
-    /**
      * Enable the new pages which is implemented with SPA.
      * @hide
      */
@@ -211,7 +204,6 @@
         DEFAULT_FLAGS.put(SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST, "true");
         DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_MODIFIER_KEY, "true");
         DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_TRACKPAD, "true");
-        DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_TRACKPAD_GESTURE, "false");
         DEFAULT_FLAGS.put(SETTINGS_ENABLE_SPA, "true");
         DEFAULT_FLAGS.put(SETTINGS_ENABLE_SPA_PHASE2, "false");
         DEFAULT_FLAGS.put(SETTINGS_ENABLE_SPA_METRICS, "true");
@@ -237,7 +229,6 @@
         PERSISTENT_FLAGS.add(SETTINGS_APP_ALLOW_DARK_THEME_ACTIVATION_AT_BEDTIME);
         PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_MODIFIER_KEY);
         PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_TRACKPAD);
-        PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_TRACKPAD_GESTURE);
         PERSISTENT_FLAGS.add(SETTINGS_ENABLE_SPA);
         PERSISTENT_FLAGS.add(SETTINGS_ENABLE_SPA_PHASE2);
         PERSISTENT_FLAGS.add(SETTINGS_PREFER_ACCESSIBILITY_MENU_IN_SYSTEM);
diff --git a/core/java/android/util/SystemPropertySetter.java b/core/java/android/util/SystemPropertySetter.java
new file mode 100644
index 0000000..bf18f75
--- /dev/null
+++ b/core/java/android/util/SystemPropertySetter.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.SystemProperties;
+
+/**
+ * This class provides a single, static function to set a system property.  The function retries on
+ * error.  System properties should be reliable but there have been reports of failures in the set()
+ * command on lower-end devices.  Clients may want to use this method instead of calling
+ * {@link SystemProperties.set} directly.
+ * @hide
+ */
+public class SystemPropertySetter {
+
+    /**
+     * The default retryDelayMs for {@link #setWithRetry}.  This value has been found to give
+     * reasonable behavior in the field.
+     */
+    public static final int PROPERTY_FAILURE_RETRY_DELAY_MILLIS = 200;
+
+    /**
+     * The default retryLimit for {@link #setWithRetry}.  This value has been found to give
+     * reasonable behavior in the field.
+     */
+    public static final int PROPERTY_FAILURE_RETRY_LIMIT = 5;
+
+    /**
+     * Set the value for the given {@code key} to {@code val}.  This method retries using the
+     * standard parameters, above, if the native method throws a RuntimeException.
+     *
+     * @param key The name of the property to be set.
+     * @param val The new string value of the property.
+     * @throws IllegalArgumentException for non read-only properties if the {@code val} exceeds
+     * 91 characters.
+     * @throws RuntimeException if the property cannot be set, for example, if it was blocked by
+     * SELinux. libc will log the underlying reason.
+     */
+    public static void setWithRetry(@NonNull String key, @Nullable String val) {
+        setWithRetry(key, val,PROPERTY_FAILURE_RETRY_DELAY_MILLIS, PROPERTY_FAILURE_RETRY_LIMIT);
+    }
+
+    /**
+     * Set the value for the given {@code key} to {@code val}.  This method retries if the native
+     * method throws a RuntimeException.  If the {@code maxRetry} count is exceeded, the method
+     * throws the first RuntimeException that was seen.
+     *
+     * @param key The name of the property to be set.
+     * @param val The new string value of the property.
+     * @param maxRetry The maximum number of times; must be non-negative.
+     * @param retryDelayMs The number of milliseconds to wait between retries; must be positive.
+     * @throws IllegalArgumentException for non read-only properties if the {@code val} exceeds
+     * 91 characters, or if the retry parameters are invalid.
+     * @throws RuntimeException if the property cannot be set, for example, if it was blocked by
+     * SELinux. libc will log the underlying reason.
+     */
+    public static void setWithRetry(@NonNull String key, @Nullable String val, int maxRetry,
+            long retryDelayMs) {
+        if (maxRetry < 0) {
+            throw new IllegalArgumentException("invalid retry count: " + maxRetry);
+        }
+        if (retryDelayMs <= 0) {
+            throw new IllegalArgumentException("invalid retry delay: " + retryDelayMs);
+        }
+
+        RuntimeException failure = null;
+        for (int attempt = 0; attempt < maxRetry; attempt++) {
+            try {
+                SystemProperties.set(key, val);
+                return;
+            } catch (RuntimeException e) {
+                if (failure == null) {
+                    failure = e;
+                }
+                try {
+                    Thread.sleep(retryDelayMs);
+                } catch (InterruptedException x) {
+                    // Ignore this exception.  The desired delay is only approximate and
+                    // there is no issue if the sleep sometimes terminates early.
+                }
+            }
+        }
+        // This point is reached only if SystemProperties.set() fails at least once.
+        // Rethrow the first exception that was received.
+        throw failure;
+    }
+}
diff --git a/core/java/android/view/DragEvent.java b/core/java/android/view/DragEvent.java
index b65e3eb..77af312 100644
--- a/core/java/android/view/DragEvent.java
+++ b/core/java/android/view/DragEvent.java
@@ -157,6 +157,11 @@
     private float mOffsetY;
 
     /**
+     * The id of the display where the `mX` and `mY` of this event belongs to.
+     */
+    private int mDisplayId;
+
+    /**
      * The View#DRAG_FLAG_* flags used to start the current drag, only provided if the target window
      * has the {@link WindowManager.LayoutParams#PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP} flag
      * and is only sent with {@link #ACTION_DRAG_STARTED} and {@link #ACTION_DROP}.
@@ -297,14 +302,15 @@
     private DragEvent() {
     }
 
-    private void init(int action, float x, float y, float offsetX, float offsetY, int flags,
-            ClipDescription description, ClipData data, SurfaceControl dragSurface,
+    private void init(int action, float x, float y, float offsetX, float offsetY, int displayId,
+            int flags, ClipDescription description, ClipData data, SurfaceControl dragSurface,
             IDragAndDropPermissions dragAndDropPermissions, Object localState, boolean result) {
         mAction = action;
         mX = x;
         mY = y;
         mOffsetX = offsetX;
         mOffsetY = offsetY;
+        mDisplayId = displayId;
         mFlags = flags;
         mClipDescription = description;
         mClipData = data;
@@ -315,20 +321,20 @@
     }
 
     static DragEvent obtain() {
-        return DragEvent.obtain(0, 0f, 0f, 0f, 0f, 0, null, null, null, null, null, false);
+        return DragEvent.obtain(0, 0f, 0f, 0f, 0f, 0, 0, null, null, null, null, null, false);
     }
 
     /** @hide */
     public static DragEvent obtain(int action, float x, float y, float offsetX, float offsetY,
-            int flags, Object localState, ClipDescription description, ClipData data,
+            int displayId, int flags, Object localState, ClipDescription description, ClipData data,
             SurfaceControl dragSurface, IDragAndDropPermissions dragAndDropPermissions,
             boolean result) {
         final DragEvent ev;
         synchronized (gRecyclerLock) {
             if (gRecyclerTop == null) {
                 ev = new DragEvent();
-                ev.init(action, x, y, offsetX, offsetY, flags, description, data, dragSurface,
-                        dragAndDropPermissions, localState, result);
+                ev.init(action, x, y, offsetX, offsetY, displayId, flags, description, data,
+                        dragSurface, dragAndDropPermissions, localState, result);
                 return ev;
             }
             ev = gRecyclerTop;
@@ -339,7 +345,7 @@
         ev.mRecycled = false;
         ev.mNext = null;
 
-        ev.init(action, x, y, offsetX, offsetY, flags, description, data, dragSurface,
+        ev.init(action, x, y, offsetX, offsetY, displayId, flags, description, data, dragSurface,
                 dragAndDropPermissions, localState, result);
 
         return ev;
@@ -349,8 +355,9 @@
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public static DragEvent obtain(DragEvent source) {
         return obtain(source.mAction, source.mX, source.mY, source.mOffsetX, source.mOffsetY,
-                source.mFlags, source.mLocalState, source.mClipDescription, source.mClipData,
-                source.mDragSurface, source.mDragAndDropPermissions, source.mDragResult);
+                source.mDisplayId, source.mFlags, source.mLocalState, source.mClipDescription,
+                source.mClipData, source.mDragSurface, source.mDragAndDropPermissions,
+                source.mDragResult);
     }
 
     /**
@@ -398,6 +405,11 @@
         return mOffsetY;
     }
 
+    /** @hide */
+    public int getDisplayId() {
+        return mDisplayId;
+    }
+
     /**
      * Returns the {@link android.content.ClipData} object sent to the system as part of the call
      * to
diff --git a/core/java/android/view/InputEventConsistencyVerifier.java b/core/java/android/view/InputEventConsistencyVerifier.java
index 5c38a15..195896d 100644
--- a/core/java/android/view/InputEventConsistencyVerifier.java
+++ b/core/java/android/view/InputEventConsistencyVerifier.java
@@ -81,7 +81,7 @@
 
     // Bitfield of pointer ids that are currently down.
     // Assumes that the largest possible pointer id is 31, which is potentially subject to change.
-    // (See MAX_POINTER_ID in frameworks/base/include/ui/Input.h)
+    // (See MAX_POINTER_ID in frameworks/native/include/input/input.h)
     private int mTouchEventStreamPointers;
 
     // The device id and source of the current stream of touch events.
diff --git a/core/java/android/view/InputEventReceiver.java b/core/java/android/view/InputEventReceiver.java
index 2cc05b0..1c36eaf 100644
--- a/core/java/android/view/InputEventReceiver.java
+++ b/core/java/android/view/InputEventReceiver.java
@@ -177,7 +177,7 @@
      *                 drag
      *                 if true, the window associated with this input channel has just lost drag
      */
-    public void onDragEvent(boolean isExiting, float x, float y) {
+    public void onDragEvent(boolean isExiting, float x, float y, int displayId) {
     }
 
     /**
diff --git a/core/java/android/view/InputWindowHandle.java b/core/java/android/view/InputWindowHandle.java
index 6cd4a40..3e529cc 100644
--- a/core/java/android/view/InputWindowHandle.java
+++ b/core/java/android/view/InputWindowHandle.java
@@ -57,7 +57,7 @@
             InputConfig.NO_INPUT_CHANNEL,
             InputConfig.NOT_FOCUSABLE,
             InputConfig.NOT_TOUCHABLE,
-            InputConfig.PREVENT_SPLITTING,
+            InputConfig.DEPRECATED_PREVENT_SPLITTING,
             InputConfig.DUPLICATE_TOUCH_TO_WALLPAPER,
             InputConfig.IS_WALLPAPER,
             InputConfig.PAUSE_DISPATCHING,
diff --git a/core/java/android/view/PointerIcon.java b/core/java/android/view/PointerIcon.java
index b21e85a..da3a817f 100644
--- a/core/java/android/view/PointerIcon.java
+++ b/core/java/android/view/PointerIcon.java
@@ -514,10 +514,14 @@
             final TypedArray a = resources.obtainAttributes(
                     parser, com.android.internal.R.styleable.PointerIcon);
             bitmapRes = a.getResourceId(com.android.internal.R.styleable.PointerIcon_bitmap, 0);
-            hotSpotX = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotX, 0)
-                    * pointerScale;
-            hotSpotY = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotY, 0)
-                    * pointerScale;
+            // Cast the hotspot dimensions to int before scaling to match the scaling logic of
+            // the bitmap, whose intrinsic size is also an int before it is scaled.
+            final int unscaledHotSpotX =
+                    (int) a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotX, 0);
+            final int unscaledHotSpotY =
+                    (int) a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotY, 0);
+            hotSpotX = unscaledHotSpotX * pointerScale;
+            hotSpotY = unscaledHotSpotY * pointerScale;
             a.recycle();
         } catch (Exception ex) {
             throw new IllegalArgumentException("Exception parsing pointer icon resource.", ex);
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index 833f2d9..e665c08 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -160,6 +160,10 @@
             float l, float t, float r, float b);
     private static native void nativeSetCornerRadius(long transactionObj, long nativeObject,
             float cornerRadius);
+    private static native void nativeSetClientDrawnCornerRadius(long transactionObj,
+            long nativeObject, float clientDrawnCornerRadius);
+    private static native void nativeSetClientDrawnShadows(long transactionObj,
+            long nativeObject, float clientDrawnShadows);
     private static native void nativeSetBackgroundBlurRadius(long transactionObj, long nativeObject,
             int blurRadius);
     private static native void nativeSetLayerStack(long transactionObj, long nativeObject,
@@ -3654,6 +3658,66 @@
             return this;
         }
 
+
+        /**
+         * Disables corner radius of a {@link SurfaceControl}. When the radius set by
+         * {@link Transaction#setCornerRadius(SurfaceControl, float)} is equal to
+         * clientDrawnCornerRadius the corner radius drawn by SurfaceFlinger is disabled.
+         *
+         * @param sc SurfaceControl
+         * @param clientDrawnCornerRadius Corner radius drawn by the client
+         * @return Itself.
+         * @hide
+         */
+        @NonNull
+        public Transaction setClientDrawnCornerRadius(@NonNull SurfaceControl sc,
+                                                            float clientDrawnCornerRadius) {
+            checkPreconditions(sc);
+            if (SurfaceControlRegistry.sCallStackDebuggingEnabled) {
+                SurfaceControlRegistry.getProcessInstance().checkCallStackDebugging(
+                        "setClientDrawnCornerRadius", this, sc, "clientDrawnCornerRadius="
+                        + clientDrawnCornerRadius);
+            }
+            if (Flags.ignoreCornerRadiusAndShadows()) {
+                nativeSetClientDrawnCornerRadius(mNativeObject, sc.mNativeObject,
+                                                                clientDrawnCornerRadius);
+            } else {
+                Log.w(TAG, "setClientDrawnCornerRadius was called but"
+                            + "ignore_corner_radius_and_shadows flag is disabled");
+            }
+
+            return this;
+        }
+
+        /**
+         * Disables shadows of a {@link SurfaceControl}. When the radius set by
+         * {@link Transaction#setClientDrawnShadows(SurfaceControl, float)} is equal to
+         * clientDrawnShadowRadius the shadows drawn by SurfaceFlinger is disabled.
+         *
+         * @param sc SurfaceControl
+         * @param clientDrawnShadowRadius Shadow radius drawn by the client
+         * @return Itself.
+         * @hide
+         */
+        @NonNull
+        public Transaction setClientDrawnShadows(@NonNull SurfaceControl sc,
+                                                        float clientDrawnShadowRadius) {
+            checkPreconditions(sc);
+            if (SurfaceControlRegistry.sCallStackDebuggingEnabled) {
+                SurfaceControlRegistry.getProcessInstance().checkCallStackDebugging(
+                        "setClientDrawnShadows", this, sc,
+                        "clientDrawnShadowRadius=" + clientDrawnShadowRadius);
+            }
+            if (Flags.ignoreCornerRadiusAndShadows()) {
+                nativeSetClientDrawnShadows(mNativeObject, sc.mNativeObject,
+                                                        clientDrawnShadowRadius);
+            } else {
+                Log.w(TAG, "setClientDrawnShadows was called but"
+                            + "ignore_corner_radius_and_shadows flag is disabled");
+            }
+            return this;
+        }
+
         /**
          * Sets the background blur radius of the {@link SurfaceControl}.
          *
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 8ef0b0e..36671b90 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -10561,13 +10561,13 @@
         }
 
         @Override
-        public void onDragEvent(boolean isExiting, float x, float y) {
+        public void onDragEvent(boolean isExiting, float x, float y, int displayId) {
             // force DRAG_EXITED_EVENT if appropriate
             DragEvent event = DragEvent.obtain(
-                    isExiting ? DragEvent.ACTION_DRAG_EXITED : DragEvent.ACTION_DRAG_LOCATION,
-                    x, y, 0 /* offsetX */, 0 /* offsetY */, 0 /* flags */, null/* localState */,
-                    null/* description */, null /* data */, null /* dragSurface */,
-                    null /* dragAndDropPermissions */, false /* result */);
+                    isExiting ? DragEvent.ACTION_DRAG_EXITED : DragEvent.ACTION_DRAG_LOCATION, x, y,
+                    0 /* offsetX */, 0 /* offsetY */, displayId, 0 /* flags */,
+                    null/* localState */, null/* description */, null /* data */,
+                    null /* dragSurface */, null /* dragAndDropPermissions */, false /* result */);
             dispatchDragEvent(event);
         }
 
diff --git a/core/java/android/view/WindowInsetsController.java b/core/java/android/view/WindowInsetsController.java
index 19c98a2..cdfa7c81 100644
--- a/core/java/android/view/WindowInsetsController.java
+++ b/core/java/android/view/WindowInsetsController.java
@@ -34,6 +34,9 @@
 
 /**
  * Interface to control windows that generate insets.
+ *
+ * For guidance, see <a href="https://developer.android.com/develop/ui/views/layout/immersive">
+ * Hide system bars for immersive mode</a>.
  */
 public interface WindowInsetsController {
 
diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java
index 617e476..25bd713 100644
--- a/core/java/android/view/WindowManagerGlobal.java
+++ b/core/java/android/view/WindowManagerGlobal.java
@@ -50,7 +50,6 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.policy.PhoneWindow;
 import com.android.internal.util.FastPrintWriter;
 
 import java.io.FileDescriptor;
@@ -376,8 +375,7 @@
 
         if (context != null && wparams.type > LAST_APPLICATION_WINDOW) {
             final TypedArray styles = context.obtainStyledAttributes(R.styleable.Window);
-            if (PhoneWindow.isOptingOutEdgeToEdgeEnforcement(
-                    context.getApplicationInfo(), true /* local */, styles)) {
+            if (styles.getBoolean(R.styleable.Window_windowOptOutEdgeToEdgeEnforcement, false)) {
                 wparams.privateFlags |= PRIVATE_FLAG_OPT_OUT_EDGE_TO_EDGE;
             }
             styles.recycle();
@@ -455,6 +453,7 @@
             try {
                 root.setView(view, wparams, panelParentView, userId);
             } catch (RuntimeException e) {
+                Log.e(TAG, "Couldn't add view: " + view, e);
                 final int viewIndex = (index >= 0) ? index : (mViews.size() - 1);
                 // BadTokenException or InvalidDisplayException, clean up.
                 if (viewIndex >= 0) {
diff --git a/core/java/android/view/WindowManagerPolicyConstants.java b/core/java/android/view/WindowManagerPolicyConstants.java
index 1f341ca..6d2c0d00 100644
--- a/core/java/android/view/WindowManagerPolicyConstants.java
+++ b/core/java/android/view/WindowManagerPolicyConstants.java
@@ -30,7 +30,8 @@
  * @hide
  */
 public interface WindowManagerPolicyConstants {
-    // Policy flags.  These flags are also defined in frameworks/base/include/ui/Input.h and
+    // Policy flags. These flags are also defined in
+    // frameworks/native/include/input/Input.h and
     // frameworks/native/libs/input/android/os/IInputConstants.aidl
     int FLAG_WAKE = 0x00000001;
     int FLAG_VIRTUAL = 0x00000002;
diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java
index fd57aec..64277b1 100644
--- a/core/java/android/view/accessibility/AccessibilityManager.java
+++ b/core/java/android/view/accessibility/AccessibilityManager.java
@@ -148,6 +148,18 @@
     /** @hide */
     public static final int AUTOCLICK_DELAY_DEFAULT = 600;
 
+    /** @hide */
+    public static final int AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT = 60;
+
+    /** @hide */
+    public static final int AUTOCLICK_CURSOR_AREA_SIZE_MIN = 20;
+
+    /** @hide */
+    public static final int AUTOCLICK_CURSOR_AREA_SIZE_MAX = 100;
+
+    /** @hide */
+    public static final int AUTOCLICK_CURSOR_AREA_INCREMENT_SIZE = 20;
+
     /**
      * Activity action: Launch UI to manage which accessibility service or feature is assigned
      * to the navigation bar Accessibility button.
diff --git a/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig b/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig
index b3bd92b..c871d56 100644
--- a/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig
+++ b/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig
@@ -45,3 +45,10 @@
     description: "If true, the APIs to manage content protection device policy will be enabled."
     bug: "319477846"
 }
+
+flag {
+    name: "exported_settings_activity_enabled"
+    namespace: "content_protection"
+    description: "If true, the content protection Settings Activity will be exported for launching externally."
+    bug: "385310141"
+}
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 71a832d..99fe0cb 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -18,7 +18,6 @@
 
 import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static android.graphics.Paint.NEW_FONT_VARIATION_MANAGEMENT;
 import static android.view.ContentInfo.FLAG_CONVERT_TO_PLAIN_TEXT;
 import static android.view.ContentInfo.SOURCE_AUTOFILL;
 import static android.view.ContentInfo.SOURCE_CLIPBOARD;
@@ -5544,13 +5543,32 @@
             return true;
         }
 
-        final boolean useFontVariationStore = Flags.typefaceRedesignReadonly()
-                && CompatChanges.isChangeEnabled(NEW_FONT_VARIATION_MANAGEMENT);
         boolean effective;
-        if (useFontVariationStore) {
+        if (Flags.typefaceRedesignReadonly()) {
             if (mFontWeightAdjustment != 0
                     && mFontWeightAdjustment != Configuration.FONT_WEIGHT_ADJUSTMENT_UNDEFINED) {
-                mTextPaint.setFontVariationSettings(fontVariationSettings, mFontWeightAdjustment);
+                List<FontVariationAxis> axes = FontVariationAxis.fromFontVariationSettingsForList(
+                        fontVariationSettings);
+                if (axes == null) {
+                    return false;  // invalid format of the font variation settings.
+                }
+                boolean wghtAdjusted = false;
+                for (int i = 0; i < axes.size(); ++i) {
+                    FontVariationAxis axis = axes.get(i);
+                    if (axis.getOpenTypeTagValue() == 0x77676874 /* wght */) {
+                        axes.set(i, new FontVariationAxis("wght",
+                                Math.clamp(axis.getStyleValue() + mFontWeightAdjustment,
+                                        FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX)));
+                        wghtAdjusted = true;
+                    }
+                }
+                if (!wghtAdjusted) {
+                    axes.add(new FontVariationAxis("wght",
+                            Math.clamp(400 + mFontWeightAdjustment,
+                                    FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX)));
+                }
+                mTextPaint.setFontVariationSettings(
+                        FontVariationAxis.toFontVariationSettings(axes));
             } else {
                 mTextPaint.setFontVariationSettings(fontVariationSettings);
             }
diff --git a/core/java/android/widget/flags/flags.aconfig b/core/java/android/widget/flags/flags.aconfig
index 88eb043..83a4c86 100644
--- a/core/java/android/widget/flags/flags.aconfig
+++ b/core/java/android/widget/flags/flags.aconfig
@@ -2,7 +2,7 @@
 container: "system"
 flag {
   name: "enable_fading_view_group"
-  namespace: "system_performance"
+  namespace: "wear_systems"
   description: "FRP screen during OOBE must have fading and scaling animation in Wear Watches"
   bug: "348515581"
   metadata {
diff --git a/core/java/android/window/DisplayAreaOrganizer.java b/core/java/android/window/DisplayAreaOrganizer.java
index 84ce247..bd711fc 100644
--- a/core/java/android/window/DisplayAreaOrganizer.java
+++ b/core/java/android/window/DisplayAreaOrganizer.java
@@ -121,6 +121,14 @@
     public static final int FEATURE_WINDOWING_LAYER = FEATURE_SYSTEM_FIRST + 9;
 
     /**
+     * Display area for rendering app zoom out. When there are multiple layers on the screen,
+     * we want to render these layers based on a depth model. Here we zoom out the layer behind,
+     * whether it's an app or the homescreen.
+     * @hide
+     */
+    public static final int FEATURE_APP_ZOOM_OUT = FEATURE_SYSTEM_FIRST + 10;
+
+    /**
      * The last boundary of display area for system features
      */
     public static final int FEATURE_SYSTEM_LAST = 10_000;
diff --git a/core/java/android/window/WindowInfosListenerForTest.java b/core/java/android/window/WindowInfosListenerForTest.java
index ac9bec3..6461f2a 100644
--- a/core/java/android/window/WindowInfosListenerForTest.java
+++ b/core/java/android/window/WindowInfosListenerForTest.java
@@ -103,12 +103,6 @@
         public final boolean isFocusable;
 
         /**
-         * True if the window is preventing splitting
-         */
-        @SuppressLint("UnflaggedApi") // The API is only used for tests.
-        public final boolean isPreventSplitting;
-
-        /**
          * True if the window duplicates touches received to wallpaper.
          */
         @SuppressLint("UnflaggedApi") // The API is only used for tests.
@@ -133,8 +127,6 @@
             this.transform = transform;
             this.isTouchable = (inputConfig & InputConfig.NOT_TOUCHABLE) == 0;
             this.isFocusable = (inputConfig & InputConfig.NOT_FOCUSABLE) == 0;
-            this.isPreventSplitting = (inputConfig
-                            & InputConfig.PREVENT_SPLITTING) != 0;
             this.isDuplicateTouchToWallpaper = (inputConfig
                             & InputConfig.DUPLICATE_TOUCH_TO_WALLPAPER) != 0;
             this.isWatchOutsideTouch = (inputConfig
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index 20e3f6b..2911b0a 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -464,7 +464,12 @@
      * Returns false if the legacy back behavior should be used.
      */
     public boolean isOnBackInvokedCallbackEnabled() {
-        return isOnBackInvokedCallbackEnabled(mChecker.getContext());
+        final Context hostContext = mChecker.getContext();
+        if (hostContext == null) {
+            Log.w(TAG, "OnBackInvokedCallback is disabled, host context is removed!");
+            return false;
+        }
+        return isOnBackInvokedCallbackEnabled(hostContext);
     }
 
     /**
@@ -695,7 +700,12 @@
          */
         public boolean checkApplicationCallbackRegistration(int priority,
                 OnBackInvokedCallback callback) {
-            if (!WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(getContext())
+            final Context hostContext = getContext();
+            if (hostContext == null) {
+                Log.w(TAG, "OnBackInvokedCallback is disabled, host context is removed!");
+                return false;
+            }
+            if (!WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(hostContext)
                     && !(callback instanceof CompatOnBackInvokedCallback)) {
                 Log.w(TAG,
                         "OnBackInvokedCallback is not enabled for the application."
@@ -720,7 +730,7 @@
             return true;
         }
 
-        private Context getContext() {
+        @Nullable private Context getContext() {
             return mContext.get();
         }
     }
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index ccb1e2b..be0b4fe 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -478,10 +478,10 @@
 }
 
 flag {
-    name: "enable_multiple_desktops"
+    name: "enable_multiple_desktops_frontend"
     namespace: "lse_desktop_experience"
-    description: "Enable multiple desktop sessions for desktop windowing."
-    bug: "379158791"
+    description: "Enable multiple desktop sessions for desktop windowing (frontend)."
+    bug: "362720309"
 }
 
 flag {
@@ -531,8 +531,11 @@
 }
 
 flag {
-    name: "enable_desktop_wallpaper_activity_on_system_user"
+    name: "enable_desktop_wallpaper_activity_for_system_user"
     namespace: "lse_desktop_experience"
     description: "Enables starting DesktopWallpaperActivity on system user."
     bug: "385294350"
+    metadata {
+       purpose: PURPOSE_BUGFIX
+    }
 }
\ No newline at end of file
diff --git a/core/java/android/window/flags/window_surfaces.aconfig b/core/java/android/window/flags/window_surfaces.aconfig
index bb47707..8ff2e6a 100644
--- a/core/java/android/window/flags/window_surfaces.aconfig
+++ b/core/java/android/window/flags/window_surfaces.aconfig
@@ -91,6 +91,14 @@
 }
 
 flag {
+  name: "ignore_corner_radius_and_shadows"
+  namespace: "window_surfaces"
+  description: "Ignore the corner radius and shadows of a SurfaceControl"
+  bug: "375624570"
+  is_fixed_read_only: true
+} # ignore_corner_radius_and_shadows
+
+flag {
     name: "jank_api"
     namespace: "window_surfaces"
     description: "Adds the jank data listener to AttachedSurfaceControl"
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 091f86e..7a1078f 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -113,13 +113,6 @@
 }
 
 flag {
-    name: "predictive_back_system_anims"
-    namespace: "systemui"
-    description: "Predictive back for system animations"
-    bug: "320510464"
-}
-
-flag {
     name: "remove_activity_starter_dream_callback"
     namespace: "windowing_frontend"
     description: "Avoid a race with DreamManagerService callbacks for isDreaming by checking Activity state directly"
@@ -422,8 +415,19 @@
 }
 
 flag {
+    name: "keep_app_window_hide_while_locked"
+    namespace: "windowing_frontend"
+    description: "Do not let app window visible while device is locked"
+    is_fixed_read_only: true
+    bug: "378088391"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "port_window_size_animation"
-    namespace: "systemui"
+    namespace: "windowing_frontend"
     description: "Port window-resize animation from legacy to shell"
     bug: "384976265"
 }
diff --git a/core/java/com/android/internal/accessibility/util/AccessibilityUtils.java b/core/java/com/android/internal/accessibility/util/AccessibilityUtils.java
index 0b1ecf7..d03bb5c 100644
--- a/core/java/com/android/internal/accessibility/util/AccessibilityUtils.java
+++ b/core/java/com/android/internal/accessibility/util/AccessibilityUtils.java
@@ -29,6 +29,7 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
 import android.os.Build;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -351,4 +352,24 @@
         }
         return result;
     }
+
+    /** Returns the {@link ComponentName} of an installed accessibility service by label. */
+    @Nullable
+    public static ComponentName getInstalledAccessibilityServiceComponentNameByLabel(
+            Context context, String label) {
+        AccessibilityManager accessibilityManager =
+                context.getSystemService(AccessibilityManager.class);
+        List<AccessibilityServiceInfo> serviceInfos =
+                accessibilityManager.getInstalledAccessibilityServiceList();
+
+        for (AccessibilityServiceInfo service : serviceInfos) {
+            final ServiceInfo serviceInfo = service.getResolveInfo().serviceInfo;
+            if (label.equals(serviceInfo.loadLabel(context.getPackageManager()).toString())
+                    && (serviceInfo.applicationInfo.isSystemApp()
+                            || serviceInfo.applicationInfo.isUpdatedSystemApp())) {
+                return new ComponentName(serviceInfo.packageName, serviceInfo.name);
+            }
+        }
+        return null;
+    }
 }
diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java
index fc41537..d1adfc9 100644
--- a/core/java/com/android/internal/jank/Cuj.java
+++ b/core/java/com/android/internal/jank/Cuj.java
@@ -246,8 +246,19 @@
      */
     public static final int CUJ_DESKTOP_MODE_UNMAXIMIZE_WINDOW = 119;
 
+    /**
+     * Track moving overview task to desktop interaction from overview menu.
+     *
+     * <p> Tracking starts when the overview task is moved to desktop via the overview menu.
+     * Tracking finishes when successfully made a call to `IDesktopMode.moveToDesktop`,
+     * without waiting for transition completion.
+     * </p>
+     * TODO(b/387471509): Update the CUJ to wait for transition completion.
+     */
+    public static final int CUJ_DESKTOP_MODE_ENTER_FROM_OVERVIEW_MENU = 120;
+
     // When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE.
-    @VisibleForTesting static final int LAST_CUJ = CUJ_DESKTOP_MODE_UNMAXIMIZE_WINDOW;
+    @VisibleForTesting static final int LAST_CUJ = CUJ_DESKTOP_MODE_ENTER_FROM_OVERVIEW_MENU;
 
     /** @hide */
     @IntDef({
@@ -358,7 +369,8 @@
             CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE,
             CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE,
             CUJ_DESKTOP_MODE_SNAP_RESIZE,
-            CUJ_DESKTOP_MODE_UNMAXIMIZE_WINDOW
+            CUJ_DESKTOP_MODE_UNMAXIMIZE_WINDOW,
+            CUJ_DESKTOP_MODE_ENTER_FROM_OVERVIEW_MENU
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {}
@@ -480,6 +492,7 @@
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE;
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_SNAP_RESIZE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_SNAP_RESIZE;
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_UNMAXIMIZE_WINDOW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_UNMAXIMIZE_WINDOW;
+        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_ENTER_FROM_OVERVIEW_MENU] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_ENTER_FROM_OVERVIEW_MENU;
     }
 
     private Cuj() {
@@ -714,6 +727,8 @@
                 return "DESKTOP_MODE_SNAP_RESIZE";
             case CUJ_DESKTOP_MODE_UNMAXIMIZE_WINDOW:
                 return "DESKTOP_MODE_UNMAXIMIZE_WINDOW";
+            case CUJ_DESKTOP_MODE_ENTER_FROM_OVERVIEW_MENU:
+                return "DESKTOP_MODE_ENTER_FROM_OVERVIEW_MENU";
         }
         return "UNKNOWN";
     }
diff --git a/core/java/com/android/internal/notification/SystemNotificationChannels.java b/core/java/com/android/internal/notification/SystemNotificationChannels.java
index 4aebde5..972c2ea 100644
--- a/core/java/com/android/internal/notification/SystemNotificationChannels.java
+++ b/core/java/com/android/internal/notification/SystemNotificationChannels.java
@@ -49,6 +49,7 @@
     public static final String NETWORK_ALERTS = "NETWORK_ALERTS";
     public static final String NETWORK_AVAILABLE = "NETWORK_AVAILABLE";
     public static final String VPN = "VPN";
+    public static final String TIME = "TIME";
     /**
      * @deprecated Legacy device admin channel with low importance which is no longer used,
      *  Use the high importance {@link #DEVICE_ADMIN} channel instead.
@@ -67,6 +68,7 @@
     @Deprecated public static final String SYSTEM_CHANGES_DEPRECATED = "SYSTEM_CHANGES";
     public static final String SYSTEM_CHANGES = "SYSTEM_CHANGES_ALERTS";
     public static final String ACCESSIBILITY_MAGNIFICATION = "ACCESSIBILITY_MAGNIFICATION";
+    public static final String ACCESSIBILITY_HEARING_DEVICE = "ACCESSIBILITY_HEARING_DEVICE";
     public static final String ACCESSIBILITY_SECURITY_POLICY = "ACCESSIBILITY_SECURITY_POLICY";
     public static final String ABUSIVE_BACKGROUND_APPS = "ABUSIVE_BACKGROUND_APPS";
 
@@ -145,6 +147,12 @@
                 NotificationManager.IMPORTANCE_LOW);
         channelsList.add(vpn);
 
+        final NotificationChannel time = new NotificationChannel(
+                TIME,
+                context.getString(R.string.notification_channel_system_time),
+                NotificationManager.IMPORTANCE_DEFAULT);
+        channelsList.add(time);
+
         final NotificationChannel deviceAdmin = new NotificationChannel(
                 DEVICE_ADMIN,
                 getDeviceAdminNotificationChannelName(context),
@@ -203,6 +211,13 @@
         newFeaturePrompt.setBlockable(true);
         channelsList.add(newFeaturePrompt);
 
+        final NotificationChannel accessibilityHearingDeviceChannel = new NotificationChannel(
+                ACCESSIBILITY_HEARING_DEVICE,
+                context.getString(R.string.notification_channel_accessibility_hearing_device),
+                NotificationManager.IMPORTANCE_HIGH);
+        accessibilityHearingDeviceChannel.setBlockable(true);
+        channelsList.add(accessibilityHearingDeviceChannel);
+
         final NotificationChannel accessibilitySecurityPolicyChannel = new NotificationChannel(
                 ACCESSIBILITY_SECURITY_POLICY,
                 context.getString(R.string.notification_channel_accessibility_security_policy),
diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java
index c9c4be1..dc440e3 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -19,6 +19,7 @@
 import static android.os.BatteryStats.HistoryItem.EVENT_FLAG_FINISH;
 import static android.os.BatteryStats.HistoryItem.EVENT_FLAG_START;
 import static android.os.BatteryStats.HistoryItem.EVENT_STATE_CHANGE;
+import static android.os.Trace.TRACE_TAG_SYSTEM_SERVER;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -215,6 +216,7 @@
     private final ArraySet<PowerStats.Descriptor> mWrittenPowerStatsDescriptors = new ArraySet<>();
     private byte mLastHistoryStepLevel = 0;
     private boolean mMutable = true;
+    private int mIteratorCookie;
     private final BatteryStatsHistory mWritableHistory;
 
     private static class BatteryHistoryFile implements Comparable<BatteryHistoryFile> {
@@ -289,6 +291,7 @@
         }
 
         void load() {
+            Trace.asyncTraceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0);
             mDirectory.mkdirs();
             if (!mDirectory.exists()) {
                 Slog.wtf(TAG, "HistoryDir does not exist:" + mDirectory.getPath());
@@ -325,8 +328,11 @@
                         }
                     } finally {
                         unlock();
+                        Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0);
                     }
                 });
+            } else {
+                Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0);
             }
         }
 
@@ -418,6 +424,7 @@
         }
 
         void writeToParcel(Parcel out, boolean useBlobs) {
+            Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.writeToParcel");
             lock();
             try {
                 final long start = SystemClock.uptimeMillis();
@@ -443,6 +450,7 @@
                 }
             } finally {
                 unlock();
+                Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
             }
         }
 
@@ -482,34 +490,39 @@
         }
 
         private void cleanup() {
-            if (mDirectory == null) {
-                return;
-            }
-
-            if (!tryLock()) {
-                mCleanupNeeded = true;
-                return;
-            }
-
-            mCleanupNeeded = false;
+            Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.cleanup");
             try {
-                // if free disk space is less than 100MB, delete oldest history file.
-                if (!hasFreeDiskSpace(mDirectory)) {
-                    BatteryHistoryFile oldest = mHistoryFiles.remove(0);
-                    oldest.atomicFile.delete();
+                if (mDirectory == null) {
+                    return;
                 }
 
-                // if there is more history stored than allowed, delete oldest history files.
-                int size = getSize();
-                while (size > mMaxHistorySize) {
-                    BatteryHistoryFile oldest = mHistoryFiles.get(0);
-                    int length = (int) oldest.atomicFile.getBaseFile().length();
-                    oldest.atomicFile.delete();
-                    mHistoryFiles.remove(0);
-                    size -= length;
+                if (!tryLock()) {
+                    mCleanupNeeded = true;
+                    return;
+                }
+
+                mCleanupNeeded = false;
+                try {
+                    // if free disk space is less than 100MB, delete oldest history file.
+                    if (!hasFreeDiskSpace(mDirectory)) {
+                        BatteryHistoryFile oldest = mHistoryFiles.remove(0);
+                        oldest.atomicFile.delete();
+                    }
+
+                    // if there is more history stored than allowed, delete oldest history files.
+                    int size = getSize();
+                    while (size > mMaxHistorySize) {
+                        BatteryHistoryFile oldest = mHistoryFiles.get(0);
+                        int length = (int) oldest.atomicFile.getBaseFile().length();
+                        oldest.atomicFile.delete();
+                        mHistoryFiles.remove(0);
+                        size -= length;
+                    }
+                } finally {
+                    unlock();
                 }
             } finally {
-                unlock();
+                Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
             }
         }
     }
@@ -710,13 +723,18 @@
      * in the system directory, so it is not safe while actively writing history.
      */
     public BatteryStatsHistory copy() {
-        synchronized (this) {
-            // Make a copy of battery history to avoid concurrent modification.
-            Parcel historyBufferCopy = Parcel.obtain();
-            historyBufferCopy.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize());
+        Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.copy");
+        try {
+            synchronized (this) {
+                // Make a copy of battery history to avoid concurrent modification.
+                Parcel historyBufferCopy = Parcel.obtain();
+                historyBufferCopy.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize());
 
-            return new BatteryStatsHistory(historyBufferCopy, mSystemDir, 0, 0, null, null, null,
-                    null, mEventLogger, this);
+                return new BatteryStatsHistory(historyBufferCopy, mSystemDir, 0, 0, null, null,
+                        null, null, mEventLogger, this);
+            }
+        } finally {
+            Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
         }
     }
 
@@ -826,7 +844,7 @@
      */
     @NonNull
     public BatteryStatsHistoryIterator iterate(long startTimeMs, long endTimeMs) {
-        if (mMutable) {
+        if (mMutable || mIteratorCookie != 0) {
             return copy().iterate(startTimeMs, endTimeMs);
         }
 
@@ -837,7 +855,12 @@
         mCurrentParcel = null;
         mCurrentParcelEnd = 0;
         mParcelIndex = 0;
-        return new BatteryStatsHistoryIterator(this, startTimeMs, endTimeMs);
+        BatteryStatsHistoryIterator iterator = new BatteryStatsHistoryIterator(
+                this, startTimeMs, endTimeMs);
+        mIteratorCookie = System.identityHashCode(iterator);
+        Trace.asyncTraceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.iterate",
+                mIteratorCookie);
+        return iterator;
     }
 
     /**
@@ -848,6 +871,9 @@
         if (mHistoryDir != null) {
             mHistoryDir.unlock();
         }
+        Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.iterate",
+                mIteratorCookie);
+        mIteratorCookie = 0;
     }
 
     /**
@@ -949,28 +975,33 @@
      * @return true if success, false otherwise.
      */
     public boolean readFileToParcel(Parcel out, AtomicFile file) {
-        byte[] raw = null;
+        Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.read");
         try {
-            final long start = SystemClock.uptimeMillis();
-            raw = file.readFully();
-            if (DEBUG) {
-                Slog.d(TAG, "readFileToParcel:" + file.getBaseFile().getPath()
-                        + " duration ms:" + (SystemClock.uptimeMillis() - start));
+            byte[] raw = null;
+            try {
+                final long start = SystemClock.uptimeMillis();
+                raw = file.readFully();
+                if (DEBUG) {
+                    Slog.d(TAG, "readFileToParcel:" + file.getBaseFile().getPath()
+                            + " duration ms:" + (SystemClock.uptimeMillis() - start));
+                }
+            } catch (Exception e) {
+                Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e);
+                return false;
             }
-        } catch (Exception e) {
-            Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e);
-            return false;
+            out.unmarshall(raw, 0, raw.length);
+            out.setDataPosition(0);
+            if (!verifyVersion(out)) {
+                return false;
+            }
+            // skip monotonic time field.
+            out.readLong();
+            // skip monotonic size field
+            out.readLong();
+            return true;
+        } finally {
+            Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
         }
-        out.unmarshall(raw, 0, raw.length);
-        out.setDataPosition(0);
-        if (!verifyVersion(out)) {
-            return false;
-        }
-        // skip monotonic time field.
-        out.readLong();
-        // skip monotonic size field
-        out.readLong();
-        return true;
     }
 
     /**
diff --git a/core/java/com/android/internal/policy/IKeyguardService.aidl b/core/java/com/android/internal/policy/IKeyguardService.aidl
index d62c8f3..73c2265 100644
--- a/core/java/com/android/internal/policy/IKeyguardService.aidl
+++ b/core/java/com/android/internal/policy/IKeyguardService.aidl
@@ -53,21 +53,21 @@
      *
      * @param pmSleepReason One of PowerManager.GO_TO_SLEEP_REASON_*, detailing the specific reason
      * we're going to sleep, such as GO_TO_SLEEP_REASON_POWER_BUTTON or GO_TO_SLEEP_REASON_TIMEOUT.
-     * @param cameraGestureTriggered whether the camera gesture was triggered between
-     *                               {@link #onStartedGoingToSleep} and this method; if it's been
-     *                               triggered, we shouldn't lock the device.
+     * @param powerButtonLaunchGestureTriggered whether the power button double tap gesture was
+     *                               triggered between {@link #onStartedGoingToSleep} and this
+     *                               method; if it's been triggered, we shouldn't lock the device.
      */
-    void onFinishedGoingToSleep(int pmSleepReason, boolean cameraGestureTriggered);
+    void onFinishedGoingToSleep(int pmSleepReason, boolean powerButtonLaunchGestureTriggered);
 
     /**
      * Called when the device has started waking up.
 
      * @param pmWakeReason One of PowerManager.WAKE_REASON_*, detailing the reason we're waking up,
      * such as WAKE_REASON_POWER_BUTTON or WAKE_REASON_GESTURE.
-     * @param cameraGestureTriggered Whether we're waking up due to a power button double tap
-     * gesture.
+     * @param powerButtonLaunchGestureTriggered Whether we're waking up due to a power button
+     * double tap gesture.
      */
-    void onStartedWakingUp(int pmWakeReason,  boolean cameraGestureTriggered);
+    void onStartedWakingUp(int pmWakeReason,  boolean powerButtonLaunchGestureTriggered);
 
     /**
      * Called when the device has finished waking up.
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index 67e358a4..e0529b3 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -184,14 +184,6 @@
     private static final long ENFORCE_EDGE_TO_EDGE = 309578419;
 
     /**
-     * Disable opting out the edge-to-edge enforcement.
-     * {@link Build.VERSION_CODES#BAKLAVA} or above.
-     */
-    @ChangeId
-    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.BAKLAVA)
-    private static final long DISABLE_OPT_OUT_EDGE_TO_EDGE = 377864165;
-
-    /**
      * Override the layout in display cutout mode behavior. This will only apply if the edge to edge
      * is not enforced.
      */
@@ -458,7 +450,7 @@
      */
     public static boolean isEdgeToEdgeEnforced(ApplicationInfo info, boolean local,
             TypedArray windowStyle) {
-        return !isOptingOutEdgeToEdgeEnforcement(info, local, windowStyle)
+        return !windowStyle.getBoolean(R.styleable.Window_windowOptOutEdgeToEdgeEnforcement, false)
                 && (info.targetSdkVersion >= ENFORCE_EDGE_TO_EDGE_SDK_VERSION
                         || (Flags.enforceEdgeToEdge() && (local
                                 // Calling this doesn't require a permission.
@@ -467,26 +459,6 @@
                                 : info.isChangeEnabled(ENFORCE_EDGE_TO_EDGE))));
     }
 
-    /**
-     * Returns whether the given application is opting out edge-to-edge enforcement.
-     *
-     * @param info The application to query.
-     * @param local Whether this is called from the process of the given application.
-     * @param windowStyle The style of the window.
-     * @return {@code true} if the edge-to-edge enforcement is opting out. Otherwise, {@code false}.
-     */
-    public static boolean isOptingOutEdgeToEdgeEnforcement(ApplicationInfo info, boolean local,
-            TypedArray windowStyle) {
-        final boolean disabled = (Flags.disableOptOutEdgeToEdge() && (local
-                // Calling this doesn't require a permission.
-                ? CompatChanges.isChangeEnabled(DISABLE_OPT_OUT_EDGE_TO_EDGE)
-                // Calling this requires permissions.
-                : info.isChangeEnabled(DISABLE_OPT_OUT_EDGE_TO_EDGE)));
-        return !disabled && windowStyle.getBoolean(
-                R.styleable.Window_windowOptOutEdgeToEdgeEnforcement, false /* default */);
-
-    }
-
     @Override
     public final void setContainer(Window container) {
         super.setContainer(container);
@@ -2514,7 +2486,6 @@
 
         TypedArray a = getWindowStyle();
         WindowManager.LayoutParams params = getAttributes();
-        ApplicationInfo appInfo = getContext().getApplicationInfo();
 
         if (false) {
             System.out.println("From style:");
@@ -2526,7 +2497,8 @@
             System.out.println(s);
         }
 
-        mEdgeToEdgeEnforced = isEdgeToEdgeEnforced(appInfo, true /* local */, a);
+        mEdgeToEdgeEnforced = isEdgeToEdgeEnforced(
+                getContext().getApplicationInfo(), true /* local */, a);
         if (mEdgeToEdgeEnforced) {
             getAttributes().privateFlags |= PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED;
             mDecorFitsSystemWindows = false;
@@ -2535,7 +2507,8 @@
             // mNavigationBarColor is not reset here because it might be used to draw the scrim.
         }
         if (CompatChanges.isChangeEnabled(OVERRIDE_LAYOUT_IN_DISPLAY_CUTOUT_MODE)
-                && !isOptingOutEdgeToEdgeEnforcement(appInfo, true /* local */, a)) {
+                && !a.getBoolean(R.styleable.Window_windowOptOutEdgeToEdgeEnforcement,
+                false /* defValue */)) {
             getAttributes().privateFlags |= PRIVATE_FLAG_OVERRIDE_LAYOUT_IN_DISPLAY_CUTOUT_MODE;
         }
 
diff --git a/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java b/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java
index 8df3f2a..e522b50 100644
--- a/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java
+++ b/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java
@@ -94,14 +94,21 @@
 
         /** Used for testing */
         @Disabled
-        @ChangeId public static final long TEST_COMPAT_ID_2 = 368131701L;
+        @ChangeId
+        public static final long TEST_COMPAT_ID_2 = 368131701L;
 
         /** Used for testing */
         @EnabledAfter(targetSdkVersion = S)
-        @ChangeId public static final long TEST_COMPAT_ID_3 = 368131659L;
+        @ChangeId
+        public static final long TEST_COMPAT_ID_3 = 368131659L;
 
         /** Used for testing */
         @EnabledAfter(targetSdkVersion = UPSIDE_DOWN_CAKE)
-        @ChangeId public static final long TEST_COMPAT_ID_4 = 368132057L;
+        @ChangeId
+        public static final long TEST_COMPAT_ID_4 = 368132057L;
+
+        /** Used for testing */
+        @ChangeId
+        public static final long TEST_COMPAT_ID_5 = 387558811L;
     }
 }
diff --git a/core/java/com/android/internal/security/VerityUtils.java b/core/java/com/android/internal/security/VerityUtils.java
index 7f7ea8b..3750076 100644
--- a/core/java/com/android/internal/security/VerityUtils.java
+++ b/core/java/com/android/internal/security/VerityUtils.java
@@ -36,7 +36,6 @@
 import com.android.internal.org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
 import com.android.internal.org.bouncycastle.operator.OperatorCreationException;
 
-import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.ByteBuffer;
@@ -53,12 +52,6 @@
 public abstract class VerityUtils {
     private static final String TAG = "VerityUtils";
 
-    /**
-     * File extension of the signature file. For example, foo.apk.fsv_sig is the signature file of
-     * foo.apk.
-     */
-    public static final String FSVERITY_SIGNATURE_FILE_EXTENSION = ".fsv_sig";
-
     /** SHA256 hash size. */
     private static final int HASH_SIZE_BYTES = 32;
 
@@ -67,16 +60,6 @@
                 || SystemProperties.getInt("ro.apk_verity.mode", 0) == 2;
     }
 
-    /** Returns true if the given file looks like containing an fs-verity signature. */
-    public static boolean isFsveritySignatureFile(File file) {
-        return file.getName().endsWith(FSVERITY_SIGNATURE_FILE_EXTENSION);
-    }
-
-    /** Returns the fs-verity signature file path of the given file. */
-    public static String getFsveritySignatureFilePath(String filePath) {
-        return filePath + FSVERITY_SIGNATURE_FILE_EXTENSION;
-    }
-
     /** Enables fs-verity for the file without signature. */
     public static void setUpFsverity(@NonNull String filePath) throws IOException {
         int errno = enableFsverityNative(filePath);
diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
index f14e1f6..ec0954d 100644
--- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
@@ -239,4 +239,7 @@
 
     /** Unbundle a categorized notification */
     void unbundleNotification(String key);
+
+    /** Rebundle an (un)categorized notification */
+    void rebundleNotification(String key);
 }
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index 39ddea6..7470770 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -65,6 +65,7 @@
 import android.view.InputDevice;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
 import com.android.server.LocalServices;
 
 import com.google.android.collect.Lists;
@@ -75,6 +76,7 @@
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
@@ -292,6 +294,56 @@
 
     }
 
+    /**
+     * This exists temporarily due to trunk-stable policies.
+     * Please use ArrayUtils directly if you can.
+     */
+    public static byte[] newNonMovableByteArray(int length) {
+        if (!android.security.Flags.secureArrayZeroization()) {
+            return new byte[length];
+        }
+        return ArrayUtils.newNonMovableByteArray(length);
+    }
+
+    /**
+     * This exists temporarily due to trunk-stable policies.
+     * Please use ArrayUtils directly if you can.
+     */
+    public static char[] newNonMovableCharArray(int length) {
+        if (!android.security.Flags.secureArrayZeroization()) {
+            return new char[length];
+        }
+        return ArrayUtils.newNonMovableCharArray(length);
+    }
+
+    /**
+     * This exists temporarily due to trunk-stable policies.
+     * Please use ArrayUtils directly if you can.
+     */
+    public static void zeroize(byte[] array) {
+        if (!android.security.Flags.secureArrayZeroization()) {
+            if (array != null) {
+                Arrays.fill(array, (byte) 0);
+            }
+            return;
+        }
+        ArrayUtils.zeroize(array);
+    }
+
+    /**
+     * This exists temporarily due to trunk-stable policies.
+     * Please use ArrayUtils directly if you can.
+     */
+    public static void zeroize(char[] array) {
+        if (!android.security.Flags.secureArrayZeroization()) {
+            if (array != null) {
+                Arrays.fill(array, (char) 0);
+            }
+            return;
+        }
+        ArrayUtils.zeroize(array);
+    }
+
     @UnsupportedAppUsage
     public DevicePolicyManager getDevicePolicyManager() {
         if (mDevicePolicyManager == null) {
diff --git a/core/java/com/android/internal/widget/LockscreenCredential.java b/core/java/com/android/internal/widget/LockscreenCredential.java
index 54b9a22..92ce990 100644
--- a/core/java/com/android/internal/widget/LockscreenCredential.java
+++ b/core/java/com/android/internal/widget/LockscreenCredential.java
@@ -246,7 +246,7 @@
      */
     public void zeroize() {
         if (mCredential != null) {
-            Arrays.fill(mCredential, (byte) 0);
+            LockPatternUtils.zeroize(mCredential);
             mCredential = null;
         }
     }
@@ -346,7 +346,7 @@
             byte[] sha1 = MessageDigest.getInstance("SHA-1").digest(saltedPassword);
             byte[] md5 = MessageDigest.getInstance("MD5").digest(saltedPassword);
 
-            Arrays.fill(saltedPassword, (byte) 0);
+            LockPatternUtils.zeroize(saltedPassword);
             return HexEncoding.encodeToString(ArrayUtils.concat(sha1, md5));
         } catch (NoSuchAlgorithmException e) {
             throw new AssertionError("Missing digest algorithm: ", e);
diff --git a/core/java/com/android/internal/widget/NotificationExpandButton.java b/core/java/com/android/internal/widget/NotificationExpandButton.java
index 80bc4fd..dd12f69 100644
--- a/core/java/com/android/internal/widget/NotificationExpandButton.java
+++ b/core/java/com/android/internal/widget/NotificationExpandButton.java
@@ -56,8 +56,6 @@
     private int mDefaultTextColor;
     private int mHighlightPillColor;
     private int mHighlightTextColor;
-    // Track whether this ever had mExpanded = true, so that we don't highlight it anymore.
-    private boolean mWasExpanded = false;
 
     public NotificationExpandButton(Context context) {
         this(context, null, 0, 0);
@@ -136,7 +134,6 @@
         int contentDescriptionId;
         if (mExpanded) {
             if (notificationsRedesignTemplates()) {
-                mWasExpanded = true;
                 drawableId = R.drawable.ic_notification_2025_collapse;
             } else {
                 drawableId = R.drawable.ic_collapse_notification;
@@ -156,8 +153,6 @@
         if (!notificationsRedesignTemplates()) {
             // changing the expanded state can affect the number display
             updateNumber();
-        } else {
-            updateColors();
         }
     }
 
@@ -197,43 +192,22 @@
         );
     }
 
-    /**
-     * Use highlight colors for the expander for groups (when the number is showing) that haven't
-     * been opened before, as long as the colors are available.
-     */
-    private boolean shouldBeHighlighted() {
-        return !mWasExpanded && shouldShowNumber()
-                && mHighlightPillColor != 0 && mHighlightTextColor != 0;
-    }
-
     private void updateColors() {
-        if (notificationsRedesignTemplates()) {
-            if (shouldBeHighlighted()) {
+        if (shouldShowNumber()) {
+            if (mHighlightPillColor != 0) {
                 mPillDrawable.setTintList(ColorStateList.valueOf(mHighlightPillColor));
-                mIconView.setColorFilter(mHighlightTextColor);
+            }
+            mIconView.setColorFilter(mHighlightTextColor);
+            if (mHighlightTextColor != 0) {
                 mNumberView.setTextColor(mHighlightTextColor);
-            } else {
-                mPillDrawable.setTintList(ColorStateList.valueOf(mDefaultPillColor));
-                mIconView.setColorFilter(mDefaultTextColor);
-                mNumberView.setTextColor(mDefaultTextColor);
             }
         } else {
-            if (shouldShowNumber()) {
-                if (mHighlightPillColor != 0) {
-                    mPillDrawable.setTintList(ColorStateList.valueOf(mHighlightPillColor));
-                }
-                mIconView.setColorFilter(mHighlightTextColor);
-                if (mHighlightTextColor != 0) {
-                    mNumberView.setTextColor(mHighlightTextColor);
-                }
-            } else {
-                if (mDefaultPillColor != 0) {
-                    mPillDrawable.setTintList(ColorStateList.valueOf(mDefaultPillColor));
-                }
-                mIconView.setColorFilter(mDefaultTextColor);
-                if (mDefaultTextColor != 0) {
-                    mNumberView.setTextColor(mDefaultTextColor);
-                }
+            if (mDefaultPillColor != 0) {
+                mPillDrawable.setTintList(ColorStateList.valueOf(mDefaultPillColor));
+            }
+            mIconView.setColorFilter(mDefaultTextColor);
+            if (mDefaultTextColor != 0) {
+                mNumberView.setTextColor(mDefaultTextColor);
             }
         }
     }
diff --git a/core/java/com/android/internal/widget/NotificationProgressBar.java b/core/java/com/android/internal/widget/NotificationProgressBar.java
index 8cd7843..904b73f 100644
--- a/core/java/com/android/internal/widget/NotificationProgressBar.java
+++ b/core/java/com/android/internal/widget/NotificationProgressBar.java
@@ -23,6 +23,7 @@
 import android.content.res.ColorStateList;
 import android.content.res.TypedArray;
 import android.graphics.Canvas;
+import android.graphics.Color;
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
@@ -31,6 +32,7 @@
 import android.os.Bundle;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.util.Pair;
 import android.view.RemotableViewMethod;
 import android.widget.ProgressBar;
 import android.widget.RemoteViews;
@@ -40,14 +42,15 @@
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
-import com.android.internal.widget.NotificationProgressDrawable.Part;
-import com.android.internal.widget.NotificationProgressDrawable.Point;
-import com.android.internal.widget.NotificationProgressDrawable.Segment;
+import com.android.internal.widget.NotificationProgressDrawable.DrawablePart;
+import com.android.internal.widget.NotificationProgressDrawable.DrawablePoint;
+import com.android.internal.widget.NotificationProgressDrawable.DrawableSegment;
 
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.SortedSet;
 import java.util.TreeSet;
 
@@ -56,18 +59,26 @@
  * represent Notification ProgressStyle progress, such as for ridesharing and navigation.
  */
 @RemoteViews.RemoteView
-public final class NotificationProgressBar extends ProgressBar {
+public final class NotificationProgressBar extends ProgressBar implements
+        NotificationProgressDrawable.BoundsChangeListener {
     private static final String TAG = "NotificationProgressBar";
+    private static final boolean DEBUG = false;
 
     private NotificationProgressDrawable mNotificationProgressDrawable;
+    private final Rect mProgressDrawableBounds = new Rect();
 
     private NotificationProgressModel mProgressModel;
 
     @Nullable
-    private List<Part> mProgressDrawableParts = null;
+    private List<Part> mParts = null;
+
+    // List of drawable parts before segment splitting by process.
+    @Nullable
+    private List<DrawablePart> mProgressDrawableParts = null;
 
     @Nullable
     private Drawable mTracker = null;
+    private boolean mHasTrackerIcon = false;
 
     /** @see R.styleable#NotificationProgressBar_trackerHeight */
     private final int mTrackerHeight;
@@ -76,7 +87,13 @@
     private final Matrix mMatrix = new Matrix();
     private Matrix mTrackerDrawMatrix = null;
 
-    private float mScale = 0;
+    private float mProgressFraction = 0;
+    /**
+     * The location of progress on the stretched and rescaled progress bar, in fraction. Used for
+     * calculating the tracker position. If stretching and rescaling is not needed, ==
+     * mProgressFraction.
+     */
+    private float mAdjustedProgressFraction = 0;
     /** Indicates whether mTrackerPos needs to be recalculated before the tracker is drawn. */
     private boolean mTrackerPosIsDirty = false;
 
@@ -96,20 +113,21 @@
             int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
 
-        final TypedArray a = context.obtainStyledAttributes(
-                attrs, R.styleable.NotificationProgressBar, defStyleAttr, defStyleRes);
+        final TypedArray a = context.obtainStyledAttributes(attrs,
+                R.styleable.NotificationProgressBar, defStyleAttr, defStyleRes);
         saveAttributeDataForStyleable(context, R.styleable.NotificationProgressBar, attrs, a,
                 defStyleAttr,
                 defStyleRes);
 
         try {
             mNotificationProgressDrawable = getNotificationProgressDrawable();
+            mNotificationProgressDrawable.setBoundsChangeListener(this);
         } catch (IllegalStateException ex) {
             Log.e(TAG, "Can't get NotificationProgressDrawable", ex);
         }
 
         // Supports setting the tracker in xml, but ProgressStyle notifications set/override it
-        // via {@code setProgressTrackerIcon}.
+        // via {@code #setProgressTrackerIcon}.
         final Drawable tracker = a.getDrawable(R.styleable.NotificationProgressBar_tracker);
         setTracker(tracker);
 
@@ -126,8 +144,7 @@
      */
     @RemotableViewMethod
     public void setProgressModel(@Nullable Bundle bundle) {
-        Preconditions.checkArgument(bundle != null,
-                "Bundle shouldn't be null");
+        Preconditions.checkArgument(bundle != null, "Bundle shouldn't be null");
 
         mProgressModel = NotificationProgressModel.fromBundle(bundle);
         final boolean isIndeterminate = mProgressModel.isIndeterminate();
@@ -137,20 +154,25 @@
             final int indeterminateColor = mProgressModel.getIndeterminateColor();
             setIndeterminateTintList(ColorStateList.valueOf(indeterminateColor));
         } else {
+            // TODO: b/372908709 - maybe don't rerun the entire calculation every time the
+            //  progress model is updated? For example, if the segments and parts aren't changed,
+            //  there is no need to call `processAndConvertToViewParts` again.
+
             final int progress = mProgressModel.getProgress();
             final int progressMax = mProgressModel.getProgressMax();
-            mProgressDrawableParts = processAndConvertToDrawableParts(mProgressModel.getSegments(),
+
+            mParts = processAndConvertToViewParts(mProgressModel.getSegments(),
                     mProgressModel.getPoints(),
                     progress,
-                    progressMax,
-                    mProgressModel.isStyledByProgress());
-
-            if (mNotificationProgressDrawable != null) {
-                mNotificationProgressDrawable.setParts(mProgressDrawableParts);
-            }
+                    progressMax);
 
             setMax(progressMax);
             setProgress(progress);
+
+            if (mNotificationProgressDrawable != null
+                    && mNotificationProgressDrawable.getBounds().width() != 0) {
+                updateDrawableParts();
+            }
         }
     }
 
@@ -200,9 +222,7 @@
         } else {
             progressTrackerDrawable = null;
         }
-        return () -> {
-            setTracker(progressTrackerDrawable);
-        };
+        return () -> setTracker(progressTrackerDrawable);
     }
 
     private void setTracker(@Nullable Drawable tracker) {
@@ -226,8 +246,14 @@
         final boolean trackerSizeChanged = trackerSizeChanged(tracker, mTracker);
 
         mTracker = tracker;
-        if (mNotificationProgressDrawable != null) {
-            mNotificationProgressDrawable.setHasTrackerIcon(mTracker != null);
+        final boolean hasTrackerIcon = (mTracker != null);
+        if (mHasTrackerIcon != hasTrackerIcon) {
+            mHasTrackerIcon = hasTrackerIcon;
+            if (mNotificationProgressDrawable != null
+                    && mNotificationProgressDrawable.getBounds().width() != 0
+                    && mProgressModel.isStyledByProgress()) {
+                updateDrawableParts();
+            }
         }
 
         configureTrackerBounds();
@@ -293,6 +319,8 @@
         mTrackerDrawMatrix.postTranslate(Math.round(dx), Math.round(dy));
     }
 
+    // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't
+    // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}.
     @Override
     public synchronized void setProgress(int progress) {
         super.setProgress(progress);
@@ -300,6 +328,8 @@
         onMaybeVisualProgressChanged();
     }
 
+    // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't
+    // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}.
     @Override
     public void setProgress(int progress, boolean animate) {
         // Animation isn't supported by NotificationProgressBar.
@@ -308,6 +338,8 @@
         onMaybeVisualProgressChanged();
     }
 
+    // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't
+    // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}.
     @Override
     public synchronized void setMin(int min) {
         super.setMin(min);
@@ -315,6 +347,8 @@
         onMaybeVisualProgressChanged();
     }
 
+    // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't
+    // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}.
     @Override
     public synchronized void setMax(int max) {
         super.setMax(max);
@@ -323,10 +357,10 @@
     }
 
     private void onMaybeVisualProgressChanged() {
-        float scale = getScale();
-        if (mScale == scale) return;
+        float progressFraction = getProgressFraction();
+        if (mProgressFraction == progressFraction) return;
 
-        mScale = scale;
+        mProgressFraction = progressFraction;
         mTrackerPosIsDirty = true;
         invalidate();
     }
@@ -350,8 +384,7 @@
         super.drawableStateChanged();
 
         final Drawable tracker = mTracker;
-        if (tracker != null && tracker.isStateful()
-                && tracker.setState(getDrawableState())) {
+        if (tracker != null && tracker.isStateful() && tracker.setState(getDrawableState())) {
             invalidateDrawable(tracker);
         }
     }
@@ -372,6 +405,65 @@
         updateTrackerAndBarPos(w, h);
     }
 
+    @Override
+    public void onDrawableBoundsChanged() {
+        final Rect progressDrawableBounds = mNotificationProgressDrawable.getBounds();
+
+        if (mProgressDrawableBounds.equals(progressDrawableBounds)) return;
+
+        if (mProgressDrawableBounds.width() != progressDrawableBounds.width()) {
+            updateDrawableParts();
+        }
+
+        mProgressDrawableBounds.set(progressDrawableBounds);
+    }
+
+    private void updateDrawableParts() {
+        if (DEBUG) {
+            Log.d(TAG, "updateDrawableParts() called. mNotificationProgressDrawable = "
+                    + mNotificationProgressDrawable + ", mParts = " + mParts);
+        }
+
+        if (mNotificationProgressDrawable == null) return;
+        if (mParts == null) return;
+
+        final float width = mNotificationProgressDrawable.getBounds().width();
+        if (width == 0) {
+            if (mProgressDrawableParts != null) {
+                if (DEBUG) {
+                    Log.d(TAG, "Clearing mProgressDrawableParts");
+                }
+                mProgressDrawableParts.clear();
+                mNotificationProgressDrawable.setParts(mProgressDrawableParts);
+            }
+            return;
+        }
+
+        mProgressDrawableParts = processAndConvertToDrawableParts(
+                mParts,
+                width,
+                mNotificationProgressDrawable.getSegSegGap(),
+                mNotificationProgressDrawable.getSegPointGap(),
+                mNotificationProgressDrawable.getPointRadius(),
+                mHasTrackerIcon
+        );
+        Pair<List<DrawablePart>, Float> p = maybeStretchAndRescaleSegments(
+                mParts,
+                mProgressDrawableParts,
+                mNotificationProgressDrawable.getSegmentMinWidth(),
+                mNotificationProgressDrawable.getPointRadius(),
+                getProgressFraction(),
+                width,
+                mProgressModel.isStyledByProgress(),
+                mHasTrackerIcon ? 0F : mNotificationProgressDrawable.getSegSegGap());
+
+        if (DEBUG) {
+            Log.d(TAG, "Updating NotificationProgressDrawable parts");
+        }
+        mNotificationProgressDrawable.setParts(p.first);
+        mAdjustedProgressFraction = p.second / width;
+    }
+
     private void updateTrackerAndBarPos(int w, int h) {
         final int paddedHeight = h - mPaddingTop - mPaddingBottom;
         final Drawable bar = getCurrentDrawable();
@@ -402,11 +494,11 @@
         }
 
         if (tracker != null) {
-            setTrackerPos(w, tracker, mScale, trackerOffsetY);
+            setTrackerPos(w, tracker, mAdjustedProgressFraction, trackerOffsetY);
         }
     }
 
-    private float getScale() {
+    private float getProgressFraction() {
         int min = getMin();
         int max = getMax();
         int range = max - min;
@@ -416,19 +508,19 @@
     /**
      * Updates the tracker drawable bounds.
      *
-     * @param w Width of the view, including padding
-     * @param tracker Drawable used for the tracker
-     * @param scale Current progress between 0 and 1
-     * @param offsetY Vertical offset for centering. If set to
-     *            {@link Integer#MIN_VALUE}, the current offset will be used.
+     * @param w                Width of the view, including padding
+     * @param tracker          Drawable used for the tracker
+     * @param progressFraction Current progress between 0 and 1
+     * @param offsetY          Vertical offset for centering. If set to
+     *                         {@link Integer#MIN_VALUE}, the current offset will be used.
      */
-    private void setTrackerPos(int w, Drawable tracker, float scale, int offsetY) {
+    private void setTrackerPos(int w, Drawable tracker, float progressFraction, int offsetY) {
         int available = w - mPaddingLeft - mPaddingRight;
         final int trackerWidth = tracker.getIntrinsicWidth();
         final int trackerHeight = tracker.getIntrinsicHeight();
         available -= ((mTrackerHeight <= 0) ? trackerWidth : mTrackerWidth);
 
-        final int trackerPos = (int) (scale * available + 0.5f);
+        final int trackerPos = (int) (progressFraction * available + 0.5f);
 
         final int top, bottom;
         if (offsetY == Integer.MIN_VALUE) {
@@ -448,8 +540,8 @@
         if (background != null) {
             final int bkgOffsetX = mPaddingLeft;
             final int bkgOffsetY = mPaddingTop;
-            background.setHotspotBounds(left + bkgOffsetX, top + bkgOffsetY,
-                    right + bkgOffsetX, bottom + bkgOffsetY);
+            background.setHotspotBounds(left + bkgOffsetX, top + bkgOffsetY, right + bkgOffsetX,
+                    bottom + bkgOffsetY);
         }
 
         // Canvas will be translated, so 0,0 is where we start drawing
@@ -482,7 +574,7 @@
         if (mTracker == null) return;
 
         if (mTrackerPosIsDirty) {
-            setTrackerPos(getWidth(), mTracker, mScale, Integer.MIN_VALUE);
+            setTrackerPos(getWidth(), mTracker, mAdjustedProgressFraction, Integer.MIN_VALUE);
         }
 
         final int saveCount = canvas.save();
@@ -531,7 +623,7 @@
 
         final Drawable tracker = mTracker;
         if (tracker != null) {
-            setTrackerPos(getWidth(), tracker, mScale, Integer.MIN_VALUE);
+            setTrackerPos(getWidth(), tracker, mAdjustedProgressFraction, Integer.MIN_VALUE);
 
             // Since we draw translated, the drawable's bounds that it signals
             // for invalidation won't be the actual bounds we want invalidated,
@@ -541,16 +633,14 @@
     }
 
     /**
-     * Processes the ProgressStyle data and convert to list of {@code
-     * NotificationProgressDrawable.Part}.
+     * Processes the ProgressStyle data and convert to a list of {@code Part}.
      */
     @VisibleForTesting
-    public static List<Part> processAndConvertToDrawableParts(
+    public static List<Part> processAndConvertToViewParts(
             List<ProgressStyle.Segment> segments,
             List<ProgressStyle.Point> points,
             int progress,
-            int progressMax,
-            boolean isStyledByProgress
+            int progressMax
     ) {
         if (segments.isEmpty()) {
             throw new IllegalArgumentException("List of segments shouldn't be empty");
@@ -571,6 +661,7 @@
         if (progress < 0 || progress > progressMax) {
             throw new IllegalArgumentException("Invalid progress : " + progress);
         }
+
         for (ProgressStyle.Point point : points) {
             final int pos = point.getPosition();
             if (pos < 0 || pos > progressMax) {
@@ -583,23 +674,21 @@
         final Map<Integer, ProgressStyle.Point> positionToPointMap = generatePositionToPointMap(
                 points);
         final SortedSet<Integer> sortedPos = generateSortedPositionSet(startToSegmentMap,
-                positionToPointMap, progress, isStyledByProgress);
+                positionToPointMap);
 
-        final Map<Integer, ProgressStyle.Segment> startToSplitSegmentMap =
-                splitSegmentsByPointsAndProgress(
-                        startToSegmentMap, sortedPos, progressMax);
+        final Map<Integer, ProgressStyle.Segment> startToSplitSegmentMap = splitSegmentsByPoints(
+                startToSegmentMap, sortedPos, progressMax);
 
-        return convertToDrawableParts(startToSplitSegmentMap, positionToPointMap, sortedPos,
-                progress, progressMax,
-                isStyledByProgress);
+        return convertToViewParts(startToSplitSegmentMap, positionToPointMap, sortedPos,
+                progressMax);
     }
 
     // Any segment with a point on it gets split by the point.
-    // If isStyledByProgress is true, also split the segment with the progress value in its range.
-    private static Map<Integer, ProgressStyle.Segment> splitSegmentsByPointsAndProgress(
+    private static Map<Integer, ProgressStyle.Segment> splitSegmentsByPoints(
             Map<Integer, ProgressStyle.Segment> startToSegmentMap,
             SortedSet<Integer> sortedPos,
-            int progressMax) {
+            int progressMax
+    ) {
         int prevSegStart = 0;
         for (Integer pos : sortedPos) {
             if (pos == 0 || pos == progressMax) continue;
@@ -610,8 +699,7 @@
 
             final ProgressStyle.Segment prevSeg = startToSegmentMap.get(prevSegStart);
             final ProgressStyle.Segment leftSeg = new ProgressStyle.Segment(
-                    pos - prevSegStart).setColor(
-                    prevSeg.getColor());
+                    pos - prevSegStart).setColor(prevSeg.getColor());
             final ProgressStyle.Segment rightSeg = new ProgressStyle.Segment(
                     prevSegStart + prevSeg.getLength() - pos).setColor(prevSeg.getColor());
 
@@ -624,32 +712,21 @@
         return startToSegmentMap;
     }
 
-    private static List<Part> convertToDrawableParts(
+    private static List<Part> convertToViewParts(
             Map<Integer, ProgressStyle.Segment> startToSegmentMap,
             Map<Integer, ProgressStyle.Point> positionToPointMap,
             SortedSet<Integer> sortedPos,
-            int progress,
-            int progressMax,
-            boolean isStyledByProgress
+            int progressMax
     ) {
         List<Part> parts = new ArrayList<>();
-        boolean styleRemainingParts = false;
         for (Integer pos : sortedPos) {
             if (positionToPointMap.containsKey(pos)) {
                 final ProgressStyle.Point point = positionToPointMap.get(pos);
-                final int color = maybeGetFadedColor(point.getColor(), styleRemainingParts);
-                parts.add(new Point(null, color, styleRemainingParts));
-            }
-            // We want the Point at the current progress to be filled (not faded), but a Segment
-            // starting at this progress to be faded.
-            if (isStyledByProgress && !styleRemainingParts && pos == progress) {
-                styleRemainingParts = true;
+                parts.add(new Point(point.getColor()));
             }
             if (startToSegmentMap.containsKey(pos)) {
                 final ProgressStyle.Segment seg = startToSegmentMap.get(pos);
-                final int color = maybeGetFadedColor(seg.getColor(), styleRemainingParts);
-                parts.add(new Segment(
-                        (float) seg.getLength() / progressMax, color, styleRemainingParts));
+                parts.add(new Segment((float) seg.getLength() / progressMax, seg.getColor()));
             }
         }
 
@@ -660,11 +737,24 @@
     private static int maybeGetFadedColor(@ColorInt int color, boolean fade) {
         if (!fade) return color;
 
-        return NotificationProgressDrawable.getFadedColor(color);
+        return getFadedColor(color);
+    }
+
+    /**
+     * Get a color with an opacity that's 40% of the input color.
+     */
+    @ColorInt
+    static int getFadedColor(@ColorInt int color) {
+        return Color.argb(
+                (int) (Color.alpha(color) * 0.4f + 0.5f),
+                Color.red(color),
+                Color.green(color),
+                Color.blue(color));
     }
 
     private static Map<Integer, ProgressStyle.Segment> generateStartToSegmentMap(
-            List<ProgressStyle.Segment> segments) {
+            List<ProgressStyle.Segment> segments
+    ) {
         final Map<Integer, ProgressStyle.Segment> startToSegmentMap = new HashMap<>();
 
         int currentStart = 0;  // Initial start position is 0
@@ -681,7 +771,8 @@
     }
 
     private static Map<Integer, ProgressStyle.Point> generatePositionToPointMap(
-            List<ProgressStyle.Point> points) {
+            List<ProgressStyle.Point> points
+    ) {
         final Map<Integer, ProgressStyle.Point> positionToPointMap = new HashMap<>();
 
         for (ProgressStyle.Point point : points) {
@@ -693,14 +784,392 @@
 
     private static SortedSet<Integer> generateSortedPositionSet(
             Map<Integer, ProgressStyle.Segment> startToSegmentMap,
-            Map<Integer, ProgressStyle.Point> positionToPointMap, int progress,
-            boolean isStyledByProgress) {
+            Map<Integer, ProgressStyle.Point> positionToPointMap
+    ) {
         final SortedSet<Integer> sortedPos = new TreeSet<>(startToSegmentMap.keySet());
         sortedPos.addAll(positionToPointMap.keySet());
-        if (isStyledByProgress) {
-            sortedPos.add(progress);
-        }
 
         return sortedPos;
     }
+
+    /**
+     * Processes the list of {@code Part} and convert to a list of {@code DrawablePart}.
+     */
+    @VisibleForTesting
+    public static List<DrawablePart> processAndConvertToDrawableParts(
+            List<Part> parts,
+            float totalWidth,
+            float segSegGap,
+            float segPointGap,
+            float pointRadius,
+            boolean hasTrackerIcon
+    ) {
+        List<DrawablePart> drawableParts = new ArrayList<>();
+
+        // generally, we will start drawing at (x, y) and end at (x+w, y)
+        float x = (float) 0;
+
+        final int nParts = parts.size();
+        for (int iPart = 0; iPart < nParts; iPart++) {
+            final Part part = parts.get(iPart);
+            final Part prevPart = iPart == 0 ? null : parts.get(iPart - 1);
+            final Part nextPart = iPart + 1 == nParts ? null : parts.get(iPart + 1);
+            if (part instanceof Segment segment) {
+                final float segWidth = segment.mFraction * totalWidth;
+                // Advance the start position to account for a point immediately prior.
+                final float startOffset = getSegStartOffset(prevPart, pointRadius, segPointGap, x);
+                final float start = x + startOffset;
+                // Retract the end position to account for the padding and a point immediately
+                // after.
+                final float endOffset = getSegEndOffset(segment, nextPart, pointRadius, segPointGap,
+                        segSegGap, x + segWidth, totalWidth, hasTrackerIcon);
+                final float end = x + segWidth - endOffset;
+
+                drawableParts.add(new DrawableSegment(start, end, segment.mColor, segment.mFaded));
+
+                segment.mStart = x;
+                segment.mEnd = x + segWidth;
+
+                // Advance the current position to account for the segment's fraction of the total
+                // width (ignoring offset and padding)
+                x += segWidth;
+            } else if (part instanceof Point point) {
+                final float pointWidth = 2 * pointRadius;
+                float start = x - pointRadius;
+                if (start < 0) start = 0;
+                float end = start + pointWidth;
+                if (end > totalWidth) {
+                    end = totalWidth;
+                    if (totalWidth > pointWidth) start = totalWidth - pointWidth;
+                }
+
+                drawableParts.add(new DrawablePoint(start, end, point.mColor));
+            }
+        }
+
+        return drawableParts;
+    }
+
+    private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap,
+            float startX) {
+        if (!(prevPart instanceof Point)) return 0F;
+        final float pointOffset = (startX < pointRadius) ? (pointRadius - startX) : 0;
+        return pointOffset + pointRadius + segPointGap;
+    }
+
+    private static float getSegEndOffset(Segment seg, Part nextPart, float pointRadius,
+            float segPointGap, float segSegGap, float endX, float totalWidth,
+            boolean hasTrackerIcon) {
+        if (nextPart == null) return 0F;
+        if (nextPart instanceof Segment nextSeg) {
+            if (!seg.mFaded && nextSeg.mFaded) {
+                // @see Segment#mFaded
+                return hasTrackerIcon ? 0F : segSegGap;
+            }
+            return segSegGap;
+        }
+
+        final float pointWidth = 2 * pointRadius;
+        final float pointOffset = (endX + pointRadius > totalWidth && totalWidth > pointWidth)
+                ? (endX + pointRadius - totalWidth) : 0;
+        return segPointGap + pointRadius + pointOffset;
+    }
+
+    /**
+     * Processes the list of {@code DrawablePart} data and convert to a pair of:
+     * - list of processed {@code DrawablePart}.
+     * - location of progress on the stretched and rescaled progress bar.
+     */
+    @VisibleForTesting
+    public static Pair<List<DrawablePart>, Float> maybeStretchAndRescaleSegments(
+            List<Part> parts,
+            List<DrawablePart> drawableParts,
+            float segmentMinWidth,
+            float pointRadius,
+            float progressFraction,
+            float totalWidth,
+            boolean isStyledByProgress,
+            float progressGap
+    ) {
+        final List<DrawableSegment> drawableSegments = drawableParts
+                .stream()
+                .filter(DrawableSegment.class::isInstance)
+                .map(DrawableSegment.class::cast)
+                .toList();
+        float totalExcessWidth = 0;
+        float totalPositiveExcessWidth = 0;
+        for (DrawableSegment drawableSegment : drawableSegments) {
+            final float excessWidth = drawableSegment.getWidth() - segmentMinWidth;
+            totalExcessWidth += excessWidth;
+            if (excessWidth > 0) totalPositiveExcessWidth += excessWidth;
+        }
+
+        // All drawable segments are above minimum width. No need to stretch and rescale.
+        if (totalExcessWidth == totalPositiveExcessWidth) {
+            return maybeSplitDrawableSegmentsByProgress(
+                    parts,
+                    drawableParts,
+                    progressFraction,
+                    totalWidth,
+                    isStyledByProgress,
+                    progressGap);
+        }
+
+        if (totalExcessWidth < 0) {
+            // TODO: b/372908709 - throw an error so that the caller can catch and go to fallback
+            //  option. (instead of return.)
+            Log.w(TAG, "Not enough width to satisfy the minimum width for segments.");
+            return maybeSplitDrawableSegmentsByProgress(
+                    parts,
+                    drawableParts,
+                    progressFraction,
+                    totalWidth,
+                    isStyledByProgress,
+                    progressGap);
+        }
+
+        final int nParts = drawableParts.size();
+        float startOffset = 0;
+        for (int iPart = 0; iPart < nParts; iPart++) {
+            final DrawablePart drawablePart = drawableParts.get(iPart);
+            if (drawablePart instanceof DrawableSegment drawableSegment) {
+                final float origDrawableSegmentWidth = drawableSegment.getWidth();
+
+                float drawableSegmentWidth = segmentMinWidth;
+                // Allocate the totalExcessWidth to the segments above minimum, proportionally to
+                // their initial excessWidth.
+                if (origDrawableSegmentWidth > segmentMinWidth) {
+                    drawableSegmentWidth +=
+                            totalExcessWidth * (origDrawableSegmentWidth - segmentMinWidth)
+                                    / totalPositiveExcessWidth;
+                }
+
+                final float widthDiff = drawableSegmentWidth - drawableSegment.getWidth();
+
+                // Adjust drawable segments to new widths
+                drawableSegment.setStart(drawableSegment.getStart() + startOffset);
+                drawableSegment.setEnd(
+                        drawableSegment.getStart() + origDrawableSegmentWidth + widthDiff);
+
+                // Also adjust view segments to new width. (For view segments, only start is
+                // needed?)
+                // Check that segments and drawableSegments are of the same size?
+                final Segment segment = (Segment) parts.get(iPart);
+                final float origSegmentWidth = segment.getWidth();
+                segment.mStart = segment.mStart + startOffset;
+                segment.mEnd = segment.mStart + origSegmentWidth + widthDiff;
+
+                // Increase startOffset for the subsequent segments.
+                startOffset += widthDiff;
+            } else if (drawablePart instanceof DrawablePoint drawablePoint) {
+                drawablePoint.setStart(drawablePoint.getStart() + startOffset);
+                drawablePoint.setEnd(drawablePoint.getStart() + 2 * pointRadius);
+            }
+        }
+
+        return maybeSplitDrawableSegmentsByProgress(
+                parts,
+                drawableParts,
+                progressFraction,
+                totalWidth,
+                isStyledByProgress,
+                progressGap);
+    }
+
+    /**
+     * Find the location of progress on the stretched and rescaled progress bar.
+     * If isStyledByProgress is true, also split the drawable segment with the progress value in its
+     * range. Style the drawable parts after process with reduced opacity and segment height.
+     */
+    private static Pair<List<DrawablePart>, Float> maybeSplitDrawableSegmentsByProgress(
+            // Needed to get the original segment start and end positions in pixels.
+            List<Part> parts,
+            List<DrawablePart> drawableParts,
+            float progressFraction,
+            float totalWidth,
+            boolean isStyledByProgress,
+            float progressGap
+    ) {
+        if (progressFraction == 1) return new Pair<>(drawableParts, totalWidth);
+
+        int iPartFirstSegmentToStyle = -1;
+        int iPartSegmentToSplit = -1;
+        float rescaledProgressX = 0;
+        float startFraction = 0;
+        final int nParts = parts.size();
+        for (int iPart = 0; iPart < nParts; iPart++) {
+            final Part part = parts.get(iPart);
+            if (!(part instanceof Segment)) continue;
+            final Segment segment = (Segment) part;
+            if (startFraction == progressFraction) {
+                iPartFirstSegmentToStyle = iPart;
+                rescaledProgressX = segment.mStart;
+                break;
+            } else if (startFraction < progressFraction
+                    && progressFraction < startFraction + segment.mFraction) {
+                iPartSegmentToSplit = iPart;
+                rescaledProgressX = segment.mStart
+                        + (progressFraction - startFraction) / segment.mFraction
+                        * segment.getWidth();
+                break;
+            }
+            startFraction += segment.mFraction;
+        }
+
+        if (!isStyledByProgress) return new Pair<>(drawableParts, rescaledProgressX);
+
+        List<DrawablePart> splitDrawableParts = new ArrayList<>();
+        boolean styleRemainingParts = false;
+        for (int iPart = 0; iPart < nParts; iPart++) {
+            final DrawablePart drawablePart = drawableParts.get(iPart);
+            if (drawablePart instanceof DrawablePoint drawablePoint) {
+                final int color = maybeGetFadedColor(drawablePoint.getColor(), styleRemainingParts);
+                splitDrawableParts.add(
+                        new DrawablePoint(drawablePoint.getStart(), drawablePoint.getEnd(), color));
+            }
+            if (iPart == iPartFirstSegmentToStyle) styleRemainingParts = true;
+            if (drawablePart instanceof DrawableSegment drawableSegment) {
+                if (iPart == iPartSegmentToSplit) {
+                    if (rescaledProgressX <= drawableSegment.getStart()) {
+                        styleRemainingParts = true;
+                        final int color = maybeGetFadedColor(drawableSegment.getColor(), true);
+                        splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(),
+                                drawableSegment.getEnd(), color, true));
+                    } else if (drawableSegment.getStart() < rescaledProgressX
+                            && rescaledProgressX < drawableSegment.getEnd()) {
+                        splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(),
+                                rescaledProgressX - progressGap, drawableSegment.getColor()));
+                        final int color = maybeGetFadedColor(drawableSegment.getColor(), true);
+                        splitDrawableParts.add(
+                                new DrawableSegment(rescaledProgressX, drawableSegment.getEnd(),
+                                        color, true));
+                        styleRemainingParts = true;
+                    } else {
+                        splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(),
+                                drawableSegment.getEnd(), drawableSegment.getColor()));
+                        styleRemainingParts = true;
+                    }
+                } else {
+                    final int color = maybeGetFadedColor(drawableSegment.getColor(),
+                            styleRemainingParts);
+                    splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(),
+                            drawableSegment.getEnd(), color, styleRemainingParts));
+                }
+            }
+        }
+
+        return new Pair<>(splitDrawableParts, rescaledProgressX);
+    }
+
+    /**
+     * A part of the progress bar, which is either a {@link Segment} with non-zero length, or a
+     * {@link Point} with zero length.
+     */
+    // TODO: b/372908709 - maybe this should be made private? Only test the final
+    //  NotificationDrawable.Parts.
+    public interface Part {
+    }
+
+    /**
+     * A segment is a part of the progress bar with non-zero length. For example, it can
+     * represent a portion in a navigation journey with certain traffic condition.
+     */
+    public static final class Segment implements Part {
+        private final float mFraction;
+        @ColorInt
+        private final int mColor;
+        /**
+         * Whether the segment is faded or not.
+         * <p>
+         * <pre>
+         *     When mFaded is set to true, a combination of the following is done to the segment:
+         *       1. The drawing color is mColor with opacity updated to 40%.
+         *       2. The gap between faded and non-faded segments is:
+         *          - the segment-segment gap, when there is no tracker icon
+         *          - 0, when there is tracker icon
+         *     </pre>
+         * </p>
+         */
+        private final boolean mFaded;
+
+        /** Start position (in pixels) */
+        private float mStart;
+        /** End position (in pixels */
+        private float mEnd;
+
+        public Segment(float fraction, @ColorInt int color) {
+            this(fraction, color, false);
+        }
+
+        public Segment(float fraction, @ColorInt int color, boolean faded) {
+            mFraction = fraction;
+            mColor = color;
+            mFaded = faded;
+        }
+
+        /** Returns the calculated drawing width of the part */
+        public float getWidth() {
+            return mEnd - mStart;
+        }
+
+        @Override
+        public String toString() {
+            return "Segment(fraction=" + this.mFraction + ", color=" + this.mColor + ", faded="
+                    + this.mFaded + "), mStart = " + this.mStart + ", mEnd = " + this.mEnd;
+        }
+
+        // Needed for unit tests
+        @Override
+        public boolean equals(@androidx.annotation.Nullable Object other) {
+            if (this == other) return true;
+
+            if (other == null || getClass() != other.getClass()) return false;
+
+            Segment that = (Segment) other;
+            if (Float.compare(this.mFraction, that.mFraction) != 0) return false;
+            if (this.mColor != that.mColor) return false;
+            return this.mFaded == that.mFaded;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mFraction, mColor, mFaded);
+        }
+    }
+
+    /**
+     * A point is a part of the progress bar with zero length. Points are designated points within a
+     * progress bar to visualize distinct stages or milestones. For example, a stop in a multi-stop
+     * ride-share journey.
+     */
+    public static final class Point implements Part {
+        @ColorInt
+        private final int mColor;
+
+        public Point(@ColorInt int color) {
+            mColor = color;
+        }
+
+        @Override
+        public String toString() {
+            return "Point(color=" + this.mColor + ")";
+        }
+
+        // Needed for unit tests.
+        @Override
+        public boolean equals(@androidx.annotation.Nullable Object other) {
+            if (this == other) return true;
+
+            if (other == null || getClass() != other.getClass()) return false;
+
+            Point that = (Point) other;
+
+            return this.mColor == that.mColor;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mColor);
+        }
+    }
 }
diff --git a/core/java/com/android/internal/widget/NotificationProgressDrawable.java b/core/java/com/android/internal/widget/NotificationProgressDrawable.java
index 8629a1c..4ece81c 100644
--- a/core/java/com/android/internal/widget/NotificationProgressDrawable.java
+++ b/core/java/com/android/internal/widget/NotificationProgressDrawable.java
@@ -21,7 +21,6 @@
 import android.content.res.Resources.Theme;
 import android.content.res.TypedArray;
 import android.graphics.Canvas;
-import android.graphics.Color;
 import android.graphics.ColorFilter;
 import android.graphics.Paint;
 import android.graphics.PixelFormat;
@@ -49,22 +48,24 @@
 
 /**
  * This is used by NotificationProgressBar for displaying a custom background. It composes of
- * segments, which have non-zero length, and points, which have zero length.
+ * segments, which have non-zero length varying drawing width, and points, which have zero length
+ * and fixed size for drawing.
  *
- * @see Segment
- * @see Point
+ * @see DrawableSegment
+ * @see DrawablePoint
  */
 public final class NotificationProgressDrawable extends Drawable {
     private static final String TAG = "NotifProgressDrawable";
 
+    @Nullable
+    private BoundsChangeListener mBoundsChangeListener = null;
+
     private State mState;
     private boolean mMutated;
 
-    private final ArrayList<Part> mParts = new ArrayList<>();
-    private boolean mHasTrackerIcon;
+    private final ArrayList<DrawablePart> mParts = new ArrayList<>();
 
     private final RectF mSegRectF = new RectF();
-    private final Rect mPointRect = new Rect();
     private final RectF mPointRectF = new RectF();
 
     private final Paint mFillPaint = new Paint();
@@ -80,33 +81,37 @@
     }
 
     /**
-     * <p>Set the segment default color for the drawable.</p>
-     * <p>Note: changing this property will affect all instances of a drawable loaded from a
-     * resource. It is recommended to invoke {@link #mutate()} before changing this property.</p>
-     *
-     * @param color The color of the stroke
-     * @see #mutate()
+     * Returns the gap between two segments.
      */
-    public void setSegmentDefaultColor(@ColorInt int color) {
-        mState.setSegmentColor(color);
+    public float getSegSegGap() {
+        return mState.mSegSegGap;
     }
 
     /**
-     * <p>Set the point rect default color for the drawable.</p>
-     * <p>Note: changing this property will affect all instances of a drawable loaded from a
-     * resource. It is recommended to invoke {@link #mutate()} before changing this property.</p>
-     *
-     * @param color The color of the point rect
-     * @see #mutate()
+     * Returns the gap between a segment and a point.
      */
-    public void setPointRectDefaultColor(@ColorInt int color) {
-        mState.setPointRectColor(color);
+    public float getSegPointGap() {
+        return mState.mSegPointGap;
+    }
+
+    /**
+     * Returns the gap between a segment and a point.
+     */
+    public float getSegmentMinWidth() {
+        return mState.mSegmentMinWidth;
+    }
+
+    /**
+     * Returns the radius for the points.
+     */
+    public float getPointRadius() {
+        return mState.mPointRadius;
     }
 
     /**
      * Set the segments and points that constitute the drawable.
      */
-    public void setParts(List<Part> parts) {
+    public void setParts(List<DrawablePart> parts) {
         mParts.clear();
         mParts.addAll(parts);
 
@@ -116,51 +121,22 @@
     /**
      * Set the segments and points that constitute the drawable.
      */
-    public void setParts(@NonNull Part... parts) {
+    public void setParts(@NonNull DrawablePart... parts) {
         setParts(Arrays.asList(parts));
     }
 
-    /**
-     * Set whether a tracker is drawn on top of this NotificationProgressDrawable.
-     */
-    public void setHasTrackerIcon(boolean hasTrackerIcon) {
-        if (mHasTrackerIcon != hasTrackerIcon) {
-            mHasTrackerIcon = hasTrackerIcon;
-            invalidateSelf();
-        }
-    }
-
     @Override
     public void draw(@NonNull Canvas canvas) {
-        final float pointRadius =
-                mState.mPointRadius; // how big the point icon will be, halved
-
-        // generally, we will start drawing at (x, y) and end at (x+w, y)
-        float x = (float) getBounds().left;
+        final float pointRadius = mState.mPointRadius;
+        final float left = (float) getBounds().left;
         final float centerY = (float) getBounds().centerY();
-        final float totalWidth = (float) getBounds().width();
-        float segPointGap = mState.mSegPointGap;
 
         final int numParts = mParts.size();
         for (int iPart = 0; iPart < numParts; iPart++) {
-            final Part part = mParts.get(iPart);
-            final Part prevPart = iPart == 0 ? null : mParts.get(iPart - 1);
-            final Part nextPart = iPart + 1 == numParts ? null : mParts.get(iPart + 1);
-            if (part instanceof Segment segment) {
-                final float segWidth = segment.mFraction * totalWidth;
-                // Advance the start position to account for a point immediately prior.
-                final float startOffset = getSegStartOffset(prevPart, pointRadius, segPointGap, x);
-                final float start = x + startOffset;
-                // Retract the end position to account for the padding and a point immediately
-                // after.
-                final float endOffset = getSegEndOffset(segment, nextPart, pointRadius, segPointGap,
-                        mState.mSegSegGap, x + segWidth, totalWidth, mHasTrackerIcon);
-                final float end = x + segWidth - endOffset;
-
-                // Advance the current position to account for the segment's fraction of the total
-                // width (ignoring offset and padding)
-                x += segWidth;
-
+            final DrawablePart part = mParts.get(iPart);
+            final float start = left + part.mStart;
+            final float end = left + part.mEnd;
+            if (part instanceof DrawableSegment segment) {
                 // No space left to draw the segment
                 if (start > end) continue;
 
@@ -168,69 +144,25 @@
                         : mState.mSegmentHeight / 2F;
                 final float cornerRadius = mState.mSegmentCornerRadius;
 
-                mFillPaint.setColor(segment.mColor != Color.TRANSPARENT ? segment.mColor
-                        : (segment.mFaded ? mState.mFadedSegmentColor : mState.mSegmentColor));
+                mFillPaint.setColor(segment.mColor);
 
                 mSegRectF.set(start, centerY - radiusY, end, centerY + radiusY);
                 canvas.drawRoundRect(mSegRectF, cornerRadius, cornerRadius, mFillPaint);
-            } else if (part instanceof Point point) {
-                final float pointWidth = 2 * pointRadius;
-                float start = x - pointRadius;
-                if (start < 0) start = 0;
-                float end = start + pointWidth;
-                if (end > totalWidth) {
-                    end = totalWidth;
-                    if (totalWidth > pointWidth) start = totalWidth - pointWidth;
-                }
-                mPointRect.set((int) start, (int) (centerY - pointRadius), (int) end,
-                        (int) (centerY + pointRadius));
+            } else if (part instanceof DrawablePoint point) {
+                // TODO: b/367804171 - actually use a vector asset for the default point
+                //  rather than drawing it as a box?
+                mPointRectF.set(start, centerY - pointRadius, end, centerY + pointRadius);
+                final float inset = mState.mPointRectInset;
+                final float cornerRadius = mState.mPointRectCornerRadius;
+                mPointRectF.inset(inset, inset);
 
-                if (point.mIcon != null) {
-                    point.mIcon.setBounds(mPointRect);
-                    point.mIcon.draw(canvas);
-                } else {
-                    // TODO: b/367804171 - actually use a vector asset for the default point
-                    //  rather than drawing it as a box?
-                    mPointRectF.set(start, centerY - pointRadius, end, centerY + pointRadius);
-                    final float inset = mState.mPointRectInset;
-                    final float cornerRadius = mState.mPointRectCornerRadius;
-                    mPointRectF.inset(inset, inset);
+                mFillPaint.setColor(point.mColor);
 
-                    mFillPaint.setColor(point.mColor != Color.TRANSPARENT ? point.mColor
-                            : (point.mFaded ? mState.mFadedPointRectColor
-                                    : mState.mPointRectColor));
-
-                    canvas.drawRoundRect(mPointRectF, cornerRadius, cornerRadius, mFillPaint);
-                }
+                canvas.drawRoundRect(mPointRectF, cornerRadius, cornerRadius, mFillPaint);
             }
         }
     }
 
-    private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap,
-            float startX) {
-        if (!(prevPart instanceof Point)) return 0F;
-        final float pointOffset = (startX < pointRadius) ? (pointRadius - startX) : 0;
-        return pointOffset + pointRadius + segPointGap;
-    }
-
-    private static float getSegEndOffset(Segment seg, Part nextPart, float pointRadius,
-            float segPointGap,
-            float segSegGap, float endX, float totalWidth, boolean hasTrackerIcon) {
-        if (nextPart == null) return 0F;
-        if (nextPart instanceof Segment nextSeg) {
-            if (!seg.mFaded && nextSeg.mFaded) {
-                // @see Segment#mFaded
-                return hasTrackerIcon ? 0F : segSegGap;
-            }
-            return segSegGap;
-        }
-
-        final float pointWidth = 2 * pointRadius;
-        final float pointOffset = (endX + pointRadius > totalWidth && totalWidth > pointWidth)
-                ? (endX + pointRadius - totalWidth) : 0;
-        return segPointGap + pointRadius + pointOffset;
-    }
-
     @Override
     public @Config int getChangingConfigurations() {
         return super.getChangingConfigurations() | mState.getChangingConfigurations();
@@ -260,6 +192,19 @@
         return PixelFormat.UNKNOWN;
     }
 
+    public void setBoundsChangeListener(BoundsChangeListener listener) {
+        mBoundsChangeListener = listener;
+    }
+
+    @Override
+    protected void onBoundsChange(Rect bounds) {
+        super.onBoundsChange(bounds);
+
+        if (mBoundsChangeListener != null) {
+            mBoundsChangeListener.onDrawableBoundsChanged();
+        }
+    }
+
     @Override
     public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
             @NonNull AttributeSet attrs, @Nullable Resources.Theme theme)
@@ -384,6 +329,8 @@
         // Extract the theme attributes, if any.
         state.mThemeAttrsSegments = a.extractThemeAttrs();
 
+        state.mSegmentMinWidth = a.getDimension(
+                R.styleable.NotificationProgressDrawableSegments_minWidth, state.mSegmentMinWidth);
         state.mSegmentHeight = a.getDimension(
                 R.styleable.NotificationProgressDrawableSegments_height, state.mSegmentHeight);
         state.mFadedSegmentHeight = a.getDimension(
@@ -392,9 +339,6 @@
         state.mSegmentCornerRadius = a.getDimension(
                 R.styleable.NotificationProgressDrawableSegments_cornerRadius,
                 state.mSegmentCornerRadius);
-        final int color = a.getColor(R.styleable.NotificationProgressDrawableSegments_color,
-                state.mSegmentColor);
-        setSegmentDefaultColor(color);
     }
 
     private void updatePointsFromTypedArray(TypedArray a) {
@@ -413,9 +357,6 @@
         state.mPointRectCornerRadius = a.getDimension(
                 R.styleable.NotificationProgressDrawablePoints_cornerRadius,
                 state.mPointRectCornerRadius);
-        final int color = a.getColor(R.styleable.NotificationProgressDrawablePoints_color,
-                state.mPointRectColor);
-        setPointRectDefaultColor(color);
     }
 
     static int resolveDensity(@Nullable Resources r, int parentDensity) {
@@ -464,65 +405,59 @@
     }
 
     /**
-     * A part of the progress bar, which is either a S{@link Segment} with non-zero length, or a
-     * {@link Point} with zero length.
+     * Listener to receive updates about drawable bounds changing
      */
-    public interface Part {
+    public interface BoundsChangeListener {
+        /** Called when bounds have changed */
+        void onDrawableBoundsChanged();
     }
 
     /**
-     * A segment is a part of the progress bar with non-zero length. For example, it can
-     * represent a portion in a navigation journey with certain traffic condition.
-     *
+     * A part of the progress drawable, which is either a {@link DrawableSegment} with non-zero
+     * length and varying drawing width, or a {@link DrawablePoint} with zero length and fixed size
+     * for drawing.
      */
-    public static final class Segment implements Part {
-        private final float mFraction;
-        @ColorInt private final int mColor;
-        /** Whether the segment is faded or not.
-         * <p>
-         *     <pre>
-         *     When mFaded is set to true, a combination of the following is done to the segment:
-         *       1. The drawing color is mColor with opacity updated to 40%.
-         *       2. The gap between faded and non-faded segments is:
-         *          - the segment-segment gap, when there is no tracker icon
-         *          - 0, when there is tracker icon
-         *     </pre>
-         * </p>
-         */
-        private final boolean mFaded;
+    public abstract static class DrawablePart {
+        // TODO: b/372908709 - maybe rename start/end to left/right, to be consistent with the
+        //  bounds rect.
+        /** Start position for drawing (in pixels) */
+        protected float mStart;
+        /** End position for drawing (in pixels) */
+        protected float mEnd;
+        /** Drawing color. */
+        @ColorInt protected final int mColor;
 
-        public Segment(float fraction) {
-            this(fraction, Color.TRANSPARENT);
-        }
-
-        public Segment(float fraction, @ColorInt int color) {
-            this(fraction, color, false);
-        }
-
-        public Segment(float fraction, @ColorInt int color, boolean faded) {
-            mFraction = fraction;
+        protected DrawablePart(float start, float end, @ColorInt int color) {
+            mStart = start;
+            mEnd = end;
             mColor = color;
-            mFaded = faded;
         }
 
-        public float getFraction() {
-            return this.mFraction;
+        public float getStart() {
+            return this.mStart;
+        }
+
+        public void setStart(float start) {
+            mStart = start;
+        }
+
+        public float getEnd() {
+            return this.mEnd;
+        }
+
+        public void setEnd(float end) {
+            mEnd = end;
+        }
+
+        /** Returns the calculated drawing width of the part */
+        public float getWidth() {
+            return mEnd - mStart;
         }
 
         public int getColor() {
             return this.mColor;
         }
 
-        public boolean getFaded() {
-            return this.mFaded;
-        }
-
-        @Override
-        public String toString() {
-            return "Segment(fraction=" + this.mFraction + ", color=" + this.mColor + ", faded="
-                    + this.mFaded + ')';
-        }
-
         // Needed for unit tests
         @Override
         public boolean equals(@Nullable Object other) {
@@ -530,80 +465,79 @@
 
             if (other == null || getClass() != other.getClass()) return false;
 
-            Segment that = (Segment) other;
-            if (Float.compare(this.mFraction, that.mFraction) != 0) return false;
-            if (this.mColor != that.mColor) return false;
-            return this.mFaded == that.mFaded;
+            DrawablePart that = (DrawablePart) other;
+            if (Float.compare(this.mStart, that.mStart) != 0) return false;
+            if (Float.compare(this.mEnd, that.mEnd) != 0) return false;
+            return this.mColor == that.mColor;
         }
 
         @Override
         public int hashCode() {
-            return Objects.hash(mFraction, mColor, mFaded);
+            return Objects.hash(mStart, mEnd, mColor);
         }
     }
 
     /**
-     * A point is a part of the progress bar with zero length. Points are designated points within a
-     * progressbar to visualize distinct stages or milestones. For example, a stop in a multi-stop
-     * ride-share journey.
+     * A segment is a part of the progress bar with non-zero length. For example, it can
+     * represent a portion in a navigation journey with certain traffic condition.
+     * <p>
+     * The start and end positions for drawing a segment are assumed to have been adjusted for
+     * the Points and gaps neighboring the segment.
+     * </p>
      */
-    public static final class Point implements Part {
-        @Nullable
-        private final Drawable mIcon;
-        @ColorInt private final int mColor;
+    public static final class DrawableSegment extends DrawablePart {
+        /**
+         * Whether the segment is faded or not.
+         * <p>
+         * Faded segments and non-faded segments are drawn with different heights.
+         * </p>
+         */
         private final boolean mFaded;
 
-        public Point(@Nullable Drawable icon) {
-            this(icon, Color.TRANSPARENT, false);
+        public DrawableSegment(float start, float end, int color) {
+            this(start, end, color, false);
         }
 
-        public Point(@Nullable Drawable icon, @ColorInt int color) {
-            this(icon, color, false);
-
-        }
-
-        public Point(@Nullable Drawable icon, @ColorInt int color, boolean faded) {
-            mIcon = icon;
-            mColor = color;
+        public DrawableSegment(float start, float end, int color, boolean faded) {
+            super(start, end, color);
             mFaded = faded;
         }
 
-        @Nullable
-        public Drawable getIcon() {
-            return this.mIcon;
-        }
-
-        public int getColor() {
-            return this.mColor;
-        }
-
-        public boolean getFaded() {
-            return this.mFaded;
-        }
-
         @Override
         public String toString() {
-            return "Point(icon=" + this.mIcon + ", color=" + this.mColor + ", faded=" + this.mFaded
-                    + ")";
+            return "Segment(start=" + this.mStart + ", end=" + this.mEnd + ", color=" + this.mColor
+                    + ", faded=" + this.mFaded + ')';
         }
 
         // Needed for unit tests.
         @Override
         public boolean equals(@Nullable Object other) {
-            if (this == other) return true;
+            if (!super.equals(other)) return false;
 
-            if (other == null || getClass() != other.getClass()) return false;
-
-            Point that = (Point) other;
-
-            if (!Objects.equals(this.mIcon, that.mIcon)) return false;
-            if (this.mColor != that.mColor) return false;
+            DrawableSegment that = (DrawableSegment) other;
             return this.mFaded == that.mFaded;
         }
 
         @Override
         public int hashCode() {
-            return Objects.hash(mIcon, mColor, mFaded);
+            return Objects.hash(super.hashCode(), mFaded);
+        }
+    }
+
+    /**
+     * A point is a part of the progress bar with zero length. Points are designated points within a
+     * progress bar to visualize distinct stages or milestones. For example, a stop in a multi-stop
+     * ride-share journey.
+     */
+    public static final class DrawablePoint extends DrawablePart {
+        public DrawablePoint(float start, float end, int color) {
+            super(start, end, color);
+        }
+
+        @Override
+        public String toString() {
+            return "Point(start=" + this.mStart + ", end=" + this.mEnd + ", color=" + this.mColor
+                    + ")";
         }
     }
 
@@ -628,16 +562,14 @@
         int mChangingConfigurations;
         float mSegSegGap = 0.0f;
         float mSegPointGap = 0.0f;
+        float mSegmentMinWidth = 0.0f;
         float mSegmentHeight;
         float mFadedSegmentHeight;
         float mSegmentCornerRadius;
-        int mSegmentColor;
-        int mFadedSegmentColor;
+        // how big the point icon will be, halved
         float mPointRadius;
         float mPointRectInset;
         float mPointRectCornerRadius;
-        int mPointRectColor;
-        int mFadedPointRectColor;
 
         int[] mThemeAttrs;
         int[] mThemeAttrsSegments;
@@ -652,16 +584,13 @@
             mChangingConfigurations = orig.mChangingConfigurations;
             mSegSegGap = orig.mSegSegGap;
             mSegPointGap = orig.mSegPointGap;
+            mSegmentMinWidth = orig.mSegmentMinWidth;
             mSegmentHeight = orig.mSegmentHeight;
             mFadedSegmentHeight = orig.mFadedSegmentHeight;
             mSegmentCornerRadius = orig.mSegmentCornerRadius;
-            mSegmentColor = orig.mSegmentColor;
-            mFadedSegmentColor = orig.mFadedSegmentColor;
             mPointRadius = orig.mPointRadius;
             mPointRectInset = orig.mPointRectInset;
             mPointRectCornerRadius = orig.mPointRectCornerRadius;
-            mPointRectColor = orig.mPointRectColor;
-            mFadedPointRectColor = orig.mFadedPointRectColor;
 
             mThemeAttrs = orig.mThemeAttrs;
             mThemeAttrsSegments = orig.mThemeAttrsSegments;
@@ -674,6 +603,18 @@
         }
 
         private void applyDensityScaling(int sourceDensity, int targetDensity) {
+            if (mSegSegGap > 0) {
+                mSegSegGap = scaleFromDensity(
+                        mSegSegGap, sourceDensity, targetDensity);
+            }
+            if (mSegPointGap > 0) {
+                mSegPointGap = scaleFromDensity(
+                        mSegPointGap, sourceDensity, targetDensity);
+            }
+            if (mSegmentMinWidth > 0) {
+                mSegmentMinWidth = scaleFromDensity(
+                        mSegmentMinWidth, sourceDensity, targetDensity);
+            }
             if (mSegmentHeight > 0) {
                 mSegmentHeight = scaleFromDensity(
                         mSegmentHeight, sourceDensity, targetDensity);
@@ -740,28 +681,6 @@
                 applyDensityScaling(sourceDensity, targetDensity);
             }
         }
-
-        public void setSegmentColor(int color) {
-            mSegmentColor = color;
-            mFadedSegmentColor = getFadedColor(color);
-        }
-
-        public void setPointRectColor(int color) {
-            mPointRectColor = color;
-            mFadedPointRectColor = getFadedColor(color);
-        }
-    }
-
-    /**
-     * Get a color with an opacity that's 25% of the input color.
-     */
-    @ColorInt
-    static int getFadedColor(@ColorInt int color) {
-        return Color.argb(
-                (int) (Color.alpha(color) * 0.4f + 0.5f),
-                Color.red(color),
-                Color.green(color),
-                Color.blue(color));
     }
 
     @Override
diff --git a/core/jni/android_hardware_UsbDeviceConnection.cpp b/core/jni/android_hardware_UsbDeviceConnection.cpp
index b1221ee..68ef3d4 100644
--- a/core/jni/android_hardware_UsbDeviceConnection.cpp
+++ b/core/jni/android_hardware_UsbDeviceConnection.cpp
@@ -165,19 +165,25 @@
         return -1;
     }
 
-    jbyte* bufferBytes = NULL;
-    if (buffer) {
-        bufferBytes = (jbyte*)env->GetPrimitiveArrayCritical(buffer, NULL);
+    bool is_dir_in = (requestType & USB_ENDPOINT_DIR_MASK) == USB_DIR_IN;
+    std::unique_ptr<jbyte[]> bufferBytes(new (std::nothrow) jbyte[length]);
+    if (!bufferBytes) {
+        jniThrowException(env, "java/lang/OutOfMemoryError", NULL);
+        return -1;
     }
 
-    jint result = usb_device_control_transfer(device, requestType, request,
-            value, index, bufferBytes + start, length, timeout);
-
-    if (bufferBytes) {
-        env->ReleasePrimitiveArrayCritical(buffer, bufferBytes, 0);
+    if (!is_dir_in && buffer) {
+        env->GetByteArrayRegion(buffer, start, length, bufferBytes.get());
     }
 
-    return result;
+    jint bytes_transferred = usb_device_control_transfer(device, requestType, request,
+            value, index, bufferBytes.get(), length, timeout);
+
+    if (bytes_transferred > 0 && is_dir_in) {
+        env->SetByteArrayRegion(buffer, start, bytes_transferred, bufferBytes.get());
+    }
+
+    return bytes_transferred;
 }
 
 static jint
diff --git a/core/jni/android_os_Debug.cpp b/core/jni/android_os_Debug.cpp
index 3c2dccd..9ef17e8 100644
--- a/core/jni/android_os_Debug.cpp
+++ b/core/jni/android_os_Debug.cpp
@@ -729,6 +729,17 @@
     return gpuPrivateMem / 1024;
 }
 
+static jlong android_os_Debug_getKernelCmaUsageKb(JNIEnv* env, jobject clazz) {
+    jlong totalKernelCmaUsageKb = -1;
+    uint64_t size;
+
+    if (meminfo::ReadKernelCmaUsageKb(&size)) {
+        totalKernelCmaUsageKb = size;
+    }
+
+    return totalKernelCmaUsageKb;
+}
+
 static jlong android_os_Debug_getDmabufMappedSizeKb(JNIEnv* env, jobject clazz) {
     jlong dmabufPss = 0;
     std::vector<dmabufinfo::DmaBuffer> dmabufs;
@@ -836,6 +847,7 @@
         {"getGpuTotalUsageKb", "()J", (void*)android_os_Debug_getGpuTotalUsageKb},
         {"isVmapStack", "()Z", (void*)android_os_Debug_isVmapStack},
         {"logAllocatorStats", "()Z", (void*)android_os_Debug_logAllocatorStats},
+        {"getKernelCmaUsageKb", "()J", (void*)android_os_Debug_getKernelCmaUsageKb},
 };
 
 int register_android_os_Debug(JNIEnv *env)
diff --git a/core/jni/android_util_AssetManager.cpp b/core/jni/android_util_AssetManager.cpp
index 57bfc70..b2649a4 100644
--- a/core/jni/android_util_AssetManager.cpp
+++ b/core/jni/android_util_AssetManager.cpp
@@ -198,7 +198,7 @@
   auto assetmanager = LockAndStartAssetManager(ptr);
   const ScopedUtfChars package_name_utf8(env, package_name);
   CHECK(package_name_utf8.c_str() != nullptr);
-  const std::string std_package_name(package_name_utf8.c_str());
+  const std::string_view std_package_name(package_name_utf8.c_str());
   const std::unordered_map<std::string, std::string>* map = nullptr;
 
   assetmanager->ForEachPackage([&](const std::string& this_package_name, uint8_t package_id) {
diff --git a/core/jni/android_view_InputEventReceiver.cpp b/core/jni/android_view_InputEventReceiver.cpp
index 3a1e883..6272fb1 100644
--- a/core/jni/android_view_InputEventReceiver.cpp
+++ b/core/jni/android_view_InputEventReceiver.cpp
@@ -441,7 +441,8 @@
                     }
                     env->CallVoidMethod(receiverObj.get(), gInputEventReceiverClassInfo.onDragEvent,
                                         jboolean(dragEvent->isExiting()), dragEvent->getX(),
-                                        dragEvent->getY());
+                                        dragEvent->getY(),
+                                        static_cast<jint>(dragEvent->getDisplayId().val()));
                     finishInputEvent(seq, /*handled=*/true);
                     continue;
                 }
@@ -643,7 +644,7 @@
             GetMethodIDOrDie(env, gInputEventReceiverClassInfo.clazz, "onPointerCaptureEvent",
                              "(Z)V");
     gInputEventReceiverClassInfo.onDragEvent =
-            GetMethodIDOrDie(env, gInputEventReceiverClassInfo.clazz, "onDragEvent", "(ZFF)V");
+            GetMethodIDOrDie(env, gInputEventReceiverClassInfo.clazz, "onDragEvent", "(ZFFI)V");
     gInputEventReceiverClassInfo.onTouchModeChanged =
             GetMethodIDOrDie(env, gInputEventReceiverClassInfo.clazz, "onTouchModeChanged", "(Z)V");
     gInputEventReceiverClassInfo.onBatchedInputEventPending =
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index 0c243d1..6f69e40 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -1113,6 +1113,22 @@
     transaction->setCornerRadius(ctrl, cornerRadius);
 }
 
+static void nativeSetClientDrawnCornerRadius(JNIEnv* env, jclass clazz, jlong transactionObj,
+                                             jlong nativeObject, jfloat clientDrawnCornerRadius) {
+    auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
+
+    SurfaceControl* const ctrl = reinterpret_cast<SurfaceControl*>(nativeObject);
+    transaction->setClientDrawnCornerRadius(ctrl, clientDrawnCornerRadius);
+}
+
+static void nativeSetClientDrawnShadows(JNIEnv* env, jclass clazz, jlong transactionObj,
+                                        jlong nativeObject, jfloat clientDrawnShadowRadius) {
+    auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
+
+    SurfaceControl* const ctrl = reinterpret_cast<SurfaceControl*>(nativeObject);
+    transaction->setClientDrawnShadowRadius(ctrl, clientDrawnShadowRadius);
+}
+
 static void nativeSetBackgroundBlurRadius(JNIEnv* env, jclass clazz, jlong transactionObj,
          jlong nativeObject, jint blurRadius) {
     auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
@@ -2547,6 +2563,10 @@
             (void*)nativeSetCrop },
     {"nativeSetCornerRadius", "(JJF)V",
             (void*)nativeSetCornerRadius },
+    {"nativeSetClientDrawnCornerRadius", "(JJF)V",
+            (void*) nativeSetClientDrawnCornerRadius },
+    {"nativeSetClientDrawnShadows", "(JJF)V",
+            (void*) nativeSetClientDrawnShadows },
     {"nativeSetBackgroundBlurRadius", "(JJI)V",
             (void*)nativeSetBackgroundBlurRadius },
     {"nativeSetLayerStack", "(JJI)V",
diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto
index cf81ba1..5d0b340 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -107,6 +107,8 @@
         optional SettingProto accessibility_key_gesture_targets = 59 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto hct_rect_prompt_status = 60 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto em_value = 61 [ (android.privacy).dest = DEST_AUTOMATIC ];
+        // Settings for accessibility autoclick
+        optional SettingProto autoclick_cursor_area_size = 62 [ (android.privacy).dest = DEST_AUTOMATIC ];
 
     }
     optional Accessibility accessibility = 2;
@@ -578,6 +580,7 @@
         optional SettingProto activate_on_dock = 3 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto activate_on_sleep = 4 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto default_component = 5 [ (android.privacy).dest = DEST_AUTOMATIC ];
+        optional SettingProto activate_on_postured = 6 [ (android.privacy).dest = DEST_AUTOMATIC ];
     }
     optional Screensaver screensaver = 47;
 
@@ -645,14 +648,8 @@
     optional SettingProto theme_customization_overlay_packages = 75 [ (android.privacy).dest = DEST_AUTOMATIC ];
     optional SettingProto trust_agents_initialized = 57 [ (android.privacy).dest = DEST_AUTOMATIC ];
 
-    message TrackpadGesture {
-        optional SettingProto trackpad_gesture_back_enabled = 1 [ (android.privacy).dest = DEST_AUTOMATIC ];
-        optional SettingProto trackpad_gesture_home_enabled = 2 [ (android.privacy).dest = DEST_AUTOMATIC ];
-        optional SettingProto trackpad_gesture_overview_enabled = 3 [ (android.privacy).dest = DEST_AUTOMATIC ];
-        optional SettingProto trackpad_gesture_notification_enabled = 4 [ (android.privacy).dest = DEST_AUTOMATIC ];
-        optional SettingProto trackpad_gesture_quick_switch_enabled = 5 [ (android.privacy).dest = DEST_AUTOMATIC ];
-    }
-    optional TrackpadGesture trackpad_gesture = 94;
+    reserved 94;  // formerly trackpad_gesture
+    reserved "trackpad_gesture";
 
     message Tts {
         option (android.msg_privacy).dest = DEST_EXPLICIT;
diff --git a/core/proto/android/providers/settings/system.proto b/core/proto/android/providers/settings/system.proto
index 0d99200..64c9f54 100644
--- a/core/proto/android/providers/settings/system.proto
+++ b/core/proto/android/providers/settings/system.proto
@@ -229,6 +229,7 @@
         optional SettingProto swap_primary_button = 2 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto scrolling_acceleration = 3 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto pointer_acceleration_enabled = 4 [ (android.privacy).dest = DEST_AUTOMATIC ];
+        optional SettingProto scrolling_speed = 5 [ (android.privacy).dest = DEST_AUTOMATIC ];
     }
 
     optional Mouse mouse = 38;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index dc95471..ed021b6 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -5405,10 +5405,9 @@
     <permission android:name="android.permission.CHANGE_ACCESSIBILITY_VOLUME"
                 android:protectionLevel="signature" />
 
-    <!-- @FlaggedApi("com.android.server.accessibility.motion_event_observing")
-    @hide
-    @TestApi
-    Allows an accessibility service to observe motion events without consuming them. -->
+    <!-- @TestApi Allows an accessibility service to observe motion events
+         without consuming them.
+         @hide -->
     <permission android:name="android.permission.ACCESSIBILITY_MOTION_EVENT_OBSERVING"
                 android:protectionLevel="signature" />
 
diff --git a/core/res/res/drawable/notification_progress.xml b/core/res/res/drawable/notification_progress.xml
index 5d272fb..ff5450e 100644
--- a/core/res/res/drawable/notification_progress.xml
+++ b/core/res/res/drawable/notification_progress.xml
@@ -24,6 +24,7 @@
             android:segPointGap="@dimen/notification_progress_segPoint_gap">
             <segments
                 android:color="?attr/colorProgressBackgroundNormal"
+                android:minWidth="@dimen/notification_progress_segments_min_width"
                 android:height="@dimen/notification_progress_segments_height"
                 android:fadedHeight="@dimen/notification_progress_segments_faded_height"
                 android:cornerRadius="@dimen/notification_progress_segments_corner_radius"/>
diff --git a/core/res/res/layout/notification_2025_conversation_header.xml b/core/res/res/layout/notification_2025_conversation_header.xml
index db79e79..1bde173 100644
--- a/core/res/res/layout/notification_2025_conversation_header.xml
+++ b/core/res/res/layout/notification_2025_conversation_header.xml
@@ -136,10 +136,10 @@
 
     <ImageView
         android:id="@+id/phishing_alert"
-        android:layout_width="@dimen/notification_phishing_alert_size"
-        android:layout_height="@dimen/notification_phishing_alert_size"
-        android:layout_marginStart="@dimen/notification_conversation_header_separating_margin"
-        android:baseline="10dp"
+        android:layout_width="@dimen/notification_2025_badge_size"
+        android:layout_height="@dimen/notification_2025_badge_size"
+        android:layout_marginStart="@dimen/notification_2025_badge_margin"
+        android:baseline="@dimen/notification_2025_badge_baseline"
         android:scaleType="fitCenter"
         android:src="@drawable/ic_dialog_alert_material"
         android:visibility="gone"
@@ -148,10 +148,10 @@
 
     <ImageView
         android:id="@+id/profile_badge"
-        android:layout_width="@dimen/notification_badge_size"
-        android:layout_height="@dimen/notification_badge_size"
-        android:layout_marginStart="@dimen/notification_conversation_header_separating_margin"
-        android:baseline="10dp"
+        android:layout_width="@dimen/notification_2025_badge_size"
+        android:layout_height="@dimen/notification_2025_badge_size"
+        android:layout_marginStart="@dimen/notification_2025_badge_margin"
+        android:baseline="@dimen/notification_2025_badge_baseline"
         android:scaleType="fitCenter"
         android:visibility="gone"
         android:contentDescription="@string/notification_work_profile_content_description"
@@ -159,10 +159,10 @@
 
     <ImageView
         android:id="@+id/alerted_icon"
-        android:layout_width="@dimen/notification_alerted_size"
-        android:layout_height="@dimen/notification_alerted_size"
-        android:layout_marginStart="@dimen/notification_conversation_header_separating_margin"
-        android:baseline="10dp"
+        android:layout_width="@dimen/notification_2025_badge_size"
+        android:layout_height="@dimen/notification_2025_badge_size"
+        android:layout_marginStart="@dimen/notification_2025_badge_margin"
+        android:baseline="@dimen/notification_2025_badge_baseline"
         android:contentDescription="@string/notification_alerted_content_description"
         android:scaleType="fitCenter"
         android:src="@drawable/ic_notifications_alerted"
diff --git a/core/res/res/layout/notification_2025_expand_button.xml b/core/res/res/layout/notification_2025_expand_button.xml
index c8263c2..1c36754 100644
--- a/core/res/res/layout/notification_2025_expand_button.xml
+++ b/core/res/res/layout/notification_2025_expand_button.xml
@@ -22,6 +22,7 @@
     android:layout_height="wrap_content"
     android:layout_gravity="top|end"
     android:contentDescription="@string/expand_button_content_description_collapsed"
+    android:padding="@dimen/notification_2025_margin"
     >
 
     <LinearLayout
diff --git a/core/res/res/layout/notification_2025_template_collapsed_base.xml b/core/res/res/layout/notification_2025_template_collapsed_base.xml
index c827dcb..d29b7af 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_base.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_base.xml
@@ -87,7 +87,7 @@
                 >
 
                 <!--
-                NOTE: The notification_top_line_views layout contains the app_name_text.
+                NOTE: The notification_2025_top_line_views layout contains the app_name_text.
                 In order to include the title view at the beginning, the Notification.Builder
                 has logic to hide that view whenever this title view is to be visible.
                 -->
@@ -104,7 +104,7 @@
                     android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title"
                     />
 
-                <include layout="@layout/notification_top_line_views" />
+                <include layout="@layout/notification_2025_top_line_views" />
 
             </NotificationTopLineView>
 
@@ -168,7 +168,6 @@
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_gravity="top|end"
-                android:layout_margin="@dimen/notification_2025_margin"
                 />
 
         </FrameLayout>
diff --git a/core/res/res/layout/notification_2025_template_collapsed_call.xml b/core/res/res/layout/notification_2025_template_collapsed_call.xml
index ce38c164..6f3c15a 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_call.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_call.xml
@@ -70,7 +70,6 @@
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_gravity="top|end"
-                android:layout_margin="@dimen/notification_2025_margin"
                 />
 
         </FrameLayout>
diff --git a/core/res/res/layout/notification_2025_template_collapsed_media.xml b/core/res/res/layout/notification_2025_template_collapsed_media.xml
index 0021b83..5beab50 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_media.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_media.xml
@@ -89,7 +89,7 @@
                 >
 
                 <!--
-                NOTE: The notification_top_line_views layout contains the app_name_text.
+                NOTE: The notification_2025_top_line_views layout contains the app_name_text.
                 In order to include the title view at the beginning, the Notification.Builder
                 has logic to hide that view whenever this title view is to be visible.
                 -->
@@ -106,7 +106,7 @@
                     android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title"
                     />
 
-                <include layout="@layout/notification_top_line_views" />
+                <include layout="@layout/notification_2025_top_line_views" />
 
             </NotificationTopLineView>
 
@@ -189,7 +189,6 @@
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_gravity="top|end"
-                android:layout_margin="@dimen/notification_2025_margin"
                 />
 
         </FrameLayout>
diff --git a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml
index f3e4ce1..d7c3263 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml
@@ -115,7 +115,7 @@
                         >
 
                         <!--
-                        NOTE: The notification_top_line_views layout contains the app_name_text.
+                        NOTE: The notification_2025_top_line_views layout contains the app_name_text.
                         In order to include the title view at the beginning, the Notification.Builder
                         has logic to hide that view whenever this title view is to be visible.
                         -->
@@ -132,7 +132,7 @@
                             android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title"
                             />
 
-                        <include layout="@layout/notification_top_line_views" />
+                        <include layout="@layout/notification_2025_top_line_views" />
 
                     </NotificationTopLineView>
 
@@ -193,7 +193,6 @@
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
                         android:layout_gravity="top|end"
-                        android:layout_margin="@dimen/notification_2025_margin"
                         />
 
                 </FrameLayout>
diff --git a/core/res/res/layout/notification_2025_template_conversation.xml b/core/res/res/layout/notification_2025_template_conversation.xml
index 6be5a1c..24b6ad0 100644
--- a/core/res/res/layout/notification_2025_template_conversation.xml
+++ b/core/res/res/layout/notification_2025_template_conversation.xml
@@ -152,7 +152,6 @@
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:layout_gravity="top|end"
-                    android:layout_margin="@dimen/notification_2025_margin"
                     />
             </LinearLayout>
         </LinearLayout>
diff --git a/core/res/res/layout/notification_2025_template_header.xml b/core/res/res/layout/notification_2025_template_header.xml
index 3f34eb3..72b3798 100644
--- a/core/res/res/layout/notification_2025_template_header.xml
+++ b/core/res/res/layout/notification_2025_template_header.xml
@@ -68,7 +68,7 @@
         android:theme="@style/Theme.DeviceDefault.Notification"
         >
 
-        <include layout="@layout/notification_top_line_views" />
+        <include layout="@layout/notification_2025_top_line_views" />
 
     </NotificationTopLineView>
 
@@ -85,7 +85,6 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_gravity="top|end"
-        android:layout_margin="@dimen/notification_2025_margin"
         android:layout_alignParentEnd="true" />
 
     <include layout="@layout/notification_close_button"
diff --git a/core/res/res/layout/notification_2025_template_heads_up_base.xml b/core/res/res/layout/notification_2025_template_heads_up_base.xml
index e4ff835..084ec7d 100644
--- a/core/res/res/layout/notification_2025_template_heads_up_base.xml
+++ b/core/res/res/layout/notification_2025_template_heads_up_base.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?><!--
-  ~ Copyright (C) 2014 The Android Open Source Project
+  ~ Copyright (C) 2024 The Android Open Source Project
   ~
   ~ Licensed under the Apache License, Version 2.0 (the "License");
   ~ you may not use this file except in compliance with the License.
diff --git a/core/res/res/layout/notification_2025_top_line_views.xml b/core/res/res/layout/notification_2025_top_line_views.xml
new file mode 100644
index 0000000..7487346
--- /dev/null
+++ b/core/res/res/layout/notification_2025_top_line_views.xml
@@ -0,0 +1,159 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2025 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
+  -->
+<!--
+ This layout file should be included inside a NotificationTopLineView, sometimes after a
+ <TextView android:id="@+id/title"/>
+-->
+<merge
+    xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <TextView
+        android:id="@+id/app_name_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="@dimen/notification_header_separating_margin"
+        android:singleLine="true"
+        android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info"
+        android:visibility="?attr/notificationHeaderAppNameVisibility"
+        />
+
+    <TextView
+        android:id="@+id/header_text_secondary_divider"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info"
+        android:layout_marginStart="@dimen/notification_header_separating_margin"
+        android:layout_marginEnd="@dimen/notification_header_separating_margin"
+        android:text="@string/notification_header_divider_symbol"
+        android:visibility="gone"
+        />
+
+    <TextView
+        android:id="@+id/header_text_secondary"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info"
+        android:layout_marginStart="@dimen/notification_header_separating_margin"
+        android:layout_marginEnd="@dimen/notification_header_separating_margin"
+        android:visibility="gone"
+        android:singleLine="true"
+        />
+
+    <TextView
+        android:id="@+id/header_text_divider"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info"
+        android:layout_marginStart="@dimen/notification_header_separating_margin"
+        android:layout_marginEnd="@dimen/notification_header_separating_margin"
+        android:text="@string/notification_header_divider_symbol"
+        android:visibility="gone"
+        />
+
+    <TextView
+        android:id="@+id/header_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info"
+        android:layout_marginStart="@dimen/notification_header_separating_margin"
+        android:layout_marginEnd="@dimen/notification_header_separating_margin"
+        android:visibility="gone"
+        android:singleLine="true"
+        />
+
+    <TextView
+        android:id="@+id/time_divider"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info"
+        android:layout_marginStart="@dimen/notification_header_separating_margin"
+        android:layout_marginEnd="@dimen/notification_header_separating_margin"
+        android:text="@string/notification_header_divider_symbol"
+        android:singleLine="true"
+        android:visibility="gone"
+        />
+
+    <DateTimeView
+        android:id="@+id/time"
+        android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Time"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/notification_header_separating_margin"
+        android:layout_marginEnd="@dimen/notification_header_separating_margin"
+        android:showRelative="true"
+        android:singleLine="true"
+        android:visibility="gone"
+        />
+
+    <ViewStub
+        android:id="@+id/chronometer"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/notification_header_separating_margin"
+        android:layout_marginEnd="@dimen/notification_header_separating_margin"
+        android:layout="@layout/notification_template_part_chronometer"
+        android:visibility="gone"
+        />
+
+    <ImageButton
+        android:id="@+id/feedback"
+        android:layout_width="@dimen/notification_feedback_size"
+        android:layout_height="@dimen/notification_feedback_size"
+        android:layout_marginStart="@dimen/notification_header_separating_margin"
+        android:baseline="13dp"
+        android:scaleType="fitCenter"
+        android:src="@drawable/ic_feedback_indicator"
+        android:background="?android:selectableItemBackgroundBorderless"
+        android:visibility="gone"
+        android:contentDescription="@string/notification_feedback_indicator"
+        />
+
+    <ImageView
+        android:id="@+id/phishing_alert"
+        android:layout_width="@dimen/notification_2025_badge_size"
+        android:layout_height="@dimen/notification_2025_badge_size"
+        android:layout_marginStart="@dimen/notification_2025_badge_margin"
+        android:baseline="@dimen/notification_2025_badge_baseline"
+        android:scaleType="fitCenter"
+        android:src="@drawable/ic_dialog_alert_material"
+        android:visibility="gone"
+        android:contentDescription="@string/notification_phishing_alert_content_description"
+        />
+
+    <ImageView
+        android:id="@+id/profile_badge"
+        android:layout_width="@dimen/notification_2025_badge_size"
+        android:layout_height="@dimen/notification_2025_badge_size"
+        android:layout_marginStart="@dimen/notification_2025_badge_margin"
+        android:baseline="@dimen/notification_2025_badge_baseline"
+        android:scaleType="fitCenter"
+        android:visibility="gone"
+        android:contentDescription="@string/notification_work_profile_content_description"
+        />
+
+    <ImageView
+        android:id="@+id/alerted_icon"
+        android:layout_width="@dimen/notification_2025_badge_size"
+        android:layout_height="@dimen/notification_2025_badge_size"
+        android:layout_marginStart="@dimen/notification_2025_badge_margin"
+        android:baseline="@dimen/notification_2025_badge_baseline"
+        android:contentDescription="@string/notification_alerted_content_description"
+        android:scaleType="fitCenter"
+        android:src="@drawable/ic_notifications_alerted"
+        android:visibility="gone"
+        />
+</merge>
+
diff --git a/core/res/res/values-round-watch/dimens.xml b/core/res/res/values-round-watch/dimens.xml
index f288b41..59ee554 100644
--- a/core/res/res/values-round-watch/dimens.xml
+++ b/core/res/res/values-round-watch/dimens.xml
@@ -26,6 +26,6 @@
     <item name="input_extract_action_button_height" type="dimen">32dp</item>
     <item name="input_extract_action_icon_padding" type="dimen">5dp</item>
 
-    <item name="global_actions_vertical_padding_percentage" type="fraction">20.8%</item>
+    <item name="global_actions_vertical_padding_percentage" type="fraction">21.8%</item>
     <item name="global_actions_horizontal_padding_percentage" type="fraction">5.2%</item>
 </resources>
diff --git a/core/res/res/values-watch/config.xml b/core/res/res/values-watch/config.xml
index e6295ea..4ff3f88 100644
--- a/core/res/res/values-watch/config.xml
+++ b/core/res/res/values-watch/config.xml
@@ -101,6 +101,13 @@
          P.S this is a change only intended for wear devices. -->
     <bool name="config_enableViewGroupScalingFading">true</bool>
 
-    <!-- Allow the gesture to double tap the power button to trigger a target action. -->
-    <bool name="config_doubleTapPowerGestureEnabled">false</bool>
+    <!-- Controls the double tap power button gesture to trigger a target action.
+         0: Gesture is disabled
+         1: Launch camera mode, allowing the user to disable/enable the double tap power gesture
+            from launching the camera application.
+         2: Multi target mode, allowing the user to select one of the targets defined in
+            config_doubleTapPowerGestureMultiTargetDefaultAction and to disable/enable the double
+            tap power gesture from triggering the selected target action.
+    -->
+    <integer name="config_doubleTapPowerGestureMode">0</integer>
 </resources>
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 728c856..8372aec 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -7572,25 +7572,31 @@
     <!-- NotificationProgressDrawable class -->
     <!-- ================================== -->
 
-    <!-- Drawable used to render a segmented bar, with segments and points. -->
+    <!-- Drawable used to render a notification progress bar, with segments and points. -->
     <!-- @hide internal use only -->
     <declare-styleable name="NotificationProgressDrawable">
-        <!-- Default color for the parts. -->
+        <!-- The gap between two segments. -->
         <attr name="segSegGap" format="dimension" />
+        <!-- The gap between a segment and a point. -->
         <attr name="segPointGap" format="dimension" />
     </declare-styleable>
 
     <!-- Used to config the segments of a NotificationProgressDrawable. -->
     <!-- @hide internal use only -->
     <declare-styleable name="NotificationProgressDrawableSegments">
-        <!-- Height of the solid segments -->
+        <!-- TODO: b/372908709 - maybe move this to NotificationProgressBar, because that's the only
+         place this is used actually. Same for NotificationProgressDrawable.segSegGap/segPointGap
+         above. -->
+        <!-- Minimum required drawing width. The drawing width refers to the width after
+         the original segments have been adjusted for the neighboring Points and gaps. This is
+         enforced by stretching the segments that are too short. -->
+        <attr name="minWidth" format="dimension" />
+        <!-- Height of the solid segments. -->
         <attr name="height" />
-        <!-- Height of the faded segments -->
-        <attr name="fadedHeight" format="dimension"/>
+        <!-- Height of the faded segments. -->
+        <attr name="fadedHeight" format="dimension" />
         <!-- Corner radius of the segment rect. -->
         <attr name="cornerRadius" format="dimension" />
-        <!-- Default color of the segment. -->
-        <attr name="color" />
     </declare-styleable>
 
     <!-- Used to config the points of a NotificationProgressDrawable. -->
@@ -7602,8 +7608,6 @@
         <attr name="inset" />
         <!-- Corner radius of the point rect. -->
         <attr name="cornerRadius"/>
-        <!-- Default color of the point rect. -->
-        <attr name="color" />
     </declare-styleable>
 
     <!-- ========================== -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 45a5d85..e14cffd 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2083,6 +2083,18 @@
          See com.android.server.timezonedetector.TimeZoneDetectorStrategy for more information. -->
     <bool name="config_supportTelephonyTimeZoneFallback" translatable="false">true</bool>
 
+    <!-- Whether the time notifications feature is enabled. Settings this to false means the feature
+         cannot be used. Setting this to true means the feature can be enabled on the device. -->
+    <bool name="config_enableTimeZoneNotificationsSupported" translatable="false">true</bool>
+
+    <!-- Whether the time zone notifications tracking feature is enabled. Settings this to false
+         means the feature cannot be used. -->
+    <bool name="config_enableTimeZoneNotificationsTrackingSupported" translatable="false">true</bool>
+
+    <!-- Whether the time zone manual change tracking feature is enabled. Settings this to false
+         means the feature cannot be used. -->
+    <bool name="config_enableTimeZoneManualChangeTrackingSupported" translatable="false">true</bool>
+
     <!-- Whether to enable network location overlay which allows network location provider to be
          replaced by an app at run-time. When disabled, only the
          config_networkLocationProviderPackageName package will be searched for network location
@@ -2754,6 +2766,9 @@
     <bool name="config_dreamsActivatedOnDockByDefault">true</bool>
     <!-- If supported and enabled, are dreams activated when asleep and charging? (by default) -->
     <bool name="config_dreamsActivatedOnSleepByDefault">false</bool>
+    <!-- If supported and enabled, are dreams enabled while device is stationary and upright?
+         (by default) -->
+    <bool name="config_dreamsActivatedOnPosturedByDefault">false</bool>
     <!-- ComponentName of the default dream (Settings.Secure.DEFAULT_SCREENSAVER_COMPONENT) -->
     <string name="config_dreamsDefaultComponent" translatable="false">com.android.deskclock/com.android.deskclock.Screensaver</string>
     <!-- ComponentNames of the dreams that we should hide -->
@@ -4244,12 +4259,19 @@
          is non-interactive. -->
     <bool name="config_cameraDoubleTapPowerGestureEnabled">true</bool>
 
-    <!-- Allow the gesture to double tap the power button to trigger a target action. -->
-    <bool name="config_doubleTapPowerGestureEnabled">true</bool>
-    <!-- Default target action for double tap of the power button gesture.
+    <!-- Controls the double tap power button gesture to trigger a target action.
+         0: Gesture is disabled
+         1: Launch camera mode, allowing the user to disable/enable the double tap power gesture
+            from launching the camera application.
+         2: Multi target mode, allowing the user to select one of the targets defined in
+            config_doubleTapPowerGestureMultiTargetDefaultAction and to disable/enable the double
+            tap power gesture from triggering the selected target action.
+    -->
+    <integer name="config_doubleTapPowerGestureMode">2</integer>
+    <!-- Default target action for double tap of the power button gesture in multi target mode.
          0: Launch camera
          1: Launch wallet -->
-    <integer name="config_defaultDoubleTapPowerGestureAction">0</integer>
+    <integer name="config_doubleTapPowerGestureMultiTargetDefaultAction">0</integer>
 
     <!-- Allow the gesture to quick tap the power button multiple times to start the emergency sos
          experience while the device is non-interactive. -->
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 2adb791..d6b8704 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -608,12 +608,23 @@
     <!-- Size of the feedback indicator for notifications -->
     <dimen name="notification_feedback_size">20dp</dimen>
 
+    <!-- Size of the (work) profile badge for notifications -->
+    <dimen name="notification_badge_size">12dp</dimen>
+
+    <!-- Size of the (work) profile badge for notifications (2025 redesign version).
+         Scales with font size. Chosen to look good alongside notification_subtext_size text. -->
+    <dimen name="notification_2025_badge_size">14sp</dimen>
+
+    <!-- Baseline for aligning icons in the top line (like the work profile icon or alerting icon)
+         to the text properly. This is equal to notification_2025_badge_size - 2sp. -->
+    <dimen name="notification_2025_badge_baseline">12sp</dimen>
+
+    <!-- Spacing for the top line icons (e.g. the work profile badge). -->
+    <dimen name="notification_2025_badge_margin">4dp</dimen>
+
     <!-- Size of the phishing alert for notifications -->
     <dimen name="notification_phishing_alert_size">@dimen/notification_badge_size</dimen>
 
-    <!-- Size of the profile badge for notifications -->
-    <dimen name="notification_badge_size">12dp</dimen>
-
     <!-- Size of the alerted icon for notifications -->
     <dimen name="notification_alerted_size">@dimen/notification_badge_size</dimen>
 
@@ -888,6 +899,8 @@
     <dimen name="notification_progress_segSeg_gap">4dp</dimen>
     <!-- The gap between a segment and a point in the notification progress bar -->
     <dimen name="notification_progress_segPoint_gap">4dp</dimen>
+    <!-- The minimum required drawing width of the notification progress bar segments -->
+    <dimen name="notification_progress_segments_min_width">16dp</dimen>
     <!-- The height of the notification progress bar segments -->
     <dimen name="notification_progress_segments_height">6dp</dimen>
     <!-- The height of the notification progress bar faded segments -->
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 6313054..fa4c21d 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -853,6 +853,9 @@
     <!-- Text shown when viewing channel settings for notifications related to vpn status -->
     <string name="notification_channel_vpn">VPN status</string>
 
+    <!-- Text shown when viewing channel settings for notifications related to system time -->
+    <string name="notification_channel_system_time">Time and time zones</string>
+
     <!-- Notification channel name. This channel sends high-priority alerts from the user's IT admin for key updates about the user's work device or work profile. -->
     <string name="notification_channel_device_admin">Alerts from your IT admin</string>
 
@@ -881,6 +884,10 @@
     <string name="notification_channel_accessibility_magnification">Magnification</string>
 
     <!-- Text shown when viewing channel settings for notifications related to accessibility
+     hearing device. [CHAR_LIMIT=NONE]-->
+    <string name="notification_channel_accessibility_hearing_device">Hearing device</string>
+
+    <!-- Text shown when viewing channel settings for notifications related to accessibility
          security policy. [CHAR_LIMIT=NONE]-->
     <string name="notification_channel_accessibility_security_policy">Accessibility usage</string>
 
@@ -3875,6 +3882,12 @@
     <string name="carrier_app_notification_title">New SIM inserted</string>
     <string name="carrier_app_notification_text">Tap to set it up</string>
 
+    <!-- Time zone notification strings -->
+    <!-- Title for time zone change notifications -->
+    <string name="time_zone_change_notification_title">Your time zone changed</string>
+    <!-- Body for time zone change notifications -->
+    <string name="time_zone_change_notification_body">You\'re now in <xliff:g id="time_zone_display_name">%1$s</xliff:g> (<xliff:g id="time_zone_offset">%2$s</xliff:g>)</string>
+
     <!-- Date/Time picker dialogs strings -->
 
     <!-- The title of the time picker dialog. [CHAR LIMIT=NONE] -->
@@ -4985,6 +4998,19 @@
     <!-- Text used to describe system navigation features, shown within a UI allowing a user to assign system magnification features to the Accessibility button in the navigation bar. -->
     <string name="accessibility_magnification_chooser_text">Magnification</string>
 
+    <!-- Notification title for switching input to the phone's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] -->
+    <string name="hearing_device_switch_phone_mic_notification_title">Switch to phone mic?</string>
+    <!-- Notification title for switching input to the hearing device's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] -->
+    <string name="hearing_device_switch_hearing_mic_notification_title">Switch to hearing aid mic?</string>
+    <!-- Notification content for switching input to the phone's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] -->
+    <string name="hearing_device_switch_phone_mic_notification_text">For better sound or if your hearing aid battery is low. This only switches your mic during the call.</string>
+    <!-- Notification content for switching input to the hearing device's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] -->
+    <string name="hearing_device_switch_hearing_mic_notification_text">You can use your hearing aid microphone for hands-free calling. This only switches your mic during the call.</string>
+    <!-- Notification action button. Click it will switch the input between phone's microphone and hearing device's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] -->
+    <string name="hearing_device_notification_switch_button">Switch</string>
+    <!-- Notification action button. Click it will open the bluetooth device details page for this hearing device. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] -->
+    <string name="hearing_device_notification_settings_button">Settings</string>
+
     <!-- Text spoken when the current user is switched if accessibility is enabled. [CHAR LIMIT=none] -->
     <string name="user_switched">Current user <xliff:g id="name" example="Bob">%1$s</xliff:g>.</string>
     <!-- Message shown when switching to a user [CHAR LIMIT=none] -->
@@ -6620,6 +6646,8 @@
     <string name="satellite_notification_title">Auto connected to satellite</string>
     <!-- Notification summary when satellite service is auto connected. [CHAR LIMIT=NONE] -->
     <string name="satellite_notification_summary">You can send and receive messages without a mobile or Wi-Fi network</string>
+    <!-- Notification summary when satellite service connected with data service supported. [CHAR LIMIT=NONE] -->
+    <string name="satellite_notification_summary_with_data">You can send and receive messages and use limited data by satellite</string>
     <!-- Notification title when satellite service can be manually enabled. -->
     <string name="satellite_notification_manual_title">Use satellite messaging?</string>
     <!-- Notification summary when satellite service can be manually enabled. [CHAR LIMIT=NONE] -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index a18f923..68008e5 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -575,6 +575,7 @@
   <java-symbol type="dimen" name="notification_top_pad_large_text" />
   <java-symbol type="dimen" name="notification_top_pad_large_text_narrow" />
   <java-symbol type="dimen" name="notification_badge_size" />
+  <java-symbol type="dimen" name="notification_2025_badge_size" />
   <java-symbol type="dimen" name="immersive_mode_cling_width" />
   <java-symbol type="dimen" name="accessibility_magnification_indicator_width" />
   <java-symbol type="dimen" name="circular_display_mask_thickness" />
@@ -2329,6 +2330,7 @@
   <java-symbol type="bool" name="config_dreamsEnabledOnBattery" />
   <java-symbol type="bool" name="config_dreamsActivatedOnDockByDefault" />
   <java-symbol type="bool" name="config_dreamsActivatedOnSleepByDefault" />
+  <java-symbol type="bool" name="config_dreamsActivatedOnPosturedByDefault" />
   <java-symbol type="integer" name="config_dreamsBatteryLevelMinimumWhenPowered" />
   <java-symbol type="integer" name="config_dreamsBatteryLevelMinimumWhenNotPowered" />
   <java-symbol type="integer" name="config_dreamsBatteryLevelDrainCutoff" />
@@ -2377,6 +2379,9 @@
   <java-symbol type="string" name="config_secondaryLocationTimeZoneProviderPackageName" />
   <java-symbol type="bool" name="config_enableTelephonyTimeZoneDetection" />
   <java-symbol type="bool" name="config_supportTelephonyTimeZoneFallback" />
+  <java-symbol type="bool" name="config_enableTimeZoneNotificationsSupported" />
+  <java-symbol type="bool" name="config_enableTimeZoneNotificationsTrackingSupported" />
+  <java-symbol type="bool" name="config_enableTimeZoneManualChangeTrackingSupported" />
   <java-symbol type="bool" name="config_autoResetAirplaneMode" />
   <java-symbol type="string" name="config_notificationAccessConfirmationActivity" />
   <java-symbol type="bool" name="config_preventImeStartupUnlessTextEditor" />
@@ -3167,8 +3172,8 @@
   <java-symbol type="integer" name="config_cameraLiftTriggerSensorType" />
   <java-symbol type="string" name="config_cameraLiftTriggerSensorStringType" />
   <java-symbol type="bool" name="config_cameraDoubleTapPowerGestureEnabled" />
-  <java-symbol type="bool" name="config_doubleTapPowerGestureEnabled" />
-  <java-symbol type="integer" name="config_defaultDoubleTapPowerGestureAction" />
+  <java-symbol type="integer" name="config_doubleTapPowerGestureMode" />
+  <java-symbol type="integer" name="config_doubleTapPowerGestureMultiTargetDefaultAction" />
   <java-symbol type="bool" name="config_emergencyGestureEnabled" />
   <java-symbol type="bool" name="config_defaultEmergencyGestureEnabled" />
   <java-symbol type="bool" name="config_defaultEmergencyGestureSoundEnabled" />
@@ -3839,6 +3844,13 @@
   <java-symbol type="string" name="reduce_bright_colors_feature_name" />
   <java-symbol type="string" name="one_handed_mode_feature_name" />
 
+  <java-symbol type="string" name="hearing_device_switch_phone_mic_notification_title" />
+  <java-symbol type="string" name="hearing_device_switch_hearing_mic_notification_title" />
+  <java-symbol type="string" name="hearing_device_switch_phone_mic_notification_text" />
+  <java-symbol type="string" name="hearing_device_switch_hearing_mic_notification_text" />
+  <java-symbol type="string" name="hearing_device_notification_switch_button" />
+  <java-symbol type="string" name="hearing_device_notification_settings_button" />
+
   <!-- com.android.internal.widget.RecyclerView -->
   <java-symbol type="id" name="item_touch_helper_previous_elevation"/>
   <java-symbol type="dimen" name="item_touch_helper_max_drag_scroll_per_frame"/>
@@ -3939,6 +3951,7 @@
   <java-symbol type="dimen" name="notification_progress_tracker_height" />
   <java-symbol type="dimen" name="notification_progress_segSeg_gap" />
   <java-symbol type="dimen" name="notification_progress_segPoint_gap" />
+  <java-symbol type="dimen" name="notification_progress_segments_min_width" />
   <java-symbol type="dimen" name="notification_progress_segments_height" />
   <java-symbol type="dimen" name="notification_progress_segments_faded_height" />
   <java-symbol type="dimen" name="notification_progress_segments_corner_radius" />
@@ -4018,6 +4031,7 @@
   <java-symbol type="string" name="notification_channel_network_available" />
   <java-symbol type="array" name="config_defaultCloudSearchServices" />
   <java-symbol type="string" name="notification_channel_vpn" />
+  <java-symbol type="string" name="notification_channel_system_time" />
   <java-symbol type="string" name="notification_channel_device_admin" />
   <java-symbol type="string" name="notification_channel_alerts" />
   <java-symbol type="string" name="notification_channel_retail_mode" />
@@ -4025,8 +4039,11 @@
   <java-symbol type="string" name="notification_channel_heavy_weight_app" />
   <java-symbol type="string" name="notification_channel_system_changes" />
   <java-symbol type="string" name="notification_channel_accessibility_magnification" />
+  <java-symbol type="string" name="notification_channel_accessibility_hearing_device" />
   <java-symbol type="string" name="notification_channel_accessibility_security_policy" />
   <java-symbol type="string" name="notification_channel_display" />
+  <java-symbol type="string" name="time_zone_change_notification_title" />
+  <java-symbol type="string" name="time_zone_change_notification_body" />
   <java-symbol type="string" name="config_defaultAutofillService" />
   <java-symbol type="string" name="config_defaultFieldClassificationService" />
   <java-symbol type="string" name="config_defaultOnDeviceSpeechRecognitionService" />
@@ -5635,6 +5652,7 @@
   <!-- System notification for satellite service -->
   <java-symbol type="string" name="satellite_notification_title" />
   <java-symbol type="string" name="satellite_notification_summary" />
+  <java-symbol type="string" name="satellite_notification_summary_with_data" />
   <java-symbol type="string" name="satellite_notification_manual_title" />
   <java-symbol type="string" name="satellite_notification_manual_summary" />
   <java-symbol type="string" name="satellite_notification_open_message" />
diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml
index bb5380e..06cd44e 100644
--- a/core/res/res/xml/sms_short_codes.xml
+++ b/core/res/res/xml/sms_short_codes.xml
@@ -34,7 +34,7 @@
          http://smscoin.net/software/engine/WordPress/Paid+SMS-registration/ -->
 
     <!-- Arab Emirates -->
-    <shortcode country="ae" pattern="\\d{1,5}" free="1017|1355|3214|6253|6568" />
+    <shortcode country="ae" pattern="\\d{1,5}" free="1017|1355|3214|6253|6568|999" />
 
     <!-- Albania: 5 digits, known short codes listed -->
     <shortcode country="al" pattern="\\d{5}" premium="15191|55[56]00" />
@@ -63,8 +63,8 @@
     <!-- Burkina Faso: 1-4 digits (standard system default, not country specific) -->
     <shortcode country="bf" pattern="\\d{1,4}" free="3558" />
 
-    <!-- Bulgaria: 4-5 digits, plus EU -->
-    <shortcode country="bg" pattern="\\d{4,5}" premium="18(?:16|423)|19(?:1[56]|35)" free="116\\d{3}|1988|1490" />
+    <!-- Bulgaria: 4-6 digits, plus EU -->
+    <shortcode country="bg" pattern="\\d{4,6}" premium="18(?:16|423)|19(?:1[56]|35)" free="116\\d{3}|1988|1490|162055" />
 
     <!-- Bahrain: 1-5 digits (standard system default, not country specific) -->
     <shortcode country="bh" pattern="\\d{1,5}" free="81181|85999" />
@@ -81,18 +81,21 @@
     <!-- Canada: 5-6 digits -->
     <shortcode country="ca" pattern="\\d{5,6}" premium="60999|88188|43030" standard="244444" free="455677|24470" />
 
+    <!-- DR Congo: 1-6 digits, known premium codes listed -->
+    <shortcode country="cd" pattern="\\d{1,6}" free="444123" />
+
     <!-- Switzerland: 3-5 digits: http://www.swisscom.ch/fxres/kmu/thirdpartybusiness_code_of_conduct_en.pdf -->
     <shortcode country="ch" pattern="[2-9]\\d{2,4}" premium="543|83111|30118" free="98765|30075|30047" />
 
     <!-- Chile: 4-5 digits (not confirmed), known premium codes listed -->
-    <shortcode country="cl" pattern="\\d{4,5}" free="9963|9240|1038" />
+    <shortcode country="cl" pattern="\\d{4,5}" free="9963|9240|1038|4848" />
 
     <!-- China: premium shortcodes start with "1066", free shortcodes start with "1065":
          http://clients.txtnation.com/entries/197192-china-premium-sms-short-code-requirements -->
     <shortcode country="cn" premium="1066.*" free="1065.*" />
 
     <!-- Colombia: 1-6 digits (not confirmed) -->
-    <shortcode country="co" pattern="\\d{1,6}" free="890350|908160|892255|898002|898880|899960|899948|87739|85517|491289" />
+    <shortcode country="co" pattern="\\d{1,6}" free="890350|908160|892255|898002|898880|899960|899948|87739|85517|491289|890119" />
 
     <!-- Costa Rica  -->
     <shortcode country="cr" pattern="\\d{1,6}" free="466453" />
@@ -116,6 +119,9 @@
     <!-- Dominican Republic: 1-6 digits (standard system default, not country specific) -->
     <shortcode country="do" pattern="\\d{1,6}" free="912892|912" />
 
+    <!-- Algeria: 1-5 digits, known premium codes listed -->
+    <shortcode country="dz" pattern="\\d{1,5}" free="63071" />
+
     <!-- Ecuador: 1-6 digits (standard system default, not country specific) -->
     <shortcode country="ec" pattern="\\d{1,6}" free="466453|18512" />
 
@@ -123,20 +129,23 @@
          http://www.tja.ee/public/documents/Elektrooniline_side/Oigusaktid/ENG/Estonian_Numbering_Plan_annex_06_09_2010.mht -->
     <shortcode country="ee" pattern="1\\d{2,4}" premium="90\\d{5}|15330|1701[0-3]" free="116\\d{3}|95034" />
 
-    <!-- Egypt: 4-5 digits, known codes listed -->
-    <shortcode country="eg" pattern="\\d{4,5}" free="1499|10020" />
+    <!-- Egypt: 4-6 digits, known codes listed -->
+    <shortcode country="eg" pattern="\\d{4,6}" free="1499|10020|100158" />
 
     <!-- Spain: 5-6 digits: 25xxx, 27xxx, 280xx, 35xxx, 37xxx, 795xxx, 797xxx, 995xxx, 997xxx, plus EU.
          http://www.legallink.es/?q=en/content/which-current-regulatory-status-premium-rate-services-spain -->
     <shortcode country="es" premium="[23][57]\\d{3}|280\\d{2}|[79]9[57]\\d{3}" free="116\\d{3}|22791|222145|22189" />
 
+    <!-- Ethiopia: 1-4 digits, known codes listed -->
+    <shortcode country="et" pattern="\\d{1,4}" free="8527" />
+
     <!-- Finland: 5-6 digits, premium 0600, 0700: http://en.wikipedia.org/wiki/Telephone_numbers_in_Finland -->
     <shortcode country="fi" pattern="\\d{5,6}" premium="0600.*|0700.*|171(?:59|63)" free="116\\d{3}|14789|17110" />
 
     <!-- France: 5 digits, free: 3xxxx, premium [4-8]xxxx, plus EU:
          http://clients.txtnation.com/entries/161972-france-premium-sms-short-code-requirements,
          visual voicemail code for Orange: 21101 -->
-    <shortcode country="fr" premium="[4-8]\\d{4}" free="3\\d{4}|116\\d{3}|21101|20366|555|2051|33033" />
+    <shortcode country="fr" premium="[4-8]\\d{4}" free="3\\d{4}|116\\d{3}|21101|20366|555|2051|33033|21727" />
 
     <!-- United Kingdom (Great Britain): 4-6 digits, common codes [5-8]xxxx, plus EU:
          http://www.short-codes.com/media/Co-regulatoryCodeofPracticeforcommonshortcodes170206.pdf,
@@ -179,17 +188,17 @@
     <shortcode country="il" pattern="\\d{1,5}" premium="4422|4545" free="37477|6681" />
 
     <!-- Iran: 4-8 digits, known premium codes listed -->
-    <shortcode country="ir" pattern="\\d{4,8}" free="700791|700792|100016|30008360" />
+    <shortcode country="ir" pattern="\\d{4,8}" free="700792|100016|30008360" />
 
     <!-- Italy: 5 digits (premium=41xxx,42xxx), plus EU:
          https://www.itu.int/dms_pub/itu-t/oth/02/02/T020200006B0001PDFE.pdf -->
     <shortcode country="it" pattern="\\d{5}" premium="44[0-4]\\d{2}|47[0-4]\\d{2}|48[0-4]\\d{2}|44[5-9]\\d{4}|47[5-9]\\d{4}|48[5-9]\\d{4}|455\\d{2}|499\\d{2}" free="116\\d{3}|4112503|40\\d{0,12}" standard="430\\d{2}|431\\d{2}|434\\d{4}|435\\d{4}|439\\d{7}" />
 
     <!-- Jordan: 1-5 digits (standard system default, not country specific) -->
-    <shortcode country="jo" pattern="\\d{1,5}" free="99066" />
+    <shortcode country="jo" pattern="\\d{1,5}" free="99066|99390" />
 
     <!-- Japan: 8083 used by SOFTBANK_DCB_2 -->
-    <shortcode country="jp" pattern="\\d{1,5}" free="8083" />
+    <shortcode country="jp" pattern="\\d{1,9}" free="8083|00050320" />
 
     <!-- Kenya: 5 digits, known premium codes listed -->
     <shortcode country="ke" pattern="\\d{5}" free="21725|21562|40520|23342|40023|24088|23054" />
@@ -206,6 +215,9 @@
     <!-- Kuwait: 1-5 digits (standard system default, not country specific) -->
     <shortcode country="kw" pattern="\\d{1,5}" free="1378|50420|94006|55991|50976|7112" />
 
+    <!-- Lesotho: 4-5 digits, known codes listed -->
+    <shortcode country="ls" pattern="\\d{4,5}" free="32012" />
+
     <!-- Lithuania: 3-5 digits, known premium codes listed, plus EU -->
     <shortcode country="lt" pattern="\\d{3,5}" premium="13[89]1|1394|16[34]5" free="116\\d{3}|1399|1324" />
 
@@ -222,11 +234,14 @@
     <!-- Macedonia: 1-6 digits (not confirmed), known premium codes listed -->
     <shortcode country="mk" pattern="\\d{1,6}" free="129005|122" />
 
+    <!-- Mali: 1-5 digits, known codes listed -->
+    <shortcode country="ml" pattern="\\d{1,5}" free="36098" />
+
     <!-- Mongolia : 1-6 digits (standard system default, not country specific) -->
     <shortcode country="mn" pattern="\\d{1,6}" free="44444|45678|445566" />
 
     <!-- Malawi: 1-5 digits (standard system default, not country specific) -->
-    <shortcode country="mw" pattern="\\d{1,5}" free="4276|4305" />
+    <shortcode country="mw" pattern="\\d{1,5}" free="4276|4305|4326" />
 
     <!-- Mozambique: 1-5 digits (standard system default, not country specific) -->
     <shortcode country="mz" pattern="\\d{1,5}" free="1714" />
@@ -323,11 +338,14 @@
     <!-- Tajikistan: 4 digits, known premium codes listed -->
     <shortcode country="tj" pattern="\\d{4}" premium="11[3-7]1|4161|4333|444[689]" />
 
-    <!-- Tanzania: 1-5 digits (standard system default, not country specific) -->
-    <shortcode country="tz" pattern="\\d{1,5}" free="15046|15234|15324|15610" />
+    <!-- Timor-Leste 1-5 digits, known codes listed  -->
+    <shortcode country="tl" pattern="\\d{1,5}" free="46645" />
 
-    <!-- Tunisia: 5 digits, known premium codes listed -->
-    <shortcode country="tn" pattern="\\d{5}" free="85799" />
+    <!-- Tanzania: 1-5 digits (standard system default, not country specific) -->
+    <shortcode country="tz" pattern="\\d{1,5}" free="15046|15324|15610" />
+
+    <!-- Tunisia: 1-6 digits, known premium codes listed -->
+    <shortcode country="tn" pattern="\\d{1,6}" free="85799|772024" />
 
     <!-- Turkey -->
     <shortcode country="tr" pattern="\\d{1,5}" free="7529|5528|6493|3193" />
@@ -336,7 +354,7 @@
     <shortcode country="ua" pattern="\\d{4}" premium="444[3-9]|70[579]4|7540" />
 
     <!-- Uganda(UG): 4 digits (standard system default, not country specific) -->
-    <shortcode country="ug" pattern="\\d{4}" free="8000|8009" />
+    <shortcode country="ug" pattern="\\d{4}" free="8009" />
 
     <!-- USA: 5-6 digits (premium codes from https://www.premiumsmsrefunds.com/ShortCodes.htm),
          visual voicemail code for T-Mobile: 122 -->
@@ -349,7 +367,7 @@
     <shortcode country="ve" pattern="\\d{1,6}" free="538352" />
 
     <!-- Vietnam: 1-6 digits (standard system default, not country specific) -->
-    <shortcode country="vn" pattern="\\d{1,6}" free="5001|9055|8079|90002|118989" />
+    <shortcode country="vn" pattern="\\d{1,6}" free="5001|9055|90002|118989|46645" />
 
     <!-- Mayotte (French Territory): 1-5 digits (not confirmed) -->
     <shortcode country="yt" pattern="\\d{1,5}" free="38600,36300,36303,959" />
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index 3bbb951..1b6746c 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -313,6 +313,7 @@
         "res/xml/power_profile_test_modem.xml",
     ],
     auto_gen_config: true,
+    team: "trendy_team_ravenwood",
 }
 
 test_module_config {
diff --git a/core/tests/coretests/src/android/app/NotificationManagerTest.java b/core/tests/coretests/src/android/app/NotificationManagerTest.java
index 6538ce8..3d6e122 100644
--- a/core/tests/coretests/src/android/app/NotificationManagerTest.java
+++ b/core/tests/coretests/src/android/app/NotificationManagerTest.java
@@ -16,6 +16,8 @@
 
 package android.app;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
@@ -25,8 +27,12 @@
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.pm.ParceledListSlice;
+import android.os.UserHandle;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
 import android.platform.test.flag.junit.SetFlagsRule;
@@ -35,6 +41,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -42,6 +49,7 @@
 
 import java.time.Instant;
 import java.time.InstantSource;
+import java.util.List;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
@@ -50,14 +58,24 @@
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
-    private Context mContext;
     private NotificationManagerWithMockService mNotificationManager;
     private final FakeClock mClock = new FakeClock();
 
+    private PackageTestableContext mContext;
+
     @Before
     public void setUp() {
-        mContext = ApplicationProvider.getApplicationContext();
+        mContext = new PackageTestableContext(ApplicationProvider.getApplicationContext());
         mNotificationManager = new NotificationManagerWithMockService(mContext, mClock);
+
+        // Caches must be in test mode in order to be used in tests.
+        PropertyInvalidatedCache.setTestMode(true);
+        mNotificationManager.setChannelCacheToTestMode();
+    }
+
+    @After
+    public void tearDown() {
+        PropertyInvalidatedCache.setTestMode(false);
     }
 
     @Test
@@ -243,12 +261,161 @@
                 anyInt(), any(), anyInt());
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+    public void getNotificationChannel_cachedUntilInvalidated() throws Exception {
+        // Invalidate the cache first because the cache won't do anything until then
+        NotificationManager.invalidateNotificationChannelCache();
+
+        // It doesn't matter what the returned contents are, as long as we return a channel.
+        // This setup must set up getNotificationChannels(), as that's the method called.
+        when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(),
+                anyInt())).thenReturn(new ParceledListSlice<>(List.of(exampleChannel())));
+
+        // ask for the same channel 100 times without invalidating the cache
+        for (int i = 0; i < 100; i++) {
+            NotificationChannel unused = mNotificationManager.getNotificationChannel("id");
+        }
+
+        // invalidate the cache; then ask again
+        NotificationManager.invalidateNotificationChannelCache();
+        NotificationChannel unused = mNotificationManager.getNotificationChannel("id");
+
+        verify(mNotificationManager.mBackendService, times(2))
+                .getNotificationChannels(any(), any(), anyInt());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+    public void getNotificationChannel_sameApp_oneCall() throws Exception {
+        NotificationManager.invalidateNotificationChannelCache();
+
+        NotificationChannel c1 = new NotificationChannel("id1", "name1",
+                NotificationManager.IMPORTANCE_DEFAULT);
+        NotificationChannel c2 = new NotificationChannel("id2", "name2",
+                NotificationManager.IMPORTANCE_NONE);
+
+        when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(),
+                anyInt())).thenReturn(new ParceledListSlice<>(List.of(c1, c2)));
+
+        assertThat(mNotificationManager.getNotificationChannel("id1")).isEqualTo(c1);
+        assertThat(mNotificationManager.getNotificationChannel("id2")).isEqualTo(c2);
+        assertThat(mNotificationManager.getNotificationChannel("id3")).isNull();
+
+        verify(mNotificationManager.mBackendService, times(1))
+                .getNotificationChannels(any(), any(), anyInt());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+    public void getNotificationChannels_cachedUntilInvalidated() throws Exception {
+        NotificationManager.invalidateNotificationChannelCache();
+        when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(),
+                anyInt())).thenReturn(new ParceledListSlice<>(List.of(exampleChannel())));
+
+        // ask for channels 100 times without invalidating the cache
+        for (int i = 0; i < 100; i++) {
+            List<NotificationChannel> unused = mNotificationManager.getNotificationChannels();
+        }
+
+        // invalidate the cache; then ask again
+        NotificationManager.invalidateNotificationChannelCache();
+        List<NotificationChannel> res = mNotificationManager.getNotificationChannels();
+
+        verify(mNotificationManager.mBackendService, times(2))
+                .getNotificationChannels(any(), any(), anyInt());
+        assertThat(res).containsExactlyElementsIn(List.of(exampleChannel()));
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+    public void getNotificationChannel_channelAndConversationLookup() throws Exception {
+        NotificationManager.invalidateNotificationChannelCache();
+
+        // Full list of channels: c1; conv1 = child of c1; c2 is unrelated
+        NotificationChannel c1 = new NotificationChannel("id", "name",
+                NotificationManager.IMPORTANCE_DEFAULT);
+        NotificationChannel conv1 = new NotificationChannel("", "name_conversation",
+                NotificationManager.IMPORTANCE_DEFAULT);
+        conv1.setConversationId("id", "id_conversation");
+        NotificationChannel c2 = new NotificationChannel("other", "name2",
+                NotificationManager.IMPORTANCE_DEFAULT);
+
+        when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), anyInt()))
+                .thenReturn(new ParceledListSlice<>(List.of(c1, conv1, c2)));
+
+        // Lookup for channel c1 and c2: returned as expected
+        assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(c1);
+        assertThat(mNotificationManager.getNotificationChannel("other")).isEqualTo(c2);
+
+        // Lookup for conv1 should return conv1
+        assertThat(mNotificationManager.getNotificationChannel("id", "id_conversation")).isEqualTo(
+                conv1);
+
+        // Lookup for a different conversation channel that doesn't exist, whose parent channel id
+        // is "id", should return c1
+        assertThat(mNotificationManager.getNotificationChannel("id", "nonexistent")).isEqualTo(c1);
+
+        // Lookup of a nonexistent channel is null
+        assertThat(mNotificationManager.getNotificationChannel("id3")).isNull();
+
+        // All of that should have been one call to getNotificationChannels()
+        verify(mNotificationManager.mBackendService, times(1))
+                .getNotificationChannels(any(), any(), anyInt());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+    public void getNotificationChannel_differentPackages() throws Exception {
+        NotificationManager.invalidateNotificationChannelCache();
+        final String pkg1 = "one";
+        final String pkg2 = "two";
+        final int userId = 0;
+        final int userId1 = 1;
+
+        // multiple channels with the same ID, but belonging to different packages/users
+        NotificationChannel channel1 = new NotificationChannel("id", "name1",
+                NotificationManager.IMPORTANCE_DEFAULT);
+        NotificationChannel channel2 = channel1.copy();
+        channel2.setName("name2");
+        NotificationChannel channel3 = channel1.copy();
+        channel3.setName("name3");
+
+        when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg1),
+                eq(userId))).thenReturn(new ParceledListSlice<>(List.of(channel1)));
+        when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg2),
+                eq(userId))).thenReturn(new ParceledListSlice<>(List.of(channel2)));
+        when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg1),
+                eq(userId1))).thenReturn(new ParceledListSlice<>(List.of(channel3)));
+
+        // set our context to pretend to be from package 1 and userId 0
+        mContext.setParameters(pkg1, pkg1, userId);
+        assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(channel1);
+
+        // now package 2
+        mContext.setParameters(pkg2, pkg2, userId);
+        assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(channel2);
+
+        // now pkg1 for a different user
+        mContext.setParameters(pkg1, pkg1, userId1);
+        assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(channel3);
+
+        // Those should have been three different calls
+        verify(mNotificationManager.mBackendService, times(3))
+                .getNotificationChannels(any(), any(), anyInt());
+    }
+
     private Notification exampleNotification() {
         return new Notification.Builder(mContext, "channel")
                 .setSmallIcon(android.R.drawable.star_big_on)
                 .build();
     }
 
+    private NotificationChannel exampleChannel() {
+        return new NotificationChannel("id", "channel_name",
+                NotificationManager.IMPORTANCE_DEFAULT);
+    }
+
     private static class NotificationManagerWithMockService extends NotificationManager {
 
         private final INotificationManager mBackendService;
@@ -264,6 +431,48 @@
         }
     }
 
+    // Helper context wrapper class where we can control just the return values of getPackageName,
+    // getOpPackageName, and getUserId (used in getNotificationChannels).
+    private static class PackageTestableContext extends ContextWrapper {
+        private String mPackage;
+        private String mOpPackage;
+        private Integer mUserId;
+
+        PackageTestableContext(Context base) {
+            super(base);
+        }
+
+        void setParameters(String packageName, String opPackageName, int userId) {
+            mPackage = packageName;
+            mOpPackage = opPackageName;
+            mUserId = userId;
+        }
+
+        @Override
+        public String getPackageName() {
+            if (mPackage != null) return mPackage;
+            return super.getPackageName();
+        }
+
+        @Override
+        public String getOpPackageName() {
+            if (mOpPackage != null) return mOpPackage;
+            return super.getOpPackageName();
+        }
+
+        @Override
+        public int getUserId() {
+            if (mUserId != null) return mUserId;
+            return super.getUserId();
+        }
+
+        @Override
+        public UserHandle getUser() {
+            if (mUserId != null) return UserHandle.of(mUserId);
+            return super.getUser();
+        }
+    }
+
     private static class FakeClock implements InstantSource {
 
         private long mNowMillis = 441644400000L;
diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java
index ca6ad6f..7be6950 100644
--- a/core/tests/coretests/src/android/app/NotificationTest.java
+++ b/core/tests/coretests/src/android/app/NotificationTest.java
@@ -2504,6 +2504,21 @@
 
     @Test
     @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_setProgressSegments() {
+        final List<Notification.ProgressStyle.Segment> segments = List.of(
+                new Notification.ProgressStyle.Segment(100).setColor(Color.WHITE),
+                new Notification.ProgressStyle.Segment(50).setColor(Color.RED),
+                new Notification.ProgressStyle.Segment(50).setColor(Color.BLUE)
+        );
+
+        final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle();
+        progressStyle1.setProgressSegments(segments);
+
+        assertThat(progressStyle1.getProgressSegments()).isEqualTo(segments);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
     public void progressStyle_addProgressPoint_dropsNegativePoints() {
         // GIVEN
         final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle();
@@ -2532,6 +2547,21 @@
 
     @Test
     @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_setProgressPoints() {
+        final List<Notification.ProgressStyle.Point> points = List.of(
+                new Notification.ProgressStyle.Point(0).setColor(Color.WHITE),
+                new Notification.ProgressStyle.Point(50).setColor(Color.RED),
+                new Notification.ProgressStyle.Point(100).setColor(Color.BLUE)
+        );
+
+        final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle();
+        progressStyle1.setProgressPoints(points);
+
+        assertThat(progressStyle1.getProgressPoints()).isEqualTo(points);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
     public void progressStyle_createProgressModel_ignoresPointsExceedingMax() {
         // GIVEN
         final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle();
@@ -2673,11 +2703,58 @@
 
     @Test
     @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_setProgressIndeterminate() {
+        final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle();
+        progressStyle1.setProgressIndeterminate(true);
+        assertThat(progressStyle1.isProgressIndeterminate()).isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
     public void progressStyle_styledByProgress_defaultValueTrue() {
         final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle();
 
         assertThat(progressStyle1.isStyledByProgress()).isTrue();
     }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_setStyledByProgress() {
+        final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle();
+        progressStyle1.setStyledByProgress(false);
+        assertThat(progressStyle1.isStyledByProgress()).isFalse();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_point() {
+        final int id = 1;
+        final int position = 10;
+        final int color = Color.RED;
+
+        final Notification.ProgressStyle.Point point =
+                new Notification.ProgressStyle.Point(position).setId(id).setColor(color);
+
+        assertEquals(id, point.getId());
+        assertEquals(position, point.getPosition());
+        assertEquals(color, point.getColor());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+    public void progressStyle_segment() {
+        final int id = 1;
+        final int length = 100;
+        final int color = Color.RED;
+
+        final Notification.ProgressStyle.Segment segment =
+                new Notification.ProgressStyle.Segment(length).setId(id).setColor(color);
+
+        assertEquals(id, segment.getId());
+        assertEquals(length, segment.getLength());
+        assertEquals(color, segment.getColor());
+    }
+
     private void assertValid(Notification.Colors c) {
         // Assert that all colors are populated
         assertThat(c.getBackgroundColor()).isNotEqualTo(Notification.COLOR_INVALID);
diff --git a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java
index 37ef6cb..939bf2e 100644
--- a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java
+++ b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java
@@ -207,7 +207,8 @@
         final ComponentInfo info = new ComponentInfo();
         info.applicationInfo = new ApplicationInfo();
         info.applicationInfo.uid = uid;
-        return new RegisteredServicesCache.ServiceInfo<>(type, info, null);
+        return new RegisteredServicesCache.ServiceInfo<>(type, info, null /* componentName */,
+                0 /* lastUpdateTime */);
     }
 
     private void assertNotEmptyFileCreated(TestServicesCache cache, int userId) {
@@ -301,7 +302,7 @@
 
         @Override
         protected ServiceInfo<TestServiceType> parseServiceInfo(
-                ResolveInfo resolveInfo) throws XmlPullParserException, IOException {
+                ResolveInfo resolveInfo, int userId) throws XmlPullParserException, IOException {
             int size = mServices.size();
             for (int i = 0; i < size; i++) {
                 Map<ResolveInfo, ServiceInfo<TestServiceType>> map = mServices.valueAt(i);
diff --git a/core/tests/coretests/src/android/content/pm/SystemFeaturesCacheTest.java b/core/tests/coretests/src/android/content/pm/SystemFeaturesCacheTest.java
new file mode 100644
index 0000000..ce4aa42
--- /dev/null
+++ b/core/tests/coretests/src/android/content/pm/SystemFeaturesCacheTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm;
+
+import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
+import static android.content.pm.PackageManager.FEATURE_WATCH;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Parcel;
+import android.util.ArrayMap;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SystemFeaturesCacheTest {
+
+    private SystemFeaturesCache mCache;
+
+    @Test
+    public void testNoFeatures() throws Exception {
+        SystemFeaturesCache cache = new SystemFeaturesCache(new ArrayMap<String, FeatureInfo>());
+        assertThat(cache.maybeHasFeature("", 0)).isNull();
+        assertThat(cache.maybeHasFeature(FEATURE_WATCH, 0)).isFalse();
+        assertThat(cache.maybeHasFeature(FEATURE_PICTURE_IN_PICTURE, 0)).isFalse();
+        assertThat(cache.maybeHasFeature("com.missing.feature", 0)).isNull();
+    }
+
+    @Test
+    public void testNonSdkFeature() throws Exception {
+        ArrayMap<String, FeatureInfo> features = new ArrayMap<>();
+        features.put("custom.feature", createFeature("custom.feature", 0));
+        SystemFeaturesCache cache = new SystemFeaturesCache(features);
+
+        assertThat(cache.maybeHasFeature("custom.feature", 0)).isNull();
+    }
+
+    @Test
+    public void testSdkFeature() throws Exception {
+        ArrayMap<String, FeatureInfo> features = new ArrayMap<>();
+        features.put(FEATURE_WATCH, createFeature(FEATURE_WATCH, 0));
+        SystemFeaturesCache cache = new SystemFeaturesCache(features);
+
+        assertThat(cache.maybeHasFeature(FEATURE_WATCH, 0)).isTrue();
+        assertThat(cache.maybeHasFeature(FEATURE_WATCH, -1)).isTrue();
+        assertThat(cache.maybeHasFeature(FEATURE_WATCH, 1)).isFalse();
+        assertThat(cache.maybeHasFeature(FEATURE_WATCH, Integer.MIN_VALUE)).isTrue();
+        assertThat(cache.maybeHasFeature(FEATURE_WATCH, Integer.MAX_VALUE)).isFalse();
+
+        // Other SDK-declared features should be reported as unavailable.
+        assertThat(cache.maybeHasFeature(FEATURE_PICTURE_IN_PICTURE, 0)).isFalse();
+    }
+
+    @Test
+    public void testSdkFeatureHasMinVersion() throws Exception {
+        ArrayMap<String, FeatureInfo> features = new ArrayMap<>();
+        features.put(FEATURE_WATCH, createFeature(FEATURE_WATCH, Integer.MIN_VALUE));
+        SystemFeaturesCache cache = new SystemFeaturesCache(features);
+
+        assertThat(cache.maybeHasFeature(FEATURE_WATCH, 0)).isFalse();
+
+        // If both the query and the feature version itself happen to use MIN_VALUE, we can't
+        // reliably indicate availability, so it should report an indeterminate result.
+        assertThat(cache.maybeHasFeature(FEATURE_WATCH, Integer.MIN_VALUE)).isNull();
+    }
+
+    @Test
+    public void testParcel() throws Exception {
+        ArrayMap<String, FeatureInfo> features = new ArrayMap<>();
+        features.put(FEATURE_WATCH, createFeature(FEATURE_WATCH, 0));
+        SystemFeaturesCache cache = new SystemFeaturesCache(features);
+
+        Parcel parcel = Parcel.obtain();
+        SystemFeaturesCache parceledCache;
+        try {
+            parcel.writeParcelable(cache, 0);
+            parcel.setDataPosition(0);
+            parceledCache = parcel.readParcelable(getClass().getClassLoader());
+        } finally {
+            parcel.recycle();
+        }
+
+        assertThat(parceledCache.maybeHasFeature(FEATURE_WATCH, 0))
+                .isEqualTo(cache.maybeHasFeature(FEATURE_WATCH, 0));
+        assertThat(parceledCache.maybeHasFeature(FEATURE_PICTURE_IN_PICTURE, 0))
+                .isEqualTo(cache.maybeHasFeature(FEATURE_PICTURE_IN_PICTURE, 0));
+        assertThat(parceledCache.maybeHasFeature("custom.feature", 0))
+                .isEqualTo(cache.maybeHasFeature("custom.feature", 0));
+    }
+
+    private static FeatureInfo createFeature(String name, int version) {
+        FeatureInfo fi = new FeatureInfo();
+        fi.name = name;
+        fi.version = version;
+        return fi;
+    }
+}
diff --git a/core/tests/coretests/src/android/os/OWNERS b/core/tests/coretests/src/android/os/OWNERS
index c45080f..5fd4ffc 100644
--- a/core/tests/coretests/src/android/os/OWNERS
+++ b/core/tests/coretests/src/android/os/OWNERS
@@ -10,6 +10,9 @@
 # PerformanceHintManager
 per-file PerformanceHintManagerTest.java = file:/ADPF_OWNERS
 
+# SystemHealthManager
+per-file SystemHealthManagerUnitTest.java = file:/ADPF_OWNERS
+
 # Caching
 per-file IpcDataCache* = file:/PERFORMANCE_OWNERS
 
diff --git a/core/tests/coretests/src/android/os/ParcelTest.java b/core/tests/coretests/src/android/os/ParcelTest.java
index da9d687..3e652010 100644
--- a/core/tests/coretests/src/android/os/ParcelTest.java
+++ b/core/tests/coretests/src/android/os/ParcelTest.java
@@ -361,7 +361,11 @@
 
         p.setClassCookie(ParcelTest.class, "to_be_discarded_cookie");
         p.recycle();
-        assertThat(p.getClassCookie(ParcelTest.class)).isNull();
+
+        // cannot access Parcel after it's recycled!
+        // this test is equivalent to checking hasClassCookie false
+        // after obtaining above
+        // assertThat(p.getClassCookie(ParcelTest.class)).isNull();
     }
 
     @Test
diff --git a/core/tests/coretests/src/android/security/advancedprotection/OWNERS b/core/tests/coretests/src/android/security/advancedprotection/OWNERS
new file mode 100644
index 0000000..9bf5e58
--- /dev/null
+++ b/core/tests/coretests/src/android/security/advancedprotection/OWNERS
@@ -0,0 +1 @@
+file:platform/frameworks/base:main:/core/java/android/security/advancedprotection/OWNERS
diff --git a/core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java b/core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java
index 0bf406c..2bd3f4d 100644
--- a/core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java
+++ b/core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java
@@ -17,6 +17,7 @@
 package com.android.internal.notification;
 
 import static com.android.internal.notification.SystemNotificationChannels.ABUSIVE_BACKGROUND_APPS;
+import static com.android.internal.notification.SystemNotificationChannels.ACCESSIBILITY_HEARING_DEVICE;
 import static com.android.internal.notification.SystemNotificationChannels.ACCESSIBILITY_MAGNIFICATION;
 import static com.android.internal.notification.SystemNotificationChannels.ACCESSIBILITY_SECURITY_POLICY;
 import static com.android.internal.notification.SystemNotificationChannels.ACCOUNT;
@@ -90,8 +91,8 @@
                         DEVELOPER_IMPORTANT, UPDATES, NETWORK_STATUS, NETWORK_ALERTS,
                         NETWORK_AVAILABLE, VPN, DEVICE_ADMIN, ALERTS, RETAIL_MODE, USB,
                         FOREGROUND_SERVICE, HEAVY_WEIGHT_APP, SYSTEM_CHANGES,
-                        ACCESSIBILITY_MAGNIFICATION, ACCESSIBILITY_SECURITY_POLICY,
-                        ABUSIVE_BACKGROUND_APPS);
+                        ACCESSIBILITY_MAGNIFICATION, ACCESSIBILITY_HEARING_DEVICE,
+                        ACCESSIBILITY_SECURITY_POLICY, ABUSIVE_BACKGROUND_APPS);
     }
 
     @Test
diff --git a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java
index 5613caf..5df2c12 100644
--- a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java
+++ b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java
@@ -20,12 +20,16 @@
 
 import android.app.Notification.ProgressStyle;
 import android.graphics.Color;
+import android.util.Pair;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
-import com.android.internal.widget.NotificationProgressDrawable.Part;
-import com.android.internal.widget.NotificationProgressDrawable.Point;
-import com.android.internal.widget.NotificationProgressDrawable.Segment;
+import com.android.internal.widget.NotificationProgressBar.Part;
+import com.android.internal.widget.NotificationProgressBar.Point;
+import com.android.internal.widget.NotificationProgressBar.Segment;
+import com.android.internal.widget.NotificationProgressDrawable.DrawablePart;
+import com.android.internal.widget.NotificationProgressDrawable.DrawablePoint;
+import com.android.internal.widget.NotificationProgressDrawable.DrawableSegment;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -37,181 +41,287 @@
 public class NotificationProgressBarTest {
 
     @Test(expected = IllegalArgumentException.class)
-    public void processAndConvertToDrawableParts_segmentsIsEmpty() {
+    public void processAndConvertToParts_segmentsIsEmpty() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         List<ProgressStyle.Point> points = new ArrayList<>();
         int progress = 50;
         int progressMax = 100;
-        boolean isStyledByProgress = true;
 
-        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
-                progressMax,
-                isStyledByProgress);
+        NotificationProgressBar.processAndConvertToViewParts(segments, points, progress,
+                progressMax);
     }
 
     @Test(expected = IllegalArgumentException.class)
-    public void processAndConvertToDrawableParts_segmentsLengthNotMatchingProgressMax() {
+    public void processAndConvertToParts_segmentsLengthNotMatchingProgressMax() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(50));
         segments.add(new ProgressStyle.Segment(100));
         List<ProgressStyle.Point> points = new ArrayList<>();
         int progress = 50;
         int progressMax = 100;
-        boolean isStyledByProgress = true;
 
-        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
-                progressMax,
-                isStyledByProgress);
+        NotificationProgressBar.processAndConvertToViewParts(segments, points, progress,
+                progressMax);
     }
 
     @Test(expected = IllegalArgumentException.class)
-    public void processAndConvertToDrawableParts_segmentLengthIsNegative() {
+    public void processAndConvertToParts_segmentLengthIsNegative() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(-50));
         segments.add(new ProgressStyle.Segment(150));
         List<ProgressStyle.Point> points = new ArrayList<>();
         int progress = 50;
         int progressMax = 100;
-        boolean isStyledByProgress = true;
 
-        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
-                progressMax,
-                isStyledByProgress);
+        NotificationProgressBar.processAndConvertToViewParts(segments, points, progress,
+                progressMax);
     }
 
     @Test(expected = IllegalArgumentException.class)
-    public void processAndConvertToDrawableParts_segmentLengthIsZero() {
+    public void processAndConvertToParts_segmentLengthIsZero() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(0));
         segments.add(new ProgressStyle.Segment(100));
         List<ProgressStyle.Point> points = new ArrayList<>();
         int progress = 50;
         int progressMax = 100;
-        boolean isStyledByProgress = true;
 
-        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
-                progressMax,
-                isStyledByProgress);
+        NotificationProgressBar.processAndConvertToViewParts(segments, points, progress,
+                progressMax);
     }
 
     @Test(expected = IllegalArgumentException.class)
-    public void processAndConvertToDrawableParts_progressIsNegative() {
+    public void processAndConvertToParts_progressIsNegative() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(100));
         List<ProgressStyle.Point> points = new ArrayList<>();
         int progress = -50;
         int progressMax = 100;
-        boolean isStyledByProgress = true;
 
-        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
-                progressMax,
-                isStyledByProgress);
+        NotificationProgressBar.processAndConvertToViewParts(segments, points, progress,
+                progressMax);
     }
 
     @Test
-    public void processAndConvertToDrawableParts_progressIsZero() {
+    public void processAndConvertToParts_progressIsZero() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(100).setColor(Color.RED));
         List<ProgressStyle.Point> points = new ArrayList<>();
         int progress = 0;
         int progressMax = 100;
+
+        List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+                progress, progressMax);
+
+        List<Part> expectedParts = new ArrayList<>(List.of(new Segment(1f, Color.RED)));
+
+        assertThat(parts).isEqualTo(expectedParts);
+
+        float drawableWidth = 300;
+        float segSegGap = 4;
+        float segPointGap = 4;
+        float pointRadius = 6;
+        boolean hasTrackerIcon = true;
+
+        List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts(
+                parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+
+        List<DrawablePart> expectedDrawableParts = new ArrayList<>(
+                List.of(new DrawableSegment(0, 300, Color.RED)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 16;
         boolean isStyledByProgress = true;
 
-        List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
-                segments, points, progress, progressMax, isStyledByProgress);
+        Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
+                parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
+                300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
 
-        int fadedRed = 0x7FFF0000;
-        List<Part> expected = new ArrayList<>(List.of(new Segment(1f, fadedRed, true)));
+        // Colors with 40% opacity
+        int fadedRed = 0x66FF0000;
+        expectedDrawableParts = new ArrayList<>(
+                List.of(new DrawableSegment(0, 300, fadedRed, true)));
 
-        assertThat(parts).isEqualTo(expected);
+        assertThat(p.second).isEqualTo(0);
+        assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 
     @Test
-    public void processAndConvertToDrawableParts_progressAtMax() {
+    public void processAndConvertToParts_progressAtMax() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(100).setColor(Color.RED));
         List<ProgressStyle.Point> points = new ArrayList<>();
         int progress = 100;
         int progressMax = 100;
+
+        List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+                progress, progressMax);
+
+        List<Part> expectedParts = new ArrayList<>(List.of(new Segment(1f, Color.RED)));
+
+        assertThat(parts).isEqualTo(expectedParts);
+
+        float drawableWidth = 300;
+        float segSegGap = 4;
+        float segPointGap = 4;
+        float pointRadius = 6;
+        boolean hasTrackerIcon = true;
+
+        List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts(
+                parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+
+        List<DrawablePart> expectedDrawableParts = new ArrayList<>(
+                List.of(new DrawableSegment(0, 300, Color.RED)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 16;
         boolean isStyledByProgress = true;
 
-        List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
-                segments, points, progress, progressMax, isStyledByProgress);
+        Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
+                parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
+                300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
 
-        List<Part> expected = new ArrayList<>(List.of(new Segment(1f, Color.RED)));
-
-        assertThat(parts).isEqualTo(expected);
+        assertThat(p.second).isEqualTo(300);
+        assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 
     @Test(expected = IllegalArgumentException.class)
-    public void processAndConvertToDrawableParts_progressAboveMax() {
+    public void processAndConvertToParts_progressAboveMax() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(100));
         List<ProgressStyle.Point> points = new ArrayList<>();
         int progress = 150;
         int progressMax = 100;
-        boolean isStyledByProgress = true;
 
-        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
-                progressMax, isStyledByProgress);
+        NotificationProgressBar.processAndConvertToViewParts(segments, points, progress,
+                progressMax);
     }
 
     @Test(expected = IllegalArgumentException.class)
-    public void processAndConvertToDrawableParts_pointPositionIsNegative() {
+    public void processAndConvertToParts_pointPositionIsNegative() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(100));
         List<ProgressStyle.Point> points = new ArrayList<>();
         points.add(new ProgressStyle.Point(-50).setColor(Color.RED));
         int progress = 50;
         int progressMax = 100;
-        boolean isStyledByProgress = true;
 
-        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
-                progressMax,
-                isStyledByProgress);
+        NotificationProgressBar.processAndConvertToViewParts(segments, points, progress,
+                progressMax);
     }
 
     @Test(expected = IllegalArgumentException.class)
-    public void processAndConvertToDrawableParts_pointPositionAboveMax() {
+    public void processAndConvertToParts_pointPositionAboveMax() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(100));
         List<ProgressStyle.Point> points = new ArrayList<>();
         points.add(new ProgressStyle.Point(150).setColor(Color.RED));
         int progress = 50;
         int progressMax = 100;
-        boolean isStyledByProgress = true;
 
-        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
-                progressMax,
-                isStyledByProgress);
+        NotificationProgressBar.processAndConvertToViewParts(segments, points, progress,
+                progressMax);
     }
 
     @Test
-    public void processAndConvertToDrawableParts_multipleSegmentsWithoutPoints() {
+    public void processAndConvertToParts_multipleSegmentsWithoutPoints() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(50).setColor(Color.RED));
         segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN));
         List<ProgressStyle.Point> points = new ArrayList<>();
         int progress = 60;
         int progressMax = 100;
+
+        List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+                progress, progressMax);
+
+        List<Part> expectedParts = new ArrayList<>(
+                List.of(new Segment(0.50f, Color.RED), new Segment(0.50f, Color.GREEN)));
+
+        assertThat(parts).isEqualTo(expectedParts);
+
+        float drawableWidth = 300;
+        float segSegGap = 4;
+        float segPointGap = 4;
+        float pointRadius = 6;
+        boolean hasTrackerIcon = true;
+
+        List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts(
+                parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+
+        List<DrawablePart> expectedDrawableParts = new ArrayList<>(
+                List.of(new DrawableSegment(0, 146, Color.RED),
+                        new DrawableSegment(150, 300, Color.GREEN)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 16;
         boolean isStyledByProgress = true;
+        Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
+                parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
+                300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
 
-        List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
-                segments, points, progress, progressMax, isStyledByProgress);
+        // Colors with 40% opacity
+        int fadedGreen = 0x6600FF00;
+        expectedDrawableParts = new ArrayList<>(List.of(new DrawableSegment(0, 146, Color.RED),
+                new DrawableSegment(150, 180, Color.GREEN),
+                new DrawableSegment(180, 300, fadedGreen, true)));
 
-        // Colors with 50% opacity
-        int fadedGreen = 0x7F00FF00;
-
-        List<Part> expected = new ArrayList<>(List.of(
-                new Segment(0.50f, Color.RED),
-                new Segment(0.10f, Color.GREEN),
-                new Segment(0.40f, fadedGreen, true)));
-
-        assertThat(parts).isEqualTo(expected);
+        assertThat(p.second).isEqualTo(180);
+        assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 
     @Test
-    public void processAndConvertToDrawableParts_singleSegmentWithPoints() {
+    public void processAndConvertToParts_multipleSegmentsWithoutPoints_noTracker() {
+        List<ProgressStyle.Segment> segments = new ArrayList<>();
+        segments.add(new ProgressStyle.Segment(50).setColor(Color.RED));
+        segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN));
+        List<ProgressStyle.Point> points = new ArrayList<>();
+        int progress = 60;
+        int progressMax = 100;
+        List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+                progress, progressMax);
+
+        List<Part> expectedParts = new ArrayList<>(
+                List.of(new Segment(0.50f, Color.RED), new Segment(0.50f, Color.GREEN)));
+
+        assertThat(parts).isEqualTo(expectedParts);
+
+        float drawableWidth = 300;
+        float segSegGap = 4;
+        float segPointGap = 4;
+        float pointRadius = 6;
+        boolean hasTrackerIcon = false;
+
+        List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts(
+                parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+
+        List<DrawablePart> expectedDrawableParts = new ArrayList<>(
+                List.of(new DrawableSegment(0, 146, Color.RED),
+                        new DrawableSegment(150, 300, Color.GREEN)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 16;
+        boolean isStyledByProgress = true;
+        Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
+                parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
+                300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+
+        // Colors with 40% opacity
+        int fadedGreen = 0x6600FF00;
+        expectedDrawableParts = new ArrayList<>(List.of(new DrawableSegment(0, 146, Color.RED),
+                new DrawableSegment(150, 176, Color.GREEN),
+                new DrawableSegment(180, 300, fadedGreen, true)));
+
+        assertThat(p.second).isEqualTo(180);
+        assertThat(p.first).isEqualTo(expectedDrawableParts);
+    }
+
+    @Test
+    public void processAndConvertToParts_singleSegmentWithPoints() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE));
         List<ProgressStyle.Point> points = new ArrayList<>();
@@ -221,31 +331,68 @@
         points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW));
         int progress = 60;
         int progressMax = 100;
+
+        List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+                progress, progressMax);
+
+        List<Part> expectedParts = new ArrayList<>(
+                List.of(new Segment(0.15f, Color.BLUE), new Point(Color.RED),
+                        new Segment(0.10f, Color.BLUE), new Point(Color.BLUE),
+                        new Segment(0.35f, Color.BLUE), new Point(Color.BLUE),
+                        new Segment(0.15f, Color.BLUE), new Point(Color.YELLOW),
+                        new Segment(0.25f, Color.BLUE)));
+
+        assertThat(parts).isEqualTo(expectedParts);
+
+        float drawableWidth = 300;
+        float segSegGap = 4;
+        float segPointGap = 4;
+        float pointRadius = 6;
+        boolean hasTrackerIcon = true;
+
+        List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts(
+                parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+
+        List<DrawablePart> expectedDrawableParts = new ArrayList<>(
+                List.of(new DrawableSegment(0, 35, Color.BLUE),
+                        new DrawablePoint(39, 51, Color.RED),
+                        new DrawableSegment(55, 65, Color.BLUE),
+                        new DrawablePoint(69, 81, Color.BLUE),
+                        new DrawableSegment(85, 170, Color.BLUE),
+                        new DrawablePoint(174, 186, Color.BLUE),
+                        new DrawableSegment(190, 215, Color.BLUE),
+                        new DrawablePoint(219, 231, Color.YELLOW),
+                        new DrawableSegment(235, 300, Color.BLUE)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 16;
         boolean isStyledByProgress = true;
 
-        // Colors with 50% opacity
-        int fadedBlue = 0x7F0000FF;
-        int fadedYellow = 0x7FFFFF00;
+        Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
+                parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
+                300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
 
-        List<Part> expected = new ArrayList<>(List.of(
-                new Segment(0.15f, Color.BLUE),
-                new Point(null, Color.RED),
-                new Segment(0.10f, Color.BLUE),
-                new Point(null, Color.BLUE),
-                new Segment(0.35f, Color.BLUE),
-                new Point(null, Color.BLUE),
-                new Segment(0.15f, fadedBlue, true),
-                new Point(null, fadedYellow, true),
-                new Segment(0.25f, fadedBlue, true)));
+        // Colors with 40% opacity
+        int fadedBlue = 0x660000FF;
+        int fadedYellow = 0x66FFFF00;
+        expectedDrawableParts = new ArrayList<>(
+                List.of(new DrawableSegment(0, 34.219177F, Color.BLUE),
+                        new DrawablePoint(38.219177F, 50.219177F, Color.RED),
+                        new DrawableSegment(54.219177F, 70.21918F, Color.BLUE),
+                        new DrawablePoint(74.21918F, 86.21918F, Color.BLUE),
+                        new DrawableSegment(90.21918F, 172.38356F, Color.BLUE),
+                        new DrawablePoint(176.38356F, 188.38356F, Color.BLUE),
+                        new DrawableSegment(192.38356F, 217.0137F, fadedBlue, true),
+                        new DrawablePoint(221.0137F, 233.0137F, fadedYellow),
+                        new DrawableSegment(237.0137F, 300F, fadedBlue, true)));
 
-        List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
-                segments, points, progress, progressMax, isStyledByProgress);
-
-        assertThat(parts).isEqualTo(expected);
+        assertThat(p.second).isEqualTo(182.38356F);
+        assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 
     @Test
-    public void processAndConvertToDrawableParts_multipleSegmentsWithPoints() {
+    public void processAndConvertToParts_multipleSegmentsWithPoints() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(50).setColor(Color.RED));
         segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN));
@@ -256,32 +403,68 @@
         points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW));
         int progress = 60;
         int progressMax = 100;
+
+        List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+                progress, progressMax);
+
+        List<Part> expectedParts = new ArrayList<>(
+                List.of(new Segment(0.15f, Color.RED), new Point(Color.RED),
+                        new Segment(0.10f, Color.RED), new Point(Color.BLUE),
+                        new Segment(0.25f, Color.RED), new Segment(0.10f, Color.GREEN),
+                        new Point(Color.BLUE), new Segment(0.15f, Color.GREEN),
+                        new Point(Color.YELLOW), new Segment(0.25f, Color.GREEN)));
+
+        assertThat(parts).isEqualTo(expectedParts);
+
+        float drawableWidth = 300;
+        float segSegGap = 4;
+        float segPointGap = 4;
+        float pointRadius = 6;
+        boolean hasTrackerIcon = true;
+        List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts(
+                parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+
+        List<DrawablePart> expectedDrawableParts = new ArrayList<>(
+                List.of(new DrawableSegment(0, 35, Color.RED), new DrawablePoint(39, 51, Color.RED),
+                        new DrawableSegment(55, 65, Color.RED),
+                        new DrawablePoint(69, 81, Color.BLUE),
+                        new DrawableSegment(85, 146, Color.RED),
+                        new DrawableSegment(150, 170, Color.GREEN),
+                        new DrawablePoint(174, 186, Color.BLUE),
+                        new DrawableSegment(190, 215, Color.GREEN),
+                        new DrawablePoint(219, 231, Color.YELLOW),
+                        new DrawableSegment(235, 300, Color.GREEN)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 16;
         boolean isStyledByProgress = true;
 
-        List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
-                segments, points, progress, progressMax, isStyledByProgress);
+        Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
+                parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
+                300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
 
-        // Colors with 50% opacity
-        int fadedGreen = 0x7F00FF00;
-        int fadedYellow = 0x7FFFFF00;
+        // Colors with 40% opacity
+        int fadedGreen = 0x6600FF00;
+        int fadedYellow = 0x66FFFF00;
+        expectedDrawableParts = new ArrayList<>(
+                List.of(new DrawableSegment(0, 34.095238F, Color.RED),
+                        new DrawablePoint(38.095238F, 50.095238F, Color.RED),
+                        new DrawableSegment(54.095238F, 70.09524F, Color.RED),
+                        new DrawablePoint(74.09524F, 86.09524F, Color.BLUE),
+                        new DrawableSegment(90.09524F, 148.9524F, Color.RED),
+                        new DrawableSegment(152.95238F, 172.7619F, Color.GREEN),
+                        new DrawablePoint(176.7619F, 188.7619F, Color.BLUE),
+                        new DrawableSegment(192.7619F, 217.33333F, fadedGreen, true),
+                        new DrawablePoint(221.33333F, 233.33333F, fadedYellow),
+                        new DrawableSegment(237.33333F, 299.99997F, fadedGreen, true)));
 
-        List<Part> expected = new ArrayList<>(List.of(
-                new Segment(0.15f, Color.RED),
-                new Point(null, Color.RED),
-                new Segment(0.10f, Color.RED),
-                new Point(null, Color.BLUE),
-                new Segment(0.25f, Color.RED),
-                new Segment(0.10f, Color.GREEN),
-                new Point(null, Color.BLUE),
-                new Segment(0.15f, fadedGreen, true),
-                new Point(null, fadedYellow, true),
-                new Segment(0.25f, fadedGreen, true)));
-
-        assertThat(parts).isEqualTo(expected);
+        assertThat(p.second).isEqualTo(182.7619F);
+        assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 
     @Test
-    public void processAndConvertToDrawableParts_multipleSegmentsWithPoints_notStyledByProgress() {
+    public void processAndConvertToParts_multipleSegmentsWithPoints_notStyledByProgress() {
         List<ProgressStyle.Segment> segments = new ArrayList<>();
         segments.add(new ProgressStyle.Segment(50).setColor(Color.RED));
         segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN));
@@ -291,21 +474,223 @@
         points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW));
         int progress = 60;
         int progressMax = 100;
+
+        List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+                progress, progressMax);
+
+        List<Part> expectedParts = new ArrayList<>(
+                List.of(new Segment(0.15f, Color.RED), new Point(Color.RED),
+                        new Segment(0.10f, Color.RED), new Point(Color.BLUE),
+                        new Segment(0.25f, Color.RED), new Segment(0.25f, Color.GREEN),
+                        new Point(Color.YELLOW), new Segment(0.25f, Color.GREEN)));
+
+        assertThat(parts).isEqualTo(expectedParts);
+
+        float drawableWidth = 300;
+        float segSegGap = 4;
+        float segPointGap = 4;
+        float pointRadius = 6;
+        boolean hasTrackerIcon = true;
+
+        List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts(
+                parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+
+        List<DrawablePart> expectedDrawableParts = new ArrayList<>(
+                List.of(new DrawableSegment(0, 35, Color.RED), new DrawablePoint(39, 51, Color.RED),
+                        new DrawableSegment(55, 65, Color.RED),
+                        new DrawablePoint(69, 81, Color.BLUE),
+                        new DrawableSegment(85, 146, Color.RED),
+                        new DrawableSegment(150, 215, Color.GREEN),
+                        new DrawablePoint(219, 231, Color.YELLOW),
+                        new DrawableSegment(235, 300, Color.GREEN)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 16;
         boolean isStyledByProgress = false;
 
-        List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
-                segments, points, progress, progressMax, isStyledByProgress);
+        Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
+                parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
+                300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
 
-        List<Part> expected = new ArrayList<>(List.of(
-                new Segment(0.15f, Color.RED),
-                new Point(null, Color.RED),
-                new Segment(0.10f, Color.RED),
-                new Point(null, Color.BLUE),
-                new Segment(0.25f, Color.RED),
-                new Segment(0.25f, Color.GREEN),
-                new Point(null, Color.YELLOW),
-                new Segment(0.25f, Color.GREEN)));
+        expectedDrawableParts = new ArrayList<>(
+                List.of(new DrawableSegment(0, 34.296295F, Color.RED),
+                        new DrawablePoint(38.296295F, 50.296295F, Color.RED),
+                        new DrawableSegment(54.296295F, 70.296295F, Color.RED),
+                        new DrawablePoint(74.296295F, 86.296295F, Color.BLUE),
+                        new DrawableSegment(90.296295F, 149.62962F, Color.RED),
+                        new DrawableSegment(153.62962F, 216.8148F, Color.GREEN),
+                        new DrawablePoint(220.81482F, 232.81482F, Color.YELLOW),
+                        new DrawableSegment(236.81482F, 300, Color.GREEN)));
 
-        assertThat(parts).isEqualTo(expected);
+        assertThat(p.second).isEqualTo(182.9037F);
+        assertThat(p.first).isEqualTo(expectedDrawableParts);
+    }
+
+    // The only difference from the `zeroWidthDrawableSegment` test below is the longer
+    // segmentMinWidth (= 16dp).
+    @Test
+    public void maybeStretchAndRescaleSegments_negativeWidthDrawableSegment() {
+        List<ProgressStyle.Segment> segments = new ArrayList<>();
+        segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE));
+        segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE));
+        segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE));
+        segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE));
+        List<ProgressStyle.Point> points = new ArrayList<>();
+        points.add(new ProgressStyle.Point(0).setColor(Color.BLUE));
+        int progress = 1000;
+        int progressMax = 1000;
+
+        List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+                progress, progressMax);
+
+        List<Part> expectedParts = new ArrayList<>(
+                List.of(new Point(Color.BLUE), new Segment(0.1f, Color.BLUE),
+                        new Segment(0.2f, Color.BLUE), new Segment(0.3f, Color.BLUE),
+                        new Segment(0.4f, Color.BLUE)));
+
+        assertThat(parts).isEqualTo(expectedParts);
+
+        float drawableWidth = 200;
+        float segSegGap = 4;
+        float segPointGap = 4;
+        float pointRadius = 6;
+        boolean hasTrackerIcon = true;
+        List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts(
+                parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+
+        List<DrawablePart> expectedDrawableParts = new ArrayList<>(
+                List.of(new DrawablePoint(0, 12, Color.BLUE),
+                        new DrawableSegment(16, 16, Color.BLUE),
+                        new DrawableSegment(20, 56, Color.BLUE),
+                        new DrawableSegment(60, 116, Color.BLUE),
+                        new DrawableSegment(120, 200, Color.BLUE)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 16;
+        boolean isStyledByProgress = true;
+
+        Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
+                parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
+                200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+
+        expectedDrawableParts = new ArrayList<>(List.of(new DrawablePoint(0, 12, Color.BLUE),
+                new DrawableSegment(16, 32, Color.BLUE),
+                new DrawableSegment(36, 69.41936F, Color.BLUE),
+                new DrawableSegment(73.41936F, 124.25807F, Color.BLUE),
+                new DrawableSegment(128.25807F, 200, Color.BLUE)));
+
+        assertThat(p.second).isEqualTo(200);
+        assertThat(p.first).isEqualTo(expectedDrawableParts);
+    }
+
+    // The only difference from the `negativeWidthDrawableSegment` test above is the shorter
+    // segmentMinWidth (= 10dp).
+    @Test
+    public void maybeStretchAndRescaleSegments_zeroWidthDrawableSegment() {
+        List<ProgressStyle.Segment> segments = new ArrayList<>();
+        segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE));
+        segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE));
+        segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE));
+        segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE));
+        List<ProgressStyle.Point> points = new ArrayList<>();
+        points.add(new ProgressStyle.Point(0).setColor(Color.BLUE));
+        int progress = 1000;
+        int progressMax = 1000;
+
+        List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+                progress, progressMax);
+
+        List<Part> expectedParts = new ArrayList<>(
+                List.of(new Point(Color.BLUE), new Segment(0.1f, Color.BLUE),
+                        new Segment(0.2f, Color.BLUE), new Segment(0.3f, Color.BLUE),
+                        new Segment(0.4f, Color.BLUE)));
+
+        assertThat(parts).isEqualTo(expectedParts);
+
+        float drawableWidth = 200;
+        float segSegGap = 4;
+        float segPointGap = 4;
+        float pointRadius = 6;
+        boolean hasTrackerIcon = true;
+        List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts(
+                parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+
+        List<DrawablePart> expectedDrawableParts = new ArrayList<>(
+                List.of(new DrawablePoint(0, 12, Color.BLUE),
+                        new DrawableSegment(16, 16, Color.BLUE),
+                        new DrawableSegment(20, 56, Color.BLUE),
+                        new DrawableSegment(60, 116, Color.BLUE),
+                        new DrawableSegment(120, 200, Color.BLUE)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 10;
+        boolean isStyledByProgress = true;
+
+        Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
+                parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
+                200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+
+        expectedDrawableParts = new ArrayList<>(List.of(new DrawablePoint(0, 12, Color.BLUE),
+                new DrawableSegment(16, 26, Color.BLUE),
+                new DrawableSegment(30, 64.169014F, Color.BLUE),
+                new DrawableSegment(68.169014F, 120.92958F, Color.BLUE),
+                new DrawableSegment(124.92958F, 200, Color.BLUE)));
+
+        assertThat(p.second).isEqualTo(200);
+        assertThat(p.first).isEqualTo(expectedDrawableParts);
+    }
+
+    @Test
+    public void maybeStretchAndRescaleSegments_noStretchingNecessary() {
+        List<ProgressStyle.Segment> segments = new ArrayList<>();
+        segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE));
+        segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE));
+        segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE));
+        segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE));
+        List<ProgressStyle.Point> points = new ArrayList<>();
+        points.add(new ProgressStyle.Point(0).setColor(Color.BLUE));
+        int progress = 1000;
+        int progressMax = 1000;
+
+        List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+                progress, progressMax);
+
+        List<Part> expectedParts = new ArrayList<>(
+                List.of(new Point(Color.BLUE), new Segment(0.2f, Color.BLUE),
+                        new Segment(0.1f, Color.BLUE), new Segment(0.3f, Color.BLUE),
+                        new Segment(0.4f, Color.BLUE)));
+
+        assertThat(parts).isEqualTo(expectedParts);
+
+        float drawableWidth = 200;
+        float segSegGap = 4;
+        float segPointGap = 4;
+        float pointRadius = 6;
+        boolean hasTrackerIcon = true;
+
+        List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts(
+                parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon);
+
+        List<DrawablePart> expectedDrawableParts = new ArrayList<>(
+                List.of(new DrawablePoint(0, 12, Color.BLUE),
+                        new DrawableSegment(16, 36, Color.BLUE),
+                        new DrawableSegment(40, 56, Color.BLUE),
+                        new DrawableSegment(60, 116, Color.BLUE),
+                        new DrawableSegment(120, 200, Color.BLUE)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 10;
+        boolean isStyledByProgress = true;
+
+        Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments(
+                parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax,
+                200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+
+        assertThat(p.second).isEqualTo(200);
+        assertThat(p.first).isEqualTo(expectedDrawableParts);
     }
 }
diff --git a/core/tests/systemproperties/Android.bp b/core/tests/systemproperties/Android.bp
index ed99a1f..9197dec 100644
--- a/core/tests/systemproperties/Android.bp
+++ b/core/tests/systemproperties/Android.bp
@@ -44,4 +44,5 @@
         "src/**/*.java",
     ],
     auto_gen_config: true,
+    team: "trendy_team_ravenwood",
 }
diff --git a/core/tests/timetests/src/android/app/time/TimeZoneCapabilitiesTest.java b/core/tests/timetests/src/android/app/time/TimeZoneCapabilitiesTest.java
index e368d28..cb8b5ce 100644
--- a/core/tests/timetests/src/android/app/time/TimeZoneCapabilitiesTest.java
+++ b/core/tests/timetests/src/android/app/time/TimeZoneCapabilitiesTest.java
@@ -48,12 +48,14 @@
                 .setConfigureAutoDetectionEnabledCapability(CAPABILITY_POSSESSED)
                 .setUseLocationEnabled(true)
                 .setConfigureGeoDetectionEnabledCapability(CAPABILITY_POSSESSED)
-                .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED);
+                .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED)
+                .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED);
         TimeZoneCapabilities.Builder builder2 = new TimeZoneCapabilities.Builder(TEST_USER_HANDLE)
                 .setConfigureAutoDetectionEnabledCapability(CAPABILITY_POSSESSED)
                 .setUseLocationEnabled(true)
                 .setConfigureGeoDetectionEnabledCapability(CAPABILITY_POSSESSED)
-                .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED);
+                .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED)
+                .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED);
         {
             TimeZoneCapabilities one = builder1.build();
             TimeZoneCapabilities two = builder2.build();
@@ -115,6 +117,13 @@
             TimeZoneCapabilities two = builder2.build();
             assertEquals(one, two);
         }
+
+        builder1.setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED);
+        {
+            TimeZoneCapabilities one = builder1.build();
+            TimeZoneCapabilities two = builder2.build();
+            assertNotEquals(one, two);
+        }
     }
 
     @Test
@@ -123,7 +132,8 @@
                 .setConfigureAutoDetectionEnabledCapability(CAPABILITY_POSSESSED)
                 .setUseLocationEnabled(true)
                 .setConfigureGeoDetectionEnabledCapability(CAPABILITY_POSSESSED)
-                .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED);
+                .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED)
+                .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED);
         assertRoundTripParcelable(builder.build());
 
         builder.setConfigureAutoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED);
@@ -137,6 +147,9 @@
 
         builder.setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED);
         assertRoundTripParcelable(builder.build());
+
+        builder.setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED);
+        assertRoundTripParcelable(builder.build());
     }
 
     @Test
@@ -151,6 +164,7 @@
                 .setUseLocationEnabled(true)
                 .setConfigureGeoDetectionEnabledCapability(CAPABILITY_POSSESSED)
                 .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED)
+                .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED)
                 .build();
 
         TimeZoneConfiguration configChange = new TimeZoneConfiguration.Builder()
@@ -175,6 +189,7 @@
                 .setUseLocationEnabled(true)
                 .setConfigureGeoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED)
                 .setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED)
+                .setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED)
                 .build();
 
         TimeZoneConfiguration configChange = new TimeZoneConfiguration.Builder()
@@ -191,6 +206,7 @@
                 .setUseLocationEnabled(true)
                 .setConfigureGeoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED)
                 .setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED)
+                .setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED)
                 .build();
 
         {
@@ -204,6 +220,7 @@
                             .setUseLocationEnabled(true)
                             .setConfigureGeoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED)
                             .setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED)
+                            .setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED)
                             .build();
 
             assertThat(updatedCapabilities).isEqualTo(expectedCapabilities);
@@ -221,6 +238,7 @@
                             .setUseLocationEnabled(false)
                             .setConfigureGeoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED)
                             .setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED)
+                            .setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED)
                             .build();
 
             assertThat(updatedCapabilities).isEqualTo(expectedCapabilities);
@@ -238,6 +256,7 @@
                             .setUseLocationEnabled(true)
                             .setConfigureGeoDetectionEnabledCapability(CAPABILITY_POSSESSED)
                             .setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED)
+                            .setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED)
                             .build();
 
             assertThat(updatedCapabilities).isEqualTo(expectedCapabilities);
@@ -255,6 +274,25 @@
                             .setUseLocationEnabled(true)
                             .setConfigureGeoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED)
                             .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED)
+                            .setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED)
+                            .build();
+
+            assertThat(updatedCapabilities).isEqualTo(expectedCapabilities);
+        }
+
+        {
+            TimeZoneCapabilities updatedCapabilities =
+                    new TimeZoneCapabilities.Builder(capabilities)
+                            .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED)
+                            .build();
+
+            TimeZoneCapabilities expectedCapabilities =
+                    new TimeZoneCapabilities.Builder(TEST_USER_HANDLE)
+                            .setConfigureAutoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED)
+                            .setUseLocationEnabled(true)
+                            .setConfigureGeoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED)
+                            .setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED)
+                            .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED)
                             .build();
 
             assertThat(updatedCapabilities).isEqualTo(expectedCapabilities);
diff --git a/core/tests/timetests/src/android/app/time/TimeZoneConfigurationTest.java b/core/tests/timetests/src/android/app/time/TimeZoneConfigurationTest.java
index 4ad3e41..345e912 100644
--- a/core/tests/timetests/src/android/app/time/TimeZoneConfigurationTest.java
+++ b/core/tests/timetests/src/android/app/time/TimeZoneConfigurationTest.java
@@ -43,9 +43,11 @@
         TimeZoneConfiguration completeConfig = new TimeZoneConfiguration.Builder()
                 .setAutoDetectionEnabled(true)
                 .setGeoDetectionEnabled(true)
+                .setNotificationsEnabled(true)
                 .build();
         assertTrue(completeConfig.isComplete());
         assertTrue(completeConfig.hasIsGeoDetectionEnabled());
+        assertTrue(completeConfig.hasIsNotificationsEnabled());
     }
 
     @Test
diff --git a/core/tests/utiltests/Android.bp b/core/tests/utiltests/Android.bp
index 7cf49ab..5011f7a 100644
--- a/core/tests/utiltests/Android.bp
+++ b/core/tests/utiltests/Android.bp
@@ -69,4 +69,5 @@
         "src/com/android/internal/util/**/*.java",
     ],
     auto_gen_config: true,
+    team: "trendy_team_ravenwood",
 }
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index 50c95a9..b332cf0 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -16,9 +16,10 @@
 
 package android.graphics;
 
+import static com.android.text.flags.Flags.FLAG_DEPRECATE_ELEGANT_TEXT_HEIGHT_API;
 import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE;
 import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION;
-import static com.android.text.flags.Flags.FLAG_DEPRECATE_ELEGANT_TEXT_HEIGHT_API;
+import static com.android.text.flags.Flags.FLAG_TYPEFACE_REDESIGN_READONLY;
 import static com.android.text.flags.Flags.FLAG_VERTICAL_TEXT_LAYOUT;
 
 import android.annotation.ColorInt;
@@ -34,7 +35,6 @@
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledSince;
 import android.compat.annotation.UnsupportedAppUsage;
-import android.graphics.fonts.FontStyle;
 import android.graphics.fonts.FontVariationAxis;
 import android.graphics.text.TextRunShaper;
 import android.os.Build;
@@ -58,6 +58,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Locale;
 import java.util.Objects;
 
@@ -97,6 +98,7 @@
     private LocaleList      mLocales;
     private String          mFontFeatureSettings;
     private String          mFontVariationSettings;
+    private String          mFontVariationOverride;
 
     private float           mShadowLayerRadius;
     private float           mShadowLayerDx;
@@ -2100,14 +2102,6 @@
     }
 
     /**
-     * A change ID for new font variation settings management.
-     * @hide
-     */
-    @ChangeId
-    @EnabledSince(targetSdkVersion = 36)
-    public static final long NEW_FONT_VARIATION_MANAGEMENT = 361260253L;
-
-    /**
      * Sets TrueType or OpenType font variation settings. The settings string is constructed from
      * multiple pairs of axis tag and style values. The axis tag must contain four ASCII characters
      * and must be wrapped with single quotes (U+0027) or double quotes (U+0022). Axis strings that
@@ -2136,16 +2130,12 @@
      * </li>
      * </ul>
      *
-     * <p>Note: If the application that targets API 35 or before, this function mutates the
-     * underlying typeface instance.
-     *
      * @param fontVariationSettings font variation settings. You can pass null or empty string as
      *                              no variation settings.
      *
-     * @return If the application that targets API 36 or later and is running on devices API 36 or
-     *         later, this function always returns true. Otherwise, this function returns true if
-     *         the given settings is effective to at least one font file underlying this typeface.
-     *         This function also returns true for empty settings string. Otherwise returns false.
+     * @return true if the given settings is effective to at least one font file underlying this
+     *         typeface. This function also returns true for empty settings string. Otherwise
+     *         returns false
      *
      * @throws IllegalArgumentException If given string is not a valid font variation settings
      *                                  format
@@ -2153,40 +2143,13 @@
      * @see #getFontVariationSettings()
      * @see FontVariationAxis
      */
+    // Add following API description once the setFontVariationOverride becomes public.
+    // This method generates new variation instance of the {@link Typeface} instance and set it to
+    // this object. Therefore, subsequent {@link #setTypeface(Typeface)} call will clear the font
+    // variation settings. Also, creating variation instance of the Typeface requires non trivial
+    // amount of time and memories, therefore consider using
+    // {@link #setFontVariationOverride(String, int)} for better performance.
     public boolean setFontVariationSettings(String fontVariationSettings) {
-        return setFontVariationSettings(fontVariationSettings, 0 /* wght adjust */);
-    }
-
-    /**
-     * Set font variation settings with weight adjustment
-     * @hide
-     */
-    public boolean setFontVariationSettings(String fontVariationSettings, int wghtAdjust) {
-        final boolean useFontVariationStore = Flags.typefaceRedesignReadonly()
-                && CompatChanges.isChangeEnabled(NEW_FONT_VARIATION_MANAGEMENT);
-        if (useFontVariationStore) {
-            FontVariationAxis[] axes =
-                    FontVariationAxis.fromFontVariationSettings(fontVariationSettings);
-            if (axes == null) {
-                nSetFontVariationOverride(mNativePaint, 0);
-                mFontVariationSettings = null;
-                return true;
-            }
-
-            long builderPtr = nCreateFontVariationBuilder(axes.length);
-            for (int i = 0; i < axes.length; ++i) {
-                int tag = axes[i].getOpenTypeTagValue();
-                float value = axes[i].getStyleValue();
-                if (tag == 0x77676874 /* wght */) {
-                    value = Math.clamp(value + wghtAdjust,
-                            FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX);
-                }
-                nAddFontVariationToBuilder(builderPtr, tag, value);
-            }
-            nSetFontVariationOverride(mNativePaint, builderPtr);
-            mFontVariationSettings = fontVariationSettings;
-            return true;
-        }
         final String settings = TextUtils.nullIfEmpty(fontVariationSettings);
         if (settings == mFontVariationSettings
                 || (settings != null && settings.equals(mFontVariationSettings))) {
@@ -2220,6 +2183,68 @@
     }
 
     /**
+     * Sets TrueType or OpenType font variation settings for overriding.
+     *
+     * The settings string is constructed from multiple pairs of axis tag and style values. The axis
+     * tag must contain four ASCII characters and must be wrapped with single quotes (U+0027) or
+     * double quotes (U+0022). Axis strings that are longer or shorter than four characters, or
+     * contain characters outside of U+0020..U+007E are invalid.
+     *
+     * If invalid font variation settings is provided, this method does nothing and returning false
+     * with printing error message to the logcat.
+     *
+     * Different from {@link #setFontVariationSettings(String)}, this overrides the font variation
+     * settings which is already assigned to the font instance. For example, if the underlying font
+     * is configured as {@code 'wght' 500, 'ital' 1}, and if the override is specified as
+     * {@code 'wght' 700, `wdth` 150}, then the effective font variation setting is
+     * {@code `wght' 700, 'ital' 1, 'wdth' 150}. The `wght` value is updated by override, 'ital'
+     * value is preserved because no overrides, and `wdth` value is added by override.
+     *
+     * @param fontVariationOverride font variation settings. You can pass null or empty string as
+     *                              no variation settings.
+     *
+     * @return true if the provided font variation settings is valid. Otherwise returns false.
+     *
+     * @see #getFontVariationSettings()
+     * @see #setFontVariationSettings(String)
+     * @see #getFontVariationOverride()
+     * @see FontVariationAxis
+     */
+    @FlaggedApi(FLAG_TYPEFACE_REDESIGN_READONLY)
+    public boolean setFontVariationOverride(@Nullable String fontVariationOverride) {
+        if (Objects.equals(fontVariationOverride, mFontVariationOverride)) {
+            return true;
+        }
+
+        List<FontVariationAxis> axes;
+        try {
+            axes = FontVariationAxis.fromFontVariationSettingsForList(fontVariationOverride);
+        } catch (IllegalArgumentException e) {
+            Log.i(TAG, "failed to parse font variation settings.", e);
+            return false;
+        }
+        long builderPtr = nCreateFontVariationBuilder(axes.size());
+        for (int i = 0; i < axes.size(); ++i) {
+            FontVariationAxis axis = axes.get(i);
+            nAddFontVariationToBuilder(
+                    builderPtr, axis.getOpenTypeTagValue(), axis.getStyleValue());
+        }
+        nSetFontVariationOverride(mNativePaint, builderPtr);
+        mFontVariationOverride = fontVariationOverride;
+        return true;
+    }
+
+    /**
+     * Gets the current font variation override value.
+     *
+     * @return a current font variation override value.
+     */
+    @FlaggedApi(FLAG_TYPEFACE_REDESIGN_READONLY)
+    public @Nullable String getFontVariationOverride() {
+        return mFontVariationOverride;
+    }
+
+    /**
      * Get the current value of start hyphen edit.
      *
      * The default value is 0 which is equivalent to {@link #START_HYPHEN_EDIT_NO_EDIT}.
diff --git a/graphics/java/android/graphics/fonts/FontVariationAxis.java b/graphics/java/android/graphics/fonts/FontVariationAxis.java
index d1fe2cd..30a248b 100644
--- a/graphics/java/android/graphics/fonts/FontVariationAxis.java
+++ b/graphics/java/android/graphics/fonts/FontVariationAxis.java
@@ -23,6 +23,7 @@
 import android.text.TextUtils;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 import java.util.regex.Pattern;
@@ -139,9 +140,19 @@
      */
     public static @Nullable FontVariationAxis[] fromFontVariationSettings(
             @Nullable String settings) {
-        if (settings == null || settings.isEmpty()) {
+        List<FontVariationAxis> result = fromFontVariationSettingsForList(settings);
+        if (result.isEmpty()) {
             return null;
         }
+        return result.toArray(new FontVariationAxis[0]);
+    }
+
+    /** @hide */
+    public static @NonNull List<FontVariationAxis> fromFontVariationSettingsForList(
+            @Nullable String settings) {
+        if (settings == null || settings.isEmpty()) {
+            return Collections.emptyList();
+        }
         final ArrayList<FontVariationAxis> axisList = new ArrayList<>();
         final int length = settings.length();
         for (int i = 0; i < length; i++) {
@@ -172,9 +183,9 @@
             i = endOfValueString;
         }
         if (axisList.isEmpty()) {
-            return null;
+            return Collections.emptyList();
         }
-        return axisList.toArray(new FontVariationAxis[0]);
+        return axisList;
     }
 
     /**
diff --git a/keystore/java/android/security/keystore/KeyStoreManager.java b/keystore/java/android/security/keystore/KeyStoreManager.java
index 740ccb5..13f1a72 100644
--- a/keystore/java/android/security/keystore/KeyStoreManager.java
+++ b/keystore/java/android/security/keystore/KeyStoreManager.java
@@ -312,9 +312,11 @@
      * When passed into getSupplementaryAttestationInfo, getSupplementaryAttestationInfo returns the
      * DER-encoded structure corresponding to the `Modules` schema described in the KeyMint HAL's
      * KeyCreationResult.aidl. The SHA-256 hash of this encoded structure is what's included with
-     * the tag in attestations.
+     * the tag in attestations. To ensure the returned encoded structure is the one attested to,
+     * clients should verify its SHA-256 hash matches the one in the attestation. Note that the
+     * returned structure can vary between boots.
      */
-    // TODO(b/369375199): Replace with Tag.MODULE_HASH when flagging is removed.
+    // TODO(b/380020528): Replace with Tag.MODULE_HASH when KeyMint V4 is frozen.
     public static final int MODULE_HASH = TagType.BYTES | 724;
 
     /**
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index 4c75ea4..957d1b8 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -26,8 +26,8 @@
 java_library {
     name: "wm_shell_protolog-groups",
     srcs: [
-        "src/com/android/wm/shell/protolog/ShellProtoLogGroup.java",
         ":protolog-common-src",
+        "src/com/android/wm/shell/protolog/ShellProtoLogGroup.java",
     ],
 }
 
@@ -61,8 +61,8 @@
     name: "wm_shell_protolog_src",
     srcs: [
         ":protolog-impl",
-        ":wm_shell_protolog-groups",
         ":wm_shell-sources",
+        ":wm_shell_protolog-groups",
     ],
     tools: ["protologtool"],
     cmd: "$(location protologtool) transform-protolog-calls " +
@@ -80,8 +80,8 @@
 java_genrule {
     name: "generate-wm_shell_protolog.json",
     srcs: [
-        ":wm_shell_protolog-groups",
         ":wm_shell-sources",
+        ":wm_shell_protolog-groups",
     ],
     tools: ["protologtool"],
     cmd: "$(location protologtool) generate-viewer-config " +
@@ -97,8 +97,8 @@
 java_genrule {
     name: "gen-wmshell.protolog.pb",
     srcs: [
-        ":wm_shell_protolog-groups",
         ":wm_shell-sources",
+        ":wm_shell_protolog-groups",
     ],
     tools: ["protologtool"],
     cmd: "$(location protologtool) generate-viewer-config " +
@@ -159,38 +159,39 @@
 android_library {
     name: "WindowManager-Shell",
     srcs: [
-        "src/com/android/wm/shell/EventLogTags.logtags",
         ":wm_shell_protolog_src",
         // TODO(b/168581922) protologtool do not support kotlin(*.kt)
-        ":wm_shell-sources-kt",
+        "src/com/android/wm/shell/EventLogTags.logtags",
         ":wm_shell-aidls",
         ":wm_shell-shared-aidls",
+        ":wm_shell-sources-kt",
     ],
     resource_dirs: [
         "res",
     ],
     static_libs: [
-        "androidx.appcompat_appcompat",
-        "androidx.core_core-ktx",
-        "androidx.arch.core_core-runtime",
-        "androidx.datastore_datastore",
-        "androidx.compose.material3_material3",
-        "androidx-constraintlayout_constraintlayout",
-        "androidx.dynamicanimation_dynamicanimation",
-        "androidx.recyclerview_recyclerview",
-        "kotlinx-coroutines-android",
-        "kotlinx-coroutines-core",
         "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib",
         "//frameworks/libs/systemui:iconloader_base",
+        "//packages/apps/Car/SystemUI/aconfig:com_android_systemui_car_flags_lib",
+        "PlatformAnimationLib",
+        "WindowManager-Shell-lite-proto",
+        "WindowManager-Shell-proto",
+        "WindowManager-Shell-shared",
+        "androidx-constraintlayout_constraintlayout",
+        "androidx.appcompat_appcompat",
+        "androidx.arch.core_core-runtime",
+        "androidx.compose.material3_material3",
+        "androidx.core_core-ktx",
+        "androidx.datastore_datastore",
+        "androidx.dynamicanimation_dynamicanimation",
+        "androidx.recyclerview_recyclerview",
         "com_android_launcher3_flags_lib",
         "com_android_wm_shell_flags_lib",
-        "PlatformAnimationLib",
-        "WindowManager-Shell-proto",
-        "WindowManager-Shell-lite-proto",
-        "WindowManager-Shell-shared",
-        "perfetto_trace_java_protos",
         "dagger2",
         "jsr330",
+        "kotlinx-coroutines-android",
+        "kotlinx-coroutines-core",
+        "perfetto_trace_java_protos",
     ],
     libs: [
         // Soong fails to automatically add this dependency because all the
diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml
index b2ac640..4f1cd97 100644
--- a/libs/WindowManager/Shell/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/AndroidManifest.xml
@@ -26,6 +26,7 @@
     <uses-permission android:name="android.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE" />
     <uses-permission android:name="android.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION" />
     <uses-permission android:name="android.permission.MANAGE_KEY_GESTURES" />
+    <uses-permission android:name="android.permission.MANAGE_DISPLAYS" />
 
     <application>
         <activity
diff --git a/libs/WindowManager/Shell/multivalentTests/Android.bp b/libs/WindowManager/Shell/multivalentTests/Android.bp
index eecf199..03076c0 100644
--- a/libs/WindowManager/Shell/multivalentTests/Android.bp
+++ b/libs/WindowManager/Shell/multivalentTests/Android.bp
@@ -35,7 +35,6 @@
 android_robolectric_test {
     name: "WMShellRobolectricTests",
     instrumentation_for: "WindowManagerShellRobolectric",
-    upstream: true,
     java_resource_dirs: [
         "robolectric/config",
     ],
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java
index 4300e84..2ca011b 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java
@@ -16,10 +16,12 @@
 
 package com.android.wm.shell.shared;
 
+import static android.app.WindowConfiguration.windowingModeToString;
+import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
+
 import android.annotation.IntDef;
 import android.app.ActivityManager.RecentTaskInfo;
 import android.app.TaskInfo;
-import android.app.WindowConfiguration;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -28,11 +30,14 @@
 
 import com.android.wm.shell.shared.split.SplitBounds;
 
+import kotlin.collections.CollectionsKt;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * Simple container for recent tasks which should be presented as a single task within the
@@ -43,11 +48,13 @@
     public static final int TYPE_FULLSCREEN = 1;
     public static final int TYPE_SPLIT = 2;
     public static final int TYPE_FREEFORM = 3;
+    public static final int TYPE_MIXED = 4;
 
     @IntDef(prefix = {"TYPE_"}, value = {
             TYPE_FULLSCREEN,
             TYPE_SPLIT,
-            TYPE_FREEFORM
+            TYPE_FREEFORM,
+            TYPE_MIXED
     })
     public @interface GroupType {}
 
@@ -64,7 +71,7 @@
      * TYPE_SPLIT: Contains the two split roots of each side
      * TYPE_FREEFORM: Contains the set of tasks currently in freeform mode
      */
-    @NonNull
+    @Nullable
     protected final List<TaskInfo> mTasks;
 
     /**
@@ -85,6 +92,14 @@
     protected final int[] mMinimizedTaskIds;
 
     /**
+     * Only set for TYPE_MIXED.
+     *
+     * The mixed set of task infos in this group.
+     */
+    @Nullable
+    protected final List<GroupedTaskInfo> mGroupedTasks;
+
+    /**
      * Create new for a stack of fullscreen tasks
      */
     public static GroupedTaskInfo forFullscreenTasks(@NonNull TaskInfo task) {
@@ -111,18 +126,41 @@
                 minimizedFreeformTasks.stream().mapToInt(i -> i).toArray());
     }
 
+    /**
+     * Create new for a group of grouped task infos, those grouped task infos may not be mixed
+     * themselves (ie. multiple depths of mixed grouped task infos are not allowed).
+     */
+    public static GroupedTaskInfo forMixed(@NonNull List<GroupedTaskInfo> groupedTasks) {
+        if (groupedTasks.isEmpty()) {
+            throw new IllegalArgumentException("Expected non-empty grouped task list");
+        }
+        if (groupedTasks.stream().anyMatch(task -> task.mType == TYPE_MIXED)) {
+            throw new IllegalArgumentException("Unexpected grouped task list");
+        }
+        return new GroupedTaskInfo(groupedTasks);
+    }
+
     private GroupedTaskInfo(
             @NonNull List<TaskInfo> tasks,
             @Nullable SplitBounds splitBounds,
             @GroupType int type,
             @Nullable int[] minimizedFreeformTaskIds) {
         mTasks = tasks;
+        mGroupedTasks = null;
         mSplitBounds = splitBounds;
         mType = type;
         mMinimizedTaskIds = minimizedFreeformTaskIds;
         ensureAllMinimizedIdsPresent(tasks, minimizedFreeformTaskIds);
     }
 
+    private GroupedTaskInfo(@NonNull List<GroupedTaskInfo> groupedTasks) {
+        mTasks = null;
+        mGroupedTasks = groupedTasks;
+        mSplitBounds = null;
+        mType = TYPE_MIXED;
+        mMinimizedTaskIds = null;
+    }
+
     private void ensureAllMinimizedIdsPresent(
             @NonNull List<TaskInfo> tasks,
             @Nullable int[] minimizedFreeformTaskIds) {
@@ -141,26 +179,47 @@
         for (int i = 0; i < numTasks; i++) {
             mTasks.add(new TaskInfo(parcel));
         }
+        mGroupedTasks = parcel.createTypedArrayList(GroupedTaskInfo.CREATOR);
         mSplitBounds = parcel.readTypedObject(SplitBounds.CREATOR);
         mType = parcel.readInt();
         mMinimizedTaskIds = parcel.createIntArray();
     }
 
     /**
-     * Get primary {@link RecentTaskInfo}
+     * If TYPE_MIXED, returns the root of the grouped tasks
+     * For all other types, returns this task itself
+     */
+    @NonNull
+    public GroupedTaskInfo getBaseGroupedTask() {
+        if (mType == TYPE_MIXED) {
+            return mGroupedTasks.getFirst();
+        }
+        return this;
+    }
+
+    /**
+     * Get primary {@link TaskInfo}.
+     *
+     * @throws IllegalStateException if the group is TYPE_MIXED.
      */
     @NonNull
     public TaskInfo getTaskInfo1() {
+        if (mType == TYPE_MIXED) {
+            throw new IllegalStateException("No indexed tasks for a mixed task");
+        }
         return mTasks.getFirst();
     }
 
     /**
-     * Get secondary {@link RecentTaskInfo}.
+     * Get secondary {@link TaskInfo}, used primarily for TYPE_SPLIT.
      *
-     * Used in split screen.
+     * @throws IllegalStateException if the group is TYPE_MIXED.
      */
     @Nullable
     public TaskInfo getTaskInfo2() {
+        if (mType == TYPE_MIXED) {
+            throw new IllegalStateException("No indexed tasks for a mixed task");
+        }
         if (mTasks.size() > 1) {
             return mTasks.get(1);
         }
@@ -172,9 +231,7 @@
      */
     @Nullable
     public TaskInfo getTaskById(int taskId) {
-        return mTasks.stream()
-                .filter(task -> task.taskId == taskId)
-                .findFirst().orElse(null);
+        return CollectionsKt.firstOrNull(getTaskInfoList(), taskInfo -> taskInfo.taskId == taskId);
     }
 
     /**
@@ -182,35 +239,59 @@
      */
     @NonNull
     public List<TaskInfo> getTaskInfoList() {
-        return mTasks;
+        if (mType == TYPE_MIXED) {
+            return CollectionsKt.flatMap(mGroupedTasks, groupedTaskInfo -> groupedTaskInfo.mTasks);
+        } else {
+            return mTasks;
+        }
     }
 
     /**
      * @return Whether this grouped task contains a task with the given {@code taskId}.
      */
     public boolean containsTask(int taskId) {
-        return mTasks.stream()
-                .anyMatch((task -> task.taskId == taskId));
+        return getTaskById(taskId) != null;
     }
 
     /**
-     * Return {@link SplitBounds} if this is a split screen entry or {@code null}
+     * Returns whether the group is of the given type, if this is a TYPE_MIXED group, then returns
+     * whether the root task info is of the given type.
+     */
+    public boolean isBaseType(@GroupType int type) {
+        return getBaseGroupedTask().mType == type;
+    }
+
+    /**
+     * Return {@link SplitBounds} if this is a split screen entry or {@code null}. Only valid for
+     * TYPE_SPLIT.
      */
     @Nullable
     public SplitBounds getSplitBounds() {
+        if (mType == TYPE_MIXED) {
+            throw new IllegalStateException("No split bounds for a mixed task");
+        }
         return mSplitBounds;
     }
 
     /**
-     * Get type of this recents entry. One of {@link GroupType}
+     * Get type of this recents entry. One of {@link GroupType}.
+     * Note: This is deprecated, callers should use `isBaseType()` and not make assumptions about
+     *       specific group types
      */
+    @Deprecated
     @GroupType
     public int getType() {
         return mType;
     }
 
+    /**
+     * Returns the set of minimized task ids, only valid for TYPE_FREEFORM.
+     */
     @Nullable
     public int[] getMinimizedTaskIds() {
+        if (mType == TYPE_MIXED) {
+            throw new IllegalStateException("No minimized task ids for a mixed task");
+        }
         return mMinimizedTaskIds;
     }
 
@@ -222,67 +303,64 @@
         GroupedTaskInfo other = (GroupedTaskInfo) obj;
         return mType == other.mType
                 && Objects.equals(mTasks, other.mTasks)
+                && Objects.equals(mGroupedTasks, other.mGroupedTasks)
                 && Objects.equals(mSplitBounds, other.mSplitBounds)
                 && Arrays.equals(mMinimizedTaskIds, other.mMinimizedTaskIds);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mType, mTasks, mSplitBounds, Arrays.hashCode(mMinimizedTaskIds));
+        return Objects.hash(mType, mTasks, mGroupedTasks, mSplitBounds,
+                Arrays.hashCode(mMinimizedTaskIds));
     }
 
     @Override
     public String toString() {
         StringBuilder taskString = new StringBuilder();
-        for (int i = 0; i < mTasks.size(); i++) {
-            if (i == 0) {
-                taskString.append("Task");
-            } else {
-                taskString.append(", Task");
+        if (mType == TYPE_MIXED) {
+            taskString.append("GroupedTasks=" + mGroupedTasks.stream()
+                    .map(GroupedTaskInfo::toString)
+                    .collect(Collectors.joining(",\n\t", "[\n\t", "\n]")));
+        } else {
+            taskString.append("Tasks=" + mTasks.stream()
+                    .map(taskInfo -> getTaskInfoDumpString(taskInfo))
+                    .collect(Collectors.joining(", ", "[", "]")));
+            if (mSplitBounds != null) {
+                taskString.append(", SplitBounds=").append(mSplitBounds);
             }
-            taskString.append(i + 1).append(": ").append(getTaskInfo(mTasks.get(i)));
+            taskString.append(", Type=" + typeToString(mType));
+            taskString.append(", Minimized Task IDs=" + Arrays.toString(mMinimizedTaskIds));
         }
-        if (mSplitBounds != null) {
-            taskString.append(", SplitBounds: ").append(mSplitBounds);
-        }
-        taskString.append(", Type=");
-        switch (mType) {
-            case TYPE_FULLSCREEN:
-                taskString.append("TYPE_FULLSCREEN");
-                break;
-            case TYPE_SPLIT:
-                taskString.append("TYPE_SPLIT");
-                break;
-            case TYPE_FREEFORM:
-                taskString.append("TYPE_FREEFORM");
-                break;
-        }
-        taskString.append(", Minimized Task IDs: ");
-        taskString.append(Arrays.toString(mMinimizedTaskIds));
         return taskString.toString();
     }
 
-    private String getTaskInfo(TaskInfo taskInfo) {
+    private String getTaskInfoDumpString(TaskInfo taskInfo) {
         if (taskInfo == null) {
             return null;
         }
+        final boolean isExcluded = (taskInfo.baseIntent.getFlags()
+                & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
         return "id=" + taskInfo.taskId
-                + " baseIntent=" +
-                        (taskInfo.baseIntent != null && taskInfo.baseIntent.getComponent() != null
-                                ? taskInfo.baseIntent.getComponent().flattenToString()
-                                : "null")
-                + " winMode=" + WindowConfiguration.windowingModeToString(
-                        taskInfo.getWindowingMode());
+                + " winMode=" + windowingModeToString(taskInfo.getWindowingMode())
+                + " visReq=" + taskInfo.isVisibleRequested
+                + " vis=" + taskInfo.isVisible
+                + " excluded=" + isExcluded
+                + " baseIntent="
+                + (taskInfo.baseIntent != null && taskInfo.baseIntent.getComponent() != null
+                        ? taskInfo.baseIntent.getComponent().flattenToShortString()
+                        : "null");
     }
 
     @Override
     public void writeToParcel(Parcel parcel, int flags) {
         // We don't use the parcel list methods because we want to only write the TaskInfo state
         // and not the subclasses (Recents/RunningTaskInfo) whose fields are all deprecated
-        parcel.writeInt(mTasks.size());
-        for (int i = 0; i < mTasks.size(); i++) {
+        final int tasksSize = mTasks != null ? mTasks.size() : 0;
+        parcel.writeInt(tasksSize);
+        for (int i = 0; i < tasksSize; i++) {
             mTasks.get(i).writeTaskToParcel(parcel, flags);
         }
+        parcel.writeTypedList(mGroupedTasks);
         parcel.writeTypedObject(mSplitBounds, flags);
         parcel.writeInt(mType);
         parcel.writeIntArray(mMinimizedTaskIds);
@@ -293,6 +371,16 @@
         return 0;
     }
 
+    private String typeToString(@GroupType int type) {
+        return switch (type) {
+            case TYPE_FULLSCREEN -> "FULLSCREEN";
+            case TYPE_SPLIT -> "SPLIT";
+            case TYPE_FREEFORM -> "FREEFORM";
+            case TYPE_MIXED -> "MIXED";
+            default -> "UNKNOWN";
+        };
+    }
+
     public static final Creator<GroupedTaskInfo> CREATOR = new Creator() {
         @Override
         public GroupedTaskInfo createFromParcel(Parcel in) {
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
index 755f472..2fed138 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
@@ -233,6 +233,16 @@
     }
 
     /**
+     * Returns whether the multiple desktops feature is enabled for this device (both backend and
+     * frontend implementations).
+     */
+    public static boolean enableMultipleDesktops(@NonNull Context context) {
+        return Flags.enableMultipleDesktopsBackend()
+                && Flags.enableMultipleDesktopsFrontend()
+                && canEnterDesktopMode(context);
+    }
+
+    /**
      * @return {@code true} if this device is requesting to show the app handle despite non
      * necessarily enabling desktop mode
      */
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt
index d15fbed..23498de 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt
@@ -32,9 +32,7 @@
     /** Transitions with source unknown. */
     UNKNOWN;
 
-    override fun describeContents(): Int {
-        return 0
-    }
+    override fun describeContents(): Int = 0
 
     override fun writeToParcel(dest: Parcel, flags: Int) {
         dest.writeString(name)
@@ -44,9 +42,8 @@
         @JvmField
         val CREATOR =
             object : Parcelable.Creator<DesktopModeTransitionSource> {
-                override fun createFromParcel(parcel: Parcel): DesktopModeTransitionSource {
-                    return parcel.readString()?.let { valueOf(it) } ?: UNKNOWN
-                }
+                override fun createFromParcel(parcel: Parcel): DesktopModeTransitionSource =
+                    parcel.readString()?.let { valueOf(it) } ?: UNKNOWN
 
                 override fun newArray(size: Int) = arrayOfNulls<DesktopModeTransitionSource>(size)
             }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java
index 9f01316..b098620 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java
@@ -230,6 +230,11 @@
         return mDisplayAreasInfo.get(displayId);
     }
 
+    @Nullable
+    public SurfaceControl getDisplayAreaLeash(int displayId) {
+        return mLeashes.get(displayId);
+    }
+
     /**
      * Applies the {@link DisplayAreaInfo} to the {@link DisplayAreaContext} specified by
      * {@link DisplayAreaInfo#displayId}.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java
new file mode 100644
index 0000000..5018fdb
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java
@@ -0,0 +1,288 @@
+/*
+ * 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.animation;
+
+import static com.android.wm.shell.transition.DefaultSurfaceAnimator.setupValueAnimator;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.annotation.Nullable;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.view.Choreographer;
+import android.view.SurfaceControl;
+import android.view.View;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.ClipRectAnimation;
+import android.view.animation.ScaleAnimation;
+import android.view.animation.Transformation;
+import android.view.animation.TranslateAnimation;
+
+import java.util.function.Consumer;
+
+/**
+ * Animation implementation for size-changing window container animations. Ported from
+ * {@link com.android.server.wm.WindowChangeAnimationSpec}.
+ * <p>
+ * This animation behaves slightly differently depending on whether the window is growing
+ * or shrinking:
+ * <ul>
+ * <li>If growing, it will do a clip-reveal after quicker fade-out/scale of the smaller (old)
+ * snapshot.
+ * <li>If shrinking, it will do an opposite clip-reveal on the old snapshot followed by a quicker
+ * fade-out of the bigger (old) snapshot while simultaneously shrinking the new window into
+ * place.
+ * </ul>
+ */
+public class SizeChangeAnimation {
+    private final Rect mTmpRect = new Rect();
+    final Transformation mTmpTransform = new Transformation();
+    final Matrix mTmpMatrix = new Matrix();
+    final float[] mTmpFloats = new float[9];
+    final float[] mTmpVecs = new float[4];
+
+    private final Animation mAnimation;
+    private final Animation mSnapshotAnim;
+
+    private final ValueAnimator mAnimator = ValueAnimator.ofFloat(0f, 1f);
+
+    /**
+     * The maximum of stretching applied to any surface during interpolation (since the animation
+     * is a combination of stretching/cropping/fading).
+     */
+    private static final float SCALE_FACTOR = 0.7f;
+
+    /**
+     * Since this animation is made of several sub-animations, we want to pre-arrange the
+     * sub-animations on a "virtual timeline" and then drive the overall progress in lock-step.
+     *
+     * To do this, we have a single value-animator which animates progress from 0-1 with an
+     * arbitrary duration and interpolator. Then we convert the progress to a frame in our virtual
+     * timeline to get the interpolated transforms.
+     *
+     * The APIs for arranging the sub-animations use integral frame numbers, so we need to pick
+     * an integral "duration" for our virtual timeline. That's what this constant specifies. It
+     * is effectively an animation "resolution" since it divides-up the 0-1 interpolation-space.
+     */
+    private static final int ANIMATION_RESOLUTION = 1000;
+
+    public SizeChangeAnimation(Rect startBounds, Rect endBounds) {
+        mAnimation = buildContainerAnimation(startBounds, endBounds);
+        mSnapshotAnim = buildSnapshotAnimation(startBounds, endBounds);
+    }
+
+    /**
+     * Initialize a size-change animation for a container leash.
+     */
+    public void initialize(SurfaceControl leash, SurfaceControl snapshot,
+            SurfaceControl.Transaction startT) {
+        startT.reparent(snapshot, leash);
+        startT.setPosition(snapshot, 0, 0);
+        startT.show(snapshot);
+        startT.show(leash);
+        apply(startT, leash, snapshot, 0.f);
+    }
+
+    /**
+     * Initialize a size-change animation for a view containing the leash surface(s).
+     *
+     * Note that this **will** apply {@param startToApply}!
+     */
+    public void initialize(View view, SurfaceControl leash, SurfaceControl snapshot,
+            SurfaceControl.Transaction startToApply) {
+        startToApply.reparent(snapshot, leash);
+        startToApply.setPosition(snapshot, 0, 0);
+        startToApply.show(snapshot);
+        startToApply.show(leash);
+        apply(view, startToApply, leash, snapshot, 0.f);
+    }
+
+    private ValueAnimator buildAnimatorInner(ValueAnimator.AnimatorUpdateListener updater,
+            SurfaceControl leash, SurfaceControl snapshot, Consumer<Animator> onFinish,
+            SurfaceControl.Transaction transaction, @Nullable View view) {
+        return setupValueAnimator(mAnimator, updater, (anim) -> {
+            transaction.reparent(snapshot, null);
+            if (view != null) {
+                view.setClipBounds(null);
+                view.setAnimationMatrix(null);
+                transaction.setCrop(leash, null);
+            }
+            transaction.apply();
+            transaction.close();
+            onFinish.accept(anim);
+        });
+    }
+
+    /**
+     * Build an animator which works on a pair of surface controls (where the snapshot is assumed
+     * to be a child of the main leash).
+     *
+     * @param onFinish Called when animation finishes. This is called on the anim thread!
+     */
+    public ValueAnimator buildAnimator(SurfaceControl leash, SurfaceControl snapshot,
+            Consumer<Animator> onFinish) {
+        final SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
+        Choreographer choreographer = Choreographer.getInstance();
+        return buildAnimatorInner(animator -> {
+            // The finish callback in buildSurfaceAnimation will ensure that the animation ends
+            // with fraction 1.
+            final float progress = Math.clamp(animator.getAnimatedFraction(), 0.f, 1.f);
+            apply(transaction, leash, snapshot, progress);
+            transaction.setFrameTimelineVsync(choreographer.getVsyncId());
+            transaction.apply();
+        }, leash, snapshot, onFinish, transaction, null /* view */);
+    }
+
+    /**
+     * Build an animator which works on a view that contains a pair of surface controls (where
+     * the snapshot is assumed to be a child of the main leash).
+     *
+     * @param onFinish Called when animation finishes. This is called on the anim thread!
+     */
+    public ValueAnimator buildViewAnimator(View view, SurfaceControl leash,
+            SurfaceControl snapshot, Consumer<Animator> onFinish) {
+        final SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
+        return buildAnimatorInner(animator -> {
+            // The finish callback in buildSurfaceAnimation will ensure that the animation ends
+            // with fraction 1.
+            final float progress = Math.clamp(animator.getAnimatedFraction(), 0.f, 1.f);
+            apply(view, transaction, leash, snapshot, progress);
+        }, leash, snapshot, onFinish, transaction, view);
+    }
+
+    /** Animation for the whole container (snapshot is inside this container). */
+    private static AnimationSet buildContainerAnimation(Rect startBounds, Rect endBounds) {
+        final long duration = ANIMATION_RESOLUTION;
+        boolean growing = endBounds.width() - startBounds.width()
+                + endBounds.height() - startBounds.height() >= 0;
+        long scalePeriod = (long) (duration * SCALE_FACTOR);
+        float startScaleX = SCALE_FACTOR * ((float) startBounds.width()) / endBounds.width()
+                + (1.f - SCALE_FACTOR);
+        float startScaleY = SCALE_FACTOR * ((float) startBounds.height()) / endBounds.height()
+                + (1.f - SCALE_FACTOR);
+        final AnimationSet animSet = new AnimationSet(true);
+
+        final Animation scaleAnim = new ScaleAnimation(startScaleX, 1, startScaleY, 1);
+        scaleAnim.setDuration(scalePeriod);
+        if (!growing) {
+            scaleAnim.setStartOffset(duration - scalePeriod);
+        }
+        animSet.addAnimation(scaleAnim);
+        final Animation translateAnim = new TranslateAnimation(startBounds.left,
+                endBounds.left, startBounds.top, endBounds.top);
+        translateAnim.setDuration(duration);
+        animSet.addAnimation(translateAnim);
+        Rect startClip = new Rect(startBounds);
+        Rect endClip = new Rect(endBounds);
+        startClip.offsetTo(0, 0);
+        endClip.offsetTo(0, 0);
+        final Animation clipAnim = new ClipRectAnimation(startClip, endClip);
+        clipAnim.setDuration(duration);
+        animSet.addAnimation(clipAnim);
+        animSet.initialize(startBounds.width(), startBounds.height(),
+                endBounds.width(), endBounds.height());
+        return animSet;
+    }
+
+    /** The snapshot surface is assumed to be a child of the container surface. */
+    private static AnimationSet buildSnapshotAnimation(Rect startBounds, Rect endBounds) {
+        final long duration = ANIMATION_RESOLUTION;
+        boolean growing = endBounds.width() - startBounds.width()
+                + endBounds.height() - startBounds.height() >= 0;
+        long scalePeriod = (long) (duration * SCALE_FACTOR);
+        float endScaleX = 1.f / (SCALE_FACTOR * ((float) startBounds.width()) / endBounds.width()
+                + (1.f - SCALE_FACTOR));
+        float endScaleY = 1.f / (SCALE_FACTOR * ((float) startBounds.height()) / endBounds.height()
+                + (1.f - SCALE_FACTOR));
+
+        AnimationSet snapAnimSet = new AnimationSet(true);
+        // Animation for the "old-state" snapshot that is atop the task.
+        final Animation snapAlphaAnim = new AlphaAnimation(1.f, 0.f);
+        snapAlphaAnim.setDuration(scalePeriod);
+        if (!growing) {
+            snapAlphaAnim.setStartOffset(duration - scalePeriod);
+        }
+        snapAnimSet.addAnimation(snapAlphaAnim);
+        final Animation snapScaleAnim =
+                new ScaleAnimation(endScaleX, endScaleX, endScaleY, endScaleY);
+        snapScaleAnim.setDuration(duration);
+        snapAnimSet.addAnimation(snapScaleAnim);
+        snapAnimSet.initialize(startBounds.width(), startBounds.height(),
+                endBounds.width(), endBounds.height());
+        return snapAnimSet;
+    }
+
+    private void calcCurrentClipBounds(Rect outClip, Transformation fromTransform) {
+        // The following applies an inverse scale to the clip-rect so that it crops "after" the
+        // scale instead of before.
+        mTmpVecs[1] = mTmpVecs[2] = 0;
+        mTmpVecs[0] = mTmpVecs[3] = 1;
+        fromTransform.getMatrix().mapVectors(mTmpVecs);
+
+        mTmpVecs[0] = 1.f / mTmpVecs[0];
+        mTmpVecs[3] = 1.f / mTmpVecs[3];
+        final Rect clipRect = fromTransform.getClipRect();
+        outClip.left = (int) (clipRect.left * mTmpVecs[0] + 0.5f);
+        outClip.right = (int) (clipRect.right * mTmpVecs[0] + 0.5f);
+        outClip.top = (int) (clipRect.top * mTmpVecs[3] + 0.5f);
+        outClip.bottom = (int) (clipRect.bottom * mTmpVecs[3] + 0.5f);
+    }
+
+    private void apply(SurfaceControl.Transaction t, SurfaceControl leash, SurfaceControl snapshot,
+            float progress) {
+        long currentPlayTime = (long) (((float) ANIMATION_RESOLUTION) * progress);
+        // update thumbnail surface
+        mSnapshotAnim.getTransformation(currentPlayTime, mTmpTransform);
+        t.setMatrix(snapshot, mTmpTransform.getMatrix(), mTmpFloats);
+        t.setAlpha(snapshot, mTmpTransform.getAlpha());
+
+        // update container surface
+        mAnimation.getTransformation(currentPlayTime, mTmpTransform);
+        final Matrix matrix = mTmpTransform.getMatrix();
+        t.setMatrix(leash, matrix, mTmpFloats);
+
+        calcCurrentClipBounds(mTmpRect, mTmpTransform);
+        t.setCrop(leash, mTmpRect);
+    }
+
+    private void apply(View view, SurfaceControl.Transaction tmpT, SurfaceControl leash,
+            SurfaceControl snapshot, float progress) {
+        long currentPlayTime = (long) (((float) ANIMATION_RESOLUTION) * progress);
+        // update thumbnail surface
+        mSnapshotAnim.getTransformation(currentPlayTime, mTmpTransform);
+        tmpT.setMatrix(snapshot, mTmpTransform.getMatrix(), mTmpFloats);
+        tmpT.setAlpha(snapshot, mTmpTransform.getAlpha());
+
+        // update container surface
+        mAnimation.getTransformation(currentPlayTime, mTmpTransform);
+        final Matrix matrix = mTmpTransform.getMatrix();
+        mTmpMatrix.set(matrix);
+        // animationMatrix is applied after getTranslation, so "move" the translate to the end.
+        mTmpMatrix.preTranslate(-view.getTranslationX(), -view.getTranslationY());
+        mTmpMatrix.postTranslate(view.getTranslationX(), view.getTranslationY());
+        view.setAnimationMatrix(mTmpMatrix);
+
+        calcCurrentClipBounds(mTmpRect, mTmpTransform);
+        tmpT.setCrop(leash, mTmpRect);
+        view.setClipBounds(mTmpRect);
+
+        // this takes stuff out of mTmpT so mTmpT can be re-used immediately
+        view.getViewRootImpl().applyTransactionOnDraw(tmpT);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
index 06a55d3..08079d9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
@@ -19,7 +19,6 @@
 package com.android.wm.shell.apptoweb
 
 import android.app.assist.AssistContent
-import android.app.assist.AssistContent.EXTRA_SESSION_TRANSFER_WEB_URI
 import android.content.Context
 import android.content.Intent
 import android.content.Intent.ACTION_VIEW
@@ -113,5 +112,5 @@
  * Returns the web uri from the given [AssistContent].
  */
 fun AssistContent.getSessionWebUri(): Uri? {
-    return extras.getParcelable(EXTRA_SESSION_TRANSFER_WEB_URI) ?: webUri
+    return sessionTransferUri ?: webUri
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt
index 4cc81a9..ec3637a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt
@@ -18,9 +18,11 @@
 
 import android.app.ActivityManager.RunningTaskInfo
 import android.content.Context
+import android.content.pm.PackageManager.NameNotFoundException
 import android.content.pm.verify.domain.DomainVerificationManager
 import android.graphics.Bitmap
 import android.graphics.PixelFormat
+import android.util.Slog
 import android.view.LayoutInflater
 import android.view.SurfaceControl
 import android.view.SurfaceControlViewHost
@@ -160,8 +162,15 @@
     }
 
     private fun setDefaultLinkHandlingSetting() {
-        domainVerificationManager.setDomainVerificationLinkHandlingAllowed(
-            packageName, openInAppButton.isChecked)
+        try {
+            domainVerificationManager.setDomainVerificationLinkHandlingAllowed(
+                packageName, openInAppButton.isChecked)
+        } catch (e: NameNotFoundException) {
+            Slog.e(
+                TAG,
+                "Failed to change link handling policy due to the package name is not found: " + e
+            )
+        }
     }
 
     private fun closeMenu() {
@@ -203,4 +212,8 @@
         /** Called when open by default dialog view has been released. */
         fun onDialogDismissed()
     }
+
+    companion object {
+        private const val TAG = "OpenByDefaultDialog"
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOut.java b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOut.java
new file mode 100644
index 0000000..9451374
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOut.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2025 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.appzoomout;
+
+import com.android.wm.shell.shared.annotations.ExternalThread;
+
+/**
+ * Interface to engage with the app zoom out feature.
+ */
+@ExternalThread
+public interface AppZoomOut {
+
+    /**
+     * Called when the zoom out progress is updated, which is used to scale down the current app
+     * surface from fullscreen to the max pushback level we want to apply. {@param progress} ranges
+     * between [0,1], 0 when fullscreen, 1 when it's at the max pushback level.
+     */
+    void setProgress(float progress);
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java
new file mode 100644
index 0000000..82ef00e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2025 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.appzoomout;
+
+import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.app.ActivityManager;
+import android.app.WindowConfiguration;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.util.Slog;
+import android.window.DisplayAreaInfo;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayChangeController;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.RemoteCallable;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.shared.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
+import com.android.wm.shell.sysui.ShellInit;
+
+/** Class that manages the app zoom out UI and states. */
+public class AppZoomOutController implements RemoteCallable<AppZoomOutController>,
+        ShellTaskOrganizer.FocusListener, DisplayChangeController.OnDisplayChangingListener {
+
+    private static final String TAG = "AppZoomOutController";
+
+    private final Context mContext;
+    private final ShellTaskOrganizer mTaskOrganizer;
+    private final DisplayController mDisplayController;
+    private final AppZoomOutDisplayAreaOrganizer mDisplayAreaOrganizer;
+    private final ShellExecutor mMainExecutor;
+    private final AppZoomOutImpl mImpl = new AppZoomOutImpl();
+
+    private final DisplayController.OnDisplaysChangedListener mDisplaysChangedListener =
+            new DisplayController.OnDisplaysChangedListener() {
+                @Override
+                public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
+                    if (displayId != DEFAULT_DISPLAY) {
+                        return;
+                    }
+                    updateDisplayLayout(displayId);
+                }
+
+                @Override
+                public void onDisplayAdded(int displayId) {
+                    if (displayId != DEFAULT_DISPLAY) {
+                        return;
+                    }
+                    updateDisplayLayout(displayId);
+                }
+            };
+
+
+    public static AppZoomOutController create(Context context, ShellInit shellInit,
+            ShellTaskOrganizer shellTaskOrganizer, DisplayController displayController,
+            DisplayLayout displayLayout, @ShellMainThread ShellExecutor mainExecutor) {
+        AppZoomOutDisplayAreaOrganizer displayAreaOrganizer = new AppZoomOutDisplayAreaOrganizer(
+                context, displayLayout, mainExecutor);
+        return new AppZoomOutController(context, shellInit, shellTaskOrganizer, displayController,
+                displayAreaOrganizer, mainExecutor);
+    }
+
+    @VisibleForTesting
+    AppZoomOutController(Context context, ShellInit shellInit,
+            ShellTaskOrganizer shellTaskOrganizer, DisplayController displayController,
+            AppZoomOutDisplayAreaOrganizer displayAreaOrganizer,
+            @ShellMainThread ShellExecutor mainExecutor) {
+        mContext = context;
+        mTaskOrganizer = shellTaskOrganizer;
+        mDisplayController = displayController;
+        mDisplayAreaOrganizer = displayAreaOrganizer;
+        mMainExecutor = mainExecutor;
+
+        shellInit.addInitCallback(this::onInit, this);
+    }
+
+    private void onInit() {
+        mTaskOrganizer.addFocusListener(this);
+
+        mDisplayController.addDisplayWindowListener(mDisplaysChangedListener);
+        mDisplayController.addDisplayChangingController(this);
+        updateDisplayLayout(mContext.getDisplayId());
+
+        mDisplayAreaOrganizer.registerOrganizer();
+    }
+
+    public AppZoomOut asAppZoomOut() {
+        return mImpl;
+    }
+
+    public void setProgress(float progress) {
+        mDisplayAreaOrganizer.setProgress(progress);
+    }
+
+    void updateDisplayLayout(int displayId) {
+        final DisplayLayout newDisplayLayout = mDisplayController.getDisplayLayout(displayId);
+        if (newDisplayLayout == null) {
+            Slog.w(TAG, "Failed to get new DisplayLayout.");
+            return;
+        }
+        mDisplayAreaOrganizer.setDisplayLayout(newDisplayLayout);
+    }
+
+    @Override
+    public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
+        if (taskInfo == null) {
+            return;
+        }
+        if (taskInfo.getActivityType() == WindowConfiguration.ACTIVITY_TYPE_HOME) {
+            mDisplayAreaOrganizer.setIsHomeTaskFocused(taskInfo.isFocused);
+        }
+    }
+
+    @Override
+    public void onDisplayChange(int displayId, int fromRotation, int toRotation,
+            @Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction wct) {
+        // TODO: verify if there is synchronization issues.
+        if (toRotation != ROTATION_UNDEFINED) {
+            mDisplayAreaOrganizer.onRotateDisplay(mContext, toRotation);
+        }
+    }
+
+    @Override
+    public Context getContext() {
+        return mContext;
+    }
+
+    @Override
+    public ShellExecutor getRemoteCallExecutor() {
+        return mMainExecutor;
+    }
+
+    @ExternalThread
+    private class AppZoomOutImpl implements AppZoomOut {
+        @Override
+        public void setProgress(float progress) {
+            mMainExecutor.execute(() -> AppZoomOutController.this.setProgress(progress));
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutDisplayAreaOrganizer.java
new file mode 100644
index 0000000..1c37461
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutDisplayAreaOrganizer.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2025 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.appzoomout;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.util.ArrayMap;
+import android.view.SurfaceControl;
+import android.window.DisplayAreaAppearedInfo;
+import android.window.DisplayAreaInfo;
+import android.window.DisplayAreaOrganizer;
+import android.window.WindowContainerToken;
+
+import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.wm.shell.common.DisplayLayout;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/** Display area organizer that manages the app zoom out UI and states. */
+public class AppZoomOutDisplayAreaOrganizer extends DisplayAreaOrganizer {
+
+    private static final float PUSHBACK_SCALE_FOR_LAUNCHER = 0.05f;
+    private static final float PUSHBACK_SCALE_FOR_APP = 0.025f;
+    private static final float INVALID_PROGRESS = -1;
+
+    private final DisplayLayout mDisplayLayout = new DisplayLayout();
+    private final Context mContext;
+    private final float mCornerRadius;
+    private final Map<WindowContainerToken, SurfaceControl> mDisplayAreaTokenMap =
+            new ArrayMap<>();
+
+    private float mProgress = INVALID_PROGRESS;
+    // Denote whether the home task is focused, null when it's not yet initialized.
+    @Nullable private Boolean mIsHomeTaskFocused;
+
+    public AppZoomOutDisplayAreaOrganizer(Context context,
+            DisplayLayout displayLayout, Executor mainExecutor) {
+        super(mainExecutor);
+        mContext = context;
+        mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext);
+        setDisplayLayout(displayLayout);
+    }
+
+    @Override
+    public void onDisplayAreaAppeared(DisplayAreaInfo displayAreaInfo, SurfaceControl leash) {
+        leash.setUnreleasedWarningCallSite(
+                "AppZoomOutDisplayAreaOrganizer.onDisplayAreaAppeared");
+        mDisplayAreaTokenMap.put(displayAreaInfo.token, leash);
+    }
+
+    @Override
+    public void onDisplayAreaVanished(DisplayAreaInfo displayAreaInfo) {
+        final SurfaceControl leash = mDisplayAreaTokenMap.get(displayAreaInfo.token);
+        if (leash != null) {
+            leash.release();
+        }
+        mDisplayAreaTokenMap.remove(displayAreaInfo.token);
+    }
+
+    public void registerOrganizer() {
+        final List<DisplayAreaAppearedInfo> displayAreaInfos = registerOrganizer(
+                AppZoomOutDisplayAreaOrganizer.FEATURE_APP_ZOOM_OUT);
+        for (int i = 0; i < displayAreaInfos.size(); i++) {
+            final DisplayAreaAppearedInfo info = displayAreaInfos.get(i);
+            onDisplayAreaAppeared(info.getDisplayAreaInfo(), info.getLeash());
+        }
+    }
+
+    @Override
+    public void unregisterOrganizer() {
+        super.unregisterOrganizer();
+        reset();
+    }
+
+    void setProgress(float progress) {
+        if (mProgress == progress) {
+            return;
+        }
+
+        mProgress = progress;
+        apply();
+    }
+
+    void setIsHomeTaskFocused(boolean isHomeTaskFocused) {
+        if (mIsHomeTaskFocused != null && mIsHomeTaskFocused == isHomeTaskFocused) {
+            return;
+        }
+
+        mIsHomeTaskFocused = isHomeTaskFocused;
+        apply();
+    }
+
+    private void apply() {
+        if (mIsHomeTaskFocused == null || mProgress == INVALID_PROGRESS) {
+            return;
+        }
+
+        SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+        float scale = mProgress * (mIsHomeTaskFocused
+                ? PUSHBACK_SCALE_FOR_LAUNCHER : PUSHBACK_SCALE_FOR_APP);
+        mDisplayAreaTokenMap.forEach((token, leash) -> updateSurface(tx, leash, scale));
+        tx.apply();
+    }
+
+    void setDisplayLayout(DisplayLayout displayLayout) {
+        mDisplayLayout.set(displayLayout);
+    }
+
+    private void reset() {
+        setProgress(0);
+        mProgress = INVALID_PROGRESS;
+        mIsHomeTaskFocused = null;
+    }
+
+    private void updateSurface(SurfaceControl.Transaction tx, SurfaceControl leash, float scale) {
+        if (scale == 0) {
+            // Reset when scale is set back to 0.
+            tx
+                    .setCrop(leash, null)
+                    .setScale(leash, 1, 1)
+                    .setPosition(leash, 0, 0)
+                    .setCornerRadius(leash, 0);
+            return;
+        }
+
+        tx
+                // Rounded corner can only be applied if a crop is set.
+                .setCrop(leash, 0, 0, mDisplayLayout.width(), mDisplayLayout.height())
+                .setScale(leash, 1 - scale, 1 - scale)
+                .setPosition(leash, scale * mDisplayLayout.width() * 0.5f,
+                        scale * mDisplayLayout.height() * 0.5f)
+                .setCornerRadius(leash, mCornerRadius * (1 - scale));
+    }
+
+    void onRotateDisplay(Context context, int toRotation) {
+        if (mDisplayLayout.rotation() == toRotation) {
+            return;
+        }
+        mDisplayLayout.rotateTo(context.getResources(), toRotation);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoShellModule.java
new file mode 100644
index 0000000..fc51c75
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoShellModule.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.automotive;
+
+import com.android.wm.shell.dagger.WMSingleton;
+
+import dagger.Binds;
+import dagger.Module;
+
+
+@Module
+public abstract class AutoShellModule {
+    @WMSingleton
+    @Binds
+    abstract AutoTaskStackController provideTaskStackController(AutoTaskStackControllerImpl impl);
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStack.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStack.kt
new file mode 100644
index 0000000..caacdd3
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStack.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.automotive
+
+import android.app.ActivityManager
+import android.graphics.Rect
+import android.view.SurfaceControl
+
+/**
+ * Represents an auto task stack, which is always in multi-window mode.
+ *
+ * @property id The ID of the task stack.
+ * @property displayId The ID of the display the task stack is on.
+ * @property leash The surface control leash of the task stack.
+ */
+interface AutoTaskStack {
+    val id: Int
+    val displayId: Int
+    var leash: SurfaceControl
+}
+
+/**
+ * Data class representing the state of an auto task stack.
+ *
+ * @property bounds The bounds of the task stack.
+ * @property childrenTasksVisible Whether the child tasks of the stack are visible.
+ * @property layer The layer of the task stack.
+ */
+data class AutoTaskStackState(
+    val bounds: Rect = Rect(),
+    val childrenTasksVisible: Boolean,
+    val layer: Int
+)
+
+/**
+ * Data class representing a root task stack.
+ *
+ * @property id The ID of the root task stack
+ * @property displayId The ID of the display the root task stack is on.
+ * @property leash The surface control leash of the root task stack.
+ * @property rootTaskInfo The running task info of the root task.
+ */
+data class RootTaskStack(
+    override val id: Int,
+    override val displayId: Int,
+    override var leash: SurfaceControl,
+    var rootTaskInfo: ActivityManager.RunningTaskInfo
+) : AutoTaskStack
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStackController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStackController.kt
new file mode 100644
index 0000000..15fedac
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStackController.kt
@@ -0,0 +1,229 @@
+/*
+ * 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.automotive
+
+import android.app.PendingIntent
+import android.content.Intent
+import android.os.Bundle
+import android.os.IBinder
+import android.view.SurfaceControl
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import com.android.wm.shell.shared.annotations.ShellMainThread
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TransitionFinishCallback
+
+/**
+ * Delegate interface for handling auto task stack transitions.
+ */
+interface AutoTaskStackTransitionHandlerDelegate {
+    /**
+     * Handles a transition request.
+     *
+     * @param transition The transition identifier.
+     * @param request The transition request information.
+     * @return An [AutoTaskStackTransaction] to be applied for the transition, or null if the
+     *         animation is not handled by this delegate.
+     */
+    fun handleRequest(
+        transition: IBinder, request: TransitionRequestInfo
+    ): AutoTaskStackTransaction?
+
+    /**
+     * See [Transitions.TransitionHandler.startAnimation] for more details.
+     *
+     * @param changedTaskStacks Contains the states of the task stacks that were changed as a
+     * result of this transition. The key is the [AutoTaskStack.id] and the value is the
+     * corresponding [AutoTaskStackState].
+     */
+    fun startAnimation(
+        transition: IBinder,
+        changedTaskStacks: Map<Int, AutoTaskStackState>,
+        info: TransitionInfo,
+        startTransaction: SurfaceControl.Transaction,
+        finishTransaction: SurfaceControl.Transaction,
+        finishCallback: TransitionFinishCallback
+    ): Boolean
+
+    /**
+     * See [Transitions.TransitionHandler.onTransitionConsumed] for more details.
+     *
+     * @param requestedTaskStacks contains the states of the task stacks that were requested in
+     * the transition. The key is the [AutoTaskStack.id] and the value is the corresponding
+     * [AutoTaskStackState].
+     */
+    fun onTransitionConsumed(
+        transition: IBinder,
+        requestedTaskStacks: Map<Int, AutoTaskStackState>,
+        aborted: Boolean, finishTransaction: SurfaceControl.Transaction?
+    )
+
+    /**
+     * See [Transitions.TransitionHandler.mergeAnimation] for more details.
+     *
+     * @param changedTaskStacks Contains the states of the task stacks that were changed as a
+     * result of this transition. The key is the [AutoTaskStack.id] and the value is the
+     * corresponding [AutoTaskStackState].
+     */
+    fun mergeAnimation(
+        transition: IBinder,
+        changedTaskStacks: Map<Int, AutoTaskStackState>,
+        info: TransitionInfo,
+        surfaceTransaction: SurfaceControl.Transaction,
+        mergeTarget: IBinder,
+        finishCallback: TransitionFinishCallback
+    )
+}
+
+
+/**
+ * Controller for managing auto task stacks.
+ */
+interface AutoTaskStackController {
+
+    var autoTransitionHandlerDelegate: AutoTaskStackTransitionHandlerDelegate?
+        set
+
+    /**
+     * Map of task stack IDs to their states.
+     *
+     * This gets updated right before [AutoTaskStackTransitionHandlerDelegate.startAnimation] or
+     * [AutoTaskStackTransitionHandlerDelegate.onTransitionConsumed] is called.
+     */
+    val taskStackStateMap: Map<Int, AutoTaskStackState>
+        get
+
+    /**
+     * Creates a new multi-window root task.
+     *
+     * A root task stack is placed in the default TDA of the specified display by default.
+     * Once the root task is removed, the [AutoTaskStackController] no longer holds a reference to
+     * it.
+     *
+     * @param displayId The ID of the display to create the root task stack on.
+     * @param listener The listener for root task stack events.
+     */
+    @ShellMainThread
+    fun createRootTaskStack(displayId: Int, listener: RootTaskStackListener)
+
+
+    /**
+     * Sets the default root task stack (launch root) on a display. Calling it again with a
+     * different [rootTaskStackId] will simply replace the default root task stack on the display.
+     *
+     * Note: This is helpful for passively routing tasks to a specified container. If a display
+     * doesn't have a default root task stack set, all tasks will open in fullscreen and cover
+     * the entire default TDA by default.
+     *
+     * @param displayId The ID of the display.
+     * @param rootTaskStackId The ID of the root task stack, or null to clear the default.
+     */
+    @ShellMainThread
+    fun setDefaultRootTaskStackOnDisplay(displayId: Int, rootTaskStackId: Int?)
+
+    /**
+     * Starts a transaction with the specified [transaction].
+     * Returns the transition identifier.
+     */
+    @ShellMainThread
+    fun startTransition(transaction: AutoTaskStackTransaction): IBinder?
+}
+
+internal sealed class TaskStackOperation {
+    data class ReparentTask(
+        val taskId: Int,
+        val parentTaskStackId: Int,
+        val onTop: Boolean
+    ) : TaskStackOperation()
+
+    data class SendPendingIntent(
+        val sender: PendingIntent,
+        val intent: Intent,
+        val options: Bundle?
+    ) : TaskStackOperation()
+
+    data class SetTaskStackState(
+        val taskStackId: Int,
+        val state: AutoTaskStackState
+    ) : TaskStackOperation()
+}
+
+data class AutoTaskStackTransaction internal constructor(
+    internal val operations: MutableList<TaskStackOperation> = mutableListOf()
+) {
+    constructor() : this(
+        mutableListOf()
+    )
+
+    /** See [WindowContainerTransaction.reparent] for more details. */
+    fun reparentTask(
+        taskId: Int,
+        parentTaskStackId: Int,
+        onTop: Boolean
+    ): AutoTaskStackTransaction {
+        operations.add(TaskStackOperation.ReparentTask(taskId, parentTaskStackId, onTop))
+        return this
+    }
+
+    /** See [WindowContainerTransaction.sendPendingIntent] for more details. */
+    fun sendPendingIntent(
+        sender: PendingIntent,
+        intent: Intent,
+        options: Bundle?
+    ): AutoTaskStackTransaction {
+        operations.add(TaskStackOperation.SendPendingIntent(sender, intent, options))
+        return this
+    }
+
+    /**
+     * Adds a set task stack state operation to the transaction.
+     *
+     * If an operation with the same task stack ID already exists, it is replaced with the new one.
+     *
+     * @param taskStackId The ID of the task stack.
+     * @param state The new state of the task stack.
+     * @return The transaction with the added operation.
+     */
+    fun setTaskStackState(taskStackId: Int, state: AutoTaskStackState): AutoTaskStackTransaction {
+        val existingOperation = operations.find {
+            it is TaskStackOperation.SetTaskStackState && it.taskStackId == taskStackId
+        }
+        if (existingOperation != null) {
+            val index = operations.indexOf(existingOperation)
+            operations[index] = TaskStackOperation.SetTaskStackState(taskStackId, state)
+        } else {
+            operations.add(TaskStackOperation.SetTaskStackState(taskStackId, state))
+        }
+        return this
+    }
+
+    /**
+     * Returns a map of task stack IDs to their states from the set task stack state operations.
+     *
+     * @return The map of task stack IDs to states.
+     */
+    fun getTaskStackStates(): Map<Int, AutoTaskStackState> {
+        val states = mutableMapOf<Int, AutoTaskStackState>()
+        operations.forEach { operation ->
+            if (operation is TaskStackOperation.SetTaskStackState) {
+                states[operation.taskStackId] = operation.state
+            }
+        }
+        return states
+    }
+}
+
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStackControllerImpl.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStackControllerImpl.kt
new file mode 100644
index 0000000..f8f2842
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStackControllerImpl.kt
@@ -0,0 +1,534 @@
+/*
+ * 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.automotive
+
+import android.app.ActivityManager
+import android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT
+import android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
+import android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED
+import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
+import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
+import android.os.IBinder
+import android.util.Log
+import android.util.Slog
+import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
+import android.view.WindowManager
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import android.window.WindowContainerTransaction
+import com.android.systemui.car.Flags.autoTaskStackWindowing
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.dagger.WMSingleton
+import com.android.wm.shell.shared.TransitionUtil
+import com.android.wm.shell.shared.annotations.ShellMainThread
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TransitionFinishCallback
+import javax.inject.Inject
+
+const val TAG = "AutoTaskStackController"
+
+@WMSingleton
+class AutoTaskStackControllerImpl @Inject constructor(
+    val taskOrganizer: ShellTaskOrganizer,
+    @ShellMainThread private val shellMainThread: ShellExecutor,
+    val transitions: Transitions,
+    val shellInit: ShellInit,
+    val rootTdaOrganizer: RootTaskDisplayAreaOrganizer
+) : AutoTaskStackController, Transitions.TransitionHandler {
+    override var autoTransitionHandlerDelegate: AutoTaskStackTransitionHandlerDelegate? = null
+    override val taskStackStateMap = mutableMapOf<Int, AutoTaskStackState>()
+
+    private val DBG = Log.isLoggable(TAG, Log.DEBUG)
+    private val taskStackMap = mutableMapOf<Int, AutoTaskStack>()
+    private val pendingTransitions = ArrayList<PendingTransition>()
+    private val mTaskStackStateTranslator = TaskStackStateTranslator()
+    private val appTasksMap = mutableMapOf<Int, ActivityManager.RunningTaskInfo>()
+    private val defaultRootTaskPerDisplay = mutableMapOf<Int, Int>()
+
+    init {
+        if (!autoTaskStackWindowing()) {
+            throw IllegalStateException("Failed to initialize" +
+                    "AutoTaskStackController as the auto_task_stack_windowing TS flag is disabled.")
+        } else {
+            shellInit.addInitCallback(this::onInit, this);
+        }
+    }
+
+    fun onInit() {
+        transitions.addHandler(this)
+    }
+
+    /** Translates the [AutoTaskStackState] to relevant WM and surface transactions. */
+    inner class TaskStackStateTranslator {
+        // TODO(b/384946072): Move to an interface with 2 implementations, one for root task and
+        //  other for TDA
+        fun applyVisibilityAndBounds(
+            wct: WindowContainerTransaction,
+            taskStack: AutoTaskStack,
+            state: AutoTaskStackState
+        ) {
+            if (taskStack !is RootTaskStack) {
+                Slog.e(TAG, "Unsupported task stack, unable to convertToWct")
+                return
+            }
+            wct.setBounds(taskStack.rootTaskInfo.token, state.bounds)
+            wct.reorder(taskStack.rootTaskInfo.token, /* onTop= */ state.childrenTasksVisible)
+        }
+
+        fun reorderLeash(
+            taskStack: AutoTaskStack,
+            state: AutoTaskStackState,
+            transaction: Transaction
+        ) {
+            if (taskStack !is RootTaskStack) {
+                Slog.e(TAG, "Unsupported task stack, unable to reorder leash")
+                return
+            }
+            Slog.d(TAG, "Setting the layer ${state.layer}")
+            transaction.setLayer(taskStack.leash, state.layer)
+        }
+
+        fun restoreLeash(taskStack: AutoTaskStack, transaction: Transaction) {
+            if (taskStack !is RootTaskStack) {
+                Slog.e(TAG, "Unsupported task stack, unable to restore leash")
+                return
+            }
+
+            val rootTdaInfo = rootTdaOrganizer.getDisplayAreaInfo(taskStack.displayId)
+            if (rootTdaInfo == null ||
+                rootTdaInfo.featureId != taskStack.rootTaskInfo.displayAreaFeatureId
+            ) {
+                Slog.e(TAG, "Cannot find the rootTDA for the root task stack ${taskStack.id}")
+                return
+            }
+            if (DBG) {
+                Slog.d(TAG, "Reparenting ${taskStack.id} leash to DA ${rootTdaInfo.featureId}")
+            }
+            transaction.reparent(
+                taskStack.leash,
+                rootTdaOrganizer.getDisplayAreaLeash(taskStack.displayId)
+            )
+        }
+    }
+
+    inner class RootTaskStackListenerAdapter(
+        val rootTaskStackListener: RootTaskStackListener,
+    ) : ShellTaskOrganizer.TaskListener {
+        private var rootTaskStack: RootTaskStack? = null
+
+        // TODO(b/384948029): Notify car service for all the children tasks' events
+        override fun onTaskAppeared(
+            taskInfo: ActivityManager.RunningTaskInfo?,
+            leash: SurfaceControl?
+        ) {
+            if (taskInfo == null) {
+                throw IllegalArgumentException("taskInfo can't be null in onTaskAppeared")
+            }
+            if (leash == null) {
+                throw IllegalArgumentException("leash can't be null in onTaskAppeared")
+            }
+            if (DBG) Slog.d(TAG, "onTaskAppeared = ${taskInfo.taskId}")
+
+            if (rootTaskStack == null) {
+                val rootTask =
+                    RootTaskStack(taskInfo.taskId, taskInfo.displayId, leash, taskInfo)
+                taskStackMap[rootTask.id] = rootTask
+
+                rootTaskStack = rootTask;
+                rootTaskStackListener.onRootTaskStackCreated(rootTask);
+                return
+            }
+            appTasksMap[taskInfo.taskId] = taskInfo
+            rootTaskStackListener.onTaskAppeared(taskInfo, leash)
+        }
+
+        override fun onTaskInfoChanged(taskInfo: ActivityManager.RunningTaskInfo?) {
+            if (taskInfo == null) {
+                throw IllegalArgumentException("taskInfo can't be null in onTaskInfoChanged")
+            }
+            if (DBG) Slog.d(TAG, "onTaskInfoChanged = ${taskInfo.taskId}")
+            var previousRootTaskStackInfo = rootTaskStack ?: run {
+                Slog.e(TAG, "Received onTaskInfoChanged, when root task stack is null")
+                return@onTaskInfoChanged
+            }
+            rootTaskStack?.let {
+                if (taskInfo.taskId == previousRootTaskStackInfo.id) {
+                    previousRootTaskStackInfo = previousRootTaskStackInfo.copy(rootTaskInfo = taskInfo)
+                    taskStackMap[previousRootTaskStackInfo.id] = previousRootTaskStackInfo
+                    rootTaskStack = previousRootTaskStackInfo;
+                    rootTaskStackListener.onRootTaskStackInfoChanged(it)
+                    return
+                }
+            }
+
+            appTasksMap[taskInfo.taskId] = taskInfo
+            rootTaskStackListener.onTaskInfoChanged(taskInfo)
+        }
+
+        override fun onTaskVanished(taskInfo: ActivityManager.RunningTaskInfo?) {
+            if (taskInfo == null) {
+                throw IllegalArgumentException("taskInfo can't be null in onTaskVanished")
+            }
+            if (DBG) Slog.d(TAG, "onTaskVanished  = ${taskInfo.taskId}")
+            var rootTask = rootTaskStack ?: run {
+                Slog.e(TAG, "Received onTaskVanished, when root task stack is null")
+                return@onTaskVanished
+            }
+            if (taskInfo.taskId == rootTask.id) {
+                rootTask = rootTask.copy(rootTaskInfo = taskInfo)
+                rootTaskStack = rootTask
+                rootTaskStackListener.onRootTaskStackDestroyed(rootTask)
+                taskStackMap.remove(rootTask.id)
+                taskStackStateMap.remove(rootTask.id)
+                rootTaskStack = null
+                return
+            }
+            appTasksMap.remove(taskInfo.taskId)
+            rootTaskStackListener.onTaskVanished(taskInfo)
+        }
+
+        override fun onBackPressedOnTaskRoot(taskInfo: ActivityManager.RunningTaskInfo?) {
+            if (taskInfo == null) {
+                throw IllegalArgumentException("taskInfo can't be null in onBackPressedOnTaskRoot")
+            }
+            super.onBackPressedOnTaskRoot(taskInfo)
+            rootTaskStackListener.onBackPressedOnTaskRoot(taskInfo)
+        }
+    }
+
+    override fun createRootTaskStack(
+        displayId: Int,
+        listener: RootTaskStackListener
+    ) {
+        if (!autoTaskStackWindowing()) {
+            Slog.e(
+                TAG, "Failed to create root task stack as the " +
+                        "auto_task_stack_windowing TS flag is disabled."
+            )
+            return
+        }
+        taskOrganizer.createRootTask(
+            displayId,
+            WINDOWING_MODE_MULTI_WINDOW,
+            RootTaskStackListenerAdapter(listener),
+            /* removeWithTaskOrganizer= */ true
+        )
+    }
+
+    override fun setDefaultRootTaskStackOnDisplay(displayId: Int, rootTaskStackId: Int?) {
+        if (!autoTaskStackWindowing()) {
+            Slog.e(
+                TAG, "Failed to set default root task stack as the " +
+                        "auto_task_stack_windowing TS flag is disabled."
+            )
+            return
+        }
+        var wct = WindowContainerTransaction()
+
+        // Clear the default root task stack if already set
+        defaultRootTaskPerDisplay[displayId]?.let { existingDefaultRootTaskStackId ->
+            (taskStackMap[existingDefaultRootTaskStackId] as? RootTaskStack)?.let { rootTaskStack ->
+                wct.setLaunchRoot(rootTaskStack.rootTaskInfo.token, null, null)
+            }
+        }
+
+        if (rootTaskStackId != null) {
+            var taskStack =
+                taskStackMap[rootTaskStackId] ?: run { return@setDefaultRootTaskStackOnDisplay }
+            if (DBG) Slog.d(TAG, "setting launch root for  = ${taskStack.id}")
+            if (taskStack !is RootTaskStack) {
+                throw IllegalArgumentException(
+                    "Cannot set a non root task stack as default root task " +
+                            "stack"
+                )
+            }
+            wct.setLaunchRoot(
+                taskStack.rootTaskInfo.token,
+                intArrayOf(WINDOWING_MODE_UNDEFINED),
+                intArrayOf(
+                    ACTIVITY_TYPE_STANDARD, ACTIVITY_TYPE_UNDEFINED, ACTIVITY_TYPE_RECENTS,
+
+                    // TODO(b/386242708): Figure out if this flag will ever be used for automotive
+                    //  assistant. Based on output, remove it from here and fix the
+                    //  AssistantStackTests accordingly.
+                    ACTIVITY_TYPE_ASSISTANT
+                )
+            )
+        }
+
+        taskOrganizer.applyTransaction(wct)
+    }
+
+    override fun startTransition(transaction: AutoTaskStackTransaction): IBinder? {
+        if (!autoTaskStackWindowing()) {
+            Slog.e(
+                TAG, "Failed to start transaction as the " +
+                        "auto_task_stack_windowing TS flag is disabled."
+            )
+            return null
+        }
+        if (transaction.operations.isEmpty()) {
+            Slog.e(TAG, "Operations empty, no transaction started")
+            return null
+        }
+        if (DBG) Slog.d(TAG, "startTransaction ${transaction.operations}")
+
+        var wct = WindowContainerTransaction()
+        convertToWct(transaction, wct)
+        var pending = PendingTransition(
+            TRANSIT_CHANGE,
+            wct,
+            transaction,
+        )
+        return startTransitionNow(pending)
+    }
+
+    override fun handleRequest(
+        transition: IBinder,
+        request: TransitionRequestInfo
+    ): WindowContainerTransaction? {
+        if (DBG) {
+            Slog.d(TAG, "handle request, id=${request.debugId}, type=${request.type}, " +
+                    "triggertask = ${request.triggerTask ?: "null"}")
+        }
+        val ast = autoTransitionHandlerDelegate?.handleRequest(transition, request)
+            ?: run { return@handleRequest null }
+
+        if (ast.operations.isEmpty()) {
+            return null
+        }
+        var wct = WindowContainerTransaction()
+        convertToWct(ast, wct)
+
+        pendingTransitions.add(
+            PendingTransition(request.type, wct, ast).apply { isClaimed = transition }
+        )
+        return wct
+    }
+
+    fun updateTaskStackStates(taskStatStates: Map<Int, AutoTaskStackState>) {
+        taskStackStateMap.putAll(taskStatStates)
+    }
+
+    override fun startAnimation(
+        transition: IBinder,
+        info: TransitionInfo,
+        startTransaction: Transaction,
+        finishTransaction: Transaction,
+        finishCallback: TransitionFinishCallback
+    ): Boolean {
+        if (DBG) Slog.d(TAG, "  startAnimation, id=${info.debugId} = changes=" + info.changes)
+        val pending: PendingTransition? = findPending(transition)
+        if (pending != null) {
+            pendingTransitions.remove(pending)
+            updateTaskStackStates(pending.transaction.getTaskStackStates())
+        }
+
+        reorderLeashes(startTransaction)
+        reorderLeashes(finishTransaction)
+
+        for (chg in info.changes) {
+            // TODO(b/384946072): handle the da stack similarly. The below implementation only
+            // handles the root task stack
+
+            val taskInfo = chg.taskInfo ?: continue
+            val taskStack = taskStackMap[taskInfo.taskId] ?: continue
+
+            // Restore the leashes for the task stacks to ensure correct z-order competition
+            if (taskStackMap.containsKey(taskInfo.taskId)) {
+                mTaskStackStateTranslator.restoreLeash(
+                    taskStack,
+                    startTransaction
+                )
+                if (TransitionUtil.isOpeningMode(chg.mode)) {
+                    // Clients can still manipulate the alpha, but this ensures that the default
+                    // behavior is natural
+                    startTransaction.setAlpha(chg.leash, 1f)
+                }
+                continue
+            }
+        }
+
+        val isPlayedByDelegate = autoTransitionHandlerDelegate?.startAnimation(
+            transition,
+            pending?.transaction?.getTaskStackStates() ?: mapOf(),
+            info,
+            startTransaction,
+            finishTransaction,
+            {
+                shellMainThread.execute {
+                    finishCallback.onTransitionFinished(it)
+                    startNextTransition()
+                }
+            }
+        ) ?: false
+
+        if (isPlayedByDelegate) {
+            if (DBG) Slog.d(TAG, "${info.debugId} played");
+            return true;
+        }
+
+        // If for an animation which is not played by the delegate, contains a change in a known
+        // task stack, it should be leveraged to correct the leashes. So, handle the animation in
+        // this case.
+        if (info.changes.any { taskStackMap.containsKey(it.taskInfo?.taskId) }) {
+            startTransaction.apply()
+            finishCallback.onTransitionFinished(null)
+            startNextTransition()
+            if (DBG) Slog.d(TAG, "${info.debugId} played");
+            return true
+        }
+        return false;
+    }
+
+    fun convertToWct(ast: AutoTaskStackTransaction, wct: WindowContainerTransaction) {
+        ast.operations.forEach { operation ->
+            when (operation) {
+                is TaskStackOperation.ReparentTask -> {
+                    val appTask = appTasksMap[operation.taskId]
+
+                    if (appTask == null) {
+                        Slog.e(
+                            TAG, "task with id=$operation.taskId not found, failed to " +
+                                    "reparent."
+                        )
+                        return@forEach
+                    }
+                    if (!taskStackMap.containsKey(operation.parentTaskStackId)) {
+                        Slog.e(
+                            TAG, "task stack with id=${operation.parentTaskStackId} not " +
+                                    "found, failed to reparent"
+                        )
+                        return@forEach
+                    }
+                    // TODO(b/384946072): Handle a display area stack as well
+                    wct.reparent(
+                        appTask.token,
+                        (taskStackMap[operation.parentTaskStackId] as RootTaskStack)
+                            .rootTaskInfo.token,
+                        operation.onTop
+                    )
+                }
+
+                is TaskStackOperation.SendPendingIntent -> wct.sendPendingIntent(
+                    operation.sender,
+                    operation.intent,
+                    operation.options
+                )
+
+                is TaskStackOperation.SetTaskStackState -> {
+                    taskStackMap[operation.taskStackId]?.let { taskStack ->
+                        mTaskStackStateTranslator.applyVisibilityAndBounds(
+                            wct,
+                            taskStack,
+                            operation.state
+                        )
+                    }
+                        ?: Slog.w(TAG, "AutoTaskStack with id ${operation.taskStackId} " +
+                                "not found.")
+                }
+            }
+        }
+    }
+
+    override fun mergeAnimation(
+        transition: IBinder,
+        info: TransitionInfo,
+        surfaceTransaction: Transaction,
+        mergeTarget: IBinder,
+        finishCallback: TransitionFinishCallback
+    ) {
+        val pending: PendingTransition? = findPending(transition)
+
+        autoTransitionHandlerDelegate?.mergeAnimation(
+            transition,
+            pending?.transaction?.getTaskStackStates() ?: mapOf(),
+            info,
+            surfaceTransaction,
+            mergeTarget,
+            /* finishCallback = */ {
+                shellMainThread.execute {
+                    finishCallback.onTransitionFinished(it)
+                }
+            }
+        )
+    }
+
+    override fun onTransitionConsumed(
+        transition: IBinder,
+        aborted: Boolean,
+        finishTransaction: Transaction?
+    ) {
+        val pending: PendingTransition? = findPending(transition)
+        if (pending != null) {
+            pendingTransitions.remove(pending)
+            updateTaskStackStates(pending.transaction.getTaskStackStates())
+            // Still update the surface order because this means wm didn't lead to any change
+            if (finishTransaction != null) {
+                reorderLeashes(finishTransaction)
+            }
+        }
+        autoTransitionHandlerDelegate?.onTransitionConsumed(
+            transition,
+            pending?.transaction?.getTaskStackStates() ?: mapOf(),
+            aborted,
+            finishTransaction
+        )
+    }
+
+    private fun reorderLeashes(transaction: SurfaceControl.Transaction) {
+        taskStackStateMap.forEach { (taskId, taskStackState) ->
+            taskStackMap[taskId]?.let { taskStack ->
+                mTaskStackStateTranslator.reorderLeash(taskStack, taskStackState, transaction)
+            } ?: Slog.w(TAG, "Warning: AutoTaskStack with id $taskId not found.")
+        }
+    }
+
+    private fun findPending(claimed: IBinder) = pendingTransitions.find { it.isClaimed == claimed }
+
+    private fun startTransitionNow(pending: PendingTransition): IBinder {
+        val claimedTransition = transitions.startTransition(pending.mType, pending.wct, this)
+        pending.isClaimed = claimedTransition
+        pendingTransitions.add(pending)
+        return claimedTransition
+    }
+
+    fun startNextTransition() {
+        if (pendingTransitions.isEmpty()) return
+        val pending: PendingTransition = pendingTransitions[0]
+        if (pending.isClaimed != null) {
+            // Wait for this to start animating.
+            return
+        }
+        pending.isClaimed = transitions.startTransition(pending.mType, pending.wct, this)
+    }
+
+    internal class PendingTransition(
+        @field:WindowManager.TransitionType @param:WindowManager.TransitionType val mType: Int,
+        val wct: WindowContainerTransaction,
+        val transaction: AutoTaskStackTransaction,
+    ) {
+        var isClaimed: IBinder? = null
+    }
+
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/RootTaskStackListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/RootTaskStackListener.kt
new file mode 100644
index 0000000..9d121b1
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/RootTaskStackListener.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.automotive
+
+import com.android.wm.shell.ShellTaskOrganizer
+
+/**
+ * A [TaskListener] which simplifies the interface when used for
+ * [ShellTaskOrganizer.createRootTask].
+ *
+ * [onRootTaskStackCreated], [onRootTaskStackInfoChanged], [onRootTaskStackDestroyed] will be called
+ * for the underlying root task.
+ * The [onTaskAppeared], [onTaskInfoChanged], [onTaskVanished] are called for the children tasks.
+ */
+interface RootTaskStackListener : ShellTaskOrganizer.TaskListener {
+    fun onRootTaskStackCreated(rootTaskStack: RootTaskStack)
+    fun onRootTaskStackInfoChanged(rootTaskStack: RootTaskStack)
+    fun onRootTaskStackDestroyed(rootTaskStack: RootTaskStack)
+}
\ No newline at end of file
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 e96bc02..8dabd54 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
@@ -28,7 +28,6 @@
 import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER;
 
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_HOME;
-import static com.android.window.flags.Flags.predictiveBackSystemAnims;
 import static com.android.window.flags.Flags.unifyBackNavigationTransition;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
 
@@ -40,23 +39,17 @@
 import android.app.IActivityTaskManager;
 import android.app.TaskInfo;
 import android.content.ComponentName;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.Configuration;
-import android.database.ContentObserver;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.hardware.input.InputManager;
-import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteCallback;
 import android.os.RemoteException;
 import android.os.SystemClock;
-import android.os.SystemProperties;
-import android.os.UserHandle;
-import android.provider.Settings.Global;
 import android.util.Log;
 import android.view.IRemoteAnimationRunner;
 import android.view.InputDevice;
@@ -92,7 +85,6 @@
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.shared.TransitionUtil;
-import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
 import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.sysui.ConfigurationChangeListener;
 import com.android.wm.shell.sysui.ShellCommandHandler;
@@ -102,7 +94,6 @@
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Predicate;
 
 /**
@@ -111,14 +102,7 @@
 public class BackAnimationController implements RemoteCallable<BackAnimationController>,
         ConfigurationChangeListener {
     private static final String TAG = "ShellBackPreview";
-    private static final int SETTING_VALUE_OFF = 0;
-    private static final int SETTING_VALUE_ON = 1;
-    public static final boolean IS_ENABLED =
-            SystemProperties.getInt("persist.wm.debug.predictive_back",
-                    SETTING_VALUE_ON) == SETTING_VALUE_ON;
 
-    /** Predictive back animation developer option */
-    private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false);
     /**
      * Max duration to wait for an animation to finish before triggering the real back.
      */
@@ -148,11 +132,9 @@
     private boolean mReceivedNullNavigationInfo = false;
     private final IActivityTaskManager mActivityTaskManager;
     private final Context mContext;
-    private final ContentResolver mContentResolver;
     private final ShellController mShellController;
     private final ShellCommandHandler mShellCommandHandler;
     private final ShellExecutor mShellExecutor;
-    private final Handler mBgHandler;
     private final WindowManager mWindowManager;
     private final Transitions mTransitions;
     @VisibleForTesting
@@ -245,7 +227,6 @@
             @NonNull ShellInit shellInit,
             @NonNull ShellController shellController,
             @NonNull @ShellMainThread ShellExecutor shellExecutor,
-            @NonNull @ShellBackgroundThread Handler backgroundHandler,
             Context context,
             @NonNull BackAnimationBackground backAnimationBackground,
             ShellBackAnimationRegistry shellBackAnimationRegistry,
@@ -256,10 +237,8 @@
                 shellInit,
                 shellController,
                 shellExecutor,
-                backgroundHandler,
                 ActivityTaskManager.getService(),
                 context,
-                context.getContentResolver(),
                 backAnimationBackground,
                 shellBackAnimationRegistry,
                 shellCommandHandler,
@@ -272,10 +251,8 @@
             @NonNull ShellInit shellInit,
             @NonNull ShellController shellController,
             @NonNull @ShellMainThread ShellExecutor shellExecutor,
-            @NonNull @ShellBackgroundThread Handler bgHandler,
             @NonNull IActivityTaskManager activityTaskManager,
             Context context,
-            ContentResolver contentResolver,
             @NonNull BackAnimationBackground backAnimationBackground,
             ShellBackAnimationRegistry shellBackAnimationRegistry,
             ShellCommandHandler shellCommandHandler,
@@ -285,10 +262,8 @@
         mShellExecutor = shellExecutor;
         mActivityTaskManager = activityTaskManager;
         mContext = context;
-        mContentResolver = contentResolver;
         mRequirePointerPilfer =
                 context.getResources().getBoolean(R.bool.config_backAnimationRequiresPointerPilfer);
-        mBgHandler = bgHandler;
         shellInit.addInitCallback(this::onInit, this);
         mAnimationBackground = backAnimationBackground;
         mShellBackAnimationRegistry = shellBackAnimationRegistry;
@@ -305,8 +280,6 @@
     }
 
     private void onInit() {
-        setupAnimationDeveloperSettingsObserver(mContentResolver, mBgHandler);
-        updateEnableAnimationFromFlags();
         createAdapter();
         mShellController.addExternalInterface(IBackAnimation.DESCRIPTOR,
                 this::createExternalInterface, this);
@@ -314,42 +287,6 @@
         mShellController.addConfigurationChangeListener(this);
     }
 
-    private void setupAnimationDeveloperSettingsObserver(
-            @NonNull ContentResolver contentResolver,
-            @NonNull @ShellBackgroundThread final Handler backgroundHandler) {
-        if (predictiveBackSystemAnims()) {
-            ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation aconfig flag is enabled, therefore "
-                    + "developer settings flag is ignored and no content observer registered");
-            return;
-        }
-        ContentObserver settingsObserver = new ContentObserver(backgroundHandler) {
-            @Override
-            public void onChange(boolean selfChange, Uri uri) {
-                updateEnableAnimationFromFlags();
-            }
-        };
-        contentResolver.registerContentObserver(
-                Global.getUriFor(Global.ENABLE_BACK_ANIMATION),
-                false, settingsObserver, UserHandle.USER_SYSTEM
-        );
-    }
-
-    /**
-     * Updates {@link BackAnimationController#mEnableAnimations} based on the current values of the
-     * aconfig flag and the developer settings flag
-     */
-    @ShellBackgroundThread
-    private void updateEnableAnimationFromFlags() {
-        boolean isEnabled = predictiveBackSystemAnims() || isDeveloperSettingEnabled();
-        mEnableAnimations.set(isEnabled);
-        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation enabled=%s", isEnabled);
-    }
-
-    private boolean isDeveloperSettingEnabled() {
-        return Global.getInt(mContext.getContentResolver(),
-                Global.ENABLE_BACK_ANIMATION, SETTING_VALUE_OFF) == SETTING_VALUE_ON;
-    }
-
     public BackAnimation getBackAnimationImpl() {
         return mBackAnimation;
     }
@@ -617,14 +554,13 @@
     private void startBackNavigation(@NonNull BackTouchTracker touchTracker) {
         try {
             startLatencyTracking();
-            final BackAnimationAdapter adapter = mEnableAnimations.get()
-                    ? mBackAnimationAdapter : null;
-            if (adapter != null && mShellBackAnimationRegistry.hasSupportedAnimatorsChanged()) {
-                adapter.updateSupportedAnimators(
+            if (mBackAnimationAdapter != null
+                    && mShellBackAnimationRegistry.hasSupportedAnimatorsChanged()) {
+                mBackAnimationAdapter.updateSupportedAnimators(
                         mShellBackAnimationRegistry.getSupportedAnimators());
             }
             mBackNavigationInfo = mActivityTaskManager.startBackNavigation(
-                    mNavigationObserver, adapter);
+                    mNavigationObserver, mBackAnimationAdapter);
             onBackNavigationInfoReceived(mBackNavigationInfo, touchTracker);
         } catch (RemoteException remoteException) {
             Log.e(TAG, "Failed to initAnimation", remoteException);
@@ -696,9 +632,7 @@
     }
 
     private boolean shouldDispatchToAnimator() {
-        return mEnableAnimations.get()
-                && mBackNavigationInfo != null
-                && mBackNavigationInfo.isPrepareRemoteAnimation();
+        return mBackNavigationInfo != null && mBackNavigationInfo.isPrepareRemoteAnimation();
     }
 
     private void tryPilferPointers() {
@@ -1093,7 +1027,6 @@
                 () -> mShellExecutor.execute(this::onBackAnimationFinished));
 
         if (mApps.length >= 1) {
-            mCurrentTracker.updateStartLocation();
             BackMotionEvent startEvent = mCurrentTracker.createStartEvent(mApps[0]);
             dispatchOnBackStarted(mActiveCallback, startEvent);
             if (startEvent.getSwipeEdge() == EDGE_NONE) {
@@ -1194,7 +1127,6 @@
      */
     private void dump(PrintWriter pw, String prefix) {
         pw.println(prefix + "BackAnimationController state:");
-        pw.println(prefix + "  mEnableAnimations=" + mEnableAnimations.get());
         pw.println(prefix + "  mBackGestureStarted=" + mBackGestureStarted);
         pw.println(prefix + "  mPostCommitAnimationInProgress=" + mPostCommitAnimationInProgress);
         pw.println(prefix + "  mShouldStartOnNextMoveEvent=" + mShouldStartOnNextMoveEvent);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
index 4569cf3..b9fccc1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
@@ -106,11 +106,12 @@
 
     private Runnable mFinishedCallback;
     private RemoteAnimationTarget[] mApps;
-    private IRemoteAnimationFinishedCallback mRemoteCallback;
+    private RemoteAnimationFinishedStub mRemoteCallback;
 
     private static class RemoteAnimationFinishedStub extends IRemoteAnimationFinishedCallback.Stub {
         //the binder callback should not hold strong reference to it to avoid memory leak.
-        private WeakReference<BackAnimationRunner> mRunnerRef;
+        private final WeakReference<BackAnimationRunner> mRunnerRef;
+        private boolean mAbandoned;
 
         private RemoteAnimationFinishedStub(BackAnimationRunner runner) {
             mRunnerRef = new WeakReference<>(runner);
@@ -118,23 +119,29 @@
 
         @Override
         public void onAnimationFinished() {
-            BackAnimationRunner runner = mRunnerRef.get();
+            synchronized (this) {
+                if (mAbandoned) {
+                    return;
+                }
+            }
+            final BackAnimationRunner runner = mRunnerRef.get();
             if (runner == null) {
                 return;
             }
-            if (runner.shouldMonitorCUJ(runner.mApps)) {
-                InteractionJankMonitor.getInstance().end(runner.mCujType);
-            }
+            runner.onAnimationFinish(this);
+        }
 
-            runner.mFinishedCallback.run();
-            for (int i = runner.mApps.length - 1; i >= 0; --i) {
-                 SurfaceControl sc = runner.mApps[i].leash;
-                 if (sc != null && sc.isValid()) {
-                     sc.release();
-                 }
+        void abandon() {
+            synchronized (this) {
+                mAbandoned = true;
+                final BackAnimationRunner runner = mRunnerRef.get();
+                if (runner == null) {
+                    return;
+                }
+                if (runner.shouldMonitorCUJ(runner.mApps)) {
+                    InteractionJankMonitor.getInstance().end(runner.mCujType);
+                }
             }
-            runner.mApps = null;
-            runner.mFinishedCallback = null;
         }
     }
 
@@ -144,13 +151,16 @@
      */
     void startAnimation(RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers,
             RemoteAnimationTarget[] nonApps, Runnable finishedCallback) {
-        InteractionJankMonitor interactionJankMonitor = InteractionJankMonitor.getInstance();
+        if (mRemoteCallback != null) {
+            mRemoteCallback.abandon();
+            mRemoteCallback = null;
+        }
+        mRemoteCallback = new RemoteAnimationFinishedStub(this);
         mFinishedCallback = finishedCallback;
         mApps = apps;
-        if (mRemoteCallback == null) mRemoteCallback = new RemoteAnimationFinishedStub(this);
         mWaitingAnimation = false;
         if (shouldMonitorCUJ(apps)) {
-            interactionJankMonitor.begin(
+            InteractionJankMonitor.getInstance().begin(
                     apps[0].leash, mContext, mHandler, mCujType);
         }
         try {
@@ -161,6 +171,28 @@
         }
     }
 
+    void onAnimationFinish(RemoteAnimationFinishedStub finished) {
+        mHandler.post(() -> {
+            if (mRemoteCallback != null && finished != mRemoteCallback) {
+                return;
+            }
+            if (shouldMonitorCUJ(mApps)) {
+                InteractionJankMonitor.getInstance().end(mCujType);
+            }
+
+            mFinishedCallback.run();
+            for (int i = mApps.length - 1; i >= 0; --i) {
+                final SurfaceControl sc = mApps[i].leash;
+                if (sc != null && sc.isValid()) {
+                    sc.release();
+                }
+            }
+            mApps = null;
+            mFinishedCallback = null;
+            mRemoteCallback = null;
+        });
+    }
+
     @VisibleForTesting
     boolean shouldMonitorCUJ(RemoteAnimationTarget[] apps) {
         return apps.length > 0 && mCujType != NO_CUJ;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
index f532be6..72be066 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
@@ -21,6 +21,7 @@
 import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayTopology;
 import android.os.RemoteException;
 import android.util.ArraySet;
 import android.util.Size;
@@ -34,6 +35,7 @@
 
 import androidx.annotation.BinderThread;
 
+import com.android.window.flags.Flags;
 import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingListener;
 import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.sysui.ShellInit;
@@ -54,6 +56,7 @@
     private final ShellExecutor mMainExecutor;
     private final Context mContext;
     private final IWindowManager mWmService;
+    private final DisplayManager mDisplayManager;
     private final DisplayChangeController mChangeController;
     private final IDisplayWindowListener mDisplayContainerListener;
 
@@ -61,10 +64,11 @@
     private final ArrayList<OnDisplaysChangedListener> mDisplayChangedListeners = new ArrayList<>();
 
     public DisplayController(Context context, IWindowManager wmService, ShellInit shellInit,
-            ShellExecutor mainExecutor) {
+            ShellExecutor mainExecutor, DisplayManager displayManager) {
         mMainExecutor = mainExecutor;
         mContext = context;
         mWmService = wmService;
+        mDisplayManager = displayManager;
         // TODO: Inject this instead
         mChangeController = new DisplayChangeController(mWmService, shellInit, mainExecutor);
         mDisplayContainerListener = new DisplayWindowListenerImpl();
@@ -74,7 +78,7 @@
     }
 
     /**
-     * Initializes the window listener.
+     * Initializes the window listener and the topology listener.
      */
     public void onInit() {
         try {
@@ -82,6 +86,12 @@
             for (int i = 0; i < displayIds.length; i++) {
                 onDisplayAdded(displayIds[i]);
             }
+
+            if (Flags.enableConnectedDisplaysWindowDrag()) {
+                mDisplayManager.registerTopologyListener(mMainExecutor,
+                        this::onDisplayTopologyChanged);
+                onDisplayTopologyChanged(mDisplayManager.getDisplayTopology());
+            }
         } catch (RemoteException e) {
             throw new RuntimeException("Unable to register display controller");
         }
@@ -91,8 +101,7 @@
      * Gets a display by id from DisplayManager.
      */
     public Display getDisplay(int displayId) {
-        final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
-        return displayManager.getDisplay(displayId);
+        return mDisplayManager.getDisplay(displayId);
     }
 
     /**
@@ -221,6 +230,14 @@
         }
     }
 
+    private void onDisplayTopologyChanged(DisplayTopology topology) {
+        // TODO(b/381472611): Call DisplayTopology#getCoordinates and update values in
+        //                    DisplayLayout when DM code is ready.
+        for (int i = 0; i < mDisplayChangedListeners.size(); ++i) {
+            mDisplayChangedListeners.get(i).onTopologyChanged();
+        }
+    }
+
     private void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
         synchronized (mDisplays) {
             final DisplayRecord dr = mDisplays.get(displayId);
@@ -408,5 +425,10 @@
          */
         default void onKeepClearAreasChanged(int displayId, Set<Rect> restricted,
                 Set<Rect> unrestricted) {}
+
+        /**
+         * Called when the display topology has changed.
+         */
+        default void onTopologyChanged() {}
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
index b6a1686..4973a6f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java
@@ -31,7 +31,9 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Insets;
+import android.graphics.PointF;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.os.SystemProperties;
 import android.provider.Settings;
 import android.util.DisplayMetrics;
@@ -71,9 +73,12 @@
     public static final int NAV_BAR_RIGHT = 1 << 1;
     public static final int NAV_BAR_BOTTOM = 1 << 2;
 
+    private static final String TAG = "DisplayLayout";
+
     private int mUiMode;
     private int mWidth;
     private int mHeight;
+    private RectF mGlobalBoundsDp;
     private DisplayCutout mCutout;
     private int mRotation;
     private int mDensityDpi;
@@ -109,6 +114,7 @@
         return mUiMode == other.mUiMode
                 && mWidth == other.mWidth
                 && mHeight == other.mHeight
+                && Objects.equals(mGlobalBoundsDp, other.mGlobalBoundsDp)
                 && Objects.equals(mCutout, other.mCutout)
                 && mRotation == other.mRotation
                 && mDensityDpi == other.mDensityDpi
@@ -127,8 +133,8 @@
 
     @Override
     public int hashCode() {
-        return Objects.hash(mUiMode, mWidth, mHeight, mCutout, mRotation, mDensityDpi,
-                mNonDecorInsets, mStableInsets, mHasNavigationBar, mHasStatusBar,
+        return Objects.hash(mUiMode, mWidth, mHeight, mGlobalBoundsDp, mCutout, mRotation,
+                mDensityDpi, mNonDecorInsets, mStableInsets, mHasNavigationBar, mHasStatusBar,
                 mNavBarFrameHeight, mTaskbarFrameHeight, mAllowSeamlessRotationDespiteNavBarMoving,
                 mNavigationBarCanMove, mReverseDefaultRotation, mInsetsState);
     }
@@ -170,6 +176,7 @@
         mUiMode = dl.mUiMode;
         mWidth = dl.mWidth;
         mHeight = dl.mHeight;
+        mGlobalBoundsDp = dl.mGlobalBoundsDp;
         mCutout = dl.mCutout;
         mRotation = dl.mRotation;
         mDensityDpi = dl.mDensityDpi;
@@ -193,6 +200,7 @@
         mRotation = info.rotation;
         mCutout = info.displayCutout;
         mDensityDpi = info.logicalDensityDpi;
+        mGlobalBoundsDp = new RectF(0, 0, pxToDp(mWidth), pxToDp(mHeight));
         mHasNavigationBar = hasNavigationBar;
         mHasStatusBar = hasStatusBar;
         mAllowSeamlessRotationDespiteNavBarMoving = res.getBoolean(
@@ -255,6 +263,11 @@
         recalcInsets(res);
     }
 
+    /** Update the global bounds of this layout, in DP. */
+    public void setGlobalBoundsDp(RectF bounds) {
+        mGlobalBoundsDp = bounds;
+    }
+
     /** Get this layout's non-decor insets. */
     public Rect nonDecorInsets() {
         return mNonDecorInsets;
@@ -265,16 +278,21 @@
         return mStableInsets;
     }
 
-    /** Get this layout's width. */
+    /** Get this layout's width in pixels. */
     public int width() {
         return mWidth;
     }
 
-    /** Get this layout's height. */
+    /** Get this layout's height in pixels. */
     public int height() {
         return mHeight;
     }
 
+    /** Get this layout's global bounds in the multi-display coordinate system in DP. */
+    public RectF globalBoundsDp() {
+        return mGlobalBoundsDp;
+    }
+
     /** Get this layout's display rotation. */
     public int rotation() {
         return mRotation;
@@ -486,4 +504,48 @@
                 ? R.dimen.navigation_bar_frame_height_landscape
                 : R.dimen.navigation_bar_frame_height);
     }
+
+    /**
+     * Converts a pixel value to a density-independent pixel (dp) value.
+     *
+     * @param px The pixel value to convert.
+     * @return The equivalent value in DP units.
+     */
+    public float pxToDp(Number px) {
+        return px.floatValue() * DisplayMetrics.DENSITY_DEFAULT / mDensityDpi;
+    }
+
+    /**
+     * Converts a density-independent pixel (dp) value to a pixel value.
+     *
+     * @param dp The DP value to convert.
+     * @return The equivalent value in pixel units.
+     */
+    public float dpToPx(Number dp) {
+        return dp.floatValue() * mDensityDpi / DisplayMetrics.DENSITY_DEFAULT;
+    }
+
+    /**
+     * Converts local pixel coordinates on this layout to global DP coordinates.
+     *
+     * @param xPx The x-coordinate in pixels, relative to the layout's origin.
+     * @param yPx The y-coordinate in pixels, relative to the layout's origin.
+     * @return A PointF object representing the coordinates in global DP units.
+     */
+    public PointF localPxToGlobalDp(Number xPx, Number yPx) {
+        return new PointF(mGlobalBoundsDp.left + pxToDp(xPx),
+                mGlobalBoundsDp.top + pxToDp(yPx));
+    }
+
+    /**
+     * Converts global DP coordinates to local pixel coordinates on this layout.
+     *
+     * @param xDp The x-coordinate in global DP units.
+     * @param yDp The y-coordinate in global DP units.
+     * @return A PointF object representing the coordinates in local pixel units on this layout.
+     */
+    public PointF globalDpToLocalPx(Number xDp, Number yDp) {
+        return new PointF(dpToPx(xDp.floatValue() - mGlobalBoundsDp.left),
+                dpToPx(yDp.floatValue() - mGlobalBoundsDp.top));
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
index b796b41..1323fe0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
@@ -20,7 +20,6 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.ActivityManager.RunningTaskInfo;
 import android.app.TaskInfo;
 import android.content.ComponentName;
 import android.content.Context;
@@ -60,7 +59,6 @@
 import com.android.wm.shell.sysui.KeyguardChangeListener;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
-import com.android.wm.shell.transition.FocusTransitionObserver;
 import com.android.wm.shell.transition.Transitions;
 
 import dagger.Lazy;
@@ -73,6 +71,7 @@
 import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.Function;
+import java.util.function.IntPredicate;
 import java.util.function.Predicate;
 
 /**
@@ -198,9 +197,6 @@
     private final CompatUIStatusManager mCompatUIStatusManager;
 
     @NonNull
-    private final FocusTransitionObserver mFocusTransitionObserver;
-
-    @NonNull
     private final Optional<DesktopUserRepositories> mDesktopUserRepositories;
 
     public CompatUIController(@NonNull Context context,
@@ -217,8 +213,7 @@
             @NonNull CompatUIShellCommandHandler compatUIShellCommandHandler,
             @NonNull AccessibilityManager accessibilityManager,
             @NonNull CompatUIStatusManager compatUIStatusManager,
-            @NonNull Optional<DesktopUserRepositories> desktopUserRepositories,
-            @NonNull FocusTransitionObserver focusTransitionObserver) {
+            @NonNull Optional<DesktopUserRepositories> desktopUserRepositories) {
         mContext = context;
         mShellController = shellController;
         mDisplayController = displayController;
@@ -235,7 +230,6 @@
                 DISAPPEAR_DELAY_MS, flags);
         mCompatUIStatusManager = compatUIStatusManager;
         mDesktopUserRepositories = desktopUserRepositories;
-        mFocusTransitionObserver = focusTransitionObserver;
         shellInit.addInitCallback(this::onInit, this);
     }
 
@@ -412,8 +406,7 @@
         // start tracking the buttons visibility for this task.
         if (mTopActivityTaskId != taskInfo.taskId
                 && !taskInfo.isTopActivityTransparent
-                && taskInfo.isVisible
-                && mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)) {
+                && taskInfo.isVisible && taskInfo.isFocused) {
             mTopActivityTaskId = taskInfo.taskId;
             setHasShownUserAspectRatioSettingsButton(false);
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java
index c493aad..151dc43 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java
@@ -20,6 +20,7 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.wm.shell.appzoomout.AppZoomOut;
 import com.android.wm.shell.back.BackAnimation;
 import com.android.wm.shell.bubbles.Bubbles;
 import com.android.wm.shell.desktopmode.DesktopMode;
@@ -112,4 +113,7 @@
      */
     @WMSingleton
     Optional<DesktopMode> getDesktopMode();
+
+    @WMSingleton
+    Optional<AppZoomOut> getAppZoomOut();
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 1408b6e..cbbe8a2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -25,6 +25,7 @@
 import android.app.ActivityTaskManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.hardware.display.DisplayManager;
 import android.os.Handler;
 import android.os.SystemProperties;
 import android.provider.Settings;
@@ -91,6 +92,7 @@
 import com.android.wm.shell.desktopmode.DesktopMode;
 import com.android.wm.shell.desktopmode.DesktopTasksController;
 import com.android.wm.shell.desktopmode.DesktopUserRepositories;
+import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider;
 import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
 import com.android.wm.shell.displayareahelper.DisplayAreaHelperController;
 import com.android.wm.shell.freeform.FreeformComponents;
@@ -108,10 +110,11 @@
 import com.android.wm.shell.shared.ShellTransitions;
 import com.android.wm.shell.shared.TransactionPool;
 import com.android.wm.shell.shared.annotations.ShellAnimationThread;
-import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
 import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.shared.annotations.ShellSplashscreenThread;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
+import com.android.wm.shell.appzoomout.AppZoomOut;
+import com.android.wm.shell.appzoomout.AppZoomOutController;
 import com.android.wm.shell.splitscreen.SplitScreen;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.startingsurface.StartingSurface;
@@ -173,8 +176,9 @@
     static DisplayController provideDisplayController(Context context,
             IWindowManager wmService,
             ShellInit shellInit,
-            @ShellMainThread ShellExecutor mainExecutor) {
-        return new DisplayController(context, wmService, shellInit, mainExecutor);
+            @ShellMainThread ShellExecutor mainExecutor,
+            DisplayManager displayManager) {
+        return new DisplayController(context, wmService, shellInit, mainExecutor, displayManager);
     }
 
     @WMSingleton
@@ -273,8 +277,7 @@
             @NonNull CompatUIState compatUIState,
             @NonNull CompatUIComponentIdGenerator componentIdGenerator,
             @NonNull CompatUIComponentFactory compatUIComponentFactory,
-            CompatUIStatusManager compatUIStatusManager,
-            @NonNull FocusTransitionObserver focusTransitionObserver) {
+            CompatUIStatusManager compatUIStatusManager) {
         if (!context.getResources().getBoolean(R.bool.config_enableCompatUIController)) {
             return Optional.empty();
         }
@@ -299,8 +302,7 @@
                         compatUIShellCommandHandler.get(),
                         accessibilityManager.get(),
                         compatUIStatusManager,
-                        desktopUserRepositories,
-                        focusTransitionObserver));
+                        desktopUserRepositories));
     }
 
     @WMSingleton
@@ -438,29 +440,24 @@
             ShellInit shellInit,
             ShellController shellController,
             @ShellMainThread ShellExecutor shellExecutor,
-            @ShellBackgroundThread Handler backgroundHandler,
             BackAnimationBackground backAnimationBackground,
             Optional<ShellBackAnimationRegistry> shellBackAnimationRegistry,
             ShellCommandHandler shellCommandHandler,
             Transitions transitions,
             @ShellMainThread Handler handler
     ) {
-        if (BackAnimationController.IS_ENABLED) {
             return shellBackAnimationRegistry.map(
                     (animations) ->
                             new BackAnimationController(
                                     shellInit,
                                     shellController,
                                     shellExecutor,
-                                    backgroundHandler,
                                     context,
                                     backAnimationBackground,
                                     animations,
                                     shellCommandHandler,
                                     transitions,
                                     handler));
-        }
-        return Optional.empty();
     }
 
     @BindsOptionalOf
@@ -1039,6 +1036,38 @@
         });
     }
 
+    @WMSingleton
+    @Provides
+    static DesktopWallpaperActivityTokenProvider provideDesktopWallpaperActivityTokenProvider() {
+        return new DesktopWallpaperActivityTokenProvider();
+    }
+
+    @WMSingleton
+    @Provides
+    static Optional<DesktopWallpaperActivityTokenProvider>
+            provideOptionalDesktopWallpaperActivityTokenProvider(
+            Context context,
+            DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider) {
+        if (DesktopModeStatus.canEnterDesktopMode(context)) {
+            return Optional.of(desktopWallpaperActivityTokenProvider);
+        }
+        return Optional.empty();
+    }
+
+    //
+    // App zoom out (optional feature)
+    //
+
+    @WMSingleton
+    @Provides
+    static Optional<AppZoomOut> provideAppZoomOut(
+            Optional<AppZoomOutController> appZoomOutController) {
+        return appZoomOutController.map((controller) -> controller.asAppZoomOut());
+    }
+
+    @BindsOptionalOf
+    abstract AppZoomOutController optionalAppZoomOutController();
+
     //
     // Task Stack
     //
@@ -1083,6 +1112,7 @@
             Optional<RecentTasksController> recentTasksOptional,
             Optional<RecentsTransitionHandler> recentsTransitionHandlerOptional,
             Optional<OneHandedController> oneHandedControllerOptional,
+            Optional<AppZoomOutController> appZoomOutControllerOptional,
             Optional<HideDisplayCutoutController> hideDisplayCutoutControllerOptional,
             Optional<ActivityEmbeddingController> activityEmbeddingOptional,
             Optional<MixedTransitionHandler> mixedTransitionHandler,
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 1916215..e8add56 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
@@ -50,6 +50,7 @@
 import com.android.wm.shell.activityembedding.ActivityEmbeddingController;
 import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser;
 import com.android.wm.shell.apptoweb.AssistContentRequester;
+import com.android.wm.shell.appzoomout.AppZoomOutController;
 import com.android.wm.shell.back.BackAnimationController;
 import com.android.wm.shell.bubbles.BubbleController;
 import com.android.wm.shell.bubbles.BubbleData;
@@ -945,7 +946,8 @@
             FocusTransitionObserver focusTransitionObserver,
             DesktopModeEventLogger desktopModeEventLogger,
             DesktopModeUiEventLogger desktopModeUiEventLogger,
-            WindowDecorTaskResourceLoader taskResourceLoader
+            WindowDecorTaskResourceLoader taskResourceLoader,
+            RecentsTransitionHandler recentsTransitionHandler
     ) {
         if (!DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) {
             return Optional.empty();
@@ -961,7 +963,7 @@
                 desktopTasksLimiter, appHandleEducationController, appToWebEducationController,
                 windowDecorCaptionHandleRepository, activityOrientationChangeHandler,
                 focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger,
-                taskResourceLoader));
+                taskResourceLoader, recentsTransitionHandler));
     }
 
     @WMSingleton
@@ -1312,10 +1314,21 @@
         return new DesktopModeUiEventLogger(uiEventLogger, packageManager);
     }
 
+    //
+    // App zoom out
+    //
+
     @WMSingleton
     @Provides
-    static DesktopWallpaperActivityTokenProvider provideDesktopWallpaperActivityTokenProvider() {
-        return new DesktopWallpaperActivityTokenProvider();
+    static AppZoomOutController provideAppZoomOutController(
+            Context context,
+            ShellInit shellInit,
+            ShellTaskOrganizer shellTaskOrganizer,
+            DisplayController displayController,
+            DisplayLayout displayLayout,
+            @ShellMainThread ShellExecutor mainExecutor) {
+        return AppZoomOutController.create(context, shellInit, shellTaskOrganizer,
+                displayController, displayLayout, mainExecutor);
     }
 
     //
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
index 03f388c..c8d0dab 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
@@ -41,6 +41,7 @@
 import com.android.wm.shell.dagger.WMShellBaseModule;
 import com.android.wm.shell.dagger.WMSingleton;
 import com.android.wm.shell.desktopmode.DesktopUserRepositories;
+import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider;
 import com.android.wm.shell.pip2.phone.PhonePipMenuController;
 import com.android.wm.shell.pip2.phone.PipController;
 import com.android.wm.shell.pip2.phone.PipMotionHelper;
@@ -82,11 +83,14 @@
             @NonNull PipTransitionState pipStackListenerController,
             @NonNull PipDisplayLayoutState pipDisplayLayoutState,
             @NonNull PipUiStateChangeController pipUiStateChangeController,
-            Optional<DesktopUserRepositories> desktopUserRepositoriesOptional) {
+            Optional<DesktopUserRepositories> desktopUserRepositoriesOptional,
+            Optional<DesktopWallpaperActivityTokenProvider>
+                    desktopWallpaperActivityTokenProviderOptional) {
         return new PipTransition(context, shellInit, shellTaskOrganizer, transitions,
                 pipBoundsState, null, pipBoundsAlgorithm, pipTaskListener,
                 pipScheduler, pipStackListenerController, pipDisplayLayoutState,
-                pipUiStateChangeController, desktopUserRepositoriesOptional);
+                pipUiStateChangeController, desktopUserRepositoriesOptional,
+                desktopWallpaperActivityTokenProviderOptional);
     }
 
     @WMSingleton
@@ -117,6 +121,7 @@
             PipTouchHandler pipTouchHandler,
             PipAppOpsListener pipAppOpsListener,
             PhonePipMenuController pipMenuController,
+            PipUiEventLogger pipUiEventLogger,
             @ShellMainThread ShellExecutor mainExecutor) {
         if (!PipUtils.isPip2ExperimentEnabled()) {
             return Optional.empty();
@@ -126,7 +131,7 @@
                     displayInsetsController, pipBoundsState, pipBoundsAlgorithm,
                     pipDisplayLayoutState, pipScheduler, taskStackListener, shellTaskOrganizer,
                     pipTransitionState, pipTouchHandler, pipAppOpsListener, pipMenuController,
-                    mainExecutor));
+                    pipUiEventLogger, mainExecutor));
         }
     }
 
@@ -137,9 +142,12 @@
             @ShellMainThread ShellExecutor mainExecutor,
             PipTransitionState pipTransitionState,
             Optional<DesktopUserRepositories> desktopUserRepositoriesOptional,
+            Optional<DesktopWallpaperActivityTokenProvider>
+                    desktopWallpaperActivityTokenProviderOptional,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
         return new PipScheduler(context, pipBoundsState, mainExecutor, pipTransitionState,
-                desktopUserRepositoriesOptional, rootTaskDisplayAreaOrganizer);
+                desktopUserRepositoriesOptional, desktopWallpaperActivityTokenProviderOptional,
+                rootTaskDisplayAreaOrganizer);
     }
 
     @WMSingleton
@@ -188,11 +196,11 @@
             FloatingContentCoordinator floatingContentCoordinator,
             PipScheduler pipScheduler,
             Optional<PipPerfHintController> pipPerfHintControllerOptional,
-            PipBoundsAlgorithm pipBoundsAlgorithm,
-            PipTransitionState pipTransitionState) {
+            PipTransitionState pipTransitionState,
+            PipUiEventLogger pipUiEventLogger) {
         return new PipMotionHelper(context, pipBoundsState, menuController, pipSnapAlgorithm,
                 floatingContentCoordinator, pipScheduler, pipPerfHintControllerOptional,
-                pipBoundsAlgorithm, pipTransitionState);
+                pipTransitionState, pipUiEventLogger);
     }
 
     @WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt
index 536dc2a..a4620d5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt
@@ -339,7 +339,7 @@
                         .setWindowCrop(leash, endBounds.width(), endBounds.height())
                         .apply()
                     onTaskResizeAnimationListener?.onAnimationEnd(taskId)
-                    finishCallback.onTransitionFinished(null /* wct */)
+                    finishCallback.onTransitionFinished(/* wct= */ null)
                 }
             )
             addUpdateListener { animation ->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt
index ca02c72..f6fd967 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt
@@ -93,9 +93,8 @@
         return matchingChanges.first()
     }
 
-    private fun isValidTaskChange(change: TransitionInfo.Change): Boolean {
-        return change.taskInfo != null && change.taskInfo?.taskId != -1
-    }
+    private fun isValidTaskChange(change: TransitionInfo.Change): Boolean =
+        change.taskInfo != null && change.taskInfo?.taskId != -1
 
     override fun handleRequest(
         transition: IBinder,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
index ceef699..e8f9a78 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
@@ -306,7 +306,7 @@
     fun logTaskInfoStateInit() {
         logTaskUpdate(
             FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INIT_STATSD,
-            /* session_id */ 0,
+            sessionId = 0,
             TaskUpdate(
                 visibleTaskCount = 0,
                 instanceId = 0,
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 e975b58..c09504e 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
@@ -434,18 +434,14 @@
         visibleFreeformTaskInfos.set(taskInfo.taskId, taskInfo)
     }
 
-    private fun TransitionInfo.Change.requireTaskInfo(): RunningTaskInfo {
-        return this.taskInfo ?: throw IllegalStateException("Expected TaskInfo in the Change")
-    }
+    private fun TransitionInfo.Change.requireTaskInfo(): RunningTaskInfo =
+        this.taskInfo ?: throw IllegalStateException("Expected TaskInfo in the Change")
 
-    private fun TaskInfo.isFreeformWindow(): Boolean {
-        return this.windowingMode == WINDOWING_MODE_FREEFORM
-    }
+    private fun TaskInfo.isFreeformWindow(): Boolean = this.windowingMode == WINDOWING_MODE_FREEFORM
 
-    private fun TransitionInfo.isExitToRecentsTransition(): Boolean {
-        return this.type == WindowManager.TRANSIT_TO_FRONT &&
+    private fun TransitionInfo.isExitToRecentsTransition(): Boolean =
+        this.type == WindowManager.TRANSIT_TO_FRONT &&
             this.flags == WindowManager.TRANSIT_FLAG_IS_RECENTS
-    }
 
     companion object {
         @VisibleForTesting const val VISIBLE_TASKS_COUNTER_NAME = "desktop_mode_visible_tasks"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
index dba8c93..cdfa14b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
@@ -24,8 +24,8 @@
 class DesktopModeShellCommandHandler(private val controller: DesktopTasksController) :
     ShellCommandHandler.ShellCommandActionHandler {
 
-    override fun onShellCommand(args: Array<String>, pw: PrintWriter): Boolean {
-        return when (args[0]) {
+    override fun onShellCommand(args: Array<String>, pw: PrintWriter): Boolean =
+        when (args[0]) {
             "moveToDesktop" -> {
                 if (!runMoveToDesktop(args, pw)) {
                     pw.println("Task not found. Please enter a valid taskId.")
@@ -47,7 +47,6 @@
                 false
             }
         }
-    }
 
     private fun runMoveToDesktop(args: Array<String>, pw: PrintWriter): Boolean {
         if (args.size < 2) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
index cd37113..32ee319 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
@@ -274,7 +274,7 @@
         lp.inputFeatures |= INPUT_FEATURE_NO_INPUT_CHANNEL;
         final WindowlessWindowManager windowManager = new WindowlessWindowManager(
                 mTaskInfo.configuration, mLeash,
-                null /* hostInputToken */);
+                /* hostInputToken= */ null);
         mViewHost = new SurfaceControlViewHost(mContext,
                 mDisplayController.getDisplay(mTaskInfo.displayId), windowManager,
                 "DesktopModeVisualIndicator");
@@ -338,7 +338,7 @@
         if (mCurrentType == NO_INDICATOR) {
             fadeInIndicator(newType);
         } else if (newType == NO_INDICATOR) {
-            fadeOutIndicator(null /* finishCallback */);
+            fadeOutIndicator(/* finishCallback= */ null);
         } else {
             final VisualIndicatorAnimator animator = VisualIndicatorAnimator.animateIndicatorType(
                     mView, mDisplayController.getDisplayLayout(mTaskInfo.displayId), mCurrentType,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
index d306664..1a58363 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
@@ -27,6 +27,7 @@
 import androidx.core.util.keyIterator
 import androidx.core.util.valueIterator
 import com.android.internal.protolog.ProtoLog
+import com.android.window.flags.Flags
 import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
 import com.android.wm.shell.shared.annotations.ShellMainThread
@@ -56,6 +57,10 @@
      * @property topTransparentFullscreenTaskId the task id of any current top transparent
      *   fullscreen task launched on top of Desktop Mode. Cleared when the transparent task is
      *   closed or sent to back. (top is at index 0).
+     * @property pipTaskId the task id of PiP task entered while in Desktop Mode.
+     * @property pipShouldKeepDesktopActive whether an active PiP window should keep the Desktop
+     *   Mode session active. Only false when we are explicitly exiting Desktop Mode (via user
+     *   action) while there is an active PiP window.
      */
     private data class DesktopTaskData(
         val activeTasks: ArraySet<Int> = ArraySet(),
@@ -66,6 +71,8 @@
         val freeformTasksInZOrder: ArrayList<Int> = ArrayList(),
         var fullImmersiveTaskId: Int? = null,
         var topTransparentFullscreenTaskId: Int? = null,
+        var pipTaskId: Int? = null,
+        var pipShouldKeepDesktopActive: Boolean = true,
     ) {
         fun deepCopy(): DesktopTaskData =
             DesktopTaskData(
@@ -76,6 +83,8 @@
                 freeformTasksInZOrder = ArrayList(freeformTasksInZOrder),
                 fullImmersiveTaskId = fullImmersiveTaskId,
                 topTransparentFullscreenTaskId = topTransparentFullscreenTaskId,
+                pipTaskId = pipTaskId,
+                pipShouldKeepDesktopActive = pipShouldKeepDesktopActive,
             )
 
         fun clear() {
@@ -86,6 +95,8 @@
             freeformTasksInZOrder.clear()
             fullImmersiveTaskId = null
             topTransparentFullscreenTaskId = null
+            pipTaskId = null
+            pipShouldKeepDesktopActive = true
         }
     }
 
@@ -104,6 +115,9 @@
     /* Tracks last bounds of task before toggled to immersive state. */
     private val boundsBeforeFullImmersiveByTaskId = SparseArray<Rect>()
 
+    /* Callback for when a pending PiP transition has been aborted. */
+    private var onPipAbortedCallback: ((Int, Int) -> Unit)? = null
+
     private var desktopGestureExclusionListener: Consumer<Region>? = null
     private var desktopGestureExclusionExecutor: Executor? = null
 
@@ -302,6 +316,54 @@
         }
     }
 
+    /** Set whether the given task is the Desktop-entered PiP task in this display. */
+    fun setTaskInPip(displayId: Int, taskId: Int, enterPip: Boolean) {
+        val desktopData = desktopTaskDataByDisplayId.getOrCreate(displayId)
+        if (enterPip) {
+            desktopData.pipTaskId = taskId
+            desktopData.pipShouldKeepDesktopActive = true
+        } else {
+            desktopData.pipTaskId =
+                if (desktopData.pipTaskId == taskId) null
+                else {
+                    logW(
+                        "setTaskInPip: taskId=$taskId did not match saved taskId=${desktopData.pipTaskId}"
+                    )
+                    desktopData.pipTaskId
+                }
+        }
+        notifyVisibleTaskListeners(displayId, getVisibleTaskCount(displayId))
+    }
+
+    /** Returns whether there is a PiP that was entered/minimized from Desktop in this display. */
+    fun isMinimizedPipPresentInDisplay(displayId: Int): Boolean =
+        desktopTaskDataByDisplayId.getOrCreate(displayId).pipTaskId != null
+
+    /** Returns whether the given task is the Desktop-entered PiP task in this display. */
+    fun isTaskMinimizedPipInDisplay(displayId: Int, taskId: Int): Boolean =
+        desktopTaskDataByDisplayId.getOrCreate(displayId).pipTaskId == taskId
+
+    /** Returns whether Desktop session should be active in this display due to active PiP. */
+    fun shouldDesktopBeActiveForPip(displayId: Int): Boolean =
+        Flags.enableDesktopWindowingPip() &&
+            isMinimizedPipPresentInDisplay(displayId) &&
+            desktopTaskDataByDisplayId.getOrCreate(displayId).pipShouldKeepDesktopActive
+
+    /** Saves whether a PiP window should keep Desktop session active in this display. */
+    fun setPipShouldKeepDesktopActive(displayId: Int, keepActive: Boolean) {
+        desktopTaskDataByDisplayId.getOrCreate(displayId).pipShouldKeepDesktopActive = keepActive
+    }
+
+    /** Saves callback to handle a pending PiP transition being aborted. */
+    fun setOnPipAbortedCallback(callbackIfPipAborted: ((Int, Int) -> Unit)?) {
+        onPipAbortedCallback = callbackIfPipAborted
+    }
+
+    /** Invokes callback to handle a pending PiP transition with the given task id being aborted. */
+    fun onPipAborted(displayId: Int, pipTaskId: Int) {
+        onPipAbortedCallback?.invoke(displayId, pipTaskId)
+    }
+
     /** Set whether the given task is the full-immersive task in this display. */
     fun setTaskInFullImmersiveState(displayId: Int, taskId: Int, immersive: Boolean) {
         val desktopData = desktopTaskDataByDisplayId.getOrCreate(displayId)
@@ -338,8 +400,12 @@
     }
 
     private fun notifyVisibleTaskListeners(displayId: Int, visibleTasksCount: Int) {
+        val visibleAndPipTasksCount =
+            if (shouldDesktopBeActiveForPip(displayId)) visibleTasksCount + 1 else visibleTasksCount
         visibleTasksListeners.forEach { (listener, executor) ->
-            executor.execute { listener.onTasksVisibilityChanged(displayId, visibleTasksCount) }
+            executor.execute {
+                listener.onTasksVisibilityChanged(displayId, visibleAndPipTasksCount)
+            }
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt
index 848d80f..f29301d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt
@@ -41,49 +41,35 @@
             return Point(x, y.toInt())
         }
 
-        override fun next(): DesktopTaskPosition {
-            return BottomRight
-        }
+        override fun next(): DesktopTaskPosition = BottomRight
     }
 
     data object BottomRight : DesktopTaskPosition() {
-        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
-            return Point(frame.right - window.width(), frame.bottom - window.height())
-        }
+        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point =
+            Point(frame.right - window.width(), frame.bottom - window.height())
 
-        override fun next(): DesktopTaskPosition {
-            return TopLeft
-        }
+        override fun next(): DesktopTaskPosition = TopLeft
     }
 
     data object TopLeft : DesktopTaskPosition() {
-        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
-            return Point(frame.left, frame.top)
-        }
+        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point =
+            Point(frame.left, frame.top)
 
-        override fun next(): DesktopTaskPosition {
-            return BottomLeft
-        }
+        override fun next(): DesktopTaskPosition = BottomLeft
     }
 
     data object BottomLeft : DesktopTaskPosition() {
-        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
-            return Point(frame.left, frame.bottom - window.height())
-        }
+        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point =
+            Point(frame.left, frame.bottom - window.height())
 
-        override fun next(): DesktopTaskPosition {
-            return TopRight
-        }
+        override fun next(): DesktopTaskPosition = TopRight
     }
 
     data object TopRight : DesktopTaskPosition() {
-        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
-            return Point(frame.right - window.width(), frame.top)
-        }
+        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point =
+            Point(frame.right - window.width(), frame.top)
 
-        override fun next(): DesktopTaskPosition {
-            return Center
-        }
+        override fun next(): DesktopTaskPosition = Center
     }
 
     /**
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 d180ea7..73d1527 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
@@ -225,7 +225,6 @@
     // Launch cookie used to identify a drag and drop transition to fullscreen after it has begun.
     // Used to prevent handleRequest from moving the new fullscreen task to freeform.
     private var dragAndDropFullscreenCookie: Binder? = null
-    private var pendingPipTransitionAndTask: Pair<IBinder, Int>? = null
 
     init {
         desktopMode = DesktopModeImpl()
@@ -310,24 +309,40 @@
         transitions.startTransition(transitionType, wct, handler).also { t ->
             handler?.setTransition(t)
         }
+
+        // launch from recent DesktopTaskView
+        desktopModeEnterExitTransitionListener?.onEnterDesktopModeTransitionStarted(
+            FREEFORM_ANIMATION_DURATION
+        )
     }
 
     /** Gets number of visible freeform tasks in [displayId]. */
     fun visibleTaskCount(displayId: Int): Int = taskRepository.getVisibleTaskCount(displayId)
 
     /**
-     * Returns true if any freeform tasks are visible or if a transparent fullscreen task exists on
-     * top in Desktop Mode.
+     * Returns true if any of the following is true:
+     * - Any freeform tasks are visible
+     * - A transparent fullscreen task exists on top in Desktop Mode
+     * - PiP on Desktop Windowing is enabled, there is an active PiP window and the desktop
+     *   wallpaper is visible.
      */
     fun isDesktopModeShowing(displayId: Int): Boolean {
+        val hasVisibleTasks = visibleTaskCount(displayId) > 0
+        val hasTopTransparentFullscreenTask =
+            taskRepository.getTopTransparentFullscreenTaskId(displayId) != null
+        val hasMinimizedPip =
+            Flags.enableDesktopWindowingPip() &&
+                taskRepository.isMinimizedPipPresentInDisplay(displayId) &&
+                desktopWallpaperActivityTokenProvider.isWallpaperActivityVisible(displayId)
         if (
             DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC
                 .isTrue() && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue()
         ) {
-            return visibleTaskCount(displayId) > 0 ||
-                taskRepository.getTopTransparentFullscreenTaskId(displayId) != null
+            return hasVisibleTasks || hasTopTransparentFullscreenTask || hasMinimizedPip
+        } else if (Flags.enableDesktopWindowingPip()) {
+            return hasVisibleTasks || hasMinimizedPip
         }
-        return visibleTaskCount(displayId) > 0
+        return hasVisibleTasks
     }
 
     /** Moves focused task to desktop mode for given [displayId]. */
@@ -587,7 +602,7 @@
     ): ((IBinder) -> Unit)? {
         val taskId = taskInfo.taskId
         desktopTilingDecorViewModel.removeTaskIfTiled(displayId, taskId)
-        performDesktopExitCleanupIfNeeded(taskId, displayId, wct)
+        performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false)
         taskRepository.addClosingTask(displayId, taskId)
         taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
             doesAnyTaskRequireTaskbarRounding(displayId, taskId)
@@ -619,8 +634,12 @@
                 )
             val requestRes = transitions.dispatchRequest(Binder(), requestInfo, /* skip= */ null)
             wct.merge(requestRes.second, true)
-            pendingPipTransitionAndTask =
-                freeformTaskTransitionStarter.startPipTransition(wct) to taskInfo.taskId
+            freeformTaskTransitionStarter.startPipTransition(wct)
+            taskRepository.setTaskInPip(taskInfo.displayId, taskInfo.taskId, enterPip = true)
+            taskRepository.setOnPipAbortedCallback { displayId, taskId ->
+                minimizeTaskInner(shellTaskOrganizer.getRunningTaskInfo(taskId)!!)
+                taskRepository.setTaskInPip(displayId, taskId, enterPip = false)
+            }
             return
         }
 
@@ -631,7 +650,7 @@
         val taskId = taskInfo.taskId
         val displayId = taskInfo.displayId
         val wct = WindowContainerTransaction()
-        performDesktopExitCleanupIfNeeded(taskId, displayId, wct)
+        performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false)
         // Notify immersive handler as it might need to exit immersive state.
         val exitResult =
             desktopImmersiveController.exitImmersiveIfApplicable(
@@ -762,7 +781,7 @@
             return
         }
         val wct = WindowContainerTransaction()
-        wct.reorder(taskInfo.token, true /* onTop */, true /* includingParents */)
+        wct.reorder(taskInfo.token, /* onTop= */ true, /* includingParents= */ true)
         startLaunchTransition(
             transitionType = TRANSIT_TO_FRONT,
             wct = wct,
@@ -884,7 +903,7 @@
         } else if (Flags.enableMoveToNextDisplayShortcut()) {
             applyFreeformDisplayChange(wct, task, displayId)
         }
-        wct.reparent(task.token, displayAreaInfo.token, true /* onTop */)
+        wct.reparent(task.token, displayAreaInfo.token, /* onTop= */ true)
         if (Flags.enableDisplayFocusInShellTransitions()) {
             // Bring the destination display to top with includingParents=true, so that the
             // destination display gains the display focus, which makes the top task in the display
@@ -893,10 +912,15 @@
         }
 
         if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
-            performDesktopExitCleanupIfNeeded(task.taskId, task.displayId, wct)
+            performDesktopExitCleanupIfNeeded(
+                task.taskId,
+                task.displayId,
+                wct,
+                forceToFullscreen = false,
+            )
         }
 
-        transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
+        transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
     }
 
     /**
@@ -1344,7 +1368,7 @@
 
     private fun addWallpaperActivity(displayId: Int, wct: WindowContainerTransaction) {
         logV("addWallpaperActivity")
-        if (Flags.enableDesktopWallpaperActivityOnSystemUser()) {
+        if (Flags.enableDesktopWallpaperActivityForSystemUser()) {
             val intent = Intent(context, DesktopWallpaperActivity::class.java)
             val options =
                 ActivityOptions.makeBasic().apply {
@@ -1393,7 +1417,7 @@
     private fun removeWallpaperActivity(wct: WindowContainerTransaction) {
         desktopWallpaperActivityTokenProvider.getToken()?.let { token ->
             logV("removeWallpaperActivity")
-            if (Flags.enableDesktopWallpaperActivityOnSystemUser()) {
+            if (Flags.enableDesktopWallpaperActivityForSystemUser()) {
                 wct.reorder(token, /* onTop= */ false)
             } else {
                 wct.removeTask(token)
@@ -1409,7 +1433,9 @@
         taskId: Int,
         displayId: Int,
         wct: WindowContainerTransaction,
+        forceToFullscreen: Boolean,
     ) {
+        taskRepository.setPipShouldKeepDesktopActive(displayId, !forceToFullscreen)
         if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
             if (!taskRepository.isOnlyVisibleNonClosingTask(taskId, displayId)) {
                 return
@@ -1417,6 +1443,12 @@
             if (displayId != DEFAULT_DISPLAY) {
                 return
             }
+        } else if (
+            Flags.enableDesktopWindowingPip() &&
+                taskRepository.isMinimizedPipPresentInDisplay(displayId) &&
+                !forceToFullscreen
+        ) {
+            return
         } else {
             if (!taskRepository.isOnlyVisibleNonClosingTask(taskId)) {
                 return
@@ -1438,13 +1470,9 @@
         }
     }
 
-    override fun getContext(): Context {
-        return context
-    }
+    override fun getContext(): Context = context
 
-    override fun getRemoteCallExecutor(): ShellExecutor {
-        return mainExecutor
-    }
+    override fun getRemoteCallExecutor(): ShellExecutor = mainExecutor
 
     override fun startAnimation(
         transition: IBinder,
@@ -1457,21 +1485,6 @@
         return false
     }
 
-    override fun onTransitionConsumed(
-        transition: IBinder,
-        aborted: Boolean,
-        finishT: Transaction?,
-    ) {
-        pendingPipTransitionAndTask?.let { (pipTransition, taskId) ->
-            if (transition == pipTransition) {
-                if (aborted) {
-                    shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { minimizeTaskInner(it) }
-                }
-                pendingPipTransitionAndTask = null
-            }
-        }
-    }
-
     override fun handleRequest(
         transition: IBinder,
         request: TransitionRequestInfo,
@@ -1645,11 +1658,10 @@
         DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() &&
             isTopActivityExemptFromDesktopWindowing(context, task)
 
-    private fun shouldHandleTaskClosing(request: TransitionRequestInfo): Boolean {
-        return ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() &&
+    private fun shouldHandleTaskClosing(request: TransitionRequestInfo): Boolean =
+        ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() &&
             TransitionUtil.isClosingType(request.type) &&
             request.triggerTask != null
-    }
 
     /** Open an existing instance of an app. */
     fun openInstance(callingTask: RunningTaskInfo, requestedTaskId: Int) {
@@ -1672,7 +1684,7 @@
                 requestedTaskId,
                 splitPosition,
                 options.toBundle(),
-                null, /* hideTaskToken */
+                /* hideTaskToken= */ null,
             )
         }
     }
@@ -1709,8 +1721,8 @@
                     fillIn,
                     splitPosition,
                     options.toBundle(),
-                    null /* hideTaskToken */,
-                    true /* forceLaunchNewTask */,
+                    /* hideTaskToken= */ null,
+                    /* forceLaunchNewTask= */ true,
                     splitIndex,
                 )
             }
@@ -1921,7 +1933,12 @@
         if (!isDesktopModeShowing(task.displayId)) return null
 
         val wct = WindowContainerTransaction()
-        performDesktopExitCleanupIfNeeded(task.taskId, task.displayId, wct)
+        performDesktopExitCleanupIfNeeded(
+            task.taskId,
+            task.displayId,
+            wct,
+            forceToFullscreen = false,
+        )
 
         if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
             taskRepository.addClosingTask(task.displayId, task.taskId)
@@ -1961,7 +1978,7 @@
             wct.setBounds(taskInfo.token, initialBounds)
         }
         wct.setWindowingMode(taskInfo.token, targetWindowingMode)
-        wct.reorder(taskInfo.token, true /* onTop */)
+        wct.reorder(taskInfo.token, /* onTop= */ true)
         if (useDesktopOverrideDensity()) {
             wct.setDensityDpi(taskInfo.token, DESKTOP_DENSITY_OVERRIDE)
         }
@@ -2048,7 +2065,12 @@
             wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
         }
 
-        performDesktopExitCleanupIfNeeded(taskInfo.taskId, taskInfo.displayId, wct)
+        performDesktopExitCleanupIfNeeded(
+            taskInfo.taskId,
+            taskInfo.displayId,
+            wct,
+            forceToFullscreen = true,
+        )
     }
 
     private fun cascadeWindow(bounds: Rect, displayLayout: DisplayLayout, displayId: Int) {
@@ -2082,7 +2104,12 @@
         // want it overridden in multi-window.
         wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
 
-        performDesktopExitCleanupIfNeeded(taskInfo.taskId, taskInfo.displayId, wct)
+        performDesktopExitCleanupIfNeeded(
+            taskInfo.taskId,
+            taskInfo.displayId,
+            wct,
+            forceToFullscreen = false,
+        )
     }
 
     /** Returns the ID of the Task that will be minimized, or null if no task will be minimized. */
@@ -2153,11 +2180,10 @@
         getFocusedFreeformTask(displayId)?.let { requestSplit(it, leftOrTop) }
     }
 
-    private fun getFocusedFreeformTask(displayId: Int): RunningTaskInfo? {
-        return shellTaskOrganizer.getRunningTasks(displayId).find { taskInfo ->
+    private fun getFocusedFreeformTask(displayId: Int): RunningTaskInfo? =
+        shellTaskOrganizer.getRunningTasks(displayId).find { taskInfo ->
             taskInfo.isFocused && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM
         }
-    }
 
     /**
      * Requests a task be transitioned from desktop to split select. Applies needed windowing
@@ -2205,14 +2231,10 @@
         }
     }
 
-    private fun getDefaultDensityDpi(): Int {
-        return context.resources.displayMetrics.densityDpi
-    }
+    private fun getDefaultDensityDpi(): Int = context.resources.displayMetrics.densityDpi
 
     /** Creates a new instance of the external interface to pass to another process. */
-    private fun createExternalInterface(): ExternalInterfaceBinder {
-        return IDesktopModeImpl(this)
-    }
+    private fun createExternalInterface(): ExternalInterfaceBinder = IDesktopModeImpl(this)
 
     /** Get connection interface between sysui and shell */
     fun asDesktopMode(): DesktopMode {
@@ -2796,7 +2818,7 @@
                 controller,
                 "visibleTaskCount",
                 { controller -> result[0] = controller.visibleTaskCount(displayId) },
-                true, /* blocking */
+                /* blocking= */ true,
             )
             return result[0]
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
index 0330a5f..e4a28e9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
@@ -138,11 +138,10 @@
             )
         }
 
-        private fun getMinimizeChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? {
-            return info.changes.find { change ->
+        private fun getMinimizeChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? =
+            info.changes.find { change ->
                 change.taskInfo?.taskId == taskId && change.mode == TRANSIT_TO_BACK
             }
-        }
 
         override fun onTransitionMerged(merged: IBinder, playing: IBinder) {
             if (activeTransitionTokensAndTasks.remove(merged) != null) {
@@ -234,7 +233,7 @@
         // If it's a running task, reorder it to back.
         taskIdToMinimize
             ?.let { shellTaskOrganizer.getRunningTaskInfo(it) }
-            ?.let { wct.reorder(it.token, false /* onTop */) }
+            ?.let { wct.reorder(it.token, /* onTop= */ false) }
         return taskIdToMinimize
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
index e7a0077..d61ffda 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
@@ -21,9 +21,11 @@
 import android.content.Context
 import android.os.IBinder
 import android.view.SurfaceControl
-import android.view.WindowManager
 import android.view.WindowManager.TRANSIT_CLOSE
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_PIP
 import android.view.WindowManager.TRANSIT_TO_BACK
+import android.view.WindowManager.TRANSIT_TO_FRONT
 import android.window.DesktopModeFlags
 import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
 import android.window.TransitionInfo
@@ -39,6 +41,8 @@
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP
+import com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP
 
 /**
  * A [Transitions.TransitionObserver] that observes shell transitions and updates the
@@ -57,6 +61,8 @@
 ) : Transitions.TransitionObserver {
 
     private var transitionToCloseWallpaper: IBinder? = null
+    /* Pending PiP transition and its associated display id and task id. */
+    private var pendingPipTransitionAndPipTask: Triple<IBinder, Int, Int>? = null
     private var currentProfileId: Int
 
     init {
@@ -90,6 +96,33 @@
             removeTaskIfNeeded(info)
         }
         removeWallpaperOnLastTaskClosingIfNeeded(transition, info)
+
+        val desktopRepository = desktopUserRepositories.getProfile(currentProfileId)
+        info.changes.forEach { change ->
+            change.taskInfo?.let { taskInfo ->
+                if (
+                    Flags.enableDesktopWindowingPip() &&
+                        desktopRepository.isTaskMinimizedPipInDisplay(
+                            taskInfo.displayId,
+                            taskInfo.taskId,
+                        )
+                ) {
+                    when (info.type) {
+                        TRANSIT_PIP ->
+                            pendingPipTransitionAndPipTask =
+                                Triple(transition, taskInfo.displayId, taskInfo.taskId)
+
+                        TRANSIT_EXIT_PIP,
+                        TRANSIT_REMOVE_PIP ->
+                            desktopRepository.setTaskInPip(
+                                taskInfo.displayId,
+                                taskInfo.taskId,
+                                enterPip = false,
+                            )
+                    }
+                }
+            }
+        }
     }
 
     private fun removeTaskIfNeeded(info: TransitionInfo) {
@@ -236,7 +269,7 @@
         if (transitionToCloseWallpaper == transition) {
             // TODO: b/362469671 - Handle merging the animation when desktop is also closing.
             desktopWallpaperActivityTokenProvider.getToken()?.let { wallpaperActivityToken ->
-                if (Flags.enableDesktopWallpaperActivityOnSystemUser()) {
+                if (Flags.enableDesktopWallpaperActivityForSystemUser()) {
                     transitions.startTransition(
                         TRANSIT_TO_BACK,
                         WindowContainerTransaction()
@@ -252,6 +285,18 @@
                 }
             }
             transitionToCloseWallpaper = null
+        } else if (pendingPipTransitionAndPipTask?.first == transition) {
+            val desktopRepository = desktopUserRepositories.getProfile(currentProfileId)
+            if (aborted) {
+                pendingPipTransitionAndPipTask?.let {
+                    desktopRepository.onPipAborted(
+                        /*displayId=*/ it.second,
+                        /* taskId=*/ it.third,
+                    )
+                }
+            }
+            desktopRepository.setOnPipAbortedCallback(null)
+            pendingPipTransitionAndPipTask = null
         }
     }
 
@@ -263,11 +308,15 @@
             change.taskInfo?.let { taskInfo ->
                 if (DesktopWallpaperActivity.isWallpaperTask(taskInfo)) {
                     when (change.mode) {
-                        WindowManager.TRANSIT_OPEN -> {
+                        TRANSIT_OPEN -> {
                             desktopWallpaperActivityTokenProvider.setToken(
                                 taskInfo.token,
                                 taskInfo.displayId,
                             )
+                            desktopWallpaperActivityTokenProvider.setWallpaperActivityIsVisible(
+                                isVisible = true,
+                                taskInfo.displayId,
+                            )
                             // After the task for the wallpaper is created, set it non-trimmable.
                             // This is important to prevent recents from trimming and removing the
                             // task.
@@ -278,6 +327,16 @@
                         }
                         TRANSIT_CLOSE ->
                             desktopWallpaperActivityTokenProvider.removeToken(taskInfo.displayId)
+                        TRANSIT_TO_FRONT ->
+                            desktopWallpaperActivityTokenProvider.setWallpaperActivityIsVisible(
+                                isVisible = true,
+                                taskInfo.displayId,
+                            )
+                        TRANSIT_TO_BACK ->
+                            desktopWallpaperActivityTokenProvider.setWallpaperActivityIsVisible(
+                                isVisible = false,
+                                taskInfo.displayId,
+                            )
                         else -> {}
                     }
                 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
index 72c0642..91f10dc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
@@ -131,7 +131,7 @@
         val pendingIntent =
             PendingIntent.getActivityAsUser(
                 context.createContextAsUser(taskUser, /* flags= */ 0),
-                0 /* requestCode */,
+                /* requestCode= */ 0,
                 launchHomeIntent,
                 FLAG_MUTABLE or FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or FILL_IN_COMPONENT,
                 options.toBundle(),
@@ -234,7 +234,7 @@
             val wct = WindowContainerTransaction()
             restoreWindowOrder(wct, state)
             state.startTransitionFinishTransaction?.apply()
-            state.startTransitionFinishCb?.onTransitionFinished(null /* wct */)
+            state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null)
             requestSplitFromScaledTask(splitPosition, wct)
             clearState()
         } else {
@@ -440,7 +440,7 @@
             val wct = WindowContainerTransaction()
             restoreWindowOrder(wct)
             state.startTransitionFinishTransaction?.apply()
-            state.startTransitionFinishCb?.onTransitionFinished(null /* wct */)
+            state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null)
             requestSplitSelect(wct, taskInfo, splitPosition)
         }
         return true
@@ -492,7 +492,7 @@
                 finishTransaction = startTransactionFinishT,
             )
             // Call finishCallback to merge animation before startTransitionFinishCb is called
-            finishCallback.onTransitionFinished(null /* wct */)
+            finishCallback.onTransitionFinished(/* wct= */ null)
             animateEndDragToDesktop(startTransaction = t, startTransitionFinishCb)
         } else if (isCancelTransition) {
             info.changes.forEach { change ->
@@ -500,8 +500,8 @@
                 startTransactionFinishT.show(change.leash)
             }
             t.apply()
-            finishCallback.onTransitionFinished(null /* wct */)
-            startTransitionFinishCb.onTransitionFinished(null /* wct */)
+            finishCallback.onTransitionFinished(/* wct= */ null)
+            startTransitionFinishCb.onTransitionFinished(/* wct= */ null)
             clearState()
         }
     }
@@ -653,7 +653,7 @@
             interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD)
         } else if (state.cancelTransitionToken == transition) {
             state.draggedTaskChange?.leash?.let { state.startTransitionFinishTransaction?.show(it) }
-            state.startTransitionFinishCb?.onTransitionFinished(null /* wct */)
+            state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null)
             clearState()
         } else {
             // This transition being aborted is neither the start, nor the cancel transition, so
@@ -741,19 +741,19 @@
                         // TODO(b/322852244): investigate why even though these "other" tasks are
                         //  reordered in front of home and behind the translucent dragged task, its
                         //  surface is not visible on screen.
-                        wct.reorder(wc, true /* toTop */)
+                        wct.reorder(wc, /* onTop= */ true)
                     }
                 val wc =
                     state.draggedTaskChange?.container
                         ?: error("Dragged task should be non-null before cancelling")
                 // Then the dragged task a the very top.
-                wct.reorder(wc, true /* toTop */)
+                wct.reorder(wc, /* onTop= */ true)
             }
             is TransitionState.FromSplit -> {
                 val wc =
                     state.splitRootChange?.container
                         ?: error("Split root should be non-null before cancelling")
-                wct.reorder(wc, true /* toTop */)
+                wct.reorder(wc, /* onTop= */ true)
             }
         }
         val homeWc =
@@ -765,9 +765,8 @@
         transitionState = null
     }
 
-    private fun isSplitTask(taskId: Int): Boolean {
-        return splitScreenController.isTaskInSplitScreen(taskId)
-    }
+    private fun isSplitTask(taskId: Int): Boolean =
+        splitScreenController.isTaskInSplitScreen(taskId)
 
     private fun getOtherSplitTask(taskId: Int): Int? {
         val splitPos = splitScreenController.getSplitPosition(taskId)
@@ -781,9 +780,8 @@
         return splitScreenController.getTaskInfo(otherTaskPos)?.taskId
     }
 
-    protected fun requireTransitionState(): TransitionState {
-        return transitionState ?: error("Expected non-null transition state")
-    }
+    protected fun requireTransitionState(): TransitionState =
+        transitionState ?: error("Expected non-null transition state")
 
     /**
      * Represents the layering (Z order) that will be given to any window based on its type during
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
index a47e937..5e84019 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
@@ -156,13 +156,11 @@
         return matchingChanges.first()
     }
 
-    private fun isWallpaper(change: TransitionInfo.Change): Boolean {
-        return (change.flags and TransitionInfo.FLAG_IS_WALLPAPER) != 0
-    }
+    private fun isWallpaper(change: TransitionInfo.Change): Boolean =
+        (change.flags and TransitionInfo.FLAG_IS_WALLPAPER) != 0
 
-    private fun isValidTaskChange(change: TransitionInfo.Change): Boolean {
-        return change.taskInfo != null && change.taskInfo?.taskId != -1
-    }
+    private fun isValidTaskChange(change: TransitionInfo.Change): Boolean =
+        change.taskInfo != null && change.taskInfo?.taskId != -1
 
     companion object {
         private const val RESIZE_DURATION_MS = 300L
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt
index a87004c..2bd7a98 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt
@@ -17,6 +17,7 @@
 package com.android.wm.shell.desktopmode.desktopwallpaperactivity
 
 import android.util.SparseArray
+import android.util.SparseBooleanArray
 import android.view.Display.DEFAULT_DISPLAY
 import android.window.WindowContainerToken
 
@@ -24,6 +25,7 @@
 class DesktopWallpaperActivityTokenProvider {
 
     private val wallpaperActivityTokenByDisplayId = SparseArray<WindowContainerToken>()
+    private val wallpaperActivityVisByDisplayId = SparseBooleanArray()
 
     fun setToken(token: WindowContainerToken, displayId: Int = DEFAULT_DISPLAY) {
         wallpaperActivityTokenByDisplayId[displayId] = token
@@ -36,4 +38,16 @@
     fun removeToken(displayId: Int = DEFAULT_DISPLAY) {
         wallpaperActivityTokenByDisplayId.delete(displayId)
     }
+
+    fun setWallpaperActivityIsVisible(
+        isVisible: Boolean = false,
+        displayId: Int = DEFAULT_DISPLAY,
+    ) {
+        wallpaperActivityVisByDisplayId.put(displayId, isVisible)
+    }
+
+    fun isWallpaperActivityVisible(displayId: Int = DEFAULT_DISPLAY): Boolean {
+        return wallpaperActivityTokenByDisplayId[displayId] != null &&
+            wallpaperActivityVisByDisplayId.get(displayId, false)
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
index 491b577..e24b2c5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
@@ -332,7 +332,9 @@
             dragSession = new DragSession(ActivityTaskManager.getInstance(),
                     mDisplayController.getDisplayLayout(displayId), event.getClipData(),
                     event.getDragFlags());
-            dragSession.initialize();
+            // Only update the running task for now to determine if we should defer to desktop to
+            // handle the drag
+            dragSession.updateRunningTask();
             final ActivityManager.RunningTaskInfo taskInfo = dragSession.runningTaskInfo;
             // Desktop tasks will have their own drag handling.
             final boolean isDesktopDrag = taskInfo != null && taskInfo.isFreeform()
@@ -340,7 +342,8 @@
             pd.isHandlingDrag = DragUtils.canHandleDrag(event) && !isDesktopDrag;
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
                     "Clip description: handlingDrag=%b itemCount=%d mimeTypes=%s flags=%s",
-                    pd.isHandlingDrag, event.getClipData().getItemCount(),
+                    pd.isHandlingDrag,
+                    event.getClipData() != null ? event.getClipData().getItemCount() : -1,
                     DragUtils.getMimeTypesConcatenated(description),
                     DragUtils.dragFlagsToString(event.getDragFlags()));
         }
@@ -355,6 +358,8 @@
                     Slog.w(TAG, "Unexpected drag start during an active drag");
                     return false;
                 }
+                // Only initialize the session after we've checked that we're handling the drag
+                dragSession.initialize(true /* skipUpdateRunningTask */);
                 pd.dragSession = dragSession;
                 pd.activeDragCount++;
                 pd.dragLayout.prepare(pd.dragSession, mLogger.logStart(pd.dragSession));
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java
index c4ff87d..279452e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java
@@ -29,7 +29,6 @@
 import android.content.ClipDescription;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
-import android.os.PersistableBundle;
 
 import androidx.annotation.Nullable;
 
@@ -44,6 +43,7 @@
  */
 public class DragSession {
     private final ActivityTaskManager mActivityTaskManager;
+    @Nullable
     private final ClipData mInitialDragData;
     private final int mInitialDragFlags;
 
@@ -66,7 +66,7 @@
     @WindowConfiguration.ActivityType
     int runningTaskActType = ACTIVITY_TYPE_STANDARD;
     boolean dragItemSupportsSplitscreen;
-    int hideDragSourceTaskId = -1;
+    final int hideDragSourceTaskId;
 
     DragSession(ActivityTaskManager activityTaskManager,
             DisplayLayout dispLayout, ClipData data, int dragFlags) {
@@ -83,7 +83,6 @@
 
     /**
      * Returns the clip description associated with the drag.
-     * @return
      */
     ClipDescription getClipDescription() {
         return mInitialDragData.getDescription();
@@ -125,8 +124,10 @@
     /**
      * Updates the session data based on the current state of the system at the start of the drag.
      */
-    void initialize() {
-        updateRunningTask();
+    void initialize(boolean skipUpdateRunningTask) {
+        if (!skipUpdateRunningTask) {
+            updateRunningTask();
+        }
 
         activityInfo = mInitialDragData.getItemAt(0).getActivityInfo();
         // TODO: This should technically check & respect config_supportsNonResizableMultiWindow
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java
index 248a112..4df9ae4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java
@@ -49,7 +49,7 @@
      * Returns whether we can handle this particular drag.
      */
     public static boolean canHandleDrag(DragEvent event) {
-        if (event.getClipData().getItemCount() <= 0) {
+        if (event.getClipData() == null || event.getClipData().getItemCount() <= 0) {
             // No clip data, ignore this drag
             return false;
         }
@@ -107,8 +107,11 @@
     /**
      * Returns a list of the mime types provided in the clip description.
      */
-    public static String getMimeTypesConcatenated(ClipDescription description) {
+    public static String getMimeTypesConcatenated(@Nullable ClipDescription description) {
         String mimeTypes = "";
+        if (description == null) {
+            return mimeTypes;
+        }
         for (int i = 0; i < description.getMimeTypeCount(); i++) {
             if (i > 0) {
                 mimeTypes += ", ";
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
index 8c6d5f5..562b260 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
@@ -59,6 +59,7 @@
 import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.common.pip.PipBoundsState;
 import com.android.wm.shell.common.pip.PipDisplayLayoutState;
+import com.android.wm.shell.common.pip.PipUiEventLogger;
 import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.pip.Pip;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
@@ -98,6 +99,7 @@
     private final PipTouchHandler mPipTouchHandler;
     private final PipAppOpsListener mPipAppOpsListener;
     private final PhonePipMenuController mPipMenuController;
+    private final PipUiEventLogger mPipUiEventLogger;
     private final ShellExecutor mMainExecutor;
     private final PipImpl mImpl;
     private final List<Consumer<Boolean>> mOnIsInPipStateChangedListeners = new ArrayList<>();
@@ -143,6 +145,7 @@
             PipTouchHandler pipTouchHandler,
             PipAppOpsListener pipAppOpsListener,
             PhonePipMenuController pipMenuController,
+            PipUiEventLogger pipUiEventLogger,
             ShellExecutor mainExecutor) {
         mContext = context;
         mShellCommandHandler = shellCommandHandler;
@@ -160,6 +163,7 @@
         mPipTouchHandler = pipTouchHandler;
         mPipAppOpsListener = pipAppOpsListener;
         mPipMenuController = pipMenuController;
+        mPipUiEventLogger = pipUiEventLogger;
         mMainExecutor = mainExecutor;
         mImpl = new PipImpl();
 
@@ -187,6 +191,7 @@
             PipTouchHandler pipTouchHandler,
             PipAppOpsListener pipAppOpsListener,
             PhonePipMenuController pipMenuController,
+            PipUiEventLogger pipUiEventLogger,
             ShellExecutor mainExecutor) {
         if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) {
             ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
@@ -197,7 +202,7 @@
                 displayController, displayInsetsController, pipBoundsState, pipBoundsAlgorithm,
                 pipDisplayLayoutState, pipScheduler, taskStackListener, shellTaskOrganizer,
                 pipTransitionState, pipTouchHandler, pipAppOpsListener, pipMenuController,
-                mainExecutor);
+                pipUiEventLogger, mainExecutor);
     }
 
     public PipImpl getPipImpl() {
@@ -238,18 +243,6 @@
         });
 
         mPipAppOpsListener.setCallback(mPipTouchHandler.getMotionHelper());
-        mPipTransitionState.addPipTransitionStateChangedListener(
-                (oldState, newState, extra) -> {
-                    if (newState == PipTransitionState.ENTERED_PIP) {
-                        final TaskInfo taskInfo = mPipTransitionState.getPipTaskInfo();
-                        if (taskInfo != null && taskInfo.topActivity != null) {
-                            mPipAppOpsListener.onActivityPinned(
-                                    taskInfo.topActivity.getPackageName());
-                        }
-                    } else if (newState == PipTransitionState.EXITED_PIP) {
-                        mPipAppOpsListener.onActivityUnpinned();
-                    }
-                });
     }
 
     private ExternalInterfaceBinder createExternalInterface() {
@@ -446,14 +439,25 @@
                 mPipTransitionState.setSwipePipToHomeState(overlay, appBounds);
                 break;
             case PipTransitionState.ENTERED_PIP:
+                final TaskInfo taskInfo = mPipTransitionState.getPipTaskInfo();
+                if (taskInfo != null && taskInfo.topActivity != null) {
+                    mPipAppOpsListener.onActivityPinned(taskInfo.topActivity.getPackageName());
+                    mPipUiEventLogger.setTaskInfo(taskInfo);
+                }
                 if (mPipTransitionState.isInSwipePipToHomeTransition()) {
+                    mPipUiEventLogger.log(
+                            PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_AUTO_ENTER);
                     mPipTransitionState.resetSwipePipToHomeState();
+                } else {
+                    mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_ENTER);
                 }
                 for (Consumer<Boolean> listener : mOnIsInPipStateChangedListeners) {
                     listener.accept(true /* inPip */);
                 }
                 break;
             case PipTransitionState.EXITED_PIP:
+                mPipAppOpsListener.onActivityUnpinned();
+                mPipUiEventLogger.setTaskInfo(null);
                 for (Consumer<Boolean> listener : mOnIsInPipStateChangedListeners) {
                     listener.accept(false /* inPip */);
                 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java
index 3729653..9babe9e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java
@@ -43,20 +43,20 @@
 import com.android.wm.shell.animation.FloatProperties;
 import com.android.wm.shell.common.FloatingContentCoordinator;
 import com.android.wm.shell.common.pip.PipAppOpsListener;
-import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.common.pip.PipBoundsState;
 import com.android.wm.shell.common.pip.PipPerfHintController;
 import com.android.wm.shell.common.pip.PipSnapAlgorithm;
+import com.android.wm.shell.common.pip.PipUiEventLogger;
 import com.android.wm.shell.pip2.animation.PipResizeAnimator;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.shared.animation.PhysicsAnimator;
 import com.android.wm.shell.shared.magnetictarget.MagnetizedObject;
 
+import java.util.Optional;
+
 import kotlin.Unit;
 import kotlin.jvm.functions.Function0;
 
-import java.util.Optional;
-
 /**
  * A helper to animate and manipulate the PiP.
  */
@@ -80,12 +80,12 @@
     private static final float DISMISS_CIRCLE_PERCENT = 0.85f;
 
     private final Context mContext;
-    private @NonNull PipBoundsState mPipBoundsState;
-    private @NonNull PipBoundsAlgorithm mPipBoundsAlgorithm;
-    private @NonNull PipScheduler mPipScheduler;
-    private @NonNull PipTransitionState mPipTransitionState;
-    private PhonePipMenuController mMenuController;
-    private PipSnapAlgorithm mSnapAlgorithm;
+    @NonNull private final PipBoundsState mPipBoundsState;
+    @NonNull private final PipScheduler mPipScheduler;
+    @NonNull private final PipTransitionState mPipTransitionState;
+    @NonNull private final PipUiEventLogger mPipUiEventLogger;
+    private final PhonePipMenuController mMenuController;
+    private final PipSnapAlgorithm mSnapAlgorithm;
 
     /** The region that all of PIP must stay within. */
     private final Rect mFloatingAllowedArea = new Rect();
@@ -168,10 +168,9 @@
             PhonePipMenuController menuController, PipSnapAlgorithm snapAlgorithm,
             FloatingContentCoordinator floatingContentCoordinator, PipScheduler pipScheduler,
             Optional<PipPerfHintController> pipPerfHintControllerOptional,
-            PipBoundsAlgorithm pipBoundsAlgorithm, PipTransitionState pipTransitionState) {
+            PipTransitionState pipTransitionState, PipUiEventLogger pipUiEventLogger) {
         mContext = context;
         mPipBoundsState = pipBoundsState;
-        mPipBoundsAlgorithm = pipBoundsAlgorithm;
         mPipScheduler = pipScheduler;
         mMenuController = menuController;
         mSnapAlgorithm = snapAlgorithm;
@@ -185,6 +184,7 @@
         };
         mPipTransitionState = pipTransitionState;
         mPipTransitionState.addPipTransitionStateChangedListener(this);
+        mPipUiEventLogger = pipUiEventLogger;
     }
 
     void init() {
@@ -850,9 +850,11 @@
         if (mPipBoundsState.getBounds().left < 0
                 && mPipBoundsState.getStashedState() != STASH_TYPE_LEFT) {
             mPipBoundsState.setStashed(STASH_TYPE_LEFT);
+            mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_LEFT);
         } else if (mPipBoundsState.getBounds().left >= 0
                 && mPipBoundsState.getStashedState() != STASH_TYPE_RIGHT) {
             mPipBoundsState.setStashed(STASH_TYPE_RIGHT);
+            mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_RIGHT);
         }
         mMenuController.hideMenu();
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java
index 7f673d2..ea8dac9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java
@@ -40,6 +40,7 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.pip.PipBoundsState;
 import com.android.wm.shell.desktopmode.DesktopUserRepositories;
+import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider;
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
 import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
@@ -59,6 +60,8 @@
     private final ShellExecutor mMainExecutor;
     private final PipTransitionState mPipTransitionState;
     private final Optional<DesktopUserRepositories> mDesktopUserRepositoriesOptional;
+    private final Optional<DesktopWallpaperActivityTokenProvider>
+            mDesktopWallpaperActivityTokenProviderOptional;
     private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
     private PipTransitionController mPipTransitionController;
     private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
@@ -73,12 +76,16 @@
             ShellExecutor mainExecutor,
             PipTransitionState pipTransitionState,
             Optional<DesktopUserRepositories> desktopUserRepositoriesOptional,
+            Optional<DesktopWallpaperActivityTokenProvider>
+                    desktopWallpaperActivityTokenProviderOptional,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
         mContext = context;
         mPipBoundsState = pipBoundsState;
         mMainExecutor = mainExecutor;
         mPipTransitionState = pipTransitionState;
         mDesktopUserRepositoriesOptional = desktopUserRepositoriesOptional;
+        mDesktopWallpaperActivityTokenProviderOptional =
+                desktopWallpaperActivityTokenProviderOptional;
         mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
 
         mSurfaceControlTransactionFactory =
@@ -260,10 +267,18 @@
 
     /** Returns whether PiP is exiting while we're in desktop mode. */
     private boolean isPipExitingToDesktopMode() {
-        return Flags.enableDesktopWindowingPip() && mDesktopUserRepositoriesOptional.isPresent()
-                && (mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount(
-                Objects.requireNonNull(mPipTransitionState.getPipTaskInfo()).displayId) > 0
-                || isDisplayInFreeform());
+        // Early return if PiP in Desktop Windowing is not supported.
+        if (!Flags.enableDesktopWindowingPip() || mDesktopUserRepositoriesOptional.isEmpty()
+                || mDesktopWallpaperActivityTokenProviderOptional.isEmpty()) {
+            return false;
+        }
+        final int displayId = Objects.requireNonNull(
+                mPipTransitionState.getPipTaskInfo()).displayId;
+        return mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount(displayId)
+                > 0
+                || mDesktopWallpaperActivityTokenProviderOptional.get().isWallpaperActivityVisible(
+                displayId)
+                || isDisplayInFreeform();
     }
 
     /**
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 8061ee9..38015ca 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
@@ -63,7 +63,9 @@
 import com.android.wm.shell.common.pip.PipMenuController;
 import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.common.split.SplitScreenUtils;
+import com.android.wm.shell.desktopmode.DesktopRepository;
 import com.android.wm.shell.desktopmode.DesktopUserRepositories;
+import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider;
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
 import com.android.wm.shell.pip2.animation.PipEnterAnimator;
@@ -110,6 +112,8 @@
     private final PipTransitionState mPipTransitionState;
     private final PipDisplayLayoutState mPipDisplayLayoutState;
     private final Optional<DesktopUserRepositories> mDesktopUserRepositoriesOptional;
+    private final Optional<DesktopWallpaperActivityTokenProvider>
+            mDesktopWallpaperActivityTokenProviderOptional;
 
     //
     // Transition caches
@@ -145,7 +149,9 @@
             PipTransitionState pipTransitionState,
             PipDisplayLayoutState pipDisplayLayoutState,
             PipUiStateChangeController pipUiStateChangeController,
-            Optional<DesktopUserRepositories> desktopUserRepositoriesOptional) {
+            Optional<DesktopUserRepositories> desktopUserRepositoriesOptional,
+            Optional<DesktopWallpaperActivityTokenProvider>
+                    desktopWallpaperActivityTokenProviderOptional) {
         super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController,
                 pipBoundsAlgorithm);
 
@@ -157,6 +163,8 @@
         mPipTransitionState.addPipTransitionStateChangedListener(this);
         mPipDisplayLayoutState = pipDisplayLayoutState;
         mDesktopUserRepositoriesOptional = desktopUserRepositoriesOptional;
+        mDesktopWallpaperActivityTokenProviderOptional =
+                desktopWallpaperActivityTokenProviderOptional;
     }
 
     @Override
@@ -826,13 +834,14 @@
             return false;
         }
 
-
         // Since opening a new task while in Desktop Mode always first open in Fullscreen
         // until DesktopMode Shell code resolves it to Freeform, PipTransition will get a
         // possibility to handle it also. In this case return false to not have it enter PiP.
         final boolean isInDesktopSession = !mDesktopUserRepositoriesOptional.isEmpty()
-                && mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount(
-                pipTask.displayId) > 0;
+                && (mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount(
+                        pipTask.displayId) > 0
+                    || mDesktopUserRepositoriesOptional.get().getCurrent()
+                        .isMinimizedPipPresentInDisplay(pipTask.displayId));
         if (isInDesktopSession) {
             return false;
         }
@@ -968,6 +977,27 @@
                         "Unexpected bundle for " + mPipTransitionState);
                 break;
             case PipTransitionState.EXITED_PIP:
+                final TaskInfo pipTask = mPipTransitionState.getPipTaskInfo();
+                final boolean desktopPipEnabled = Flags.enableDesktopWindowingPip()
+                        && mDesktopUserRepositoriesOptional.isPresent()
+                        && mDesktopWallpaperActivityTokenProviderOptional.isPresent();
+                if (desktopPipEnabled && pipTask != null) {
+                    final DesktopRepository desktopRepository =
+                            mDesktopUserRepositoriesOptional.get().getCurrent();
+                    final boolean wallpaperIsVisible =
+                            mDesktopWallpaperActivityTokenProviderOptional.get()
+                                    .isWallpaperActivityVisible(pipTask.displayId);
+                    if (desktopRepository.getVisibleTaskCount(pipTask.displayId) == 0
+                            && wallpaperIsVisible) {
+                        mTransitions.startTransition(
+                                TRANSIT_TO_BACK,
+                                new WindowContainerTransaction().reorder(
+                                        mDesktopWallpaperActivityTokenProviderOptional.get()
+                                                .getToken(pipTask.displayId), /* onTop= */ false),
+                                null
+                        );
+                    }
+                }
                 mPipTransitionState.setPinnedTaskLeash(null);
                 mPipTransitionState.setPipTaskInfo(null);
                 break;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationController.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationController.aidl
index 964e5fd..af1679f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationController.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationController.aidl
@@ -36,12 +36,6 @@
 interface IRecentsAnimationController {
 
     /**
-     * Takes a screenshot of the task associated with the given {@param taskId}. Only valid for the
-     * current set of task ids provided to the handler.
-     */
-    TaskSnapshot screenshotTask(int taskId);
-
-    /**
      * Sets the final surface transaction on a Task. This is used by Launcher to notify the system
      * that animating Activity to PiP has completed and the associated task surface should be
      * updated accordingly. This should be called before `finish`
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl
index 32c79a2..8cdb8c4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl
@@ -17,9 +17,10 @@
 package com.android.wm.shell.recents;
 
 import android.graphics.Rect;
+import android.os.Bundle;
 import android.view.RemoteAnimationTarget;
 import android.window.TaskSnapshot;
-import android.os.Bundle;
+import android.window.TransitionInfo;
 
 import com.android.wm.shell.recents.IRecentsAnimationController;
 
@@ -57,7 +58,8 @@
      */
     void onAnimationStart(in IRecentsAnimationController controller,
             in RemoteAnimationTarget[] apps, in RemoteAnimationTarget[] wallpapers,
-            in Rect homeContentInsets, in Rect minimizedHomeBounds, in Bundle extras) = 2;
+            in Rect homeContentInsets, in Rect minimizedHomeBounds, in Bundle extras,
+            in TransitionInfo info) = 2;
 
     /**
      * Called when the task of an activity that has been started while the recents animation
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index 032dac9..aeccd86 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -411,10 +411,12 @@
             mInstanceId = System.identityHashCode(this);
             mListener = listener;
             mDeathHandler = () -> {
-                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
-                        "[%d] RecentsController.DeathRecipient: binder died", mInstanceId);
-                finishInner(mWillFinishToHome, false /* leaveHint */, null /* finishCb */,
-                        "deathRecipient");
+                mExecutor.execute(() -> {
+                    ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+                            "[%d] RecentsController.DeathRecipient: binder died", mInstanceId);
+                    finishInner(mWillFinishToHome, false /* leaveHint */, null /* finishCb */,
+                            "deathRecipient");
+                });
             };
             try {
                 mListener.asBinder().linkToDeath(mDeathHandler, 0 /* flags */);
@@ -585,7 +587,8 @@
                 mListener.onAnimationStart(this,
                         apps.toArray(new RemoteAnimationTarget[apps.size()]),
                         new RemoteAnimationTarget[0],
-                        new Rect(0, 0, 0, 0), new Rect(), new Bundle());
+                        new Rect(0, 0, 0, 0), new Rect(), new Bundle(),
+                        null);
                 for (int i = 0; i < mStateListeners.size(); i++) {
                     mStateListeners.get(i).onTransitionStateChanged(TRANSITION_STATE_ANIMATING);
                 }
@@ -816,7 +819,7 @@
                 mListener.onAnimationStart(this,
                         apps.toArray(new RemoteAnimationTarget[apps.size()]),
                         wallpapers.toArray(new RemoteAnimationTarget[wallpapers.size()]),
-                        new Rect(0, 0, 0, 0), new Rect(), b);
+                        new Rect(0, 0, 0, 0), new Rect(), b, info);
                 for (int i = 0; i < mStateListeners.size(); i++) {
                     mStateListeners.get(i).onTransitionStateChanged(TRANSITION_STATE_ANIMATING);
                 }
@@ -1227,19 +1230,6 @@
         }
 
         @Override
-        public TaskSnapshot screenshotTask(int taskId) {
-            try {
-                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
-                        "[%d] RecentsController.screenshotTask: taskId=%d", mInstanceId, taskId);
-                return ActivityTaskManager.getService().takeTaskSnapshot(taskId,
-                        true /* updateCache */);
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Failed to screenshot task", e);
-            }
-            return null;
-        }
-
-        @Override
         public void setInputConsumerEnabled(boolean enabled) {
             mExecutor.execute(() -> {
                 if (mFinishCB == null || !enabled) {
@@ -1286,6 +1276,11 @@
                     "requested"));
         }
 
+        /**
+         * @param runnerFinishCb The remote finish callback to run after finish is complete, this is
+         *                       not the same as mFinishCb which reports the transition is finished
+         *                       to WM.
+         */
         private void finishInner(boolean toHome, boolean sendUserLeaveHint,
                 IResultReceiver runnerFinishCb, String reason) {
             if (finishSyntheticTransition(runnerFinishCb, reason)) {
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 90c5917..b6bd879 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
@@ -2166,7 +2166,8 @@
         wct.setForceTranslucent(mRootTaskInfo.token, translucent);
     }
 
-    /** Callback when split roots visiblility changed. */
+    /** Callback when split roots visiblility changed.
+     * NOTICE: This only be called on legacy transition. */
     @Override
     public void onStageVisibilityChanged(StageTaskListener stageListener) {
         // If split didn't active, just ignore this callback because we should already did these
@@ -2973,9 +2974,11 @@
             final int transitType = info.getType();
             TransitionInfo.Change pipChange = null;
             int closingSplitTaskId = -1;
-            // This array tracks if we are sending stages TO_BACK in this transition.
-            // TODO (b/349828130): Update for n apps
-            boolean[] stagesSentToBack = new boolean[2];
+            // This array tracks where we are sending stages (TO_BACK/TO_FRONT) in this transition.
+            // TODO (b/349828130): Update for n apps (needs to handle different indices than 0/1).
+            //  Also make sure having multiple changes per stage (2+ tasks in one stage) is being
+            //  handled properly.
+            int[] stageChanges = new int[2];
 
             for (int iC = 0; iC < info.getChanges().size(); ++iC) {
                 final TransitionInfo.Change change = info.getChanges().get(iC);
@@ -3039,18 +3042,25 @@
                                 + " with " + taskId + " before startAnimation().");
                     }
                 }
-                if (isClosingType(change.getMode()) &&
-                        getStageOfTask(taskId) != STAGE_TYPE_UNDEFINED) {
 
-                    // Record which stages are getting sent to back
-                    if (change.getMode() == TRANSIT_TO_BACK) {
-                        stagesSentToBack[getStageOfTask(taskId)] = true;
-                    }
-
+                final int stageOfTaskId = getStageOfTask(taskId);
+                if (stageOfTaskId == STAGE_TYPE_UNDEFINED) {
+                    continue;
+                }
+                if (isClosingType(change.getMode())) {
                     // (For PiP transitions) If either one of the 2 stages is closing we're assuming
                     // we'll break split
                     closingSplitTaskId = taskId;
                 }
+                if (transitType == WindowManager.TRANSIT_WAKE) {
+                    // Record which stages are receiving which changes
+                    if ((change.getMode() == TRANSIT_TO_BACK
+                            || change.getMode() == TRANSIT_TO_FRONT)
+                            && (stageOfTaskId == STAGE_TYPE_MAIN
+                            || stageOfTaskId == STAGE_TYPE_SIDE)) {
+                        stageChanges[stageOfTaskId] = change.getMode();
+                    }
+                }
             }
 
             if (pipChange != null) {
@@ -3075,19 +3085,11 @@
                 return true;
             }
 
-            // If keyguard is active, check to see if we have our TO_BACK transitions in order.
-            // This array should either be all false (no split stages sent to back) or all true
-            // (all stages sent to back). In any other case (which can happen with SHOW_ABOVE_LOCKED
-            // apps) we should break split.
-            if (mKeyguardActive) {
-                boolean isFirstStageSentToBack = stagesSentToBack[0];
-                for (boolean b : stagesSentToBack) {
-                    // Compare each boolean to the first one. If any are different, break split.
-                    if (b != isFirstStageSentToBack) {
-                        dismissSplitKeepingLastActiveStage(EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP);
-                        break;
-                    }
-                }
+            // If keyguard is active, check to see if we have all our stages showing. If one stage
+            // was moved but not the other (which can happen with SHOW_ABOVE_LOCKED apps), we should
+            // break split.
+            if (mKeyguardActive && stageChanges[STAGE_TYPE_MAIN] != stageChanges[STAGE_TYPE_SIDE]) {
+                dismissSplitKeepingLastActiveStage(EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP);
             }
 
             final ArraySet<StageTaskListener> dismissStages = record.getShouldDismissedStage();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
index bfe7412..021f659 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
@@ -240,20 +240,12 @@
     @Override
     @CallSuper
     public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
-        ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
-                "onTaskInfoChanged: taskId=%d vis=%b reqVis=%b baseAct=%s stageId=%s",
-                taskInfo.taskId, taskInfo.isVisible, taskInfo.isVisibleRequested,
-                taskInfo.baseActivity, stageTypeToString(mId));
+        ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskInfoChanged: taskId=%d taskAct=%s "
+                        + "stageId=%s",
+                taskInfo.taskId, taskInfo.baseActivity, stageTypeToString(mId));
         mWindowDecorViewModel.ifPresent(viewModel -> viewModel.onTaskInfoChanged(taskInfo));
         if (mRootTaskInfo.taskId == taskInfo.taskId) {
             mRootTaskInfo = taskInfo;
-            boolean isVisible = taskInfo.isVisible && taskInfo.isVisibleRequested;
-            if (mVisible != isVisible) {
-                ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskInfoChanged: currentVis=%b newVis=%b",
-                        mVisible, isVisible);
-                mVisible = isVisible;
-                mCallbacks.onStageVisibilityChanged(this);
-            }
         } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) {
             if (!taskInfo.supportsMultiWindow
                     || !ArrayUtils.contains(CONTROLLED_ACTIVITY_TYPES, taskInfo.getActivityType())
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java
index d8884f6..f5aaaad 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java
@@ -33,6 +33,7 @@
 import com.android.wm.shell.shared.TransactionPool;
 
 import java.util.ArrayList;
+import java.util.function.Consumer;
 
 public class DefaultSurfaceAnimator {
 
@@ -58,42 +59,12 @@
         // Animation length is already expected to be scaled.
         va.overrideDurationScale(1.0f);
         va.setDuration(anim.computeDurationHint());
-        va.addUpdateListener(updateListener);
-        va.addListener(new AnimatorListenerAdapter() {
-            // It is possible for the end/cancel to be called more than once, which may cause
-            // issues if the animating surface has already been released. Track the finished
-            // state here to skip duplicate callbacks. See b/252872225.
-            private boolean mFinished;
-
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                onFinish();
-            }
-
-            @Override
-            public void onAnimationCancel(Animator animation) {
-                onFinish();
-            }
-
-            private void onFinish() {
-                if (mFinished) return;
-                mFinished = true;
-                // Apply transformation of end state in case the animation is canceled.
-                if (va.getAnimatedFraction() < 1f) {
-                    va.setCurrentFraction(1f);
-                }
-
-                pool.release(transaction);
-                mainExecutor.execute(() -> {
-                    animations.remove(va);
-                    finishCallback.run();
-                });
-                // The update listener can continue to be called after the animation has ended if
-                // end() is called manually again before the finisher removes the animation.
-                // Remove it manually here to prevent animating a released surface.
-                // See b/252872225.
-                va.removeUpdateListener(updateListener);
-            }
+        setupValueAnimator(va, updateListener, (vanim) -> {
+            pool.release(transaction);
+            mainExecutor.execute(() -> {
+                animations.remove(vanim);
+                finishCallback.run();
+            });
         });
         animations.add(va);
     }
@@ -188,4 +159,50 @@
             }
         }
     }
+
+    /**
+     * Setup some callback logic on a value-animator. This helper ensures that a value animator
+     * finishes at its final fraction (1f) and that relevant callbacks are only called once.
+     */
+    public static ValueAnimator setupValueAnimator(ValueAnimator animator,
+            ValueAnimator.AnimatorUpdateListener updateListener,
+            Consumer<ValueAnimator> afterFinish) {
+        animator.addUpdateListener(updateListener);
+        animator.addListener(new AnimatorListenerAdapter() {
+            // It is possible for the end/cancel to be called more than once, which may cause
+            // issues if the animating surface has already been released. Track the finished
+            // state here to skip duplicate callbacks. See b/252872225.
+            private boolean mFinished;
+
+            @Override
+            public void onAnimationStart(Animator animation) {
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                onFinish();
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                onFinish();
+            }
+
+            private void onFinish() {
+                if (mFinished) return;
+                mFinished = true;
+                // Apply transformation of end state in case the animation is canceled.
+                if (animator.getAnimatedFraction() < 1f) {
+                    animator.setCurrentFraction(1f);
+                }
+                afterFinish.accept(animator);
+                // The update listener can continue to be called after the animation has ended if
+                // end() is called manually again before the finisher removes the animation.
+                // Remove it manually here to prevent animating a released surface.
+                // See b/252872225.
+                animator.removeUpdateListener(updateListener);
+            }
+        });
+        return animator;
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 1689bb5..36c3e97 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -55,6 +55,7 @@
 import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 
+import static com.android.internal.policy.TransitionAnimation.DEFAULT_APP_TRANSITION_DURATION;
 import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CHANGE;
 import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE;
 import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_CLOSE;
@@ -69,6 +70,7 @@
 import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation;
 
 import android.animation.Animator;
+import android.animation.ValueAnimator;
 import android.annotation.ColorInt;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -104,6 +106,7 @@
 import com.android.internal.protolog.ProtoLog;
 import com.android.window.flags.Flags;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
+import com.android.wm.shell.animation.SizeChangeAnimation;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.ShellExecutor;
@@ -422,6 +425,14 @@
                             ROTATION_ANIMATION_ROTATE, 0 /* flags */, animations, onAnimFinish);
                     continue;
                 }
+
+                if (Flags.portWindowSizeAnimation() && isTask
+                        && TransitionInfo.isIndependent(change, info)
+                        && change.getSnapshot() != null) {
+                    startBoundsChangeAnimation(startTransaction, animations, change, onAnimFinish,
+                            mMainExecutor);
+                    continue;
+                }
             }
 
             // Hide the invisible surface directly without animating it if there is a display
@@ -734,6 +745,21 @@
         }
     }
 
+    private void startBoundsChangeAnimation(@NonNull SurfaceControl.Transaction startT,
+            @NonNull ArrayList<Animator> animations, @NonNull TransitionInfo.Change change,
+            @NonNull Runnable finishCb, @NonNull ShellExecutor mainExecutor) {
+        final SizeChangeAnimation sca =
+                new SizeChangeAnimation(change.getStartAbsBounds(), change.getEndAbsBounds());
+        sca.initialize(change.getLeash(), change.getSnapshot(), startT);
+        final ValueAnimator va = sca.buildAnimator(change.getLeash(), change.getSnapshot(),
+                (animator) -> mainExecutor.execute(() -> {
+                    animations.remove(animator);
+                    finishCb.run();
+                }));
+        va.setDuration(DEFAULT_APP_TRANSITION_DURATION);
+        animations.add(va);
+    }
+
     @Nullable
     @Override
     public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
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 9fbda46..429e056 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
@@ -126,6 +126,8 @@
 import com.android.wm.shell.desktopmode.education.AppHandleEducationController;
 import com.android.wm.shell.desktopmode.education.AppToWebEducationController;
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+import com.android.wm.shell.recents.RecentsTransitionHandler;
+import com.android.wm.shell.recents.RecentsTransitionStateListener;
 import com.android.wm.shell.shared.FocusTransitionListener;
 import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
 import com.android.wm.shell.shared.annotations.ShellMainThread;
@@ -157,8 +159,10 @@
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 import java.util.function.Supplier;
 
 /**
@@ -247,6 +251,7 @@
     private final DesktopModeEventLogger mDesktopModeEventLogger;
     private final DesktopModeUiEventLogger mDesktopModeUiEventLogger;
     private final WindowDecorTaskResourceLoader mTaskResourceLoader;
+    private final RecentsTransitionHandler mRecentsTransitionHandler;
 
     public DesktopModeWindowDecorViewModel(
             Context context,
@@ -282,7 +287,8 @@
             FocusTransitionObserver focusTransitionObserver,
             DesktopModeEventLogger desktopModeEventLogger,
             DesktopModeUiEventLogger desktopModeUiEventLogger,
-            WindowDecorTaskResourceLoader taskResourceLoader) {
+            WindowDecorTaskResourceLoader taskResourceLoader,
+            RecentsTransitionHandler recentsTransitionHandler) {
         this(
                 context,
                 shellExecutor,
@@ -323,7 +329,8 @@
                 focusTransitionObserver,
                 desktopModeEventLogger,
                 desktopModeUiEventLogger,
-                taskResourceLoader);
+                taskResourceLoader,
+                recentsTransitionHandler);
     }
 
     @VisibleForTesting
@@ -367,7 +374,8 @@
             FocusTransitionObserver focusTransitionObserver,
             DesktopModeEventLogger desktopModeEventLogger,
             DesktopModeUiEventLogger desktopModeUiEventLogger,
-            WindowDecorTaskResourceLoader taskResourceLoader) {
+            WindowDecorTaskResourceLoader taskResourceLoader,
+            RecentsTransitionHandler recentsTransitionHandler) {
         mContext = context;
         mMainExecutor = shellExecutor;
         mMainHandler = mainHandler;
@@ -436,6 +444,7 @@
         mDesktopModeEventLogger = desktopModeEventLogger;
         mDesktopModeUiEventLogger = desktopModeUiEventLogger;
         mTaskResourceLoader = taskResourceLoader;
+        mRecentsTransitionHandler = recentsTransitionHandler;
 
         shellInit.addInitCallback(this::onInit, this);
     }
@@ -450,6 +459,10 @@
                 new DesktopModeOnTaskResizeAnimationListener());
         mDesktopTasksController.setOnTaskRepositionAnimationListener(
                 new DesktopModeOnTaskRepositionAnimationListener());
+        if (Flags.enableDesktopRecentsTransitionsCornersBugfix()) {
+            mRecentsTransitionHandler.addTransitionStateListener(
+                    new DesktopModeRecentsTransitionStateListener());
+        }
         mDisplayController.addDisplayChangingController(mOnDisplayChangingListener);
         try {
             mWindowManager.registerSystemGestureExclusionListener(mGestureExclusionListener,
@@ -1859,6 +1872,38 @@
         }
     }
 
+    private class DesktopModeRecentsTransitionStateListener
+            implements RecentsTransitionStateListener {
+        final Set<Integer> mAnimatingTaskIds = new HashSet<>();
+
+        @Override
+        public void onTransitionStateChanged(int state) {
+            switch (state) {
+                case RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED:
+                    for (int n = 0; n < mWindowDecorByTaskId.size(); n++) {
+                        int taskId = mWindowDecorByTaskId.keyAt(n);
+                        mAnimatingTaskIds.add(taskId);
+                        setIsRecentsTransitionRunningForTask(taskId, true);
+                    }
+                    return;
+                case RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING:
+                    // No Recents transition running - clean up window decorations
+                    for (int taskId : mAnimatingTaskIds) {
+                        setIsRecentsTransitionRunningForTask(taskId, false);
+                    }
+                    mAnimatingTaskIds.clear();
+                    return;
+                default:
+            }
+        }
+
+        private void setIsRecentsTransitionRunningForTask(int taskId, boolean isRecentsRunning) {
+            final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
+            if (decoration == null) return;
+            decoration.setIsRecentsTransitionRunning(isRecentsRunning);
+        }
+    }
+
     private class DragEventListenerImpl
             implements DragPositioningCallbackUtility.DragEventListener {
         @Override
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 4ac8954..39a989c 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
@@ -204,6 +204,7 @@
     private final MultiInstanceHelper mMultiInstanceHelper;
     private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository;
     private final DesktopUserRepositories mDesktopUserRepositories;
+    private boolean mIsRecentsTransitionRunning = false;
 
     private Runnable mLoadAppInfoRunnable;
     private Runnable mSetAppInfoRunnable;
@@ -498,7 +499,7 @@
                 applyStartTransactionOnDraw, shouldSetTaskVisibilityPositionAndCrop,
                 mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded, inFullImmersive,
                 mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus,
-                displayExclusionRegion);
+                displayExclusionRegion, mIsRecentsTransitionRunning);
 
         final WindowDecorLinearLayout oldRootView = mResult.mRootView;
         final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
@@ -869,7 +870,8 @@
             boolean inFullImmersiveMode,
             @NonNull InsetsState displayInsetsState,
             boolean hasGlobalFocus,
-            @NonNull Region displayExclusionRegion) {
+            @NonNull Region displayExclusionRegion,
+            boolean shouldIgnoreCornerRadius) {
         final int captionLayoutId = getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode());
         final boolean isAppHeader =
                 captionLayoutId == R.layout.desktop_mode_app_header;
@@ -1006,13 +1008,19 @@
         relayoutParams.mWindowDecorConfig = windowDecorConfig;
 
         if (DesktopModeStatus.useRoundedCorners()) {
-            relayoutParams.mCornerRadius = taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
-                    ? loadDimensionPixelSize(context.getResources(),
-                    R.dimen.desktop_windowing_freeform_rounded_corner_radius)
-                    : INVALID_CORNER_RADIUS;
+            relayoutParams.mCornerRadius = shouldIgnoreCornerRadius ? INVALID_CORNER_RADIUS :
+                    getCornerRadius(context, relayoutParams.mLayoutResId);
         }
     }
 
+    private static int getCornerRadius(@NonNull Context context, int layoutResId) {
+        if (layoutResId == R.layout.desktop_mode_app_header) {
+            return loadDimensionPixelSize(context.getResources(),
+                    R.dimen.desktop_windowing_freeform_rounded_corner_radius);
+        }
+        return INVALID_CORNER_RADIUS;
+    }
+
     /**
      * If task has focused window decor, return the caption id of the fullscreen caption size
      * resource. Otherwise, return ID_NULL and caption width be set to task width.
@@ -1740,6 +1748,17 @@
     }
 
     /**
+     * Declares whether a Recents transition is currently active.
+     *
+     * <p> When a Recents transition is active we allow that transition to take ownership of the
+     * corner radius of its task surfaces, so each window decoration should stop updating the corner
+     * radius of its task surface during that time.
+     */
+    void setIsRecentsTransitionRunning(boolean isRecentsTransitionRunning) {
+        mIsRecentsTransitionRunning = isRecentsTransitionRunning;
+    }
+
+    /**
      * Called when there is a {@link MotionEvent#ACTION_HOVER_EXIT} on the maximize window button.
      */
     void onMaximizeButtonHoverExit() {
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 5d1bedb..fa7183ad 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
@@ -967,4 +967,4 @@
             return Objects.hash(mToken, mOwner, mFrame, Arrays.hashCode(mBoundingRects), mFlags);
         }
     }
-}
\ No newline at end of file
+}
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 3f65d93..1264c01 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
@@ -231,6 +231,7 @@
     fun disposeStatusBarInputLayer() {
         if (!statusBarInputLayerExists) return
         statusBarInputLayerExists = false
+        statusBarInputLayer?.view?.setOnTouchListener(null)
         handler.post {
             statusBarInputLayer?.releaseView()
             statusBarInputLayer = null
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt
index 636549f..a6f8150 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt
@@ -176,7 +176,6 @@
      * transition
      */
     @Ignore("TODO(b/356277166): enable the tablet test")
-    @Postsubmit
     @Test
     open fun pipAppLayerPlusLetterboxCoversFullScreenOnStartTablet() {
         assumeTrue(tapl.isTablet)
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
index 40ecdec..805f4c2 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
@@ -67,6 +67,7 @@
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 @RequiresFlagsDisabled(Flags.FLAG_ENABLE_PIP2)
+@FlakyTest(bugId = 386333280)
 open class FromSplitScreenEnterPipOnUserLeaveHintTest(flicker: LegacyFlickerTest) :
     EnterPipTransition(flicker) {
     override val pipApp: PipAppHelper = PipAppHelper(instrumentation)
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt
index 4987ab7..d65f158 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt
@@ -78,7 +78,6 @@
     }
 
     @Ignore("TODO(b/356277166): enable the tablet test")
-    @Presubmit
     @Test
     override fun pipAppLayerPlusLetterboxCoversFullScreenOnStartTablet() {
         // Test app and pip app should covers the entire screen on start.
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/appzoomout/AppZoomOutControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/appzoomout/AppZoomOutControllerTest.java
new file mode 100644
index 0000000..e91a123
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/appzoomout/AppZoomOutControllerTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2025 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.appzoomout;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.testing.AndroidTestingRunner;
+import android.view.Display;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+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.sysui.ShellInit;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class AppZoomOutControllerTest extends ShellTestCase {
+
+    @Mock private ShellTaskOrganizer mTaskOrganizer;
+    @Mock private DisplayController mDisplayController;
+    @Mock private AppZoomOutDisplayAreaOrganizer mDisplayAreaOrganizer;
+    @Mock private ShellExecutor mExecutor;
+    @Mock private ActivityManager.RunningTaskInfo mRunningTaskInfo;
+
+    private AppZoomOutController mController;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        Display display = mContext.getDisplay();
+        DisplayLayout displayLayout = new DisplayLayout(mContext, display);
+        when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(displayLayout);
+
+        ShellInit shellInit = spy(new ShellInit(mExecutor));
+        mController = spy(new AppZoomOutController(mContext, shellInit, mTaskOrganizer,
+                mDisplayController, mDisplayAreaOrganizer, mExecutor));
+    }
+
+    @Test
+    public void isHomeTaskFocused_zoomOutForHome() {
+        mRunningTaskInfo.isFocused = true;
+        when(mRunningTaskInfo.getActivityType()).thenReturn(ACTIVITY_TYPE_HOME);
+        mController.onFocusTaskChanged(mRunningTaskInfo);
+
+        verify(mDisplayAreaOrganizer).setIsHomeTaskFocused(true);
+    }
+
+    @Test
+    public void isHomeTaskNotFocused_zoomOutForApp() {
+        mRunningTaskInfo.isFocused = false;
+        when(mRunningTaskInfo.getActivityType()).thenReturn(ACTIVITY_TYPE_HOME);
+        mController.onFocusTaskChanged(mRunningTaskInfo);
+
+        verify(mDisplayAreaOrganizer).setIsHomeTaskFocused(false);
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index 47ee7bb..bbdb90f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -61,9 +61,7 @@
 import android.os.RemoteException;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
-import android.provider.Settings;
 import android.testing.AndroidTestingRunner;
-import android.testing.TestableContentResolver;
 import android.testing.TestableLooper;
 import android.view.IRemoteAnimationRunner;
 import android.view.KeyEvent;
@@ -84,7 +82,6 @@
 import androidx.annotation.Nullable;
 import androidx.test.filters.SmallTest;
 
-import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestShellExecutor;
@@ -109,7 +106,6 @@
 @RunWith(AndroidTestingRunner.class)
 public class BackAnimationControllerTest extends ShellTestCase {
 
-    private static final String ANIMATION_ENABLED = "1";
     private final TestShellExecutor mShellExecutor = new TestShellExecutor();
 
     private ShellInit mShellInit;
@@ -148,8 +144,6 @@
     private Transitions.TransitionHandler mTakeoverHandler;
 
     private BackAnimationController mController;
-    private TestableContentResolver mContentResolver;
-    private TestableLooper mTestableLooper;
 
     private DefaultCrossActivityBackAnimation mDefaultCrossActivityBackAnimation;
     private CrossTaskBackAnimation mCrossTaskBackAnimation;
@@ -166,11 +160,6 @@
         MockitoAnnotations.initMocks(this);
         mContext.addMockSystemService(InputManager.class, mInputManager);
         mContext.getApplicationInfo().privateFlags |= ApplicationInfo.PRIVATE_FLAG_PRIVILEGED;
-        mContentResolver = new TestableContentResolver(mContext);
-        mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
-        Settings.Global.putString(mContentResolver, Settings.Global.ENABLE_BACK_ANIMATION,
-                ANIMATION_ENABLED);
-        mTestableLooper = TestableLooper.get(this);
         mShellInit = spy(new ShellInit(mShellExecutor));
         mDefaultCrossActivityBackAnimation = new DefaultCrossActivityBackAnimation(mContext,
                 mAnimationBackground, mRootTaskDisplayAreaOrganizer, mHandler);
@@ -187,10 +176,8 @@
                         mShellInit,
                         mShellController,
                         mShellExecutor,
-                        new Handler(mTestableLooper.getLooper()),
                         mActivityTaskManager,
                         mContext,
-                        mContentResolver,
                         mAnimationBackground,
                         mShellBackAnimationRegistry,
                         mShellCommandHandler,
@@ -342,47 +329,6 @@
     }
 
     @Test
-    public void animationDisabledFromSettings() throws RemoteException {
-        // Toggle the setting off
-        Settings.Global.putString(mContentResolver, Settings.Global.ENABLE_BACK_ANIMATION, "0");
-        ShellInit shellInit = new ShellInit(mShellExecutor);
-        mController =
-                new BackAnimationController(
-                        shellInit,
-                        mShellController,
-                        mShellExecutor,
-                        new Handler(mTestableLooper.getLooper()),
-                        mActivityTaskManager,
-                        mContext,
-                        mContentResolver,
-                        mAnimationBackground,
-                        mShellBackAnimationRegistry,
-                        mShellCommandHandler,
-                        mTransitions,
-                        mHandler);
-        shellInit.init();
-        registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
-
-        ArgumentCaptor<BackMotionEvent> backEventCaptor =
-                ArgumentCaptor.forClass(BackMotionEvent.class);
-
-        createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME,
-                /* enableAnimation = */ false,
-                /* isAnimationCallback = */ false);
-
-        triggerBackGesture();
-        releaseBackGesture();
-
-        verify(mAppCallback, times(1)).onBackInvoked();
-
-        verify(mAnimatorCallback, never()).onBackStarted(any());
-        verify(mAnimatorCallback, never()).onBackProgressed(backEventCaptor.capture());
-        verify(mAnimatorCallback, never()).onBackInvoked();
-        verify(mBackAnimationRunner, never()).onAnimationStart(
-                anyInt(), any(), any(), any(), any());
-    }
-
-    @Test
     public void gestureQueued_WhenPreviousTransitionHasNotYetEnded() throws RemoteException {
         registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
         createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java
index 1e5e153..d3de0f7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java
@@ -22,6 +22,7 @@
 import static org.mockito.Mockito.verify;
 
 import android.content.Context;
+import android.hardware.display.DisplayManager;
 import android.view.IWindowManager;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -50,12 +51,14 @@
     private @Mock IWindowManager mWM;
     private @Mock ShellInit mShellInit;
     private @Mock ShellExecutor mMainExecutor;
+    private @Mock DisplayManager mDisplayManager;
     private DisplayController mController;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mController = new DisplayController(mContext, mWM, mShellInit, mMainExecutor);
+        mController = new DisplayController(
+                mContext, mWM, mShellInit, mMainExecutor, mDisplayManager);
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java
index d467b39..b0a455d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java
@@ -33,7 +33,9 @@
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Insets;
+import android.graphics.PointF;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.view.DisplayCutout;
 import android.view.DisplayInfo;
 
@@ -58,6 +60,7 @@
 @SmallTest
 public class DisplayLayoutTest extends ShellTestCase {
     private MockitoSession mMockitoSession;
+    private static final float DELTA = 0.1f; // Constant for assertion delta
 
     @Before
     public void setup() {
@@ -130,6 +133,39 @@
         assertEquals(new Rect(40, 0, 60, 0), dl.nonDecorInsets());
     }
 
+    @Test
+    public void testDpPxConversion() {
+        int px = 100;
+        float dp = 53.33f;
+        int xPx = 100;
+        int yPx = 200;
+        float xDp = 164.33f;
+        float yDp = 328.66f;
+
+        Resources res = createResources(40, 50, false);
+        DisplayInfo info = createDisplayInfo(1000, 1500, 0, ROTATION_0);
+        DisplayLayout dl = new DisplayLayout(info, res, false, false);
+        dl.setGlobalBoundsDp(new RectF(111f, 222f, 300f, 400f));
+
+        // Test pxToDp
+        float resultDp = dl.pxToDp(px);
+        assertEquals(dp, resultDp, DELTA);
+
+        // Test dpToPx
+        float resultPx = dl.dpToPx(dp);
+        assertEquals(px, resultPx, DELTA);
+
+        // Test localPxToGlobalDp
+        PointF resultGlobalDp = dl.localPxToGlobalDp(xPx, yPx);
+        assertEquals(xDp, resultGlobalDp.x, DELTA);
+        assertEquals(yDp, resultGlobalDp.y, DELTA);
+
+        // Test globalDpToLocalPx
+        PointF resultLocalPx = dl.globalDpToLocalPx(xDp, yDp);
+        assertEquals(xPx, resultLocalPx.x, DELTA);
+        assertEquals(yPx, resultLocalPx.y, DELTA);
+    }
+
     private Resources createResources(int navLand, int navPort, boolean navMoves) {
         Configuration cfg = new Configuration();
         cfg.uiMode = UI_MODE_TYPE_NORMAL;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
index 784e190..b5c9fa1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
@@ -62,11 +62,12 @@
 import com.android.wm.shell.desktopmode.DesktopUserRepositories;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
-import com.android.wm.shell.transition.FocusTransitionObserver;
 import com.android.wm.shell.transition.Transitions;
 
 import dagger.Lazy;
 
+import java.util.Optional;
+
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
@@ -76,8 +77,6 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.util.Optional;
-
 /**
  * Tests for {@link CompatUIController}.
  *
@@ -128,8 +127,6 @@
     private DesktopUserRepositories mDesktopUserRepositories;
     @Mock
     private DesktopRepository mDesktopRepository;
-    @Mock
-    private FocusTransitionObserver mFocusTransitionObserver;
 
     @Captor
     ArgumentCaptor<OnInsetsChangedListener> mOnInsetsChangedListenerCaptor;
@@ -165,8 +162,7 @@
                 mMockDisplayController, mMockDisplayInsetsController, mMockImeController,
                 mMockSyncQueue, mMockExecutor, mMockTransitionsLazy, mDockStateReader,
                 mCompatUIConfiguration, mCompatUIShellCommandHandler, mAccessibilityManager,
-                mCompatUIStatusManager, Optional.of(mDesktopUserRepositories),
-                mFocusTransitionObserver) {
+                mCompatUIStatusManager, Optional.of(mDesktopUserRepositories)) {
             @Override
             CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo,
                     ShellTaskOrganizer.TaskListener taskListener) {
@@ -284,7 +280,6 @@
         doReturn(false).when(mMockRestartDialogLayout).updateCompatInfo(any(), any(), anyBoolean());
 
         TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true);
-        when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true);
         mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
 
         verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener));
@@ -416,7 +411,6 @@
         // Verify button remains hidden while IME is showing.
         TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true);
         mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
-        when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false);
 
         verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener,
                 /* canShow= */ false);
@@ -449,7 +443,6 @@
         // Verify button remains hidden while keyguard is showing.
         TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true);
         mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
-        when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false);
 
         verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener,
                 /* canShow= */ false);
@@ -530,7 +523,6 @@
     @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testRestartLayoutRecreatedIfNeeded() {
         final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true);
-        when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false);
         doReturn(true).when(mMockRestartDialogLayout)
                 .needsToBeRecreated(any(TaskInfo.class),
                         any(ShellTaskOrganizer.TaskListener.class));
@@ -546,7 +538,6 @@
     @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
     public void testRestartLayoutNotRecreatedIfNotNeeded() {
         final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true);
-        when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false);
         doReturn(false).when(mMockRestartDialogLayout)
                 .needsToBeRecreated(any(TaskInfo.class),
                         any(ShellTaskOrganizer.TaskListener.class));
@@ -567,8 +558,7 @@
 
         // Create new task
         final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
-                /* isVisible */ true);
-        when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true);
+                /* isVisible */ true, /* isFocused */ true);
 
         // Simulate new task being shown
         mController.updateActiveTaskInfo(taskInfo);
@@ -584,8 +574,7 @@
     public void testUpdateActiveTaskInfo_newTask_notVisibleOrFocused_notUpdated() {
         // Create new task
         final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
-                /* isVisible */ true);
-        when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true);
+                /* isVisible */ true, /* isFocused */ true);
 
         // Simulate task being shown
         mController.updateActiveTaskInfo(taskInfo);
@@ -603,8 +592,7 @@
 
         // Create visible but NOT focused task
         final TaskInfo taskInfo1 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true,
-                /* isVisible */ true);
-        when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false);
+                /* isVisible */ true, /* isFocused */ false);
 
         // Simulate new task being shown
         mController.updateActiveTaskInfo(taskInfo1);
@@ -616,8 +604,7 @@
 
         // Create focused but NOT visible task
         final TaskInfo taskInfo2 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true,
-                /* isVisible */ false);
-        when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true);
+                /* isVisible */ false, /* isFocused */ true);
 
         // Simulate new task being shown
         mController.updateActiveTaskInfo(taskInfo2);
@@ -629,8 +616,7 @@
 
         // Create NOT focused but NOT visible task
         final TaskInfo taskInfo3 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true,
-                /* isVisible */ false);
-        when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false);
+                /* isVisible */ false, /* isFocused */ false);
 
         // Simulate new task being shown
         mController.updateActiveTaskInfo(taskInfo3);
@@ -646,8 +632,7 @@
     public void testUpdateActiveTaskInfo_sameTask_notUpdated() {
         // Create new task
         final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
-                /* isVisible */ true);
-        when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true);
+                /* isVisible */ true, /* isFocused */ true);
 
         // Simulate new task being shown
         mController.updateActiveTaskInfo(taskInfo);
@@ -675,8 +660,7 @@
     public void testUpdateActiveTaskInfo_transparentTask_notUpdated() {
         // Create new task
         final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
-                /* isVisible */ true);
-        when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true);
+                /* isVisible */ true, /* isFocused */ true);
 
         // Simulate new task being shown
         mController.updateActiveTaskInfo(taskInfo);
@@ -694,8 +678,7 @@
 
         // Create transparent task
         final TaskInfo taskInfo1 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true,
-                /* isVisible */ true, /* isTopActivityTransparent */ true);
-        when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true);
+                /* isVisible */ true, /* isFocused */ true, /* isTopActivityTransparent */ true);
 
         // Simulate new task being shown
         mController.updateActiveTaskInfo(taskInfo1);
@@ -711,7 +694,6 @@
     public void testLetterboxEduLayout_notCreatedWhenLetterboxEducationIsDisabled() {
         TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true);
         taskInfo.appCompatTaskInfo.setLetterboxEducationEnabled(false);
-        when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false);
 
         mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
 
@@ -725,7 +707,6 @@
     public void testUpdateActiveTaskInfo_removeAllComponentWhenInDesktopModeFlagEnabled() {
         TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true);
         when(mDesktopUserRepositories.getCurrent().getVisibleTaskCount(DISPLAY_ID)).thenReturn(0);
-        when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false);
 
         mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
 
@@ -744,7 +725,6 @@
     public void testUpdateActiveTaskInfo_removeAllComponentWhenInDesktopModeFlagDisabled() {
         when(mDesktopUserRepositories.getCurrent().getVisibleTaskCount(DISPLAY_ID)).thenReturn(0);
         TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true);
-        when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false);
 
         mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener));
 
@@ -759,22 +739,23 @@
 
     private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat) {
         return createTaskInfo(displayId, taskId, hasSizeCompat, /* isVisible */ false,
-                /* isTopActivityTransparent */ false);
+                /* isFocused */ false, /* isTopActivityTransparent */ false);
     }
 
     private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat,
-            boolean isVisible) {
+            boolean isVisible, boolean isFocused) {
         return createTaskInfo(displayId, taskId, hasSizeCompat,
-                isVisible, /* isTopActivityTransparent */ false);
+                isVisible, isFocused, /* isTopActivityTransparent */ false);
     }
 
     private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat,
-            boolean isVisible, boolean isTopActivityTransparent) {
+            boolean isVisible, boolean isFocused, boolean isTopActivityTransparent) {
         RunningTaskInfo taskInfo = new RunningTaskInfo();
         taskInfo.taskId = taskId;
         taskInfo.displayId = displayId;
         taskInfo.appCompatTaskInfo.setTopActivityInSizeCompat(hasSizeCompat);
         taskInfo.isVisible = isVisible;
+        taskInfo.isFocused = isFocused;
         taskInfo.isTopActivityTransparent = isTopActivityTransparent;
         taskInfo.appCompatTaskInfo.setLetterboxEducationEnabled(true);
         taskInfo.appCompatTaskInfo.setTopActivityLetterboxed(true);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt
index db00f41..04f9ada 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt
@@ -142,7 +142,7 @@
         changeMode: Int = WindowManager.TRANSIT_CLOSE,
         task: RunningTaskInfo,
     ): TransitionInfo =
-        TransitionInfo(type, 0 /* flags */).apply {
+        TransitionInfo(type, /* flags= */ 0).apply {
             addChange(
                 TransitionInfo.Change(mock(), closingTaskLeash).apply {
                     mode = changeMode
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt
index d14c640..c705f5a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt
@@ -153,7 +153,7 @@
         changeMode: Int = WindowManager.TRANSIT_TO_BACK,
         task: RunningTaskInfo,
     ): TransitionInfo =
-        TransitionInfo(type, 0 /* flags */).apply {
+        TransitionInfo(type, /* flags= */ 0).apply {
             addChange(
                 TransitionInfo.Change(mock(), closingTaskLeash).apply {
                     mode = changeMode
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
index 3cf84d9..372e47c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
@@ -777,7 +777,7 @@
         task: RunningTaskInfo,
         withWallpaper: Boolean = false,
     ): TransitionInfo =
-        TransitionInfo(WindowManager.TRANSIT_CLOSE, 0 /* flags */).apply {
+        TransitionInfo(WindowManager.TRANSIT_CLOSE, /* flags= */ 0).apply {
             addChange(
                 TransitionInfo.Change(mock(), closingTaskLeash).apply {
                     mode = changeMode
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
index c0ff2f0..eb6f1d7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
@@ -52,6 +52,7 @@
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
 import org.mockito.kotlin.whenever
 
 /** Tests for [DesktopModeEventLogger]. */
@@ -90,20 +91,12 @@
 
         val sessionId = desktopModeEventLogger.currentSessionId.get()
         assertThat(sessionId).isNotEqualTo(NO_SESSION_ID)
-        verify {
-            FrameworkStatsLog.write(
-                eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED),
-                /* event */
-                eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER),
-                /* enter_reason */
-                eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER),
-                /* exit_reason */
-                eq(0),
-                /* sessionId */
-                eq(sessionId),
-            )
-        }
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyOnlyOneUiChangedLogging(
+            FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER,
+            FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER,
+            0,
+            sessionId,
+        )
         verify {
             EventLogTags.writeWmShellEnterDesktopMode(
                 eq(EnterReason.KEYBOARD_SHORTCUT_ENTER.reason),
@@ -122,20 +115,13 @@
         val sessionId = desktopModeEventLogger.currentSessionId.get()
         assertThat(sessionId).isNotEqualTo(NO_SESSION_ID)
         assertThat(sessionId).isNotEqualTo(previousSessionId)
-        verify {
-            FrameworkStatsLog.write(
-                eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED),
-                /* event */
-                eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER),
-                /* enter_reason */
-                eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER),
-                /* exit_reason */
-                eq(0),
-                /* sessionId */
-                eq(sessionId),
-            )
-        }
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyOnlyOneUiChangedLogging(
+            FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER,
+            FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER,
+            /* exit_reason */
+            0,
+            sessionId,
+        )
         verify {
             EventLogTags.writeWmShellEnterDesktopMode(
                 eq(EnterReason.KEYBOARD_SHORTCUT_ENTER.reason),
@@ -149,7 +135,7 @@
     fun logSessionExit_noOngoingSession_doesNotLog() {
         desktopModeEventLogger.logSessionExit(ExitReason.DRAG_TO_EXIT)
 
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyNoLogging()
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
     }
 
@@ -159,20 +145,13 @@
 
         desktopModeEventLogger.logSessionExit(ExitReason.DRAG_TO_EXIT)
 
-        verify {
-            FrameworkStatsLog.write(
-                eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED),
-                /* event */
-                eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__EXIT),
-                /* enter_reason */
-                eq(0),
-                /* exit_reason */
-                eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__DRAG_TO_EXIT),
-                /* sessionId */
-                eq(sessionId),
-            )
-        }
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyOnlyOneUiChangedLogging(
+            FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__EXIT,
+            /* enter_reason */
+            0,
+            FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__DRAG_TO_EXIT,
+            sessionId,
+        )
         verify {
             EventLogTags.writeWmShellExitDesktopMode(
                 eq(ExitReason.DRAG_TO_EXIT.reason),
@@ -187,7 +166,7 @@
     fun logTaskAdded_noOngoingSession_doesNotLog() {
         desktopModeEventLogger.logTaskAdded(TASK_UPDATE)
 
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyNoLogging()
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
     }
 
@@ -197,32 +176,19 @@
 
         desktopModeEventLogger.logTaskAdded(TASK_UPDATE)
 
-        verify {
-            FrameworkStatsLog.write(
-                eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
-                /* task_event */
-                eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED),
-                /* instance_id */
-                eq(TASK_UPDATE.instanceId),
-                /* uid */
-                eq(TASK_UPDATE.uid),
-                /* task_height */
-                eq(TASK_UPDATE.taskHeight),
-                /* task_width */
-                eq(TASK_UPDATE.taskWidth),
-                /* task_x */
-                eq(TASK_UPDATE.taskX),
-                /* task_y */
-                eq(TASK_UPDATE.taskY),
-                /* session_id */
-                eq(sessionId),
-                eq(UNSET_MINIMIZE_REASON),
-                eq(UNSET_UNMINIMIZE_REASON),
-                /* visible_task_count */
-                eq(TASK_COUNT),
-            )
-        }
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyOnlyOneTaskUpdateLogging(
+            FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED,
+            TASK_UPDATE.instanceId,
+            TASK_UPDATE.uid,
+            TASK_UPDATE.taskHeight,
+            TASK_UPDATE.taskWidth,
+            TASK_UPDATE.taskX,
+            TASK_UPDATE.taskY,
+            sessionId,
+            UNSET_MINIMIZE_REASON,
+            UNSET_UNMINIMIZE_REASON,
+            TASK_COUNT,
+        )
         verify {
             EventLogTags.writeWmShellDesktopModeTaskUpdate(
                 eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED),
@@ -245,7 +211,7 @@
     fun logTaskRemoved_noOngoingSession_doesNotLog() {
         desktopModeEventLogger.logTaskRemoved(TASK_UPDATE)
 
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyNoLogging()
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
     }
 
@@ -255,32 +221,19 @@
 
         desktopModeEventLogger.logTaskRemoved(TASK_UPDATE)
 
-        verify {
-            FrameworkStatsLog.write(
-                eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
-                /* task_event */
-                eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED),
-                /* instance_id */
-                eq(TASK_UPDATE.instanceId),
-                /* uid */
-                eq(TASK_UPDATE.uid),
-                /* task_height */
-                eq(TASK_UPDATE.taskHeight),
-                /* task_width */
-                eq(TASK_UPDATE.taskWidth),
-                /* task_x */
-                eq(TASK_UPDATE.taskX),
-                /* task_y */
-                eq(TASK_UPDATE.taskY),
-                /* session_id */
-                eq(sessionId),
-                eq(UNSET_MINIMIZE_REASON),
-                eq(UNSET_UNMINIMIZE_REASON),
-                /* visible_task_count */
-                eq(TASK_COUNT),
-            )
-        }
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyOnlyOneTaskUpdateLogging(
+            FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED,
+            TASK_UPDATE.instanceId,
+            TASK_UPDATE.uid,
+            TASK_UPDATE.taskHeight,
+            TASK_UPDATE.taskWidth,
+            TASK_UPDATE.taskX,
+            TASK_UPDATE.taskY,
+            sessionId,
+            UNSET_MINIMIZE_REASON,
+            UNSET_UNMINIMIZE_REASON,
+            TASK_COUNT,
+        )
         verify {
             EventLogTags.writeWmShellDesktopModeTaskUpdate(
                 eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED),
@@ -303,7 +256,7 @@
     fun logTaskInfoChanged_noOngoingSession_doesNotLog() {
         desktopModeEventLogger.logTaskInfoChanged(TASK_UPDATE)
 
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyNoLogging()
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
     }
 
@@ -313,35 +266,19 @@
 
         desktopModeEventLogger.logTaskInfoChanged(TASK_UPDATE)
 
-        verify {
-            FrameworkStatsLog.write(
-                eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
-                /* task_event */
-                eq(
-                    FrameworkStatsLog
-                        .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED
-                ),
-                /* instance_id */
-                eq(TASK_UPDATE.instanceId),
-                /* uid */
-                eq(TASK_UPDATE.uid),
-                /* task_height */
-                eq(TASK_UPDATE.taskHeight),
-                /* task_width */
-                eq(TASK_UPDATE.taskWidth),
-                /* task_x */
-                eq(TASK_UPDATE.taskX),
-                /* task_y */
-                eq(TASK_UPDATE.taskY),
-                /* session_id */
-                eq(sessionId),
-                eq(UNSET_MINIMIZE_REASON),
-                eq(UNSET_UNMINIMIZE_REASON),
-                /* visible_task_count */
-                eq(TASK_COUNT),
-            )
-        }
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyOnlyOneTaskUpdateLogging(
+            FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED,
+            TASK_UPDATE.instanceId,
+            TASK_UPDATE.uid,
+            TASK_UPDATE.taskHeight,
+            TASK_UPDATE.taskWidth,
+            TASK_UPDATE.taskX,
+            TASK_UPDATE.taskY,
+            sessionId,
+            UNSET_MINIMIZE_REASON,
+            UNSET_UNMINIMIZE_REASON,
+            TASK_COUNT,
+        )
         verify {
             EventLogTags.writeWmShellDesktopModeTaskUpdate(
                 eq(
@@ -371,37 +308,19 @@
             createTaskUpdate(minimizeReason = MinimizeReason.TASK_LIMIT)
         )
 
-        verify {
-            FrameworkStatsLog.write(
-                eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
-                /* task_event */
-                eq(
-                    FrameworkStatsLog
-                        .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED
-                ),
-                /* instance_id */
-                eq(TASK_UPDATE.instanceId),
-                /* uid */
-                eq(TASK_UPDATE.uid),
-                /* task_height */
-                eq(TASK_UPDATE.taskHeight),
-                /* task_width */
-                eq(TASK_UPDATE.taskWidth),
-                /* task_x */
-                eq(TASK_UPDATE.taskX),
-                /* task_y */
-                eq(TASK_UPDATE.taskY),
-                /* session_id */
-                eq(sessionId),
-                /* minimize_reason */
-                eq(MinimizeReason.TASK_LIMIT.reason),
-                /* unminimize_reason */
-                eq(UNSET_UNMINIMIZE_REASON),
-                /* visible_task_count */
-                eq(TASK_COUNT),
-            )
-        }
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyOnlyOneTaskUpdateLogging(
+            FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED,
+            TASK_UPDATE.instanceId,
+            TASK_UPDATE.uid,
+            TASK_UPDATE.taskHeight,
+            TASK_UPDATE.taskWidth,
+            TASK_UPDATE.taskX,
+            TASK_UPDATE.taskY,
+            sessionId,
+            MinimizeReason.TASK_LIMIT.reason,
+            UNSET_UNMINIMIZE_REASON,
+            TASK_COUNT,
+        )
         verify {
             EventLogTags.writeWmShellDesktopModeTaskUpdate(
                 eq(
@@ -431,37 +350,19 @@
             createTaskUpdate(unminimizeReason = UnminimizeReason.TASKBAR_TAP)
         )
 
-        verify {
-            FrameworkStatsLog.write(
-                eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
-                /* task_event */
-                eq(
-                    FrameworkStatsLog
-                        .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED
-                ),
-                /* instance_id */
-                eq(TASK_UPDATE.instanceId),
-                /* uid */
-                eq(TASK_UPDATE.uid),
-                /* task_height */
-                eq(TASK_UPDATE.taskHeight),
-                /* task_width */
-                eq(TASK_UPDATE.taskWidth),
-                /* task_x */
-                eq(TASK_UPDATE.taskX),
-                /* task_y */
-                eq(TASK_UPDATE.taskY),
-                /* session_id */
-                eq(sessionId),
-                /* minimize_reason */
-                eq(UNSET_MINIMIZE_REASON),
-                /* unminimize_reason */
-                eq(UnminimizeReason.TASKBAR_TAP.reason),
-                /* visible_task_count */
-                eq(TASK_COUNT),
-            )
-        }
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyOnlyOneTaskUpdateLogging(
+            FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED,
+            TASK_UPDATE.instanceId,
+            TASK_UPDATE.uid,
+            TASK_UPDATE.taskHeight,
+            TASK_UPDATE.taskWidth,
+            TASK_UPDATE.taskX,
+            TASK_UPDATE.taskY,
+            sessionId,
+            UNSET_MINIMIZE_REASON,
+            UnminimizeReason.TASKBAR_TAP.reason,
+            TASK_COUNT,
+        )
         verify {
             EventLogTags.writeWmShellDesktopModeTaskUpdate(
                 eq(
@@ -491,7 +392,7 @@
             createTaskInfo(),
         )
 
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyNoLogging()
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
     }
 
@@ -509,39 +410,17 @@
             displayController,
         )
 
-        verify {
-            FrameworkStatsLog.write(
-                eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED),
-                /* resize_trigger */
-                eq(
-                    FrameworkStatsLog
-                        .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER
-                ),
-                /* resizing_stage */
-                eq(
-                    FrameworkStatsLog
-                        .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__START_RESIZING_STAGE
-                ),
-                /* input_method */
-                eq(
-                    FrameworkStatsLog
-                        .DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD
-                ),
-                /* desktop_mode_session_id */
-                eq(sessionId),
-                /* instance_id */
-                eq(TASK_SIZE_UPDATE.instanceId),
-                /* uid */
-                eq(TASK_SIZE_UPDATE.uid),
-                /* task_width */
-                eq(TASK_SIZE_UPDATE.taskWidth),
-                /* task_height */
-                eq(TASK_SIZE_UPDATE.taskHeight),
-                /* display_area */
-                eq(DISPLAY_AREA),
-            )
-        }
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyOnlyOneTaskSizeUpdatedLogging(
+            FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER,
+            FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__START_RESIZING_STAGE,
+            FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD,
+            sessionId,
+            TASK_SIZE_UPDATE.instanceId,
+            TASK_SIZE_UPDATE.uid,
+            TASK_SIZE_UPDATE.taskWidth,
+            TASK_SIZE_UPDATE.taskHeight,
+            DISPLAY_AREA,
+        )
     }
 
     @Test
@@ -552,7 +431,7 @@
             createTaskInfo(),
         )
 
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyNoLogging()
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
     }
 
@@ -568,39 +447,17 @@
             displayController = displayController,
         )
 
-        verify {
-            FrameworkStatsLog.write(
-                eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED),
-                /* resize_trigger */
-                eq(
-                    FrameworkStatsLog
-                        .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER
-                ),
-                /* resizing_stage */
-                eq(
-                    FrameworkStatsLog
-                        .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__END_RESIZING_STAGE
-                ),
-                /* input_method */
-                eq(
-                    FrameworkStatsLog
-                        .DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD
-                ),
-                /* desktop_mode_session_id */
-                eq(sessionId),
-                /* instance_id */
-                eq(TASK_SIZE_UPDATE.instanceId),
-                /* uid */
-                eq(TASK_SIZE_UPDATE.uid),
-                /* task_width */
-                eq(TASK_SIZE_UPDATE.taskWidth),
-                /* task_height */
-                eq(TASK_SIZE_UPDATE.taskHeight),
-                /* display_area */
-                eq(DISPLAY_AREA),
-            )
-        }
-        verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
+        verifyOnlyOneTaskSizeUpdatedLogging(
+            FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER,
+            FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__END_RESIZING_STAGE,
+            FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD,
+            sessionId,
+            TASK_SIZE_UPDATE.instanceId,
+            TASK_SIZE_UPDATE.uid,
+            TASK_SIZE_UPDATE.taskWidth,
+            TASK_SIZE_UPDATE.taskHeight,
+            DISPLAY_AREA,
+        )
     }
 
     private fun startDesktopModeSession(): Int {
@@ -644,12 +501,176 @@
         }
     }
 
-    private fun createTaskInfo(): RunningTaskInfo {
-        return TestRunningTaskInfoBuilder()
+    private fun createTaskInfo(): RunningTaskInfo =
+        TestRunningTaskInfoBuilder()
             .setTaskId(TASK_ID)
             .setUid(TASK_UID)
             .setBounds(Rect(TASK_X, TASK_Y, TASK_WIDTH, TASK_HEIGHT))
             .build()
+
+    private fun verifyNoLogging() {
+        verify(
+            {
+                FrameworkStatsLog.write(
+                    eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                )
+            },
+            never(),
+        )
+        verify(
+            {
+                FrameworkStatsLog.write(
+                    eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                )
+            },
+            never(),
+        )
+        verify(
+            {
+                FrameworkStatsLog.write(
+                    eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                )
+            },
+            never(),
+        )
+    }
+
+    private fun verifyOnlyOneUiChangedLogging(
+        event: Int,
+        enterReason: Int,
+        exitReason: Int,
+        sessionId: Int,
+    ) {
+        verify({
+            FrameworkStatsLog.write(
+                eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED),
+                eq(event),
+                eq(enterReason),
+                eq(exitReason),
+                eq(sessionId),
+            )
+        })
+        verify({
+            FrameworkStatsLog.write(
+                eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+            )
+        })
+    }
+
+    private fun verifyOnlyOneTaskUpdateLogging(
+        taskEvent: Int,
+        instanceId: Int,
+        uid: Int,
+        taskHeight: Int,
+        taskWidth: Int,
+        taskX: Int,
+        taskY: Int,
+        sessionId: Int,
+        minimizeReason: Int,
+        unminimizeReason: Int,
+        visibleTaskCount: Int,
+    ) {
+        verify({
+            FrameworkStatsLog.write(
+                eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
+                eq(taskEvent),
+                eq(instanceId),
+                eq(uid),
+                eq(taskHeight),
+                eq(taskWidth),
+                eq(taskX),
+                eq(taskY),
+                eq(sessionId),
+                eq(minimizeReason),
+                eq(unminimizeReason),
+                eq(visibleTaskCount),
+            )
+        })
+        verify({
+            FrameworkStatsLog.write(
+                eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+            )
+        })
+    }
+
+    private fun verifyOnlyOneTaskSizeUpdatedLogging(
+        resizeTrigger: Int,
+        resizingStage: Int,
+        inputMethod: Int,
+        sessionId: Int,
+        instanceId: Int,
+        uid: Int,
+        taskWidth: Int,
+        taskHeight: Int,
+        displayArea: Int,
+    ) {
+        verify({
+            FrameworkStatsLog.write(
+                eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED),
+                eq(resizeTrigger),
+                eq(resizingStage),
+                eq(inputMethod),
+                eq(sessionId),
+                eq(instanceId),
+                eq(uid),
+                eq(taskWidth),
+                eq(taskHeight),
+                eq(displayArea),
+            )
+        })
+        verify({
+            FrameworkStatsLog.write(
+                eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+            )
+        })
     }
 
     private companion object {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
index 5629127..daeccce 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
@@ -25,6 +25,7 @@
 import android.view.Display.INVALID_DISPLAY
 import androidx.test.filters.SmallTest
 import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP
 import com.android.wm.shell.ShellTestCase
 import com.android.wm.shell.TestShellExecutor
 import com.android.wm.shell.common.ShellExecutor
@@ -1067,6 +1068,67 @@
         assertThat(repo.getTaskInFullImmersiveState(DEFAULT_DESKTOP_ID + 1)).isEqualTo(2)
     }
 
+    @Test
+    fun setTaskInPip_savedAsMinimizedPipInDisplay() {
+        assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isFalse()
+
+        repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true)
+
+        assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue()
+    }
+
+    @Test
+    fun removeTaskInPip_removedAsMinimizedPipInDisplay() {
+        repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true)
+        assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue()
+
+        repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = false)
+
+        assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isFalse()
+    }
+
+    @Test
+    fun setTaskInPip_multipleDisplays_bothAreInPip() {
+        repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true)
+        repo.setTaskInPip(DEFAULT_DESKTOP_ID + 1, taskId = 2, enterPip = true)
+
+        assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue()
+        assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID + 1, taskId = 2)).isTrue()
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    fun setPipShouldKeepDesktopActive_shouldKeepDesktopActive() {
+        assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isFalse()
+
+        repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true)
+        repo.setPipShouldKeepDesktopActive(DEFAULT_DESKTOP_ID, keepActive = true)
+
+        assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isTrue()
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    fun setPipShouldNotKeepDesktopActive_shouldNotKeepDesktopActive() {
+        repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true)
+        assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isTrue()
+
+        repo.setPipShouldKeepDesktopActive(DEFAULT_DESKTOP_ID, keepActive = false)
+
+        assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isFalse()
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    fun removeTaskInPip_shouldNotKeepDesktopActive() {
+        repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true)
+        assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isTrue()
+
+        repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = false)
+
+        assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isFalse()
+    }
+
     class TestListener : DesktopRepository.ActiveTasksListener {
         var activeChangesOnDefaultDisplay = 0
         var activeChangesOnSecondaryDisplay = 0
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 e032616..95ed8b4 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
@@ -82,6 +82,7 @@
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.window.flags.Flags
 import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP
 import com.android.window.flags.Flags.FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS
 import com.android.window.flags.Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP
 import com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT
@@ -352,8 +353,8 @@
         taskRepository = userRepositories.current
     }
 
-    private fun createController(): DesktopTasksController {
-        return DesktopTasksController(
+    private fun createController() =
+        DesktopTasksController(
             context,
             shellInit,
             shellCommandHandler,
@@ -387,7 +388,6 @@
             desktopWallpaperActivityTokenProvider,
             Optional.of(bubbleController),
         )
-    }
 
     @After
     fun tearDown() {
@@ -569,6 +569,38 @@
     }
 
     @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    fun isDesktopModeShowing_minimizedPipTask_wallpaperVisible_returnsTrue() {
+        val pipTask = setUpPipTask(autoEnterEnabled = true)
+        whenever(desktopWallpaperActivityTokenProvider.isWallpaperActivityVisible())
+            .thenReturn(true)
+
+        taskRepository.setTaskInPip(DEFAULT_DISPLAY, pipTask.taskId, enterPip = true)
+
+        assertThat(controller.isDesktopModeShowing(displayId = DEFAULT_DISPLAY)).isTrue()
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    fun isDesktopModeShowing_minimizedPipTask_wallpaperNotVisible_returnsFalse() {
+        val pipTask = setUpPipTask(autoEnterEnabled = true)
+        whenever(desktopWallpaperActivityTokenProvider.isWallpaperActivityVisible())
+            .thenReturn(false)
+
+        taskRepository.setTaskInPip(DEFAULT_DISPLAY, pipTask.taskId, enterPip = true)
+
+        assertThat(controller.isDesktopModeShowing(displayId = DEFAULT_DISPLAY)).isFalse()
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    fun isDesktopModeShowing_pipTaskNotMinimizedNorVisible_returnsFalse() {
+        setUpPipTask(autoEnterEnabled = true)
+
+        assertThat(controller.isDesktopModeShowing(displayId = DEFAULT_DISPLAY)).isFalse()
+    }
+
+    @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
     fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_shouldNotShowWallpaper() {
         val homeTask = setUpHomeTask(SECOND_DISPLAY)
@@ -1267,7 +1299,7 @@
         // Set task as systemUI package
         val systemUIPackageName =
             context.resources.getString(com.android.internal.R.string.config_systemUi)
-        val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
+        val baseComponent = ComponentName(systemUIPackageName, /* cls= */ "")
         val task =
             setUpFullscreenTask().apply {
                 baseActivity = baseComponent
@@ -1284,7 +1316,7 @@
         // Set task as systemUI package
         val systemUIPackageName =
             context.resources.getString(com.android.internal.R.string.config_systemUi)
-        val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
+        val baseComponent = ComponentName(systemUIPackageName, /* cls= */ "")
         val task =
             setUpFullscreenTask().apply {
                 baseActivity = baseComponent
@@ -1497,7 +1529,7 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER)
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER)
     fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_removesWallpaperActivity() {
         val task = setUpFreeformTask()
         assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
@@ -1530,7 +1562,7 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER)
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER)
     fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity() {
         val task = setUpFreeformTask()
 
@@ -1757,12 +1789,12 @@
 
         controller.moveToNextDisplay(task.taskId)
 
-        with(getLatestWct(type = TRANSIT_CHANGE)) {
-            val wallpaperChange =
-                hierarchyOps.find { op -> op.container == wallpaperToken.asBinder() }
-            assertThat(wallpaperChange).isNotNull()
-            assertThat(wallpaperChange!!.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK)
-        }
+        val wallpaperChange =
+            getLatestWct(type = TRANSIT_CHANGE).hierarchyOps.find { op ->
+                op.container == wallpaperToken.asBinder()
+            }
+        assertNotNull(wallpaperChange)
+        assertThat(wallpaperChange.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK)
     }
 
     @Test
@@ -1792,15 +1824,13 @@
 
         controller.moveToNextDisplay(task.taskId)
 
-        with(getLatestWct(type = TRANSIT_CHANGE)) {
-            val taskChange = changes[task.token.asBinder()]
-            assertThat(taskChange).isNotNull()
-            // To preserve DP size, pixel size is changed to 320x240. The ratio of the left margin
-            // to the right margin and the ratio of the top margin to bottom margin are also
-            // preserved.
-            assertThat(taskChange!!.configuration.windowConfiguration.bounds)
-                .isEqualTo(Rect(240, 160, 560, 400))
-        }
+        val taskChange = getLatestWct(type = TRANSIT_CHANGE).changes[task.token.asBinder()]
+        assertNotNull(taskChange)
+        // To preserve DP size, pixel size is changed to 320x240. The ratio of the left margin
+        // to the right margin and the ratio of the top margin to bottom margin are also
+        // preserved.
+        assertThat(taskChange.configuration.windowConfiguration.bounds)
+            .isEqualTo(Rect(240, 160, 560, 400))
     }
 
     @Test
@@ -1831,12 +1861,10 @@
 
         controller.moveToNextDisplay(task.taskId)
 
-        with(getLatestWct(type = TRANSIT_CHANGE)) {
-            val taskChange = changes[task.token.asBinder()]
-            assertThat(taskChange).isNotNull()
-            assertThat(taskChange!!.configuration.windowConfiguration.bounds)
-                .isEqualTo(Rect(960, 480, 1280, 720))
-        }
+        val taskChange = getLatestWct(type = TRANSIT_CHANGE).changes[task.token.asBinder()]
+        assertNotNull(taskChange)
+        assertThat(taskChange.configuration.windowConfiguration.bounds)
+            .isEqualTo(Rect(960, 480, 1280, 720))
     }
 
     @Test
@@ -1864,13 +1892,11 @@
 
         controller.moveToNextDisplay(task.taskId)
 
-        with(getLatestWct(type = TRANSIT_CHANGE)) {
-            val taskChange = changes[task.token.asBinder()]
-            assertThat(taskChange).isNotNull()
-            // DP size is preserved. The window is centered in the destination display.
-            assertThat(taskChange!!.configuration.windowConfiguration.bounds)
-                .isEqualTo(Rect(320, 120, 960, 600))
-        }
+        val taskChange = getLatestWct(type = TRANSIT_CHANGE).changes[task.token.asBinder()]
+        assertNotNull(taskChange)
+        // DP size is preserved. The window is centered in the destination display.
+        assertThat(taskChange.configuration.windowConfiguration.bounds)
+            .isEqualTo(Rect(320, 120, 960, 600))
     }
 
     @Test
@@ -1903,14 +1929,12 @@
 
         controller.moveToNextDisplay(task.taskId)
 
-        with(getLatestWct(type = TRANSIT_CHANGE)) {
-            val taskChange = changes[task.token.asBinder()]
-            assertThat(taskChange).isNotNull()
-            assertThat(taskChange!!.configuration.windowConfiguration.bounds.left).isAtLeast(0)
-            assertThat(taskChange.configuration.windowConfiguration.bounds.top).isAtLeast(0)
-            assertThat(taskChange.configuration.windowConfiguration.bounds.right).isAtMost(640)
-            assertThat(taskChange.configuration.windowConfiguration.bounds.bottom).isAtMost(480)
-        }
+        val taskChange = getLatestWct(type = TRANSIT_CHANGE).changes[task.token.asBinder()]
+        assertNotNull(taskChange)
+        assertThat(taskChange.configuration.windowConfiguration.bounds.left).isAtLeast(0)
+        assertThat(taskChange.configuration.windowConfiguration.bounds.top).isAtLeast(0)
+        assertThat(taskChange.configuration.windowConfiguration.bounds.right).isAtMost(640)
+        assertThat(taskChange.configuration.windowConfiguration.bounds.bottom).isAtMost(480)
     }
 
     @Test
@@ -1973,7 +1997,7 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER)
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER)
     fun onDesktopWindowClose_singleActiveTask_hasWallpaperActivityToken() {
         val task = setUpFreeformTask()
 
@@ -2019,7 +2043,7 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER)
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER)
     fun onDesktopWindowClose_multipleActiveTasks_isOnlyNonClosingTask() {
         val task1 = setUpFreeformTask()
         val task2 = setUpFreeformTask()
@@ -2033,7 +2057,7 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER)
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER)
     fun onDesktopWindowClose_multipleActiveTasks_hasMinimized() {
         val task1 = setUpFreeformTask()
         val task2 = setUpFreeformTask()
@@ -2047,6 +2071,41 @@
     }
 
     @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    fun onDesktopWindowClose_minimizedPipPresent_doesNotExitDesktop() {
+        val freeformTask = setUpFreeformTask().apply { isFocused = true }
+        val pipTask = setUpPipTask(autoEnterEnabled = true)
+
+        taskRepository.setTaskInPip(DEFAULT_DISPLAY, pipTask.taskId, enterPip = true)
+        val wct = WindowContainerTransaction()
+        controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, freeformTask)
+
+        verifyExitDesktopWCTNotExecuted()
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    fun onDesktopWindowClose_minimizedPipNotPresent_exitDesktop() {
+        val freeformTask = setUpFreeformTask()
+        val pipTask = setUpPipTask(autoEnterEnabled = true)
+        val handler = mock(TransitionHandler::class.java)
+        whenever(transitions.dispatchRequest(any(), any(), anyOrNull()))
+            .thenReturn(android.util.Pair(handler, WindowContainerTransaction()))
+
+        controller.minimizeTask(pipTask)
+        verifyExitDesktopWCTNotExecuted()
+
+        taskRepository.setTaskInPip(DEFAULT_DISPLAY, pipTask.taskId, enterPip = false)
+        val wct = WindowContainerTransaction()
+        controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, freeformTask)
+
+        // Remove wallpaper operation
+        wct.hierarchyOps.any { hop ->
+            hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
+        }
+    }
+
+    @Test
     fun onDesktopWindowMinimize_noActiveTask_doesntRemoveWallpaper() {
         val task = setUpFreeformTask(active = false)
         val transition = Binder()
@@ -2063,10 +2122,9 @@
     }
 
     @Test
-    fun onDesktopWindowMinimize_pipTask_autoEnterEnabled_startPipTransition() {
+    fun onPipTaskMinimize_autoEnterEnabled_startPipTransition() {
         val task = setUpPipTask(autoEnterEnabled = true)
         val handler = mock(TransitionHandler::class.java)
-        whenever(freeformTaskTransitionStarter.startPipTransition(any())).thenReturn(Binder())
         whenever(transitions.dispatchRequest(any(), any(), anyOrNull()))
             .thenReturn(android.util.Pair(handler, WindowContainerTransaction()))
 
@@ -2077,7 +2135,7 @@
     }
 
     @Test
-    fun onDesktopWindowMinimize_pipTask_autoEnterDisabled_startMinimizeTransition() {
+    fun onPipTaskMinimize_autoEnterDisabled_startMinimizeTransition() {
         val task = setUpPipTask(autoEnterEnabled = false)
         whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
             .thenReturn(Binder())
@@ -2089,6 +2147,22 @@
     }
 
     @Test
+    fun onPipTaskMinimize_doesntRemoveWallpaper() {
+        val task = setUpPipTask(autoEnterEnabled = true)
+        val handler = mock(TransitionHandler::class.java)
+        whenever(transitions.dispatchRequest(any(), any(), anyOrNull()))
+            .thenReturn(android.util.Pair(handler, WindowContainerTransaction()))
+
+        controller.minimizeTask(task)
+
+        val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        verify(freeformTaskTransitionStarter).startPipTransition(captor.capture())
+        captor.value.hierarchyOps.none { hop ->
+            hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
+        }
+    }
+
+    @Test
     fun onDesktopWindowMinimize_singleActiveTask_noWallpaperActivityToken_doesntRemoveWallpaper() {
         val task = setUpFreeformTask(active = true)
         val transition = Binder()
@@ -2103,7 +2177,7 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER)
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER)
     fun onTaskMinimize_singleActiveTask_hasWallpaperActivityToken_removesWallpaper() {
         val task = setUpFreeformTask()
         val transition = Binder()
@@ -2155,7 +2229,7 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER)
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER)
     fun onDesktopWindowMinimize_multipleActiveTasks_minimizesTheOnlyVisibleTask_removesWallpaper() {
         val task1 = setUpFreeformTask(active = true)
         val task2 = setUpFreeformTask(active = true)
@@ -2722,7 +2796,7 @@
         // Set task as systemUI package
         val systemUIPackageName =
             context.resources.getString(com.android.internal.R.string.config_systemUi)
-        val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
+        val baseComponent = ComponentName(systemUIPackageName, /* cls= */ "")
         val task =
             setUpFreeformTask().apply {
                 baseActivity = baseComponent
@@ -2743,7 +2817,7 @@
         // Set task as systemUI package
         val systemUIPackageName =
             context.resources.getString(com.android.internal.R.string.config_systemUi)
-        val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
+        val baseComponent = ComponentName(systemUIPackageName, /* cls= */ "")
         val task =
             setUpFullscreenTask().apply {
                 baseActivity = baseComponent
@@ -2816,7 +2890,7 @@
     @Test
     @EnableFlags(
         Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-        Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER,
+        Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
     )
     fun handleRequest_backTransition_singleTaskWithToken_removesWallpaper() {
         val task = setUpFreeformTask()
@@ -2857,7 +2931,7 @@
     @EnableFlags(
         Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
         Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION,
-        Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER,
+        Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
     )
     fun handleRequest_backTransition_multipleTasksSingleNonClosing_removesWallpaperAndTask() {
         val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
@@ -2875,7 +2949,7 @@
     @Test
     @EnableFlags(
         Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-        Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER,
+        Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
     )
     fun handleRequest_backTransition_multipleTasksSingleNonMinimized_removesWallpaperAndTask() {
         val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
@@ -2942,7 +3016,7 @@
     @Test
     @EnableFlags(
         Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-        Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER,
+        Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
     )
     fun handleRequest_closeTransition_singleTaskWithToken_withWallpaper_removesWallpaper() {
         val task = setUpFreeformTask()
@@ -2982,7 +3056,7 @@
     @Test
     @EnableFlags(
         Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-        Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER,
+        Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
     )
     fun handleRequest_closeTransition_multipleTasksSingleNonClosing_removesWallpaper() {
         val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
@@ -3000,7 +3074,7 @@
     @Test
     @EnableFlags(
         Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
-        Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER,
+        Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
     )
     fun handleRequest_closeTransition_multipleTasksSingleNonMinimized_removesWallpaper() {
         val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
@@ -3092,7 +3166,7 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER)
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER)
     fun moveFocusedTaskToFullscreen_onlyVisibleNonMinimizedTask_removesWallpaperActivity() {
         val task1 = setUpFreeformTask()
         val task2 = setUpFreeformTask()
@@ -3133,6 +3207,31 @@
     }
 
     @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    fun moveFocusedTaskToFullscreen_minimizedPipPresent_removeWallpaperActivity() {
+        val freeformTask = setUpFreeformTask()
+        val pipTask = setUpPipTask(autoEnterEnabled = true)
+        val handler = mock(TransitionHandler::class.java)
+        whenever(transitions.dispatchRequest(any(), any(), anyOrNull()))
+            .thenReturn(android.util.Pair(handler, WindowContainerTransaction()))
+
+        controller.minimizeTask(pipTask)
+        verifyExitDesktopWCTNotExecuted()
+
+        freeformTask.isFocused = true
+        controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
+
+        val wct = getLatestExitDesktopWct()
+        val taskChange = assertNotNull(wct.changes[freeformTask.token.asBinder()])
+        assertThat(taskChange.windowingMode)
+            .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
+        // Remove wallpaper operation
+        wct.hierarchyOps.any { hop ->
+            hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
+        }
+    }
+
+    @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
     fun removeDesktop_multipleTasks_removesAll() {
         val task1 = setUpFreeformTask()
@@ -3376,11 +3475,11 @@
         spyController.onDragPositioningEnd(
             task,
             mockSurface,
-            Point(100, -100), /* position */
-            PointF(200f, -200f), /* inputCoordinate */
-            Rect(100, -100, 500, 1000), /* currentDragBounds */
-            Rect(0, 50, 2000, 2000), /* validDragArea */
-            Rect() /* dragStartBounds */,
+            position = Point(100, -100),
+            inputCoordinate = PointF(200f, -200f),
+            currentDragBounds = Rect(100, -100, 500, 1000),
+            validDragArea = Rect(0, 50, 2000, 2000),
+            dragStartBounds = Rect(),
             motionEvent,
             desktopWindowDecoration,
         )
@@ -3415,11 +3514,11 @@
         spyController.onDragPositioningEnd(
             task,
             mockSurface,
-            Point(100, 200), /* position */
-            PointF(200f, 300f), /* inputCoordinate */
-            currentDragBounds, /* currentDragBounds */
-            Rect(0, 50, 2000, 2000) /* validDragArea */,
-            Rect() /* dragStartBounds */,
+            position = Point(100, 200),
+            inputCoordinate = PointF(200f, 300f),
+            currentDragBounds = currentDragBounds,
+            validDragArea = Rect(0, 50, 2000, 2000),
+            dragStartBounds = Rect(),
             motionEvent,
             desktopWindowDecoration,
         )
@@ -3459,11 +3558,11 @@
         spyController.onDragPositioningEnd(
             task,
             mockSurface,
-            Point(100, 50), /* position */
-            PointF(200f, 300f), /* inputCoordinate */
-            Rect(100, 50, 500, 1000), /* currentDragBounds */
-            Rect(0, 50, 2000, 2000) /* validDragArea */,
-            Rect() /* dragStartBounds */,
+            position = Point(100, 50),
+            inputCoordinate = PointF(200f, 300f),
+            currentDragBounds = Rect(100, 50, 500, 1000),
+            validDragArea = Rect(0, 50, 2000, 2000),
+            dragStartBounds = Rect(),
             motionEvent,
             desktopWindowDecoration,
         )
@@ -3498,11 +3597,11 @@
         spyController.onDragPositioningEnd(
             task,
             mockSurface,
-            Point(100, 50), /* position */
-            PointF(200f, 300f), /* inputCoordinate */
+            position = Point(100, 50),
+            inputCoordinate = PointF(200f, 300f),
             currentDragBounds,
-            Rect(0, 50, 2000, 2000) /* validDragArea */,
-            Rect() /* dragStartBounds */,
+            validDragArea = Rect(0, 50, 2000, 2000),
+            dragStartBounds = Rect(),
             motionEvent,
             desktopWindowDecoration,
         )
@@ -3555,11 +3654,11 @@
         spyController.onDragPositioningEnd(
             task,
             mockSurface,
-            Point(100, 50), /* position */
-            PointF(200f, 300f), /* inputCoordinate */
-            currentDragBounds, /* currentDragBounds */
-            Rect(0, 50, 2000, 2000) /* validDragArea */,
-            Rect() /* dragStartBounds */,
+            position = Point(100, 50),
+            inputCoordinate = PointF(200f, 300f),
+            currentDragBounds = currentDragBounds,
+            validDragArea = Rect(0, 50, 2000, 2000),
+            dragStartBounds = Rect(),
             motionEvent,
             desktopWindowDecoration,
         )
@@ -3604,7 +3703,7 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER)
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER)
     fun enterSplit_onlyVisibleNonMinimizedTask_removesWallpaperActivity() {
         val task1 = setUpFreeformTask()
         val task2 = setUpFreeformTask()
@@ -4858,12 +4957,12 @@
         return task
     }
 
-    private fun setUpPipTask(autoEnterEnabled: Boolean): RunningTaskInfo {
-        return setUpFreeformTask().apply {
+    private fun setUpPipTask(autoEnterEnabled: Boolean): RunningTaskInfo =
+        // active = false marks the task as non-visible; PiP window doesn't count as visible tasks
+        setUpFreeformTask(active = false).apply {
             pictureInPictureParams =
                 PictureInPictureParams.Builder().setAutoEnterEnabled(autoEnterEnabled).build()
         }
-    }
 
     private fun setUpHomeTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
         val task = createHomeTask(displayId)
@@ -5053,7 +5152,7 @@
         task: RunningTaskInfo?,
         @WindowManager.TransitionType type: Int = TRANSIT_OPEN,
     ): TransitionRequestInfo {
-        return TransitionRequestInfo(type, task, null /* remoteTransition */)
+        return TransitionRequestInfo(type, task, /* remoteTransition= */ null)
     }
 
     private companion object {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
index 52602f2..c8214b3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
@@ -193,10 +193,10 @@
         desktopTasksLimiter
             .getTransitionObserver()
             .onTransitionReady(
-                Binder() /* transition */,
+                /* transition= */ Binder(),
                 TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
-                StubTransaction() /* startTransaction */,
-                StubTransaction(), /* finishTransaction */
+                /* startTransaction= */ StubTransaction(),
+                /* finishTransaction= */ StubTransaction(),
             )
 
         assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse()
@@ -217,10 +217,10 @@
         desktopTasksLimiter
             .getTransitionObserver()
             .onTransitionReady(
-                taskTransition /* transition */,
+                /* transition= */ taskTransition,
                 TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
-                StubTransaction() /* startTransaction */,
-                StubTransaction(), /* finishTransaction */
+                /* startTransaction= */ StubTransaction(),
+                /* finishTransaction= */ StubTransaction(),
             )
 
         assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse()
@@ -242,8 +242,8 @@
             .onTransitionReady(
                 transition,
                 TransitionInfoBuilder(TRANSIT_OPEN).build(),
-                StubTransaction() /* startTransaction */,
-                StubTransaction(), /* finishTransaction */
+                /* startTransaction= */ StubTransaction(),
+                /* finishTransaction= */ StubTransaction(),
             )
 
         assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse()
@@ -265,8 +265,8 @@
             .onTransitionReady(
                 transition,
                 TransitionInfoBuilder(TRANSIT_OPEN).build(),
-                StubTransaction() /* startTransaction */,
-                StubTransaction(), /* finishTransaction */
+                /* startTransaction= */ StubTransaction(),
+                /* finishTransaction= */ StubTransaction(),
             )
 
         assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue()
@@ -287,8 +287,8 @@
             .onTransitionReady(
                 transition,
                 TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
-                StubTransaction() /* startTransaction */,
-                StubTransaction(), /* finishTransaction */
+                /* startTransaction= */ StubTransaction(),
+                /* finishTransaction= */ StubTransaction(),
             )
 
         assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue()
@@ -316,8 +316,8 @@
             .onTransitionReady(
                 transition,
                 TransitionInfo(TRANSIT_OPEN, TransitionInfo.FLAG_NONE).apply { addChange(change) },
-                StubTransaction() /* startTransaction */,
-                StubTransaction(), /* finishTransaction */
+                /* startTransaction= */ StubTransaction(),
+                /* finishTransaction= */ StubTransaction(),
             )
 
         assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue()
@@ -344,8 +344,8 @@
             .onTransitionReady(
                 newTransition,
                 TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
-                StubTransaction() /* startTransaction */,
-                StubTransaction(), /* finishTransaction */
+                /* startTransaction= */ StubTransaction(),
+                /* finishTransaction= */ StubTransaction(),
             )
 
         assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue()
@@ -552,8 +552,8 @@
             .onTransitionReady(
                 transition,
                 TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
-                StubTransaction() /* startTransaction */,
-                StubTransaction(), /* finishTransaction */
+                /* startTransaction= */ StubTransaction(),
+                /* finishTransaction= */ StubTransaction(),
             )
 
         desktopTasksLimiter.getTransitionObserver().onTransitionStarting(transition)
@@ -584,8 +584,8 @@
             .onTransitionReady(
                 transition,
                 TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
-                StubTransaction() /* startTransaction */,
-                StubTransaction(), /* finishTransaction */
+                /* startTransaction= */ StubTransaction(),
+                /* finishTransaction= */ StubTransaction(),
             )
 
         desktopTasksLimiter.getTransitionObserver().onTransitionStarting(transition)
@@ -617,8 +617,8 @@
             .onTransitionReady(
                 mergedTransition,
                 TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
-                StubTransaction() /* startTransaction */,
-                StubTransaction(), /* finishTransaction */
+                /* startTransaction= */ StubTransaction(),
+                /* finishTransaction= */ StubTransaction(),
             )
 
         desktopTasksLimiter.getTransitionObserver().onTransitionStarting(mergedTransition)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
index d491d44..622cb4c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
@@ -22,6 +22,7 @@
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
+import android.os.Binder
 import android.os.IBinder
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.SetFlagsRule
@@ -29,7 +30,9 @@
 import android.view.WindowManager
 import android.view.WindowManager.TRANSIT_CLOSE
 import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_PIP
 import android.view.WindowManager.TRANSIT_TO_BACK
+import android.view.WindowManager.TRANSIT_TO_FRONT
 import android.window.IWindowContainerToken
 import android.window.TransitionInfo
 import android.window.TransitionInfo.Change
@@ -38,6 +41,7 @@
 import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER
 import com.android.modules.utils.testing.ExtendedMockitoRule
 import com.android.window.flags.Flags
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP
 import com.android.wm.shell.MockToken
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.back.BackAnimationController
@@ -47,6 +51,8 @@
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP
+import com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import org.junit.Before
@@ -239,7 +245,7 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_ON_SYSTEM_USER)
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER)
     fun closeLastTask_wallpaperTokenExists_wallpaperIsRemoved() {
         val mockTransition = Mockito.mock(IBinder::class.java)
         val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM)
@@ -300,12 +306,121 @@
         verify(taskRepository).clearTopTransparentFullscreenTaskId(topTransparentTask.displayId)
     }
 
+    @Test
+    fun transitOpenWallpaper_wallpaperActivityVisibilitySaved() {
+        val wallpaperTask = createWallpaperTaskInfo()
+
+        transitionObserver.onTransitionReady(
+            transition = mock(),
+            info = createOpenChangeTransition(wallpaperTask),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+        )
+
+        verify(desktopWallpaperActivityTokenProvider)
+            .setWallpaperActivityIsVisible(isVisible = true, wallpaperTask.displayId)
+    }
+
+    @Test
+    fun transitToFrontWallpaper_wallpaperActivityVisibilitySaved() {
+        val wallpaperTask = createWallpaperTaskInfo()
+
+        transitionObserver.onTransitionReady(
+            transition = mock(),
+            info = createToFrontTransition(wallpaperTask),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+        )
+
+        verify(desktopWallpaperActivityTokenProvider)
+            .setWallpaperActivityIsVisible(isVisible = true, wallpaperTask.displayId)
+    }
+
+    @Test
+    fun transitToBackWallpaper_wallpaperActivityVisibilitySaved() {
+        val wallpaperTask = createWallpaperTaskInfo()
+
+        transitionObserver.onTransitionReady(
+            transition = mock(),
+            info = createToBackTransition(wallpaperTask),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+        )
+
+        verify(desktopWallpaperActivityTokenProvider)
+            .setWallpaperActivityIsVisible(isVisible = false, wallpaperTask.displayId)
+    }
+
+    @Test
+    fun transitCloseWallpaper_wallpaperActivityVisibilitySaved() {
+        val wallpaperTask = createWallpaperTaskInfo()
+
+        transitionObserver.onTransitionReady(
+            transition = mock(),
+            info = createCloseTransition(wallpaperTask),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+        )
+
+        verify(desktopWallpaperActivityTokenProvider).removeToken(wallpaperTask.displayId)
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    fun pendingPipTransitionAborted_taskRepositoryOnPipAbortedInvoked() {
+        val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM)
+        val pipTransition = Binder()
+        whenever(taskRepository.isTaskMinimizedPipInDisplay(any(), any())).thenReturn(true)
+
+        transitionObserver.onTransitionReady(
+            transition = pipTransition,
+            info = createOpenChangeTransition(task, TRANSIT_PIP),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+        )
+        transitionObserver.onTransitionFinished(transition = pipTransition, aborted = true)
+
+        verify(taskRepository).onPipAborted(task.displayId, task.taskId)
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    fun exitPipTransition_taskRepositoryClearTaskInPip() {
+        val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM)
+        whenever(taskRepository.isTaskMinimizedPipInDisplay(any(), any())).thenReturn(true)
+
+        transitionObserver.onTransitionReady(
+            transition = mock(),
+            info = createOpenChangeTransition(task, type = TRANSIT_EXIT_PIP),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+        )
+
+        verify(taskRepository).setTaskInPip(task.displayId, task.taskId, enterPip = false)
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+    fun removePipTransition_taskRepositoryClearTaskInPip() {
+        val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM)
+        whenever(taskRepository.isTaskMinimizedPipInDisplay(any(), any())).thenReturn(true)
+
+        transitionObserver.onTransitionReady(
+            transition = mock(),
+            info = createOpenChangeTransition(task, type = TRANSIT_REMOVE_PIP),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+        )
+
+        verify(taskRepository).setTaskInPip(task.displayId, task.taskId, enterPip = false)
+    }
+
     private fun createBackNavigationTransition(
         task: RunningTaskInfo?,
         type: Int = TRANSIT_TO_BACK,
         withWallpaper: Boolean = false,
     ): TransitionInfo {
-        return TransitionInfo(type, 0 /* flags */).apply {
+        return TransitionInfo(type, /* flags= */ 0).apply {
             addChange(
                 Change(mock(), mock()).apply {
                     mode = type
@@ -331,7 +446,7 @@
         task: RunningTaskInfo?,
         type: Int = TRANSIT_OPEN,
     ): TransitionInfo {
-        return TransitionInfo(TRANSIT_OPEN, 0 /* flags */).apply {
+        return TransitionInfo(type, /* flags= */ 0).apply {
             addChange(
                 Change(mock(), mock()).apply {
                     mode = TRANSIT_OPEN
@@ -343,8 +458,8 @@
         }
     }
 
-    private fun createCloseTransition(task: RunningTaskInfo?): TransitionInfo {
-        return TransitionInfo(TRANSIT_CLOSE, 0 /* flags */).apply {
+    private fun createCloseTransition(task: RunningTaskInfo?) =
+        TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0).apply {
             addChange(
                 Change(mock(), mock()).apply {
                     mode = TRANSIT_CLOSE
@@ -354,10 +469,9 @@
                 }
             )
         }
-    }
 
-    private fun createToBackTransition(task: RunningTaskInfo?): TransitionInfo {
-        return TransitionInfo(TRANSIT_TO_BACK, 0 /* flags */).apply {
+    private fun createToBackTransition(task: RunningTaskInfo?) =
+        TransitionInfo(TRANSIT_TO_BACK, /* flags= */ 0).apply {
             addChange(
                 Change(mock(), mock()).apply {
                     mode = TRANSIT_TO_BACK
@@ -367,6 +481,18 @@
                 }
             )
         }
+
+    private fun createToFrontTransition(task: RunningTaskInfo?): TransitionInfo {
+        return TransitionInfo(TRANSIT_TO_FRONT, 0 /* flags */).apply {
+            addChange(
+                Change(mock(), mock()).apply {
+                    mode = TRANSIT_TO_FRONT
+                    parent = null
+                    taskInfo = task
+                    flags = flags
+                }
+            )
+        }
     }
 
     private fun getLatestWct(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
index 2216d54..bf9cf00 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
@@ -676,8 +676,8 @@
             }
     }
 
-    private fun createTransitionInfo(type: Int, draggedTask: RunningTaskInfo): TransitionInfo {
-        return TransitionInfo(type, 0 /* flags */).apply {
+    private fun createTransitionInfo(type: Int, draggedTask: RunningTaskInfo) =
+        TransitionInfo(type, /* flags= */ 0).apply {
             addChange( // Home.
                 TransitionInfo.Change(mock(), homeTaskLeash).apply {
                     parent = null
@@ -700,7 +700,6 @@
                 }
             )
         }
-    }
 
     private fun systemPropertiesKey(name: String) =
         "${SpringDragToDesktopTransitionHandler.SYSTEM_PROPERTIES_GROUP}.$name"
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java
index e40bbad..1b1a5a9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java
@@ -150,4 +150,24 @@
         mController.onDrag(dragLayout, event);
         verify(mDragAndDropListener, never()).onDragStarted();
     }
+
+    @Test
+    public void testOnDragStarted_withNoClipDataOrDescription() {
+        final View dragLayout = mock(View.class);
+        final Display display = mock(Display.class);
+        doReturn(display).when(dragLayout).getDisplay();
+        doReturn(DEFAULT_DISPLAY).when(display).getDisplayId();
+
+        final DragEvent event = mock(DragEvent.class);
+        doReturn(ACTION_DRAG_STARTED).when(event).getAction();
+        doReturn(null).when(event).getClipData();
+        doReturn(null).when(event).getClipDescription();
+
+        // Ensure there's a target so that onDrag will execute
+        mController.addDisplayDropTarget(0, mContext, mock(WindowManager.class),
+                mock(FrameLayout.class), mock(DragLayout.class));
+
+        // Verify the listener is called on a valid drag action.
+        mController.onDrag(dragLayout, event);
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt
index d410151..5389c94 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt
@@ -43,7 +43,7 @@
  */
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class UnhandledDragControllerTest : ShellTestCase() {
+class GlobalDragListenerTest : ShellTestCase() {
     private val mIWindowManager = mock<IWindowManager>()
     private val mMainExecutor = mock<ShellExecutor>()
 
@@ -74,7 +74,7 @@
     @Test
     fun onUnhandledDrop_noListener_expectNotifyUnhandled() {
         // Simulate an unhandled drop
-        val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, 0, null, null, null,
+        val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, 0, 0, null, null, null,
             null, null, false)
         val wmCallback = mock<IUnhandledDragCallback>()
         mController.onUnhandledDrop(dropEvent, wmCallback)
@@ -98,7 +98,7 @@
 
         // Simulate an unhandled drop
         val dragSurface = mock<SurfaceControl>()
-        val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, 0, null, null, null,
+        val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, 0, 0, null, null, null,
             dragSurface, null, false)
         val wmCallback = mock<IUnhandledDragCallback>()
         mController.onUnhandledDrop(dropEvent, wmCallback)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java
index 0cf15ba..a284663 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java
@@ -220,7 +220,7 @@
         setRunningTask(mHomeTask);
         DragSession dragSession = new DragSession(mActivityTaskManager,
                 mLandscapeDisplayLayout, data, 0 /* dragFlags */);
-        dragSession.initialize();
+        dragSession.initialize(false /* skipUpdateRunningTask */);
         mPolicy.start(dragSession, mLoggerSessionId);
         ArrayList<Target> targets = assertExactTargetTypes(
                 mPolicy.getTargets(mInsets), TYPE_FULLSCREEN);
@@ -235,7 +235,7 @@
         setRunningTask(mFullscreenAppTask);
         DragSession dragSession = new DragSession(mActivityTaskManager,
                 mLandscapeDisplayLayout, data, 0 /* dragFlags */);
-        dragSession.initialize();
+        dragSession.initialize(false /* skipUpdateRunningTask */);
         mPolicy.start(dragSession, mLoggerSessionId);
         ArrayList<Target> targets = assertExactTargetTypes(
                 mPolicy.getTargets(mInsets), TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT);
@@ -255,7 +255,7 @@
         setRunningTask(mFullscreenAppTask);
         DragSession dragSession = new DragSession(mActivityTaskManager,
                 mPortraitDisplayLayout, data, 0 /* dragFlags */);
-        dragSession.initialize();
+        dragSession.initialize(false /* skipUpdateRunningTask */);
         mPolicy.start(dragSession, mLoggerSessionId);
         ArrayList<Target> targets = assertExactTargetTypes(
                 mPolicy.getTargets(mInsets), TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM);
@@ -276,7 +276,7 @@
         setRunningTask(mFullscreenAppTask);
         DragSession dragSession = new DragSession(mActivityTaskManager,
                 mLandscapeDisplayLayout, mActivityClipData, 0 /* dragFlags */);
-        dragSession.initialize();
+        dragSession.initialize(false /* skipUpdateRunningTask */);
         mPolicy.start(dragSession, mLoggerSessionId);
         ArrayList<Target> targets = mPolicy.getTargets(mInsets);
         for (Target t : targets) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java
index a8aa257..c42f6c3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java
@@ -30,6 +30,7 @@
 import static org.mockito.kotlin.VerificationKt.times;
 import static org.mockito.kotlin.VerificationKt.verify;
 
+import android.app.TaskInfo;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Matrix;
@@ -45,7 +46,9 @@
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.desktopmode.DesktopRepository;
 import com.android.wm.shell.desktopmode.DesktopUserRepositories;
+import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider;
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
 import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
@@ -56,6 +59,7 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 import java.util.Optional;
@@ -83,7 +87,8 @@
     @Mock private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mMockFactory;
     @Mock private SurfaceControl.Transaction mMockTransaction;
     @Mock private PipAlphaAnimator mMockAlphaAnimator;
-    @Mock private Optional<DesktopUserRepositories> mMockOptionalDesktopUserRepositories;
+    @Mock private DesktopUserRepositories mMockDesktopUserRepositories;
+    @Mock private DesktopWallpaperActivityTokenProvider mMockDesktopWallpaperActivityTokenProvider;
     @Mock private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
 
     @Captor private ArgumentCaptor<Runnable> mRunnableArgumentCaptor;
@@ -100,9 +105,13 @@
         when(mMockFactory.getTransaction()).thenReturn(mMockTransaction);
         when(mMockTransaction.setMatrix(any(SurfaceControl.class), any(Matrix.class), any()))
                 .thenReturn(mMockTransaction);
+        when(mMockDesktopUserRepositories.getCurrent())
+                .thenReturn(Mockito.mock(DesktopRepository.class));
+        when(mMockPipTransitionState.getPipTaskInfo()).thenReturn(Mockito.mock(TaskInfo.class));
 
         mPipScheduler = new PipScheduler(mMockContext, mMockPipBoundsState, mMockMainExecutor,
-                mMockPipTransitionState, mMockOptionalDesktopUserRepositories,
+                mMockPipTransitionState, Optional.of(mMockDesktopUserRepositories),
+                Optional.of(mMockDesktopWallpaperActivityTokenProvider),
                 mRootTaskDisplayAreaOrganizer);
         mPipScheduler.setPipTransitionController(mMockPipTransitionController);
         mPipScheduler.setSurfaceControlTransactionFactory(mMockFactory);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt
index fd3adab..3209664 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt
@@ -40,6 +40,7 @@
 
 /**
  * Tests for [GroupedTaskInfo]
+ * Build & Run: atest WMShellUnitTests:GroupedTaskInfoTest
  */
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -47,7 +48,7 @@
 
     @Test
     fun testSingleTask_hasCorrectType() {
-        assertThat(singleTaskGroupInfo().type).isEqualTo(TYPE_FULLSCREEN)
+        assertThat(singleTaskGroupInfo().isBaseType(TYPE_FULLSCREEN)).isTrue()
     }
 
     @Test
@@ -66,7 +67,7 @@
 
     @Test
     fun testSplitTasks_hasCorrectType() {
-        assertThat(splitTasksGroupInfo().type).isEqualTo(TYPE_SPLIT)
+        assertThat(splitTasksGroupInfo().isBaseType(TYPE_SPLIT)).isTrue()
     }
 
     @Test
@@ -87,8 +88,8 @@
 
     @Test
     fun testFreeformTasks_hasCorrectType() {
-        assertThat(freeformTasksGroupInfo(freeformTaskIds = arrayOf(1)).type)
-            .isEqualTo(TYPE_FREEFORM)
+        assertThat(freeformTasksGroupInfo(freeformTaskIds = arrayOf(1)).isBaseType(TYPE_FREEFORM))
+            .isTrue()
     }
 
     @Test
@@ -111,83 +112,155 @@
     }
 
     @Test
+    fun testMixedWithFullscreenBase_hasCorrectType() {
+        assertThat(mixedTaskGroupInfoWithFullscreenBase().isBaseType(TYPE_FULLSCREEN)).isTrue()
+    }
+
+    @Test
+    fun testMixedWithSplitBase_hasCorrectType() {
+        assertThat(mixedTaskGroupInfoWithSplitBase().isBaseType(TYPE_SPLIT)).isTrue()
+    }
+
+    @Test
+    fun testMixedWithFreeformBase_hasCorrectType() {
+        assertThat(mixedTaskGroupInfoWithFreeformBase().isBaseType(TYPE_FREEFORM)).isTrue()
+    }
+
+    @Test
+    fun testMixed_disallowEmptyMixed() {
+        assertThrows(IllegalArgumentException::class.java) {
+            GroupedTaskInfo.forMixed(listOf())
+        }
+    }
+
+    @Test
+    fun testMixed_disallowNestedMixed() {
+        assertThrows(IllegalArgumentException::class.java) {
+            GroupedTaskInfo.forMixed(listOf(
+                GroupedTaskInfo.forMixed(listOf(singleTaskGroupInfo()))))
+        }
+    }
+
+    @Test
+    fun testMixed_disallowNonMixedAccessors() {
+        val mixed = mixedTaskGroupInfoWithFullscreenBase()
+        assertThrows(IllegalStateException::class.java) {
+            mixed.taskInfo1
+        }
+        assertThrows(IllegalStateException::class.java) {
+            mixed.taskInfo2
+        }
+        assertThrows(IllegalStateException::class.java) {
+            mixed.splitBounds
+        }
+        assertThrows(IllegalStateException::class.java) {
+            mixed.minimizedTaskIds
+        }
+    }
+
+    @Test
     fun testParcelling_singleTask() {
-        val recentTaskInfo = singleTaskGroupInfo()
+        val taskInfo = singleTaskGroupInfo()
         val parcel = Parcel.obtain()
-        recentTaskInfo.writeToParcel(parcel, 0)
+        taskInfo.writeToParcel(parcel, 0)
         parcel.setDataPosition(0)
         // Read the object back from the parcel
-        val recentTaskInfoParcel: GroupedTaskInfo =
+        val taskInfoFromParcel: GroupedTaskInfo =
             GroupedTaskInfo.CREATOR.createFromParcel(parcel)
-        assertThat(recentTaskInfoParcel.type).isEqualTo(TYPE_FULLSCREEN)
-        assertThat(recentTaskInfoParcel.taskInfo1.taskId).isEqualTo(1)
-        assertThat(recentTaskInfoParcel.taskInfo2).isNull()
+        assertThat(taskInfoFromParcel.isBaseType(TYPE_FULLSCREEN)).isTrue()
+        assertThat(taskInfoFromParcel.taskInfo1.taskId).isEqualTo(1)
+        assertThat(taskInfoFromParcel.taskInfo2).isNull()
     }
 
     @Test
     fun testParcelling_splitTasks() {
-        val recentTaskInfo = splitTasksGroupInfo()
+        val taskInfo = splitTasksGroupInfo()
         val parcel = Parcel.obtain()
-        recentTaskInfo.writeToParcel(parcel, 0)
+        taskInfo.writeToParcel(parcel, 0)
         parcel.setDataPosition(0)
         // Read the object back from the parcel
-        val recentTaskInfoParcel: GroupedTaskInfo =
+        val taskInfoFromParcel: GroupedTaskInfo =
             GroupedTaskInfo.CREATOR.createFromParcel(parcel)
-        assertThat(recentTaskInfoParcel.type).isEqualTo(TYPE_SPLIT)
-        assertThat(recentTaskInfoParcel.taskInfo1.taskId).isEqualTo(1)
-        assertThat(recentTaskInfoParcel.taskInfo2).isNotNull()
-        assertThat(recentTaskInfoParcel.taskInfo2!!.taskId).isEqualTo(2)
-        assertThat(recentTaskInfoParcel.splitBounds).isNotNull()
-        assertThat(recentTaskInfoParcel.splitBounds!!.snapPosition).isEqualTo(SNAP_TO_2_50_50)
+        assertThat(taskInfoFromParcel.isBaseType(TYPE_SPLIT)).isTrue()
+        assertThat(taskInfoFromParcel.taskInfo1.taskId).isEqualTo(1)
+        assertThat(taskInfoFromParcel.taskInfo2).isNotNull()
+        assertThat(taskInfoFromParcel.taskInfo2!!.taskId).isEqualTo(2)
+        assertThat(taskInfoFromParcel.splitBounds).isNotNull()
+        assertThat(taskInfoFromParcel.splitBounds!!.snapPosition).isEqualTo(SNAP_TO_2_50_50)
     }
 
     @Test
     fun testParcelling_freeformTasks() {
-        val recentTaskInfo = freeformTasksGroupInfo(freeformTaskIds = arrayOf(1, 2, 3))
+        val taskInfo = freeformTasksGroupInfo(freeformTaskIds = arrayOf(1, 2, 3))
         val parcel = Parcel.obtain()
-        recentTaskInfo.writeToParcel(parcel, 0)
+        taskInfo.writeToParcel(parcel, 0)
         parcel.setDataPosition(0)
         // Read the object back from the parcel
-        val recentTaskInfoParcel: GroupedTaskInfo =
+        val taskInfoFromParcel: GroupedTaskInfo =
             GroupedTaskInfo.CREATOR.createFromParcel(parcel)
-        assertThat(recentTaskInfoParcel.type).isEqualTo(TYPE_FREEFORM)
-        assertThat(recentTaskInfoParcel.taskInfoList).hasSize(3)
+        assertThat(taskInfoFromParcel.isBaseType(TYPE_FREEFORM)).isTrue()
+        assertThat(taskInfoFromParcel.taskInfoList).hasSize(3)
         // Only compare task ids
         val taskIdComparator = Correspondence.transforming<TaskInfo, Int>(
             { it?.taskId }, "has taskId of"
         )
-        assertThat(recentTaskInfoParcel.taskInfoList).comparingElementsUsing(taskIdComparator)
-            .containsExactly(1, 2, 3)
+        assertThat(taskInfoFromParcel.taskInfoList).comparingElementsUsing(taskIdComparator)
+            .containsExactly(1, 2, 3).inOrder()
     }
 
     @Test
     fun testParcelling_freeformTasks_minimizedTasks() {
-        val recentTaskInfo = freeformTasksGroupInfo(
+        val taskInfo = freeformTasksGroupInfo(
             freeformTaskIds = arrayOf(1, 2, 3), minimizedTaskIds = arrayOf(2))
 
         val parcel = Parcel.obtain()
-        recentTaskInfo.writeToParcel(parcel, 0)
+        taskInfo.writeToParcel(parcel, 0)
         parcel.setDataPosition(0)
 
         // Read the object back from the parcel
-        val recentTaskInfoParcel: GroupedTaskInfo =
+        val taskInfoFromParcel: GroupedTaskInfo =
             GroupedTaskInfo.CREATOR.createFromParcel(parcel)
-        assertThat(recentTaskInfoParcel.type).isEqualTo(TYPE_FREEFORM)
-        assertThat(recentTaskInfoParcel.minimizedTaskIds).isEqualTo(arrayOf(2).toIntArray())
+        assertThat(taskInfoFromParcel.isBaseType(TYPE_FREEFORM)).isTrue()
+        assertThat(taskInfoFromParcel.minimizedTaskIds).isEqualTo(arrayOf(2).toIntArray())
     }
 
     @Test
-    fun testGetTaskById_singleTasks() {
+    fun testParcelling_mixedTasks() {
+        val taskInfo = GroupedTaskInfo.forMixed(listOf(
+                freeformTasksGroupInfo(freeformTaskIds = arrayOf(4, 5, 6),
+                    minimizedTaskIds = arrayOf(5)),
+                splitTasksGroupInfo(firstId = 2, secondId = 3),
+                singleTaskGroupInfo(id = 1)))
+
+        val parcel = Parcel.obtain()
+        taskInfo.writeToParcel(parcel, 0)
+        parcel.setDataPosition(0)
+
+        // Read the object back from the parcel
+        val taskInfoFromParcel: GroupedTaskInfo =
+            GroupedTaskInfo.CREATOR.createFromParcel(parcel)
+        assertThat(taskInfoFromParcel.isBaseType(TYPE_FREEFORM)).isTrue()
+        assertThat(taskInfoFromParcel.baseGroupedTask.minimizedTaskIds).isEqualTo(
+            arrayOf(5).toIntArray())
+        for (i in 1..6) {
+            assertThat(taskInfoFromParcel.containsTask(i)).isTrue()
+        }
+        assertThat(taskInfoFromParcel.taskInfoList).hasSize(taskInfo.taskInfoList.size)
+    }
+
+    @Test
+    fun testTaskProperties_singleTasks() {
         val task1 = createTaskInfo(id = 1234)
 
         val taskInfo = GroupedTaskInfo.forFullscreenTasks(task1)
 
         assertThat(taskInfo.getTaskById(1234)).isEqualTo(task1)
         assertThat(taskInfo.containsTask(1234)).isTrue()
+        assertThat(taskInfo.taskInfoList).isEqualTo(listOf(task1))
     }
 
     @Test
-    fun testGetTaskById_multipleTasks() {
+    fun testTaskProperties_splitTasks() {
         val task1 = createTaskInfo(id = 1)
         val task2 = createTaskInfo(id = 2)
         val splitBounds = SplitBounds(Rect(), Rect(), 1, 2, SNAP_TO_2_50_50)
@@ -198,6 +271,41 @@
         assertThat(taskInfo.getTaskById(2)).isEqualTo(task2)
         assertThat(taskInfo.containsTask(1)).isTrue()
         assertThat(taskInfo.containsTask(2)).isTrue()
+        assertThat(taskInfo.taskInfoList).isEqualTo(listOf(task1, task2))
+    }
+
+    @Test
+    fun testTaskProperties_freeformTasks() {
+        val task1 = createTaskInfo(id = 1)
+        val task2 = createTaskInfo(id = 2)
+
+        val taskInfo = GroupedTaskInfo.forFreeformTasks(listOf(task1, task2), setOf())
+
+        assertThat(taskInfo.getTaskById(1)).isEqualTo(task1)
+        assertThat(taskInfo.getTaskById(2)).isEqualTo(task2)
+        assertThat(taskInfo.containsTask(1)).isTrue()
+        assertThat(taskInfo.containsTask(2)).isTrue()
+        assertThat(taskInfo.taskInfoList).isEqualTo(listOf(task1, task2))
+    }
+
+    @Test
+    fun testTaskProperties_mixedTasks() {
+        val task1 = createTaskInfo(id = 1)
+        val task2 = createTaskInfo(id = 2)
+        val task3 = createTaskInfo(id = 3)
+        val splitBounds = SplitBounds(Rect(), Rect(), 1, 2, SNAP_TO_2_50_50)
+
+        val splitTasks = GroupedTaskInfo.forSplitTasks(task1, task2, splitBounds)
+        val fullscreenTasks = GroupedTaskInfo.forFullscreenTasks(task3)
+        val mixedTasks = GroupedTaskInfo.forMixed(listOf(splitTasks, fullscreenTasks))
+
+        assertThat(mixedTasks.getTaskById(1)).isEqualTo(task1)
+        assertThat(mixedTasks.getTaskById(2)).isEqualTo(task2)
+        assertThat(mixedTasks.getTaskById(3)).isEqualTo(task3)
+        assertThat(mixedTasks.containsTask(1)).isTrue()
+        assertThat(mixedTasks.containsTask(2)).isTrue()
+        assertThat(mixedTasks.containsTask(3)).isTrue()
+        assertThat(mixedTasks.taskInfoList).isEqualTo(listOf(task1, task2, task3))
     }
 
     private fun createTaskInfo(id: Int) = ActivityManager.RecentTaskInfo().apply {
@@ -205,14 +313,14 @@
         token = WindowContainerToken(mock(IWindowContainerToken::class.java))
     }
 
-    private fun singleTaskGroupInfo(): GroupedTaskInfo {
-        val task = createTaskInfo(id = 1)
+    private fun singleTaskGroupInfo(id: Int = 1): GroupedTaskInfo {
+        val task = createTaskInfo(id)
         return GroupedTaskInfo.forFullscreenTasks(task)
     }
 
-    private fun splitTasksGroupInfo(): GroupedTaskInfo {
-        val task1 = createTaskInfo(id = 1)
-        val task2 = createTaskInfo(id = 2)
+    private fun splitTasksGroupInfo(firstId: Int = 1, secondId: Int = 2): GroupedTaskInfo {
+        val task1 = createTaskInfo(firstId)
+        val task2 = createTaskInfo(secondId)
         val splitBounds = SplitBounds(Rect(), Rect(), 1, 2, SNAP_TO_2_50_50)
         return GroupedTaskInfo.forSplitTasks(task1, task2, splitBounds)
     }
@@ -225,4 +333,22 @@
             freeformTaskIds.map { createTaskInfo(it) }.toList(),
             minimizedTaskIds.toSet())
     }
+
+    private fun mixedTaskGroupInfoWithFullscreenBase(): GroupedTaskInfo {
+        return GroupedTaskInfo.forMixed(listOf(
+            singleTaskGroupInfo(id = 1),
+            singleTaskGroupInfo(id = 2)))
+    }
+
+    private fun mixedTaskGroupInfoWithSplitBase(): GroupedTaskInfo {
+        return GroupedTaskInfo.forMixed(listOf(
+            splitTasksGroupInfo(firstId = 2, secondId = 3),
+            singleTaskGroupInfo(id = 1)))
+    }
+
+    private fun mixedTaskGroupInfoWithFreeformBase(): GroupedTaskInfo {
+        return GroupedTaskInfo.forMixed(listOf(
+            freeformTasksGroupInfo(freeformTaskIds = arrayOf(2, 3, 4)),
+            singleTaskGroupInfo(id = 1)))
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index 22b45e8..7e5d6ce 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -24,6 +24,9 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.launcher3.Flags.FLAG_ENABLE_USE_TOP_VISIBLE_ACTIVITY_FOR_EXCLUDE_FROM_RECENT_TASK;
 import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE;
+import static com.android.wm.shell.shared.GroupedTaskInfo.TYPE_FREEFORM;
+import static com.android.wm.shell.shared.GroupedTaskInfo.TYPE_FULLSCREEN;
+import static com.android.wm.shell.shared.GroupedTaskInfo.TYPE_SPLIT;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
 
 import static org.junit.Assert.assertEquals;
@@ -346,9 +349,9 @@
         GroupedTaskInfo singleGroup2 = recentTasks.get(2);
 
         // Check that groups have expected types
-        assertEquals(GroupedTaskInfo.TYPE_FREEFORM, freeformGroup.getType());
-        assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, singleGroup1.getType());
-        assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, singleGroup2.getType());
+        assertTrue(freeformGroup.isBaseType(TYPE_FREEFORM));
+        assertTrue(singleGroup1.isBaseType(TYPE_FULLSCREEN));
+        assertTrue(singleGroup2.isBaseType(TYPE_FULLSCREEN));
 
         // Check freeform group entries
         assertEquals(t1, freeformGroup.getTaskInfoList().get(0));
@@ -385,9 +388,9 @@
         GroupedTaskInfo singleGroup = recentTasks.get(2);
 
         // Check that groups have expected types
-        assertEquals(GroupedTaskInfo.TYPE_SPLIT, splitGroup.getType());
-        assertEquals(GroupedTaskInfo.TYPE_FREEFORM, freeformGroup.getType());
-        assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, singleGroup.getType());
+        assertTrue(splitGroup.isBaseType(TYPE_SPLIT));
+        assertTrue(freeformGroup.isBaseType(TYPE_FREEFORM));
+        assertTrue(singleGroup.isBaseType(TYPE_FULLSCREEN));
 
         // Check freeform group entries
         assertEquals(t3, freeformGroup.getTaskInfoList().get(0));
@@ -420,10 +423,10 @@
 
         // Expect no grouping of tasks
         assertEquals(4, recentTasks.size());
-        assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, recentTasks.get(0).getType());
-        assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, recentTasks.get(1).getType());
-        assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, recentTasks.get(2).getType());
-        assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, recentTasks.get(3).getType());
+        assertTrue(recentTasks.get(0).isBaseType(TYPE_FULLSCREEN));
+        assertTrue(recentTasks.get(1).isBaseType(TYPE_FULLSCREEN));
+        assertTrue(recentTasks.get(2).isBaseType(TYPE_FULLSCREEN));
+        assertTrue(recentTasks.get(3).isBaseType(TYPE_FULLSCREEN));
 
         assertEquals(t1, recentTasks.get(0).getTaskInfo1());
         assertEquals(t2, recentTasks.get(1).getTaskInfo1());
@@ -457,9 +460,9 @@
         GroupedTaskInfo singleGroup2 = recentTasks.get(2);
 
         // Check that groups have expected types
-        assertEquals(GroupedTaskInfo.TYPE_FREEFORM, freeformGroup.getType());
-        assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, singleGroup1.getType());
-        assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, singleGroup2.getType());
+        assertTrue(freeformGroup.isBaseType(TYPE_FREEFORM));
+        assertTrue(singleGroup1.isBaseType(TYPE_FULLSCREEN));
+        assertTrue(singleGroup2.isBaseType(TYPE_FULLSCREEN));
 
         // Check freeform group entries
         assertEquals(3, freeformGroup.getTaskInfoList().size());
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
index 894d238..ab43119 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
@@ -169,7 +169,7 @@
         final IResultReceiver finishCallback = mock(IResultReceiver.class);
 
         final IBinder transition = startRecentsTransition(/* synthetic= */ true, runner);
-        verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any());
+        verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any(), any());
 
         // Finish and verify no transition remains and that the provided finish callback is called
         mRecentsTransitionHandler.findController(transition).finish(true /* toHome */,
@@ -184,7 +184,7 @@
         final IRecentsAnimationRunner runner = mock(IRecentsAnimationRunner.class);
 
         final IBinder transition = startRecentsTransition(/* synthetic= */ true, runner);
-        verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any());
+        verify(runner).onAnimationStart(any(), any(), any(), any(), any(), any(), any());
 
         mRecentsTransitionHandler.findController(transition).cancel("test");
         mMainExecutor.flushAll();
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 ffe8e71..79e9b9c 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
@@ -59,11 +59,12 @@
 import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
 import com.android.window.flags.Flags
 import com.android.wm.shell.R
-import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction
 import com.android.wm.shell.desktopmode.DesktopImmersiveController
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.InputMethod
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger
 import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
+import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction
+import com.android.wm.shell.recents.RecentsTransitionStateListener
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
 import com.android.wm.shell.splitscreen.SplitScreenController
@@ -539,7 +540,8 @@
         onLeftSnapClickListenerCaptor.value.invoke()
 
         verify(mockDesktopTasksController, never())
-            .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT),
+            .snapToHalfScreen(
+                eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT),
                 eq(ResizeTrigger.MAXIMIZE_BUTTON),
                 eq(InputMethod.UNKNOWN_INPUT_METHOD),
                 eq(decor),
@@ -616,11 +618,12 @@
         onRightSnapClickListenerCaptor.value.invoke()
 
         verify(mockDesktopTasksController, never())
-            .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT),
+            .snapToHalfScreen(
+                eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT),
                 eq(ResizeTrigger.MAXIMIZE_BUTTON),
                 eq(InputMethod.UNKNOWN_INPUT_METHOD),
                 eq(decor),
-        )
+            )
     }
 
     @Test
@@ -1223,6 +1226,49 @@
         verify(task2, never()).onExclusionRegionChanged(newRegion)
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX)
+    fun testRecentsTransitionStateListener_requestedState_setsTransitionRunning() {
+        val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
+        val decoration = setUpMockDecorationForTask(task)
+        onTaskOpening(task, SurfaceControl())
+
+        desktopModeRecentsTransitionStateListener.onTransitionStateChanged(
+            RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED)
+
+        verify(decoration).setIsRecentsTransitionRunning(true)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX)
+    fun testRecentsTransitionStateListener_nonRunningState_setsTransitionNotRunning() {
+        val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
+        val decoration = setUpMockDecorationForTask(task)
+        onTaskOpening(task, SurfaceControl())
+        desktopModeRecentsTransitionStateListener.onTransitionStateChanged(
+            RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED)
+
+        desktopModeRecentsTransitionStateListener.onTransitionStateChanged(
+            RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING)
+
+        verify(decoration).setIsRecentsTransitionRunning(false)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX)
+    fun testRecentsTransitionStateListener_requestedAndAnimating_setsTransitionRunningOnce() {
+        val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
+        val decoration = setUpMockDecorationForTask(task)
+        onTaskOpening(task, SurfaceControl())
+
+        desktopModeRecentsTransitionStateListener.onTransitionStateChanged(
+            RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED)
+        desktopModeRecentsTransitionStateListener.onTransitionStateChanged(
+            RecentsTransitionStateListener.TRANSITION_STATE_ANIMATING)
+
+        verify(decoration, times(1)).setIsRecentsTransitionRunning(true)
+    }
+
     private fun createOpenTaskDecoration(
         @WindowingMode windowingMode: Int,
         taskSurface: SurfaceControl = SurfaceControl(),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
index b5e8ceb..8af8285 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
@@ -40,6 +40,7 @@
 import android.view.WindowInsets.Type.statusBars
 import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.android.internal.jank.InteractionJankMonitor
+import com.android.window.flags.Flags
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.ShellTestCase
@@ -65,6 +66,8 @@
 import com.android.wm.shell.desktopmode.education.AppHandleEducationController
 import com.android.wm.shell.desktopmode.education.AppToWebEducationController
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
+import com.android.wm.shell.recents.RecentsTransitionHandler
+import com.android.wm.shell.recents.RecentsTransitionStateListener
 import com.android.wm.shell.splitscreen.SplitScreenController
 import com.android.wm.shell.sysui.ShellCommandHandler
 import com.android.wm.shell.sysui.ShellController
@@ -151,6 +154,7 @@
     protected val mockFocusTransitionObserver = mock<FocusTransitionObserver>()
     protected val mockCaptionHandleRepository = mock<WindowDecorCaptionHandleRepository>()
     protected val mockDesktopRepository: DesktopRepository = mock<DesktopRepository>()
+    protected val mockRecentsTransitionHandler = mock<RecentsTransitionHandler>()
     protected val motionEvent = mock<MotionEvent>()
     val displayLayout = mock<DisplayLayout>()
     protected lateinit var spyContext: TestableContext
@@ -164,6 +168,7 @@
     protected lateinit var mockitoSession: StaticMockitoSession
     protected lateinit var shellInit: ShellInit
     internal lateinit var desktopModeOnInsetsChangedListener: DesktopModeOnInsetsChangedListener
+    protected lateinit var desktopModeRecentsTransitionStateListener: RecentsTransitionStateListener
     protected lateinit var displayChangingListener:
             DisplayChangeController.OnDisplayChangingListener
     internal lateinit var desktopModeOnKeyguardChangedListener: DesktopModeKeyguardChangeListener
@@ -219,7 +224,8 @@
             mockFocusTransitionObserver,
             desktopModeEventLogger,
             mock<DesktopModeUiEventLogger>(),
-            mock<WindowDecorTaskResourceLoader>()
+            mock<WindowDecorTaskResourceLoader>(),
+            mockRecentsTransitionHandler,
         )
         desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController)
         whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout)
@@ -256,6 +262,13 @@
         verify(displayInsetsController)
             .addGlobalInsetsChangedListener(insetsChangedCaptor.capture())
         desktopModeOnInsetsChangedListener = insetsChangedCaptor.firstValue
+        val recentsTransitionStateListenerCaptor = argumentCaptor<RecentsTransitionStateListener>()
+        if (Flags.enableDesktopRecentsTransitionsCornersBugfix()) {
+            verify(mockRecentsTransitionHandler)
+                .addTransitionStateListener(recentsTransitionStateListenerCaptor.capture())
+            desktopModeRecentsTransitionStateListener =
+                recentsTransitionStateListenerCaptor.firstValue
+        }
         val keyguardChangedCaptor =
             argumentCaptor<DesktopModeKeyguardChangeListener>()
         verify(mockShellController).addKeyguardChangeListener(keyguardChangedCaptor.capture())
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 7dac085..9ea5fd6 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
@@ -20,7 +20,6 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-import static android.app.assist.AssistContent.EXTRA_SESSION_TRANSFER_WEB_URI;
 import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
 import static android.view.InsetsSource.FLAG_FORCE_CONSUMING;
 import static android.view.InsetsSource.FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR;
@@ -170,6 +169,7 @@
     private static final boolean DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED = false;
     private static final boolean DEFAULT_IS_IN_FULL_IMMERSIVE_MODE = false;
     private static final boolean DEFAULT_HAS_GLOBAL_FOCUS = true;
+    private static final boolean DEFAULT_SHOULD_IGNORE_CORNER_RADIUS = false;
 
     @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
 
@@ -397,6 +397,31 @@
     }
 
     @Test
+    public void updateRelayoutParams_shouldIgnoreCornerRadius_roundedCornersNotSet() {
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+        taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+        fillRoundedCornersResources(/* fillValue= */ 30);
+        RelayoutParams relayoutParams = new RelayoutParams();
+
+        DesktopModeWindowDecoration.updateRelayoutParams(
+                relayoutParams,
+                mTestableContext,
+                taskInfo,
+                mMockSplitScreenController,
+                DEFAULT_APPLY_START_TRANSACTION_ON_DRAW,
+                DEFAULT_SHOULD_SET_TASK_POSITIONING_AND_CROP,
+                DEFAULT_IS_STATUSBAR_VISIBLE,
+                DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED,
+                DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
+                new InsetsState(),
+                DEFAULT_HAS_GLOBAL_FOCUS,
+                mExclusionRegion,
+                /* shouldIgnoreCornerRadius= */ true);
+
+        assertThat(relayoutParams.mCornerRadius).isEqualTo(INVALID_CORNER_RADIUS);
+    }
+
+    @Test
     @EnableFlags(Flags.FLAG_ENABLE_APP_HEADER_WITH_TASK_DENSITY)
     public void updateRelayoutParams_appHeader_usesTaskDensity() {
         final int systemDensity = mTestableContext.getOrCreateTestableResources().getResources()
@@ -635,7 +660,8 @@
                 /* inFullImmersiveMode */ true,
                 insetsState,
                 DEFAULT_HAS_GLOBAL_FOCUS,
-                mExclusionRegion);
+                mExclusionRegion,
+                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
 
         // Takes status bar inset as padding, ignores caption bar inset.
         assertThat(relayoutParams.mCaptionTopPadding).isEqualTo(50);
@@ -660,7 +686,8 @@
                 /* inFullImmersiveMode */ true,
                 new InsetsState(),
                 DEFAULT_HAS_GLOBAL_FOCUS,
-                mExclusionRegion);
+                mExclusionRegion,
+                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
 
         assertThat(relayoutParams.mIsInsetSource).isFalse();
     }
@@ -684,7 +711,8 @@
                 DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
                 new InsetsState(),
                 DEFAULT_HAS_GLOBAL_FOCUS,
-                mExclusionRegion);
+                mExclusionRegion,
+                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
 
         // Header is always shown because it's assumed the status bar is always visible.
         assertThat(relayoutParams.mIsCaptionVisible).isTrue();
@@ -708,7 +736,8 @@
                 DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
                 new InsetsState(),
                 DEFAULT_HAS_GLOBAL_FOCUS,
-                mExclusionRegion);
+                mExclusionRegion,
+                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
 
         assertThat(relayoutParams.mIsCaptionVisible).isTrue();
     }
@@ -731,7 +760,8 @@
                 DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
                 new InsetsState(),
                 DEFAULT_HAS_GLOBAL_FOCUS,
-                mExclusionRegion);
+                mExclusionRegion,
+                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
 
         assertThat(relayoutParams.mIsCaptionVisible).isFalse();
     }
@@ -754,7 +784,8 @@
                 DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
                 new InsetsState(),
                 DEFAULT_HAS_GLOBAL_FOCUS,
-                mExclusionRegion);
+                mExclusionRegion,
+                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
 
         assertThat(relayoutParams.mIsCaptionVisible).isFalse();
     }
@@ -778,7 +809,8 @@
                 /* inFullImmersiveMode */ true,
                 new InsetsState(),
                 DEFAULT_HAS_GLOBAL_FOCUS,
-                mExclusionRegion);
+                mExclusionRegion,
+                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
 
         assertThat(relayoutParams.mIsCaptionVisible).isTrue();
 
@@ -794,7 +826,8 @@
                 /* inFullImmersiveMode */ true,
                 new InsetsState(),
                 DEFAULT_HAS_GLOBAL_FOCUS,
-                mExclusionRegion);
+                mExclusionRegion,
+                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
 
         assertThat(relayoutParams.mIsCaptionVisible).isFalse();
     }
@@ -818,7 +851,8 @@
                 /* inFullImmersiveMode */ true,
                 new InsetsState(),
                 DEFAULT_HAS_GLOBAL_FOCUS,
-                mExclusionRegion);
+                mExclusionRegion,
+                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
 
         assertThat(relayoutParams.mIsCaptionVisible).isFalse();
     }
@@ -1176,7 +1210,7 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
-    public void webUriLink_webUriLinkUsedWhenWhenAvailable() {
+    public void sessionTransferUri_sessionTransferUriUsedWhenWhenAvailable() {
         final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
         final DesktopModeWindowDecoration decor = createWindowDecoration(
                 taskInfo, TEST_URI1 /* captured link */, TEST_URI2 /* web uri */,
@@ -1188,7 +1222,7 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
-    public void webUriLink_webUriLinkUsedWhenSessionTransferUriUnavailable() {
+    public void webUri_webUriUsedWhenSessionTransferUriUnavailable() {
         final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
         final DesktopModeWindowDecoration decor = createWindowDecoration(
                 taskInfo, TEST_URI1 /* captured link */, TEST_URI2 /* web uri */,
@@ -1200,7 +1234,7 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
-    public void genericLink_genericLinkUsedWhenCapturedLinkAndWebUriUnavailable() {
+    public void genericLink_genericLinkUsedWhenCapturedLinkAndAssistContentUriUnavailable() {
         final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
         final DesktopModeWindowDecoration decor = createWindowDecoration(
                 taskInfo, null /* captured link */, null /* web uri */,
@@ -1481,7 +1515,8 @@
                 DEFAULT_IS_IN_FULL_IMMERSIVE_MODE,
                 new InsetsState(),
                 DEFAULT_HAS_GLOBAL_FOCUS,
-                mExclusionRegion);
+                mExclusionRegion,
+                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
     }
 
     private DesktopModeWindowDecoration createWindowDecoration(
@@ -1490,7 +1525,7 @@
         taskInfo.capturedLink = capturedLink;
         taskInfo.capturedLinkTimestamp = System.currentTimeMillis();
         mAssistContent.setWebUri(webUri);
-        mAssistContent.getExtras().putObject(EXTRA_SESSION_TRANSFER_WEB_URI, sessionTransferUri);
+        mAssistContent.setSessionTransferUri(sessionTransferUri);
         final String genericLinkString = genericLink == null ? null : genericLink.toString();
         doReturn(genericLinkString).when(mMockGenericLinksParser).getGenericLink(any());
         // Relayout to set captured link
diff --git a/libs/androidfw/ZipUtils.cpp b/libs/androidfw/ZipUtils.cpp
index a1385f2..f7f62c5 100644
--- a/libs/androidfw/ZipUtils.cpp
+++ b/libs/androidfw/ZipUtils.cpp
@@ -87,19 +87,29 @@
     }
 
     bool ReadAtOffset(uint8_t* buf, size_t len, off64_t offset) const override {
-        if (mInputSize < len || offset > mInputSize - len) {
-            return false;
-        }
-
-        const incfs::map_ptr<uint8_t> pos = mInput.offset(offset);
-        if (!pos.verify(len)) {
+        auto in = AccessAtOffset(buf, len, offset);
+        if (!in) {
           return false;
         }
-
-        memcpy(buf, pos.unsafe_ptr(), len);
+        memcpy(buf, in, len);
         return true;
     }
 
+    const uint8_t* AccessAtOffset(uint8_t*, size_t len, off64_t offset) const override {
+      if (offset > mInputSize - len) {
+        return nullptr;
+      }
+      const incfs::map_ptr<uint8_t> pos = mInput.offset(offset);
+      if (!pos.verify(len)) {
+        return nullptr;
+      }
+      return pos.unsafe_ptr();
+    }
+
+    bool IsZeroCopy() const override {
+      return true;
+    }
+
   private:
     const incfs::map_ptr<uint8_t> mInput;
     const size_t mInputSize;
@@ -107,7 +117,7 @@
 
 class BufferWriter final : public zip_archive::Writer {
   public:
-    BufferWriter(void* output, size_t outputSize) : Writer(),
+    BufferWriter(void* output, size_t outputSize) :
         mOutput(reinterpret_cast<uint8_t*>(output)), mOutputSize(outputSize), mBytesWritten(0) {
     }
 
@@ -121,6 +131,12 @@
         return true;
     }
 
+    Buffer GetBuffer(size_t length) override {
+        const auto remaining_size = mOutputSize - mBytesWritten;
+        return remaining_size >= length
+                   ? Buffer(mOutput + mBytesWritten, remaining_size) : Buffer();
+    }
+
   private:
     uint8_t* const mOutput;
     const size_t mOutputSize;
diff --git a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java
index 9f3c345..81d9d81 100644
--- a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java
+++ b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java
@@ -74,6 +74,7 @@
                     /* context= */ this,
                     /* onExecuteFunction= */ (platformRequest,
                             callingPackage,
+                            callingPackageSigningInfo,
                             cancellationSignal,
                             callback) -> {
                         AppFunctionService.this.onExecuteFunction(
@@ -105,15 +106,17 @@
     /**
      * Called by the system to execute a specific app function.
      *
-     * <p>This method is triggered when the system requests your AppFunctionService to handle a
-     * particular function you have registered and made available.
+     * <p>This method is the entry point for handling all app function requests in an app. When the
+     * system needs your AppFunctionService to perform a function, it will invoke this method.
      *
-     * <p>To ensure proper routing of function requests, assign a unique identifier to each
-     * function. This identifier doesn't need to be globally unique, but it must be unique within
-     * your app. For example, a function to order food could be identified as "orderFood". In most
-     * cases this identifier should come from the ID automatically generated by the AppFunctions
-     * SDK. You can determine the specific function to invoke by calling {@link
-     * ExecuteAppFunctionRequest#getFunctionIdentifier()}.
+     * <p>Each function you've registered is identified by a unique identifier. This identifier
+     * doesn't need to be globally unique, but it must be unique within your app. For example, a
+     * function to order food could be identified as "orderFood". In most cases, this identifier is
+     * automatically generated by the AppFunctions SDK.
+     *
+     * <p>You can determine which function to execute by calling {@link
+     * ExecuteAppFunctionRequest#getFunctionIdentifier()}. This allows your service to route the
+     * incoming request to the appropriate logic for handling the specific function.
      *
      * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker
      * thread and dispatch the result with the given callback. You should always report back the
@@ -132,7 +135,5 @@
             @NonNull ExecuteAppFunctionRequest request,
             @NonNull String callingPackage,
             @NonNull CancellationSignal cancellationSignal,
-            @NonNull
-                    OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException>
-                            callback);
+            @NonNull OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> callback);
 }
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index 76ad2ac..5e71d33 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -34,13 +34,6 @@
 }
 
 flag {
-  name: "high_contrast_text_luminance"
-  namespace: "accessibility"
-  description: "Use luminance to determine how to make text more high contrast, instead of RGB heuristic"
-  bug: "186567103"
-}
-
-flag {
   name: "high_contrast_text_small_text_rect"
   namespace: "accessibility"
   description: "Draw a solid rectangle background behind text instead of a stroke outline"
diff --git a/libs/hwui/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h
index e13e1365..e05c3d6 100644
--- a/libs/hwui/hwui/DrawTextFunctor.h
+++ b/libs/hwui/hwui/DrawTextFunctor.h
@@ -34,9 +34,6 @@
 namespace flags = com::android::graphics::hwui::flags;
 #else
 namespace flags {
-constexpr bool high_contrast_text_luminance() {
-    return false;
-}
 constexpr bool high_contrast_text_small_text_rect() {
     return false;
 }
@@ -114,15 +111,10 @@
         if (CC_UNLIKELY(canvas->isHighContrastText() && paint.getAlpha() != 0)) {
             // high contrast draw path
             int color = paint.getColor();
-            bool darken;
-            // This equation should match the one in core/java/android/text/Layout.java
-            if (flags::high_contrast_text_luminance()) {
-                uirenderer::Lab lab = uirenderer::sRGBToLab(color);
-                darken = lab.L <= 50;
-            } else {
-                int channelSum = SkColorGetR(color) + SkColorGetG(color) + SkColorGetB(color);
-                darken = channelSum < (128 * 3);
-            }
+            // LINT.IfChange(hct_darken)
+            uirenderer::Lab lab = uirenderer::sRGBToLab(color);
+            bool darken = lab.L <= 50;
+            // LINT.ThenChange(/core/java/android/text/Layout.java:hct_darken)
 
             // outline
             gDrawTextBlobMode = DrawTextBlobMode::HctOutline;
diff --git a/libs/hwui/hwui/MinikinUtils.cpp b/libs/hwui/hwui/MinikinUtils.cpp
index 7b45070..290df99 100644
--- a/libs/hwui/hwui/MinikinUtils.cpp
+++ b/libs/hwui/hwui/MinikinUtils.cpp
@@ -36,7 +36,7 @@
     const Typeface* resolvedFace = Typeface::resolveDefault(typeface);
     const SkFont& font = paint->getSkFont();
 
-    minikin::MinikinPaint minikinPaint(resolvedFace->fFontCollection);
+    minikin::MinikinPaint minikinPaint(resolvedFace->getFontCollection());
     /* Prepare minikin Paint */
     minikinPaint.size =
             font.isLinearMetrics() ? font.getSize() : static_cast<int>(font.getSize());
@@ -46,9 +46,9 @@
     minikinPaint.wordSpacing = paint->getWordSpacing();
     minikinPaint.fontFlags = MinikinFontSkia::packFontFlags(font);
     minikinPaint.localeListId = paint->getMinikinLocaleListId();
-    minikinPaint.fontStyle = resolvedFace->fStyle;
+    minikinPaint.fontStyle = resolvedFace->getFontStyle();
     minikinPaint.fontFeatureSettings = paint->getFontFeatureSettings();
-    if (!resolvedFace->fIsVariationInstance) {
+    if (!resolvedFace->isVariationInstance()) {
         // This is an optimization for direct private API use typically done by System UI.
         // In the public API surface, if Typeface is already configured for variation instance
         // (Target SDK <= 35) the font variation settings of Paint is not set.
@@ -132,7 +132,7 @@
 
 bool MinikinUtils::hasVariationSelector(const Typeface* typeface, uint32_t codepoint, uint32_t vs) {
     const Typeface* resolvedFace = Typeface::resolveDefault(typeface);
-    return resolvedFace->fFontCollection->hasVariationSelector(codepoint, vs);
+    return resolvedFace->getFontCollection()->hasVariationSelector(codepoint, vs);
 }
 
 float MinikinUtils::xOffsetForTextAlign(Paint* paint, const minikin::Layout& layout) {
diff --git a/libs/hwui/hwui/Typeface.cpp b/libs/hwui/hwui/Typeface.cpp
index 4dfe053..a73aac6 100644
--- a/libs/hwui/hwui/Typeface.cpp
+++ b/libs/hwui/hwui/Typeface.cpp
@@ -70,74 +70,45 @@
 
 Typeface* Typeface::createRelative(Typeface* src, Typeface::Style style) {
     const Typeface* resolvedFace = Typeface::resolveDefault(src);
-    Typeface* result = new Typeface;
-    if (result != nullptr) {
-        result->fFontCollection = resolvedFace->fFontCollection;
-        result->fBaseWeight = resolvedFace->fBaseWeight;
-        result->fAPIStyle = style;
-        result->fStyle = computeRelativeStyle(result->fBaseWeight, style);
-        result->fIsVariationInstance = resolvedFace->fIsVariationInstance;
-    }
-    return result;
+    return new Typeface(resolvedFace->getFontCollection(),
+                        computeRelativeStyle(resolvedFace->getBaseWeight(), style), style,
+                        resolvedFace->getBaseWeight(), resolvedFace->isVariationInstance());
 }
 
 Typeface* Typeface::createAbsolute(Typeface* base, int weight, bool italic) {
     const Typeface* resolvedFace = Typeface::resolveDefault(base);
-    Typeface* result = new Typeface();
-    if (result != nullptr) {
-        result->fFontCollection = resolvedFace->fFontCollection;
-        result->fBaseWeight = resolvedFace->fBaseWeight;
-        result->fAPIStyle = computeAPIStyle(weight, italic);
-        result->fStyle = computeMinikinStyle(weight, italic);
-        result->fIsVariationInstance = resolvedFace->fIsVariationInstance;
-    }
-    return result;
+    return new Typeface(resolvedFace->getFontCollection(), computeMinikinStyle(weight, italic),
+                        computeAPIStyle(weight, italic), resolvedFace->getBaseWeight(),
+                        resolvedFace->isVariationInstance());
 }
 
 Typeface* Typeface::createFromTypefaceWithVariation(Typeface* src,
                                                     const minikin::VariationSettings& variations) {
     const Typeface* resolvedFace = Typeface::resolveDefault(src);
-    Typeface* result = new Typeface();
-    if (result != nullptr) {
-        result->fFontCollection =
-                resolvedFace->fFontCollection->createCollectionWithVariation(variations);
-        if (result->fFontCollection == nullptr) {
+    const std::shared_ptr<minikin::FontCollection>& fc =
+            resolvedFace->getFontCollection()->createCollectionWithVariation(variations);
+    return new Typeface(
             // None of passed axes are supported by this collection.
             // So we will reuse the same collection with incrementing reference count.
-            result->fFontCollection = resolvedFace->fFontCollection;
-        }
-        // Do not update styles.
-        // TODO: We may want to update base weight if the 'wght' is specified.
-        result->fBaseWeight = resolvedFace->fBaseWeight;
-        result->fAPIStyle = resolvedFace->fAPIStyle;
-        result->fStyle = resolvedFace->fStyle;
-        result->fIsVariationInstance = true;
-    }
-    return result;
+            fc ? fc : resolvedFace->getFontCollection(),
+            // Do not update styles.
+            // TODO: We may want to update base weight if the 'wght' is specified.
+            resolvedFace->fStyle, resolvedFace->getAPIStyle(), resolvedFace->getBaseWeight(), true);
 }
 
 Typeface* Typeface::createWithDifferentBaseWeight(Typeface* src, int weight) {
     const Typeface* resolvedFace = Typeface::resolveDefault(src);
-    Typeface* result = new Typeface;
-    if (result != nullptr) {
-        result->fFontCollection = resolvedFace->fFontCollection;
-        result->fBaseWeight = weight;
-        result->fAPIStyle = resolvedFace->fAPIStyle;
-        result->fStyle = computeRelativeStyle(weight, result->fAPIStyle);
-        result->fIsVariationInstance = resolvedFace->fIsVariationInstance;
-    }
-    return result;
+    return new Typeface(resolvedFace->getFontCollection(),
+                        computeRelativeStyle(weight, resolvedFace->getAPIStyle()),
+                        resolvedFace->getAPIStyle(), weight, resolvedFace->isVariationInstance());
 }
 
 Typeface* Typeface::createFromFamilies(std::vector<std::shared_ptr<minikin::FontFamily>>&& families,
                                        int weight, int italic, const Typeface* fallback) {
-    Typeface* result = new Typeface;
-    if (fallback == nullptr) {
-        result->fFontCollection = minikin::FontCollection::create(std::move(families));
-    } else {
-        result->fFontCollection =
-                fallback->fFontCollection->createCollectionWithFamilies(std::move(families));
-    }
+    const std::shared_ptr<minikin::FontCollection>& fc =
+            fallback ? fallback->getFontCollection()->createCollectionWithFamilies(
+                               std::move(families))
+                     : minikin::FontCollection::create(std::move(families));
 
     if (weight == RESOLVE_BY_FONT_TABLE || italic == RESOLVE_BY_FONT_TABLE) {
         int weightFromFont;
@@ -171,11 +142,8 @@
         weight = SkFontStyle::kNormal_Weight;
     }
 
-    result->fBaseWeight = weight;
-    result->fAPIStyle = computeAPIStyle(weight, italic);
-    result->fStyle = computeMinikinStyle(weight, italic);
-    result->fIsVariationInstance = false;
-    return result;
+    return new Typeface(fc, computeMinikinStyle(weight, italic), computeAPIStyle(weight, italic),
+                        weight, false);
 }
 
 void Typeface::setDefault(const Typeface* face) {
@@ -205,11 +173,8 @@
     std::shared_ptr<minikin::FontCollection> collection =
             minikin::FontCollection::create(minikin::FontFamily::create(std::move(fonts)));
 
-    Typeface* hwTypeface = new Typeface();
-    hwTypeface->fFontCollection = collection;
-    hwTypeface->fAPIStyle = Typeface::kNormal;
-    hwTypeface->fBaseWeight = SkFontStyle::kNormal_Weight;
-    hwTypeface->fStyle = minikin::FontStyle();
+    Typeface* hwTypeface = new Typeface(collection, minikin::FontStyle(), Typeface::kNormal,
+                                        SkFontStyle::kNormal_Weight, false);
 
     Typeface::setDefault(hwTypeface);
 #endif
diff --git a/libs/hwui/hwui/Typeface.h b/libs/hwui/hwui/Typeface.h
index 97d1bf4..e8233a6 100644
--- a/libs/hwui/hwui/Typeface.h
+++ b/libs/hwui/hwui/Typeface.h
@@ -32,21 +32,39 @@
 
 struct ANDROID_API Typeface {
 public:
-    std::shared_ptr<minikin::FontCollection> fFontCollection;
+    enum Style : uint8_t { kNormal = 0, kBold = 0x01, kItalic = 0x02, kBoldItalic = 0x03 };
+    Typeface(const std::shared_ptr<minikin::FontCollection> fc, minikin::FontStyle style,
+             Style apiStyle, int baseWeight, bool isVariationInstance)
+            : fFontCollection(fc)
+            , fStyle(style)
+            , fAPIStyle(apiStyle)
+            , fBaseWeight(baseWeight)
+            , fIsVariationInstance(isVariationInstance) {}
+
+    const std::shared_ptr<minikin::FontCollection>& getFontCollection() const {
+        return fFontCollection;
+    }
 
     // resolved style actually used for rendering
-    minikin::FontStyle fStyle;
+    minikin::FontStyle getFontStyle() const { return fStyle; }
 
     // style used in the API
-    enum Style : uint8_t { kNormal = 0, kBold = 0x01, kItalic = 0x02, kBoldItalic = 0x03 };
-    Style fAPIStyle;
+    Style getAPIStyle() const { return fAPIStyle; }
 
     // base weight in CSS-style units, 1..1000
-    int fBaseWeight;
+    int getBaseWeight() const { return fBaseWeight; }
 
     // True if the Typeface is already created for variation settings.
-    bool fIsVariationInstance;
+    bool isVariationInstance() const { return fIsVariationInstance; }
 
+private:
+    std::shared_ptr<minikin::FontCollection> fFontCollection;
+    minikin::FontStyle fStyle;
+    Style fAPIStyle;
+    int fBaseWeight;
+    bool fIsVariationInstance = false;
+
+public:
     static const Typeface* resolveDefault(const Typeface* src);
 
     // The following three functions create new Typeface from an existing Typeface with a different
diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp
index 8d3a5eb..f6fdec1 100644
--- a/libs/hwui/jni/Paint.cpp
+++ b/libs/hwui/jni/Paint.cpp
@@ -609,7 +609,8 @@
         SkFont* font = &paint->getSkFont();
         const Typeface* typeface = paint->getAndroidTypeface();
         typeface = Typeface::resolveDefault(typeface);
-        minikin::FakedFont baseFont = typeface->fFontCollection->baseFontFaked(typeface->fStyle);
+        minikin::FakedFont baseFont =
+                typeface->getFontCollection()->baseFontFaked(typeface->getFontStyle());
         float saveSkewX = font->getSkewX();
         bool savefakeBold = font->isEmbolden();
         MinikinFontSkia::populateSkFont(font, baseFont.typeface().get(), baseFont.fakery);
@@ -641,7 +642,7 @@
         if (useLocale) {
             minikin::MinikinPaint minikinPaint = MinikinUtils::prepareMinikinPaint(paint, typeface);
             minikin::MinikinExtent extent =
-                    typeface->fFontCollection->getReferenceExtentForLocale(minikinPaint);
+                    typeface->getFontCollection()->getReferenceExtentForLocale(minikinPaint);
             metrics->fAscent = std::min(extent.ascent, metrics->fAscent);
             metrics->fDescent = std::max(extent.descent, metrics->fDescent);
             metrics->fTop = std::min(metrics->fAscent, metrics->fTop);
diff --git a/libs/hwui/jni/Typeface.cpp b/libs/hwui/jni/Typeface.cpp
index 707577d..63906de 100644
--- a/libs/hwui/jni/Typeface.cpp
+++ b/libs/hwui/jni/Typeface.cpp
@@ -99,17 +99,17 @@
 
 // CriticalNative
 static jint Typeface_getStyle(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) {
-    return toTypeface(faceHandle)->fAPIStyle;
+    return toTypeface(faceHandle)->getAPIStyle();
 }
 
 // CriticalNative
 static jint Typeface_getWeight(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) {
-    return toTypeface(faceHandle)->fStyle.weight();
+    return toTypeface(faceHandle)->getFontStyle().weight();
 }
 
 // Critical Native
 static jboolean Typeface_isVariationInstance(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) {
-    return toTypeface(faceHandle)->fIsVariationInstance;
+    return toTypeface(faceHandle)->isVariationInstance();
 }
 
 static jlong Typeface_createFromArray(JNIEnv *env, jobject, jlongArray familyArray,
@@ -128,18 +128,18 @@
 // CriticalNative
 static void Typeface_setDefault(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) {
     Typeface::setDefault(toTypeface(faceHandle));
-    minikin::SystemFonts::registerDefault(toTypeface(faceHandle)->fFontCollection);
+    minikin::SystemFonts::registerDefault(toTypeface(faceHandle)->getFontCollection());
 }
 
 static jobject Typeface_getSupportedAxes(JNIEnv *env, jobject, jlong faceHandle) {
     Typeface* face = toTypeface(faceHandle);
-    const size_t length = face->fFontCollection->getSupportedAxesCount();
+    const size_t length = face->getFontCollection()->getSupportedAxesCount();
     if (length == 0) {
         return nullptr;
     }
     std::vector<jint> tagVec(length);
     for (size_t i = 0; i < length; i++) {
-        tagVec[i] = face->fFontCollection->getSupportedAxisAt(i);
+        tagVec[i] = face->getFontCollection()->getSupportedAxisAt(i);
     }
     std::sort(tagVec.begin(), tagVec.end());
     const jintArray result = env->NewIntArray(length);
@@ -150,7 +150,7 @@
 static void Typeface_registerGenericFamily(JNIEnv *env, jobject, jstring familyName, jlong ptr) {
     ScopedUtfChars familyNameChars(env, familyName);
     minikin::SystemFonts::registerFallback(familyNameChars.c_str(),
-                                           toTypeface(ptr)->fFontCollection);
+                                           toTypeface(ptr)->getFontCollection());
 }
 
 #ifdef __ANDROID__
@@ -315,18 +315,19 @@
     std::vector<std::shared_ptr<minikin::FontCollection>> fontCollections;
     std::unordered_map<std::shared_ptr<minikin::FontCollection>, size_t> fcToIndex;
     for (Typeface* typeface : typefaces) {
-        bool inserted = fcToIndex.emplace(typeface->fFontCollection, fontCollections.size()).second;
+        bool inserted =
+                fcToIndex.emplace(typeface->getFontCollection(), fontCollections.size()).second;
         if (inserted) {
-            fontCollections.push_back(typeface->fFontCollection);
+            fontCollections.push_back(typeface->getFontCollection());
         }
     }
     minikin::FontCollection::writeVector(&writer, fontCollections);
     writer.write<uint32_t>(typefaces.size());
     for (Typeface* typeface : typefaces) {
-      writer.write<uint32_t>(fcToIndex.find(typeface->fFontCollection)->second);
-      typeface->fStyle.writeTo(&writer);
-      writer.write<Typeface::Style>(typeface->fAPIStyle);
-      writer.write<int>(typeface->fBaseWeight);
+        writer.write<uint32_t>(fcToIndex.find(typeface->getFontCollection())->second);
+        typeface->getFontStyle().writeTo(&writer);
+        writer.write<Typeface::Style>(typeface->getAPIStyle());
+        writer.write<int>(typeface->getBaseWeight());
     }
     return static_cast<jint>(writer.size());
 }
@@ -349,11 +350,10 @@
     std::vector<jlong> faceHandles;
     faceHandles.reserve(typefaceCount);
     for (uint32_t i = 0; i < typefaceCount; i++) {
-        Typeface* typeface = new Typeface;
-        typeface->fFontCollection = fontCollections[reader.read<uint32_t>()];
-        typeface->fStyle = minikin::FontStyle(&reader);
-        typeface->fAPIStyle = reader.read<Typeface::Style>();
-        typeface->fBaseWeight = reader.read<int>();
+        Typeface* typeface =
+                new Typeface(fontCollections[reader.read<uint32_t>()], minikin::FontStyle(&reader),
+                             reader.read<Typeface::Style>(), reader.read<int>(),
+                             false /* isVariationInstance */);
         faceHandles.push_back(toJLong(typeface));
     }
     const jlongArray result = env->NewLongArray(typefaceCount);
@@ -381,7 +381,8 @@
 
 // Critical Native
 static void Typeface_addFontCollection(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) {
-    std::shared_ptr<minikin::FontCollection> collection = toTypeface(faceHandle)->fFontCollection;
+    std::shared_ptr<minikin::FontCollection> collection =
+            toTypeface(faceHandle)->getFontCollection();
     minikin::SystemFonts::addFontMap(std::move(collection));
 }
 
diff --git a/libs/hwui/jni/text/TextShaper.cpp b/libs/hwui/jni/text/TextShaper.cpp
index d1782b2..7a4ae83 100644
--- a/libs/hwui/jni/text/TextShaper.cpp
+++ b/libs/hwui/jni/text/TextShaper.cpp
@@ -104,7 +104,7 @@
             } else {
                 fontId = fonts.size();  // This is new to us. Create new one.
                 std::shared_ptr<minikin::Font> font;
-                if (resolvedFace->fIsVariationInstance) {
+                if (resolvedFace->isVariationInstance()) {
                     // The optimization for target SDK 35 or before because the variation instance
                     // is already created and no runtime variation resolution happens on such
                     // environment.
diff --git a/libs/hwui/renderthread/HintSessionWrapper.h b/libs/hwui/renderthread/HintSessionWrapper.h
index 859cc57..4c96567 100644
--- a/libs/hwui/renderthread/HintSessionWrapper.h
+++ b/libs/hwui/renderthread/HintSessionWrapper.h
@@ -20,6 +20,7 @@
 #include <private/performance_hint_private.h>
 
 #include <future>
+#include <memory>
 #include <optional>
 #include <vector>
 
diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp
index 6571d92..a67aea4 100644
--- a/libs/hwui/renderthread/VulkanManager.cpp
+++ b/libs/hwui/renderthread/VulkanManager.cpp
@@ -729,7 +729,7 @@
         VkSemaphore semaphore;
         VkResult err = mCreateSemaphore(mDevice, &semaphoreInfo, nullptr, &semaphore);
         ALOGE_IF(VK_SUCCESS != err,
-                 "VulkanManager::makeSwapSemaphore(): Failed to create semaphore");
+                 "VulkanManager::finishFrame(): Failed to create semaphore");
 
         if (err == VK_SUCCESS) {
             sharedSemaphore = sp<SharedSemaphoreInfo>::make(mDestroySemaphore, mDevice, semaphore);
@@ -777,7 +777,7 @@
 
         int fenceFd = -1;
         VkResult err = mGetSemaphoreFdKHR(mDevice, &getFdInfo, &fenceFd);
-        ALOGE_IF(VK_SUCCESS != err, "VulkanManager::swapBuffers(): Failed to get semaphore Fd");
+        ALOGE_IF(VK_SUCCESS != err, "VulkanManager::finishFrame(): Failed to get semaphore Fd");
         drawResult.presentFence.reset(fenceFd);
     } else {
         ALOGE("VulkanManager::finishFrame(): Semaphore submission failed");
diff --git a/libs/hwui/tests/common/TestUtils.cpp b/libs/hwui/tests/common/TestUtils.cpp
index 93118aea..b51414f 100644
--- a/libs/hwui/tests/common/TestUtils.cpp
+++ b/libs/hwui/tests/common/TestUtils.cpp
@@ -183,8 +183,11 @@
 }
 
 SkFont TestUtils::defaultFont() {
-    const std::shared_ptr<minikin::MinikinFont>& minikinFont =
-      Typeface::resolveDefault(nullptr)->fFontCollection->getFamilyAt(0)->getFont(0)->baseTypeface();
+    const std::shared_ptr<minikin::MinikinFont>& minikinFont = Typeface::resolveDefault(nullptr)
+                                                                       ->getFontCollection()
+                                                                       ->getFamilyAt(0)
+                                                                       ->getFont(0)
+                                                                       ->baseTypeface();
     SkTypeface* skTypeface = reinterpret_cast<const MinikinFontSkia*>(minikinFont.get())->GetSkTypeface();
     LOG_ALWAYS_FATAL_IF(skTypeface == nullptr);
     return SkFont(sk_ref_sp(skTypeface));
diff --git a/libs/hwui/tests/unit/TypefaceTests.cpp b/libs/hwui/tests/unit/TypefaceTests.cpp
index c71c4d2..7bcd937 100644
--- a/libs/hwui/tests/unit/TypefaceTests.cpp
+++ b/libs/hwui/tests/unit/TypefaceTests.cpp
@@ -90,40 +90,40 @@
 
 TEST(TypefaceTest, createWithDifferentBaseWeight) {
     std::unique_ptr<Typeface> bold(Typeface::createWithDifferentBaseWeight(nullptr, 700));
-    EXPECT_EQ(700, bold->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
-    EXPECT_EQ(Typeface::kNormal, bold->fAPIStyle);
+    EXPECT_EQ(700, bold->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kNormal, bold->getAPIStyle());
 
     std::unique_ptr<Typeface> light(Typeface::createWithDifferentBaseWeight(nullptr, 300));
-    EXPECT_EQ(300, light->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, light->fStyle.slant());
-    EXPECT_EQ(Typeface::kNormal, light->fAPIStyle);
+    EXPECT_EQ(300, light->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, light->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kNormal, light->getAPIStyle());
 }
 
 TEST(TypefaceTest, createRelativeTest_fromRegular) {
     // In Java, Typeface.create(Typeface.DEFAULT, Typeface.NORMAL);
     std::unique_ptr<Typeface> normal(Typeface::createRelative(nullptr, Typeface::kNormal));
-    EXPECT_EQ(400, normal->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant());
-    EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle);
+    EXPECT_EQ(400, normal->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle());
 
     // In Java, Typeface.create(Typeface.DEFAULT, Typeface.BOLD);
     std::unique_ptr<Typeface> bold(Typeface::createRelative(nullptr, Typeface::kBold));
-    EXPECT_EQ(700, bold->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
-    EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+    EXPECT_EQ(700, bold->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
 
     // In Java, Typeface.create(Typeface.DEFAULT, Typeface.ITALIC);
     std::unique_ptr<Typeface> italic(Typeface::createRelative(nullptr, Typeface::kItalic));
-    EXPECT_EQ(400, italic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
-    EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+    EXPECT_EQ(400, italic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
 
     // In Java, Typeface.create(Typeface.DEFAULT, Typeface.BOLD_ITALIC);
     std::unique_ptr<Typeface> boldItalic(Typeface::createRelative(nullptr, Typeface::kBoldItalic));
-    EXPECT_EQ(700, boldItalic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
-    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle);
+    EXPECT_EQ(700, boldItalic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle());
 }
 
 TEST(TypefaceTest, createRelativeTest_BoldBase) {
@@ -132,31 +132,31 @@
     // In Java, Typeface.create(Typeface.create("sans-serif-bold"),
     // Typeface.NORMAL);
     std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal));
-    EXPECT_EQ(700, normal->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant());
-    EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle);
+    EXPECT_EQ(700, normal->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle());
 
     // In Java, Typeface.create(Typeface.create("sans-serif-bold"),
     // Typeface.BOLD);
     std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold));
-    EXPECT_EQ(1000, bold->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
-    EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+    EXPECT_EQ(1000, bold->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
 
     // In Java, Typeface.create(Typeface.create("sans-serif-bold"),
     // Typeface.ITALIC);
     std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic));
-    EXPECT_EQ(700, italic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
-    EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+    EXPECT_EQ(700, italic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
 
     // In Java, Typeface.create(Typeface.create("sans-serif-bold"),
     // Typeface.BOLD_ITALIC);
     std::unique_ptr<Typeface> boldItalic(
             Typeface::createRelative(base.get(), Typeface::kBoldItalic));
-    EXPECT_EQ(1000, boldItalic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
-    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle);
+    EXPECT_EQ(1000, boldItalic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle());
 }
 
 TEST(TypefaceTest, createRelativeTest_LightBase) {
@@ -165,31 +165,31 @@
     // In Java, Typeface.create(Typeface.create("sans-serif-light"),
     // Typeface.NORMAL);
     std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal));
-    EXPECT_EQ(300, normal->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant());
-    EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle);
+    EXPECT_EQ(300, normal->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle());
 
     // In Java, Typeface.create(Typeface.create("sans-serif-light"),
     // Typeface.BOLD);
     std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold));
-    EXPECT_EQ(600, bold->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
-    EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+    EXPECT_EQ(600, bold->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
 
     // In Java, Typeface.create(Typeface.create("sans-serif-light"),
     // Typeface.ITLIC);
     std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic));
-    EXPECT_EQ(300, italic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
-    EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+    EXPECT_EQ(300, italic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
 
     // In Java, Typeface.create(Typeface.create("sans-serif-light"),
     // Typeface.BOLD_ITALIC);
     std::unique_ptr<Typeface> boldItalic(
             Typeface::createRelative(base.get(), Typeface::kBoldItalic));
-    EXPECT_EQ(600, boldItalic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
-    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle);
+    EXPECT_EQ(600, boldItalic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle());
 }
 
 TEST(TypefaceTest, createRelativeTest_fromBoldStyled) {
@@ -198,32 +198,32 @@
     // In Java, Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.BOLD),
     // Typeface.NORMAL);
     std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal));
-    EXPECT_EQ(400, normal->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant());
-    EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle);
+    EXPECT_EQ(400, normal->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle());
 
     // In Java Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.BOLD),
     // Typeface.BOLD);
     std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold));
-    EXPECT_EQ(700, bold->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
-    EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+    EXPECT_EQ(700, bold->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
 
     // In Java, Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.BOLD),
     // Typeface.ITALIC);
     std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic));
-    EXPECT_EQ(400, normal->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
-    EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+    EXPECT_EQ(400, normal->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
 
     // In Java,
     // Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.BOLD),
     // Typeface.BOLD_ITALIC);
     std::unique_ptr<Typeface> boldItalic(
             Typeface::createRelative(base.get(), Typeface::kBoldItalic));
-    EXPECT_EQ(700, boldItalic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
-    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle);
+    EXPECT_EQ(700, boldItalic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle());
 }
 
 TEST(TypefaceTest, createRelativeTest_fromItalicStyled) {
@@ -233,33 +233,33 @@
     // Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC),
     // Typeface.NORMAL);
     std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal));
-    EXPECT_EQ(400, normal->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant());
-    EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle);
+    EXPECT_EQ(400, normal->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle());
 
     // In Java, Typeface.create(Typeface.create(Typeface.DEFAULT,
     // Typeface.ITALIC), Typeface.BOLD);
     std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold));
-    EXPECT_EQ(700, bold->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
-    EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+    EXPECT_EQ(700, bold->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
 
     // In Java,
     // Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC),
     // Typeface.ITALIC);
     std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic));
-    EXPECT_EQ(400, italic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
-    EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+    EXPECT_EQ(400, italic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
 
     // In Java,
     // Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC),
     // Typeface.BOLD_ITALIC);
     std::unique_ptr<Typeface> boldItalic(
             Typeface::createRelative(base.get(), Typeface::kBoldItalic));
-    EXPECT_EQ(700, boldItalic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
-    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle);
+    EXPECT_EQ(700, boldItalic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle());
 }
 
 TEST(TypefaceTest, createRelativeTest_fromSpecifiedStyled) {
@@ -270,27 +270,27 @@
     //     .setWeight(700).setItalic(false).build();
     // Typeface.create(typeface, Typeface.NORMAL);
     std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal));
-    EXPECT_EQ(400, normal->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant());
-    EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle);
+    EXPECT_EQ(400, normal->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle());
 
     // In Java,
     // Typeface typeface = new Typeface.Builder(invalid).setFallback("sans-serif")
     //     .setWeight(700).setItalic(false).build();
     // Typeface.create(typeface, Typeface.BOLD);
     std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold));
-    EXPECT_EQ(700, bold->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
-    EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+    EXPECT_EQ(700, bold->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
 
     // In Java,
     // Typeface typeface = new Typeface.Builder(invalid).setFallback("sans-serif")
     //     .setWeight(700).setItalic(false).build();
     // Typeface.create(typeface, Typeface.ITALIC);
     std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic));
-    EXPECT_EQ(400, italic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
-    EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+    EXPECT_EQ(400, italic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
 
     // In Java,
     // Typeface typeface = new Typeface.Builder(invalid).setFallback("sans-serif")
@@ -298,9 +298,9 @@
     // Typeface.create(typeface, Typeface.BOLD_ITALIC);
     std::unique_ptr<Typeface> boldItalic(
             Typeface::createRelative(base.get(), Typeface::kBoldItalic));
-    EXPECT_EQ(700, boldItalic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
-    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle);
+    EXPECT_EQ(700, boldItalic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle());
 }
 
 TEST(TypefaceTest, createAbsolute) {
@@ -309,45 +309,45 @@
     // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(400).setItalic(false)
     //     .build();
     std::unique_ptr<Typeface> regular(Typeface::createAbsolute(nullptr, 400, false));
-    EXPECT_EQ(400, regular->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant());
-    EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle);
+    EXPECT_EQ(400, regular->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kNormal, regular->getAPIStyle());
 
     // In Java,
     // new
     // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(700).setItalic(false)
     //     .build();
     std::unique_ptr<Typeface> bold(Typeface::createAbsolute(nullptr, 700, false));
-    EXPECT_EQ(700, bold->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
-    EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+    EXPECT_EQ(700, bold->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
 
     // In Java,
     // new
     // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(400).setItalic(true)
     //     .build();
     std::unique_ptr<Typeface> italic(Typeface::createAbsolute(nullptr, 400, true));
-    EXPECT_EQ(400, italic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
-    EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+    EXPECT_EQ(400, italic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
 
     // In Java,
     // new
     // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(700).setItalic(true)
     //     .build();
     std::unique_ptr<Typeface> boldItalic(Typeface::createAbsolute(nullptr, 700, true));
-    EXPECT_EQ(700, boldItalic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
-    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle);
+    EXPECT_EQ(700, boldItalic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle());
 
     // In Java,
     // new
     // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(1100).setItalic(true)
     //     .build();
     std::unique_ptr<Typeface> over1000(Typeface::createAbsolute(nullptr, 1100, false));
-    EXPECT_EQ(1000, over1000->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->fStyle.slant());
-    EXPECT_EQ(Typeface::kBold, over1000->fAPIStyle);
+    EXPECT_EQ(1000, over1000->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBold, over1000->getAPIStyle());
 }
 
 TEST(TypefaceTest, createFromFamilies_Single) {
@@ -355,43 +355,43 @@
     // Typeface.Builder("Roboto-Regular.ttf").setWeight(400).setItalic(false).build();
     std::unique_ptr<Typeface> regular(Typeface::createFromFamilies(
             makeSingleFamlyVector(kRobotoVariable), 400, false, nullptr /* fallback */));
-    EXPECT_EQ(400, regular->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant());
-    EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle);
+    EXPECT_EQ(400, regular->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kNormal, regular->getAPIStyle());
 
     // In Java, new
     // Typeface.Builder("Roboto-Regular.ttf").setWeight(700).setItalic(false).build();
     std::unique_ptr<Typeface> bold(Typeface::createFromFamilies(
             makeSingleFamlyVector(kRobotoVariable), 700, false, nullptr /* fallback */));
-    EXPECT_EQ(700, bold->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
-    EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+    EXPECT_EQ(700, bold->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
 
     // In Java, new
     // Typeface.Builder("Roboto-Regular.ttf").setWeight(400).setItalic(true).build();
     std::unique_ptr<Typeface> italic(Typeface::createFromFamilies(
             makeSingleFamlyVector(kRobotoVariable), 400, true, nullptr /* fallback */));
-    EXPECT_EQ(400, italic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
-    EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+    EXPECT_EQ(400, italic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
 
     // In Java,
     // new
     // Typeface.Builder("Roboto-Regular.ttf").setWeight(700).setItalic(true).build();
     std::unique_ptr<Typeface> boldItalic(Typeface::createFromFamilies(
             makeSingleFamlyVector(kRobotoVariable), 700, true, nullptr /* fallback */));
-    EXPECT_EQ(700, boldItalic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
-    EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+    EXPECT_EQ(700, boldItalic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
 
     // In Java,
     // new
     // Typeface.Builder("Roboto-Regular.ttf").setWeight(1100).setItalic(false).build();
     std::unique_ptr<Typeface> over1000(Typeface::createFromFamilies(
             makeSingleFamlyVector(kRobotoVariable), 1100, false, nullptr /* fallback */));
-    EXPECT_EQ(1000, over1000->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->fStyle.slant());
-    EXPECT_EQ(Typeface::kBold, over1000->fAPIStyle);
+    EXPECT_EQ(1000, over1000->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBold, over1000->getAPIStyle());
 }
 
 TEST(TypefaceTest, createFromFamilies_Single_resolveByTable) {
@@ -399,33 +399,33 @@
     std::unique_ptr<Typeface> regular(
             Typeface::createFromFamilies(makeSingleFamlyVector(kRegularFont), RESOLVE_BY_FONT_TABLE,
                                          RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
-    EXPECT_EQ(400, regular->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant());
-    EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle);
+    EXPECT_EQ(400, regular->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kNormal, regular->getAPIStyle());
 
     // In Java, new Typeface.Builder("Family-Bold.ttf").build();
     std::unique_ptr<Typeface> bold(
             Typeface::createFromFamilies(makeSingleFamlyVector(kBoldFont), RESOLVE_BY_FONT_TABLE,
                                          RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
-    EXPECT_EQ(700, bold->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
-    EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
+    EXPECT_EQ(700, bold->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kBold, bold->getAPIStyle());
 
     // In Java, new Typeface.Builder("Family-Italic.ttf").build();
     std::unique_ptr<Typeface> italic(
             Typeface::createFromFamilies(makeSingleFamlyVector(kItalicFont), RESOLVE_BY_FONT_TABLE,
                                          RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
-    EXPECT_EQ(400, italic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
-    EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+    EXPECT_EQ(400, italic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
 
     // In Java, new Typeface.Builder("Family-BoldItalic.ttf").build();
     std::unique_ptr<Typeface> boldItalic(Typeface::createFromFamilies(
             makeSingleFamlyVector(kBoldItalicFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE,
             nullptr /* fallback */));
-    EXPECT_EQ(700, boldItalic->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
-    EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
+    EXPECT_EQ(700, boldItalic->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant());
+    EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle());
 }
 
 TEST(TypefaceTest, createFromFamilies_Family) {
@@ -435,8 +435,8 @@
     std::unique_ptr<Typeface> typeface(
             Typeface::createFromFamilies(std::move(families), RESOLVE_BY_FONT_TABLE,
                                          RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
-    EXPECT_EQ(400, typeface->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->fStyle.slant());
+    EXPECT_EQ(400, typeface->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->getFontStyle().slant());
 }
 
 TEST(TypefaceTest, createFromFamilies_Family_withoutRegular) {
@@ -445,8 +445,8 @@
     std::unique_ptr<Typeface> typeface(
             Typeface::createFromFamilies(std::move(families), RESOLVE_BY_FONT_TABLE,
                                          RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
-    EXPECT_EQ(700, typeface->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->fStyle.slant());
+    EXPECT_EQ(700, typeface->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->getFontStyle().slant());
 }
 
 TEST(TypefaceTest, createFromFamilies_Family_withFallback) {
@@ -458,8 +458,8 @@
     std::unique_ptr<Typeface> regular(
             Typeface::createFromFamilies(makeSingleFamlyVector(kRegularFont), RESOLVE_BY_FONT_TABLE,
                                          RESOLVE_BY_FONT_TABLE, fallback.get()));
-    EXPECT_EQ(400, regular->fStyle.weight());
-    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant());
+    EXPECT_EQ(400, regular->getFontStyle().weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->getFontStyle().slant());
 }
 
 }  // namespace
diff --git a/media/TEST_MAPPING b/media/TEST_MAPPING
index e52e0b1..6a21496 100644
--- a/media/TEST_MAPPING
+++ b/media/TEST_MAPPING
@@ -1,7 +1,10 @@
 {
   "presubmit": [
     {
-      "name": "CtsMediaBetterTogetherTestCases"
+      "name": "CtsMediaRouterTestCases"
+    },
+    {
+      "name": "CtsMediaSessionTestCases"
     },
     {
       "name": "mediaroutertest"
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 54a87ad..2a740f8 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -32,6 +32,7 @@
 import android.media.FadeManagerConfiguration;
 import android.media.IAudioDeviceVolumeDispatcher;
 import android.media.IAudioFocusDispatcher;
+import android.media.IAudioManagerNative;
 import android.media.IAudioModeDispatcher;
 import android.media.IAudioRoutesObserver;
 import android.media.IAudioServerStateDispatcher;
@@ -83,6 +84,7 @@
     // When a method's argument list is changed, BpAudioManager's corresponding serialization code
     // (if any) in frameworks/native/services/audiomanager/IAudioManager.cpp must be updated.
 
+    IAudioManagerNative getNativeInterface();
     int trackPlayer(in PlayerBase.PlayerIdCard pic);
 
     oneway void playerAttributes(in int piid, in AudioAttributes attr);
diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java
index 36f62da..c9625c4 100644
--- a/media/java/android/media/MediaCodec.java
+++ b/media/java/android/media/MediaCodec.java
@@ -5866,19 +5866,31 @@
                 @NonNull MediaCodec codec, @NonNull MediaFormat format);
 
         /**
-         * Called when the metrics for this codec have been flushed due to the
-         * start of a new subsession.
+         * Called when the metrics for this codec have been flushed "mid-stream"
+         * due to the start of a new subsession during execution.
          * <p>
-         * This can happen when the codec is reconfigured after stop(), or
-         * mid-stream e.g. if the video size changes. When this happens, the
-         * metrics for the previous subsession are flushed, and
-         * {@link MediaCodec#getMetrics} will return the metrics for the
-         * new subsession. This happens just before the {@link Callback#onOutputFormatChanged}
+         * A new codec subsession normally starts when the codec is reconfigured
+         * after stop(), but it can also happen mid-stream e.g. if the video size
+         * changes. When this happens, the metrics for the previous subsession
+         * are flushed, and {@link MediaCodec#getMetrics} will return the metrics
+         * for the new subsession.
+         * <p>
+         * For subsessions that begin due to a reconfiguration, the metrics for
+         * the prior subsession can be retrieved via {@link MediaCodec#getMetrics}
+         * prior to calling {@link #configure}.
+         * <p>
+         * When a new subsession begins "mid-stream", the metrics for the prior
+         * subsession are flushed just before the {@link Callback#onOutputFormatChanged}
          * event, so this <b>optional</b> callback is provided to be able to
          * capture the final metrics for the previous subsession.
          *
          * @param codec The MediaCodec object.
-         * @param metrics The flushed metrics for this codec.
+         * @param metrics The flushed metrics for this codec. This is a
+         *                {@link PersistableBundle} containing the set of
+         *                attributes and values available for the media being
+         *                handled by this instance of MediaCodec. The attributes
+         *                are described in {@link MetricsConstants}. Additional
+         *                vendor-specific fields may also be present.
          */
         @FlaggedApi(FLAG_SUBSESSION_METRICS)
         public void onMetricsFlushed(
diff --git a/media/java/android/media/MediaMuxer.java b/media/java/android/media/MediaMuxer.java
index 678150b..4c5efc1 100644
--- a/media/java/android/media/MediaMuxer.java
+++ b/media/java/android/media/MediaMuxer.java
@@ -18,6 +18,8 @@
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.annotation.TestApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.media.MediaCodec.BufferInfo;
 import android.os.Build;
@@ -257,6 +259,8 @@
          */
         private OutputFormat() {}
         /** @hide */
+        @SuppressLint("UnflaggedApi")
+        @TestApi
         public static final int MUXER_OUTPUT_FIRST   = 0;
         /** MPEG4 media file format*/
         public static final int MUXER_OUTPUT_MPEG_4 = MUXER_OUTPUT_FIRST;
@@ -269,6 +273,8 @@
         /** Ogg media file format*/
         public static final int MUXER_OUTPUT_OGG   = MUXER_OUTPUT_FIRST + 4;
         /** @hide */
+        @SuppressLint("UnflaggedApi")
+        @TestApi
         public static final int MUXER_OUTPUT_LAST   = MUXER_OUTPUT_OGG;
     };
 
diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java
index bbb03e7..88981ea 100644
--- a/media/java/android/media/MediaRoute2Info.java
+++ b/media/java/android/media/MediaRoute2Info.java
@@ -961,8 +961,7 @@
      *
      * @hide
      */
-    @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME)
-    public boolean isVisibleTo(String packageName) {
+    public boolean isVisibleTo(@NonNull String packageName) {
         return !mIsVisibilityRestricted
                 || TextUtils.equals(getProviderPackageName(), packageName)
                 || mAllowedPackages.contains(packageName);
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index 3738312..e57148f 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -19,7 +19,6 @@
 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
 import static com.android.media.flags.Flags.FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES;
 import static com.android.media.flags.Flags.FLAG_ENABLE_GET_TRANSFERABLE_ROUTES;
-import static com.android.media.flags.Flags.FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME;
 import static com.android.media.flags.Flags.FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL;
 import static com.android.media.flags.Flags.FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2;
 import static com.android.media.flags.Flags.FLAG_ENABLE_SCREEN_OFF_SCANNING;
@@ -1406,7 +1405,6 @@
         requestCreateController(controller, route, managerRequestId);
     }
 
-    @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME)
     private List<MediaRoute2Info> getSortedRoutes(
             List<MediaRoute2Info> routes, List<String> packageOrder) {
         if (packageOrder.isEmpty()) {
@@ -1427,7 +1425,6 @@
     }
 
     @GuardedBy("mLock")
-    @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME)
     private List<MediaRoute2Info> filterRoutesWithCompositePreferenceLocked(
             List<MediaRoute2Info> routes) {
 
@@ -3654,7 +3651,6 @@
             }
         }
 
-        @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME)
         @Override
         public List<MediaRoute2Info> filterRoutesWithIndividualPreference(
                 List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryPreference) {
diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java
index 3854747..3f18eef 100644
--- a/media/java/android/media/MediaRouter2Manager.java
+++ b/media/java/android/media/MediaRouter2Manager.java
@@ -20,11 +20,9 @@
 import static android.media.MediaRouter2.SCANNING_STATE_WHILE_INTERACTIVE;
 
 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
-import static com.android.media.flags.Flags.FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME;
 
 import android.Manifest;
 import android.annotation.CallbackExecutor;
-import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
@@ -287,7 +285,6 @@
                 (route) -> sessionInfo.isSystemSession() ^ route.isSystemRoute());
     }
 
-    @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME)
     private List<MediaRoute2Info> getSortedRoutes(RouteDiscoveryPreference preference) {
         if (!preference.shouldRemoveDuplicates()) {
             synchronized (mRoutesLock) {
@@ -311,7 +308,6 @@
         return routes;
     }
 
-    @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME)
     private List<MediaRoute2Info> getFilteredRoutes(
             @NonNull RoutingSessionInfo sessionInfo,
             boolean includeSelectedRoutes,
diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig
index 4398b261..c48b5f4 100644
--- a/media/java/android/media/flags/media_better_together.aconfig
+++ b/media/java/android/media/flags/media_better_together.aconfig
@@ -11,6 +11,16 @@
 }
 
 flag {
+    name: "disable_set_bluetooth_ad2p_on_calls"
+    namespace: "media_better_together"
+    description: "Prevents calls to AudioService.setBluetoothA2dpOn(), known to cause incorrect audio routing to the built-in speakers."
+    bug: "294968421"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "enable_audio_input_device_routing_and_volume_control"
     namespace: "media_better_together"
     description: "Allows audio input devices routing and volume control via system settings."
diff --git a/media/java/android/media/soundtrigger/SoundTriggerManager.java b/media/java/android/media/soundtrigger/SoundTriggerManager.java
index 3d0c406..213bc06 100644
--- a/media/java/android/media/soundtrigger/SoundTriggerManager.java
+++ b/media/java/android/media/soundtrigger/SoundTriggerManager.java
@@ -27,6 +27,7 @@
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.annotation.TestApi;
+import android.annotation.WorkerThread;
 import android.app.ActivityThread;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ComponentName;
@@ -475,6 +476,7 @@
     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
     @UnsupportedAppUsage
     @FlaggedApi(Flags.FLAG_MANAGER_API)
+    @WorkerThread
     public int loadSoundModel(@NonNull SoundModel soundModel) {
         if (mSoundTriggerSession == null) {
             throw new IllegalStateException("No underlying SoundTriggerModule available");
@@ -518,6 +520,7 @@
     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
     @UnsupportedAppUsage
     @FlaggedApi(Flags.FLAG_MANAGER_API)
+    @WorkerThread
     public int startRecognition(@NonNull UUID soundModelId, @Nullable Bundle params,
         @NonNull ComponentName detectionService, @NonNull RecognitionConfig config) {
         Objects.requireNonNull(soundModelId);
@@ -544,6 +547,7 @@
     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
     @UnsupportedAppUsage
     @FlaggedApi(Flags.FLAG_MANAGER_API)
+    @WorkerThread
     public int stopRecognition(@NonNull UUID soundModelId) {
         if (mSoundTriggerSession == null) {
             throw new IllegalStateException("No underlying SoundTriggerModule available");
@@ -568,6 +572,7 @@
     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
     @UnsupportedAppUsage
     @FlaggedApi(Flags.FLAG_MANAGER_API)
+    @WorkerThread
     public int unloadSoundModel(@NonNull UUID soundModelId) {
         if (mSoundTriggerSession == null) {
             throw new IllegalStateException("No underlying SoundTriggerModule available");
@@ -587,6 +592,7 @@
     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
     @UnsupportedAppUsage
     @FlaggedApi(Flags.FLAG_MANAGER_API)
+    @WorkerThread
     public boolean isRecognitionActive(@NonNull UUID soundModelId) {
         if (soundModelId == null || mSoundTriggerSession == null) {
             return false;
@@ -624,6 +630,7 @@
     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
     @UnsupportedAppUsage
     @FlaggedApi(Flags.FLAG_MANAGER_API)
+    @WorkerThread
     public int getModelState(@NonNull UUID soundModelId) {
         if (mSoundTriggerSession == null) {
             throw new IllegalStateException("No underlying SoundTriggerModule available");
diff --git a/mime/Android.bp b/mime/Android.bp
index 20110f1..b609548 100644
--- a/mime/Android.bp
+++ b/mime/Android.bp
@@ -49,6 +49,17 @@
     ],
 }
 
+java_library {
+    name: "mimemap-testing-alt",
+    defaults: ["mimemap-defaults"],
+    static_libs: ["mimemap-testing-alt-res.jar"],
+    jarjar_rules: "jarjar-rules-alt.txt",
+    visibility: [
+        "//cts/tests/tests/mimemap:__subpackages__",
+        "//frameworks/base:__subpackages__",
+    ],
+}
+
 // The mimemap-res.jar and mimemap-testing-res.jar genrules produce a .jar that
 // has the resource file in a subdirectory res/ and testres/, respectively.
 // They need to be in different paths because one of them ends up in a
@@ -86,6 +97,19 @@
     cmd: "mkdir $(genDir)/testres/ && cp $(in) $(genDir)/testres/ && $(location soong_zip) -C $(genDir) -o $(out) -D $(genDir)/testres/",
 }
 
+// The same as mimemap-testing-res.jar except that the resources are placed in a different directory.
+// They get bundled with CTS so that CTS can compare a device's MimeMap implementation vs.
+// the stock Android one from when CTS was built.
+java_genrule {
+    name: "mimemap-testing-alt-res.jar",
+    tools: [
+        "soong_zip",
+    ],
+    srcs: [":mime.types.minimized-alt"],
+    out: ["mimemap-testing-alt-res.jar"],
+    cmd: "mkdir $(genDir)/testres-alt/ && cp $(in) $(genDir)/testres-alt/ && $(location soong_zip) -C $(genDir) -o $(out) -D $(genDir)/testres-alt/",
+}
+
 // Combination of all *mime.types.minimized resources.
 filegroup {
     name: "mime.types.minimized",
@@ -99,6 +123,19 @@
     ],
 }
 
+// Combination of all *mime.types.minimized resources.
+filegroup {
+    name: "mime.types.minimized-alt",
+    visibility: [
+        "//visibility:private",
+    ],
+    device_common_srcs: [
+        ":debian.mime.types.minimized-alt",
+        ":android.mime.types.minimized",
+        ":vendor.mime.types.minimized",
+    ],
+}
+
 java_genrule {
     name: "android.mime.types.minimized",
     visibility: [
diff --git a/mime/jarjar-rules-alt.txt b/mime/jarjar-rules-alt.txt
new file mode 100644
index 0000000..9a76443
--- /dev/null
+++ b/mime/jarjar-rules-alt.txt
@@ -0,0 +1 @@
+rule android.content.type.DefaultMimeMapFactory android.content.type.cts.StockAndroidAltMimeMapFactory
diff --git a/mime/jarjar-rules.txt b/mime/jarjar-rules.txt
index 145d1db..e1ea8e1 100644
--- a/mime/jarjar-rules.txt
+++ b/mime/jarjar-rules.txt
@@ -1 +1 @@
-rule android.content.type.DefaultMimeMapFactory android.content.type.cts.StockAndroidMimeMapFactory
\ No newline at end of file
+rule android.content.type.DefaultMimeMapFactory android.content.type.cts.StockAndroidMimeMapFactory
diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt
index f629c88..b30b779 100644
--- a/native/android/libandroid.map.txt
+++ b/native/android/libandroid.map.txt
@@ -320,6 +320,9 @@
     ASystemFontIterator_open; # introduced=29
     ASystemFontIterator_close; # introduced=29
     ASystemFontIterator_next; # introduced=29
+    ASystemHealth_getMaxCpuHeadroomTidsSize; # introduced=36
+    ASystemHealth_getCpuHeadroomCalculationWindowRange; # introduced=36
+    ASystemHealth_getGpuHeadroomCalculationWindowRange; # introduced=36
     ASystemHealth_getCpuHeadroom; # introduced=36
     ASystemHealth_getGpuHeadroom; # introduced=36
     ASystemHealth_getCpuHeadroomMinIntervalMillis; # introduced=36
diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp
index 68c1983..1e6a7b7 100644
--- a/native/android/performance_hint.cpp
+++ b/native/android/performance_hint.cpp
@@ -859,7 +859,7 @@
         std::vector<ANativeWindow*> windowVec(windows, windows + numWindows);
         for (auto&& window : windowVec) {
             Surface* surface = static_cast<Surface*>(window);
-            if (Surface::isValid(surface)) {
+            if (surface != nullptr) {
                 const sp<IBinder>& handle = surface->getSurfaceControlHandle();
                 if (handle != nullptr) {
                     out.push_back(handle);
diff --git a/native/android/system_health.cpp b/native/android/system_health.cpp
index f3fa9f6..5c07ac7 100644
--- a/native/android/system_health.cpp
+++ b/native/android/system_health.cpp
@@ -31,26 +31,28 @@
 struct ACpuHeadroomParams : public CpuHeadroomParamsInternal {};
 struct AGpuHeadroomParams : public GpuHeadroomParamsInternal {};
 
-const int CPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MIN = 50;
-const int CPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MAX = 10000;
-const int GPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MIN = 50;
-const int GPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MAX = 10000;
-const int CPU_HEADROOM_MAX_TID_COUNT = 5;
-
 struct ASystemHealthManager {
 public:
     static ASystemHealthManager* getInstance();
-    ASystemHealthManager(std::shared_ptr<IHintManager>& hintManager);
+
+    ASystemHealthManager(std::shared_ptr<IHintManager>& hintManager,
+                         IHintManager::HintManagerClientData&& clientData);
     ASystemHealthManager() = delete;
     ~ASystemHealthManager();
     int getCpuHeadroom(const ACpuHeadroomParams* params, float* outHeadroom);
     int getGpuHeadroom(const AGpuHeadroomParams* params, float* outHeadroom);
     int getCpuHeadroomMinIntervalMillis(int64_t* outMinIntervalMillis);
     int getGpuHeadroomMinIntervalMillis(int64_t* outMinIntervalMillis);
+    int getMaxCpuHeadroomTidsSize(size_t* outSize);
+    int getCpuHeadroomCalculationWindowRange(int32_t* _Nonnull outMinMillis,
+                                             int32_t* _Nonnull outMaxMillis);
+    int getGpuHeadroomCalculationWindowRange(int32_t* _Nonnull outMinMillis,
+                                             int32_t* _Nonnull outMaxMillis);
 
 private:
     static ASystemHealthManager* create(std::shared_ptr<IHintManager> hintManager);
     std::shared_ptr<IHintManager> mHintManager;
+    IHintManager::HintManagerClientData mClientData;
 };
 
 ASystemHealthManager* ASystemHealthManager::getInstance() {
@@ -60,10 +62,11 @@
     return instance;
 }
 
-ASystemHealthManager::ASystemHealthManager(std::shared_ptr<IHintManager>& hintManager)
-      : mHintManager(std::move(hintManager)) {}
+ASystemHealthManager::ASystemHealthManager(std::shared_ptr<IHintManager>& hintManager,
+                                           IHintManager::HintManagerClientData&& clientData)
+      : mHintManager(std::move(hintManager)), mClientData(clientData) {}
 
-ASystemHealthManager::~ASystemHealthManager() {}
+ASystemHealthManager::~ASystemHealthManager() = default;
 
 ASystemHealthManager* ASystemHealthManager::create(std::shared_ptr<IHintManager> hintManager) {
     if (!hintManager) {
@@ -74,20 +77,37 @@
         ALOGE("%s: PerformanceHint service is not ready ", __FUNCTION__);
         return nullptr;
     }
-    return new ASystemHealthManager(hintManager);
-}
-
-ASystemHealthManager* ASystemHealth_acquireManager() {
-    return ASystemHealthManager::getInstance();
+    IHintManager::HintManagerClientData clientData;
+    ndk::ScopedAStatus ret = hintManager->getClientData(&clientData);
+    if (!ret.isOk()) {
+        ALOGE("%s: PerformanceHint service is not initialized %s", __FUNCTION__, ret.getMessage());
+        return nullptr;
+    }
+    return new ASystemHealthManager(hintManager, std::move(clientData));
 }
 
 int ASystemHealthManager::getCpuHeadroom(const ACpuHeadroomParams* params, float* outHeadroom) {
+    if (!mClientData.supportInfo.headroom.isCpuSupported) return ENOTSUP;
     std::optional<hal::CpuHeadroomResult> res;
     ::ndk::ScopedAStatus ret;
     CpuHeadroomParamsInternal internalParams;
     if (!params) {
         ret = mHintManager->getCpuHeadroom(internalParams, &res);
     } else {
+        LOG_ALWAYS_FATAL_IF((int)params->tids.size() > mClientData.maxCpuHeadroomThreads,
+                            "%s: tids size should not exceed %d", __FUNCTION__,
+                            mClientData.maxCpuHeadroomThreads);
+        LOG_ALWAYS_FATAL_IF(params->calculationWindowMillis <
+                                            mClientData.supportInfo.headroom
+                                                    .cpuMinCalculationWindowMillis ||
+                                    params->calculationWindowMillis >
+                                            mClientData.supportInfo.headroom
+                                                    .cpuMaxCalculationWindowMillis,
+                            "%s: calculationWindowMillis should be in range [%d, %d] but got %d",
+                            __FUNCTION__,
+                            mClientData.supportInfo.headroom.cpuMinCalculationWindowMillis,
+                            mClientData.supportInfo.headroom.cpuMaxCalculationWindowMillis,
+                            params->calculationWindowMillis);
         ret = mHintManager->getCpuHeadroom(*params, &res);
     }
     if (!ret.isOk()) {
@@ -106,12 +126,24 @@
 }
 
 int ASystemHealthManager::getGpuHeadroom(const AGpuHeadroomParams* params, float* outHeadroom) {
+    if (!mClientData.supportInfo.headroom.isGpuSupported) return ENOTSUP;
     std::optional<hal::GpuHeadroomResult> res;
     ::ndk::ScopedAStatus ret;
     GpuHeadroomParamsInternal internalParams;
     if (!params) {
         ret = mHintManager->getGpuHeadroom(internalParams, &res);
     } else {
+        LOG_ALWAYS_FATAL_IF(params->calculationWindowMillis <
+                                            mClientData.supportInfo.headroom
+                                                    .gpuMinCalculationWindowMillis ||
+                                    params->calculationWindowMillis >
+                                            mClientData.supportInfo.headroom
+                                                    .gpuMaxCalculationWindowMillis,
+                            "%s: calculationWindowMillis should be in range [%d, %d] but got %d",
+                            __FUNCTION__,
+                            mClientData.supportInfo.headroom.gpuMinCalculationWindowMillis,
+                            mClientData.supportInfo.headroom.gpuMaxCalculationWindowMillis,
+                            params->calculationWindowMillis);
         ret = mHintManager->getGpuHeadroom(*params, &res);
     }
     if (!ret.isOk()) {
@@ -128,6 +160,7 @@
 }
 
 int ASystemHealthManager::getCpuHeadroomMinIntervalMillis(int64_t* outMinIntervalMillis) {
+    if (!mClientData.supportInfo.headroom.isCpuSupported) return ENOTSUP;
     int64_t minIntervalMillis = 0;
     ::ndk::ScopedAStatus ret = mHintManager->getCpuHeadroomMinIntervalMillis(&minIntervalMillis);
     if (!ret.isOk()) {
@@ -142,6 +175,7 @@
 }
 
 int ASystemHealthManager::getGpuHeadroomMinIntervalMillis(int64_t* outMinIntervalMillis) {
+    if (!mClientData.supportInfo.headroom.isGpuSupported) return ENOTSUP;
     int64_t minIntervalMillis = 0;
     ::ndk::ScopedAStatus ret = mHintManager->getGpuHeadroomMinIntervalMillis(&minIntervalMillis);
     if (!ret.isOk()) {
@@ -155,6 +189,57 @@
     return OK;
 }
 
+int ASystemHealthManager::getMaxCpuHeadroomTidsSize(size_t* outSize) {
+    if (!mClientData.supportInfo.headroom.isGpuSupported) return ENOTSUP;
+    *outSize = mClientData.maxCpuHeadroomThreads;
+    return OK;
+}
+
+int ASystemHealthManager::getCpuHeadroomCalculationWindowRange(int32_t* _Nonnull outMinMillis,
+                                                               int32_t* _Nonnull outMaxMillis) {
+    if (!mClientData.supportInfo.headroom.isCpuSupported) return ENOTSUP;
+    *outMinMillis = mClientData.supportInfo.headroom.cpuMinCalculationWindowMillis;
+    *outMaxMillis = mClientData.supportInfo.headroom.cpuMaxCalculationWindowMillis;
+    return OK;
+}
+
+int ASystemHealthManager::getGpuHeadroomCalculationWindowRange(int32_t* _Nonnull outMinMillis,
+                                                               int32_t* _Nonnull outMaxMillis) {
+    if (!mClientData.supportInfo.headroom.isGpuSupported) return ENOTSUP;
+    *outMinMillis = mClientData.supportInfo.headroom.gpuMinCalculationWindowMillis;
+    *outMaxMillis = mClientData.supportInfo.headroom.gpuMaxCalculationWindowMillis;
+    return OK;
+}
+
+int ASystemHealth_getMaxCpuHeadroomTidsSize(size_t* _Nonnull outSize) {
+    LOG_ALWAYS_FATAL_IF(outSize == nullptr, "%s: outSize should not be null", __FUNCTION__);
+    auto manager = ASystemHealthManager::getInstance();
+    if (manager == nullptr) return ENOTSUP;
+    return manager->getMaxCpuHeadroomTidsSize(outSize);
+}
+
+int ASystemHealth_getCpuHeadroomCalculationWindowRange(int32_t* _Nonnull outMinMillis,
+                                                       int32_t* _Nonnull outMaxMillis) {
+    LOG_ALWAYS_FATAL_IF(outMinMillis == nullptr, "%s: outMinMillis should not be null",
+                        __FUNCTION__);
+    LOG_ALWAYS_FATAL_IF(outMaxMillis == nullptr, "%s: outMaxMillis should not be null",
+                        __FUNCTION__);
+    auto manager = ASystemHealthManager::getInstance();
+    if (manager == nullptr) return ENOTSUP;
+    return manager->getCpuHeadroomCalculationWindowRange(outMinMillis, outMaxMillis);
+}
+
+int ASystemHealth_getGpuHeadroomCalculationWindowRange(int32_t* _Nonnull outMinMillis,
+                                                       int32_t* _Nonnull outMaxMillis) {
+    LOG_ALWAYS_FATAL_IF(outMinMillis == nullptr, "%s: outMinMillis should not be null",
+                        __FUNCTION__);
+    LOG_ALWAYS_FATAL_IF(outMaxMillis == nullptr, "%s: outMaxMillis should not be null",
+                        __FUNCTION__);
+    auto manager = ASystemHealthManager::getInstance();
+    if (manager == nullptr) return ENOTSUP;
+    return manager->getGpuHeadroomCalculationWindowRange(outMinMillis, outMaxMillis);
+}
+
 int ASystemHealth_getCpuHeadroom(const ACpuHeadroomParams* _Nullable params,
                                  float* _Nonnull outHeadroom) {
     LOG_ALWAYS_FATAL_IF(outHeadroom == nullptr, "%s: outHeadroom should not be null", __FUNCTION__);
@@ -189,19 +274,15 @@
 
 void ACpuHeadroomParams_setCalculationWindowMillis(ACpuHeadroomParams* _Nonnull params,
                                                    int windowMillis) {
-    LOG_ALWAYS_FATAL_IF(windowMillis < CPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MIN ||
-                                windowMillis > CPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MAX,
-                        "%s: windowMillis should be in range [50, 10000] but got %d", __FUNCTION__,
-                        windowMillis);
+    LOG_ALWAYS_FATAL_IF(windowMillis <= 0, "%s: windowMillis should be positive but got %d",
+                        __FUNCTION__, windowMillis);
     params->calculationWindowMillis = windowMillis;
 }
 
 void AGpuHeadroomParams_setCalculationWindowMillis(AGpuHeadroomParams* _Nonnull params,
                                                    int windowMillis) {
-    LOG_ALWAYS_FATAL_IF(windowMillis < GPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MIN ||
-                                windowMillis > GPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MAX,
-                        "%s: windowMillis should be in range [50, 10000] but got %d", __FUNCTION__,
-                        windowMillis);
+    LOG_ALWAYS_FATAL_IF(windowMillis <= 0, "%s: windowMillis should be positive but got %d",
+                        __FUNCTION__, windowMillis);
     params->calculationWindowMillis = windowMillis;
 }
 
@@ -214,13 +295,11 @@
 }
 
 void ACpuHeadroomParams_setTids(ACpuHeadroomParams* _Nonnull params, const int* _Nonnull tids,
-                                int tidsSize) {
+                                size_t tidsSize) {
     LOG_ALWAYS_FATAL_IF(tids == nullptr, "%s: tids should not be null", __FUNCTION__);
-    LOG_ALWAYS_FATAL_IF(tidsSize > CPU_HEADROOM_MAX_TID_COUNT, "%s: tids size should not exceed 5",
-                        __FUNCTION__);
     params->tids.resize(tidsSize);
     params->tids.clear();
-    for (int i = 0; i < tidsSize; ++i) {
+    for (int i = 0; i < (int)tidsSize; ++i) {
         LOG_ALWAYS_FATAL_IF(tids[i] <= 0, "ACpuHeadroomParams_setTids: Invalid non-positive tid %d",
                             tids[i]);
         params->tids[i] = tids[i];
@@ -269,10 +348,10 @@
     return new AGpuHeadroomParams();
 }
 
-void ACpuHeadroomParams_destroy(ACpuHeadroomParams* _Nonnull params) {
+void ACpuHeadroomParams_destroy(ACpuHeadroomParams* _Nullable params) {
     delete params;
 }
 
-void AGpuHeadroomParams_destroy(AGpuHeadroomParams* _Nonnull params) {
+void AGpuHeadroomParams_destroy(AGpuHeadroomParams* _Nullable params) {
     delete params;
 }
diff --git a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
index f68fa1a..0fa92ea 100644
--- a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
+++ b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
@@ -161,6 +161,9 @@
                          clientDataIn,
                  ::aidl::android::os::IHintManager::HintManagerClientData* _aidl_return),
                 (override));
+    MOCK_METHOD(ScopedAStatus, getClientData,
+                (::aidl::android::os::IHintManager::HintManagerClientData * _aidl_return),
+                (override));
     MOCK_METHOD(SpAIBinder, asBinder, (), (override));
     MOCK_METHOD(bool, isRemote, (), (override));
 };
@@ -602,6 +605,15 @@
     ASSERT_NE(config, nullptr);
 }
 
+TEST_F(PerformanceHintTest, TestSessionCreationWithNullLayers) {
+    EXPECT_CALL(*mMockIHintManager, createHintSessionWithConfig(_, _, _, _, _)).Times(1);
+    auto&& config = configFromCreator(
+            {.tids = mTids, .nativeWindows = {nullptr}, .surfaceControls = {nullptr}});
+    APerformanceHintManager* manager = createManager();
+    auto&& session = createSessionUsingConfig(manager, config);
+    ASSERT_TRUE(session);
+}
+
 TEST_F(PerformanceHintTest, TestSupportObject) {
     // Disable GPU and Power Efficiency support to test partial enabling
     mClientData.supportInfo.sessionModes &= ~(1 << (int)hal::SessionMode::AUTO_GPU);
diff --git a/native/android/tests/system_health/OWNERS b/native/android/tests/system_health/OWNERS
new file mode 100644
index 0000000..e3bbee92
--- /dev/null
+++ b/native/android/tests/system_health/OWNERS
@@ -0,0 +1 @@
+include /ADPF_OWNERS
diff --git a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml
index afece5f..40a786e 100644
--- a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml
+++ b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml
@@ -81,7 +81,9 @@
                     <androidx.recyclerview.widget.RecyclerView
                         android:id="@+id/device_list"
                         android:layout_width="match_parent"
-                        android:layout_height="200dp"
+                        android:layout_height="wrap_content"
+                        app:layout_constraintHeight_max="220dp"
+                        app:layout_constraintHeight_min="200dp"
                         android:scrollbars="vertical"
                         android:visibility="gone" />
 
diff --git a/packages/CredentialManager/tests/robotests/Android.bp b/packages/CredentialManager/tests/robotests/Android.bp
index 27afaaa..01f403d 100644
--- a/packages/CredentialManager/tests/robotests/Android.bp
+++ b/packages/CredentialManager/tests/robotests/Android.bp
@@ -53,7 +53,6 @@
         "android.test.mock.stubs.system",
         "truth",
     ],
-    upstream: true,
     java_resource_dirs: ["config"],
     instrumentation_for: "CredentialManagerRobo",
 }
diff --git a/packages/CredentialManager/wear/robotests/Android.bp b/packages/CredentialManager/wear/robotests/Android.bp
index 589a3d6..db3c363 100644
--- a/packages/CredentialManager/wear/robotests/Android.bp
+++ b/packages/CredentialManager/wear/robotests/Android.bp
@@ -24,6 +24,5 @@
         "framework_graphics_flags_java_lib",
     ],
     java_resource_dirs: ["config"],
-    upstream: true,
     strict_mode: false,
 }
diff --git a/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java b/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java
index 068074a..8e52a00 100644
--- a/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java
+++ b/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java
@@ -38,6 +38,7 @@
 import android.location.provider.ProviderProperties;
 import android.location.provider.ProviderRequest;
 import android.os.Bundle;
+import android.util.Log;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
@@ -301,8 +302,13 @@
                             .setWorkSource(mRequest.getWorkSource())
                             .setHiddenFromAppOps(true)
                             .build();
-                    mLocationManager.requestLocationUpdates(mProvider, request,
-                            mContext.getMainExecutor(), this);
+
+                    try {
+                        mLocationManager.requestLocationUpdates(
+                                mProvider, request, mContext.getMainExecutor(), this);
+                    } catch (IllegalArgumentException e) {
+                        Log.e(TAG, "Failed to request location updates");
+                    }
                 }
             }
         }
@@ -311,7 +317,11 @@
             synchronized (mLock) {
                 int requestCode = mNextFlushCode++;
                 mPendingFlushes.put(requestCode, callback);
-                mLocationManager.requestFlush(mProvider, this, requestCode);
+                try {
+                    mLocationManager.requestFlush(mProvider, this, requestCode);
+                } catch (IllegalArgumentException e) {
+                    Log.e(TAG, "Failed to request flush");
+                }
             }
         }
 
diff --git a/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm b/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm
new file mode 100644
index 0000000..b384a24
--- /dev/null
+++ b/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm
@@ -0,0 +1,357 @@
+# 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.
+
+#
+# Romanian keyboard layout.
+#
+
+type OVERLAY
+
+map key 86 PLUS
+
+### ROW 1
+
+key GRAVE {
+    label:                              '\u201e'
+    base:                               '\u201e'
+    shift:                              '\u201d'
+    ralt:                               '`'
+    ralt+shift:                         '~'
+}
+
+key 1 {
+    label:                              '1'
+    base:                               '1'
+    shift:                              '!'
+    ralt:                               '\u0303'
+}
+
+key 2 {
+    label:                              '2'
+    base:                               '2'
+    shift:                              '@'
+    ralt:                               '\u030C'
+}
+
+key 3 {
+    label:                              '3'
+    base:                               '3'
+    shift:                              '#'
+    ralt:                               '\u0302'
+}
+
+key 4 {
+    label:                              '4'
+    base:                               '4'
+    shift:                              '$'
+    ralt:                               '\u0306'
+}
+
+key 5 {
+    label:                              '5'
+    base:                               '5'
+    shift:                              '%'
+    ralt:                               '\u030A'
+}
+
+key 6 {
+    label:                              '6'
+    base:                               '6'
+    shift:                              '^'
+    ralt:                               '\u0328'
+}
+
+key 7 {
+    label:                              '7'
+    base:                               '7'
+    shift:                              '&'
+    ralt:                               '\u0300'
+}
+
+key 8 {
+    label:                              '8'
+    base:                               '8'
+    shift:                              '*'
+    ralt:                               '\u0307'
+}
+
+key 9 {
+    label:                              '9'
+    base:                               '9'
+    shift:                              '('
+    ralt:                               '\u0301'
+}
+
+key 0 {
+    label:                              '0'
+    base:                               '0'
+    shift:                              ')'
+    ralt:                               '\u030B'
+}
+
+key MINUS {
+    label:                              '-'
+    base:                               '-'
+    shift:                              '_'
+    ralt:                               '\u0308'
+    ralt+shift:                         '\u2013'
+}
+
+key EQUALS {
+    label:                              '='
+    base:                               '='
+    shift:                              '+'
+    ralt:                               '\u0327'
+    ralt+shift:                         '\u00b1'
+}
+
+### ROW 2
+
+key Q {
+    label:                              'Q'
+    base, capslock+shift:               'q'
+    shift, capslock:                    'Q'
+}
+
+key W {
+    label:                              'W'
+    base, capslock+shift:               'w'
+    shift, capslock:                    'W'
+}
+
+key E {
+    label:                              'E'
+    base, capslock+shift:               'e'
+    shift, capslock:                    'E'
+    ralt:                               '\u20ac'
+}
+
+key R {
+    label:                              'R'
+    base, capslock+shift:               'r'
+    shift, capslock:                    'R'
+}
+
+key T {
+    label:                              'T'
+    base, capslock+shift:               't'
+    shift, capslock:                    'T'
+}
+
+key Y {
+    label:                              'Y'
+    base, capslock+shift:               'y'
+    shift, capslock:                    'Y'
+}
+
+key U {
+    label:                              'U'
+    base, capslock+shift:               'u'
+    shift, capslock:                    'U'
+}
+
+key I {
+    label:                              'I'
+    base, capslock+shift:               'i'
+    shift, capslock:                    'I'
+}
+
+key O {
+    label:                              'O'
+    base, capslock+shift:               'o'
+    shift, capslock:                    'O'
+}
+
+key P {
+    label:                              'P'
+    base, capslock+shift:               'p'
+    shift, capslock:                    'P'
+    ralt:                               '\u00a7'
+}
+
+key LEFT_BRACKET {
+    label:                              '\u0102'
+    base, capslock+shift:               '\u0103'
+    shift, capslock:                    '\u0102'
+    ralt:                               '['
+    ralt+shift:                         '{'
+}
+
+key RIGHT_BRACKET {
+    label:                              '\u00ce'
+    base, capslock+shift:               '\u00ee'
+    shift, capslock:                    '\u00ce'
+    ralt:                               ']'
+    ralt+shift:                         '}'
+}
+
+### ROW 3
+
+key A {
+    label:                              'A'
+    base, capslock+shift:               'a'
+    shift, capslock:                    'A'
+}
+
+key S {
+    label:                              'S'
+    base, capslock+shift:               's'
+    shift, capslock:                    'S'
+    ralt:                               '\u00df'
+}
+
+key D {
+    label:                              'D'
+    base, capslock+shift:               'd'
+    shift, capslock:                    'D'
+    ralt:                               '\u0111'
+    ralt+shift, ralt+capslock:          '\u0110'
+    ralt+shift+capslock:                '\u0111'
+}
+
+key F {
+    label:                              'F'
+    base, capslock+shift:               'f'
+    shift, capslock:                    'F'
+}
+
+key G {
+    label:                              'G'
+    base, capslock+shift:               'g'
+    shift, capslock:                    'G'
+}
+
+key H {
+    label:                              'H'
+    base, capslock+shift:               'h'
+    shift, capslock:                    'H'
+}
+
+key J {
+    label:                              'J'
+    base, capslock+shift:               'j'
+    shift, capslock:                    'J'
+}
+
+key K {
+    label:                              'K'
+    base, capslock+shift:               'k'
+    shift, capslock:                    'K'
+}
+
+key L {
+    label:                              'L'
+    base, capslock+shift:               'l'
+    shift, capslock:                    'L'
+    ralt:                               '\u0142'
+    ralt+shift, ralt+capslock:          '\u0141'
+    ralt+shift+capslock:                '\u0142'
+}
+
+key SEMICOLON {
+    label:                              '\u0218'
+    base, capslock+shift:               '\u0219'
+    shift, capslock:                    '\u0218'
+    ralt:                               ';'
+    ralt+shift:                         ':'
+}
+
+key APOSTROPHE {
+    label:                              '\u021a'
+    base, capslock+shift:               '\u021b'
+    shift, capslock:                    '\u021a'
+    ralt:                               '\''
+    ralt+shift:                         '\u0022'
+}
+
+key BACKSLASH {
+    label:                              '\u00c2'
+    base, capslock+shift:               '\u00e2'
+    shift, capslock:                    '\u00c2'
+    ralt:                               '\\'
+    ralt+shift:                         '|'
+}
+
+### ROW 4
+
+key PLUS {
+    label:                              '\\'
+    base:                               '\\'
+    shift:                              '|'
+}
+
+key Z {
+    label:                              'Z'
+    base, capslock+shift:               'z'
+    shift, capslock:                    'Z'
+}
+
+key X {
+    label:                              'X'
+    base, capslock+shift:               'x'
+    shift, capslock:                    'X'
+}
+
+key C {
+    label:                              'C'
+    base, capslock+shift:               'c'
+    shift, capslock:                    'C'
+    ralt:                               '\u00a9'
+}
+
+key V {
+    label:                              'V'
+    base, capslock+shift:               'v'
+    shift, capslock:                    'V'
+}
+
+key B {
+    label:                              'B'
+    base, capslock+shift:               'b'
+    shift, capslock:                    'B'
+}
+
+key N {
+    label:                              'N'
+    base, capslock+shift:               'n'
+    shift, capslock:                    'N'
+}
+
+key M {
+    label:                              'M'
+    base, capslock+shift:               'm'
+    shift, capslock:                    'M'
+}
+
+key COMMA {
+    label:                              ','
+    base:                               ','
+    shift:                              ';'
+    ralt:                               '<'
+    ralt+shift:                         '\u00ab'
+}
+
+key PERIOD {
+    label:                              '.'
+    base:                               '.'
+    shift:                              ':'
+    ralt:                               '>'
+    ralt+shift:                         '\u00bb'
+}
+
+key SLASH {
+    label:                              '/'
+    base:                               '/'
+    shift:                              '?'
+}
diff --git a/packages/InputDevices/res/values/strings.xml b/packages/InputDevices/res/values/strings.xml
index 5a91125..bd7cdc4 100644
--- a/packages/InputDevices/res/values/strings.xml
+++ b/packages/InputDevices/res/values/strings.xml
@@ -164,4 +164,7 @@
 
     <!-- Montenegrin (Cyrillic) keyboard layout label. [CHAR LIMIT=35] -->
     <string name="keyboard_layout_montenegrin_cyrillic">Montenegrin (Cyrillic)</string>
+
+    <!-- Romanian keyboard layout label. [CHAR LIMIT=35] -->
+    <string name="keyboard_layout_romanian">Romanian</string>
 </resources>
diff --git a/packages/InputDevices/res/xml/keyboard_layouts.xml b/packages/InputDevices/res/xml/keyboard_layouts.xml
index 9309489..9ce9a87 100644
--- a/packages/InputDevices/res/xml/keyboard_layouts.xml
+++ b/packages/InputDevices/res/xml/keyboard_layouts.xml
@@ -360,4 +360,11 @@
         android:keyboardLayout="@raw/keyboard_layout_serbian_and_montenegrin_cyrillic"
         android:keyboardLocale="cnr-Cyrl-ME"
         android:keyboardLayoutType="extended" />
+
+    <keyboard-layout
+        android:name="keyboard_layout_romanian"
+        android:label="@string/keyboard_layout_romanian"
+        android:keyboardLayout="@raw/keyboard_layout_romanian"
+        android:keyboardLocale="ro-Latn-RO"
+        android:keyboardLayoutType="qwerty" />
 </keyboard-layouts>
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java
index b20117d..c99d37b 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java
@@ -19,6 +19,7 @@
 import static android.Manifest.permission;
 import static android.content.pm.PackageManager.GET_PERMISSIONS;
 import static android.content.pm.PackageManager.MATCH_ARCHIVED_PACKAGES;
+import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
 
 import android.app.Activity;
 import android.app.DialogFragment;
@@ -53,6 +54,8 @@
 
     @Override
     public void onCreate(Bundle icicle) {
+        getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
+
         super.onCreate(null);
 
         int callingUid = getLaunchedFromUid();
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveFragment.java
index 42dd382..fbb0fa4 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveFragment.java
@@ -21,10 +21,14 @@
 import android.app.DialogFragment;
 import android.content.DialogInterface;
 import android.os.Bundle;
+import android.widget.Button;
 
 public class UnarchiveFragment extends DialogFragment implements
         DialogInterface.OnClickListener {
 
+    private Dialog mDialog;
+    private Button mRestoreButton;
+
     @Override
     public Dialog onCreateDialog(Bundle savedInstanceState) {
         String appTitle = getArguments().getString(UnarchiveActivity.APP_TITLE);
@@ -40,7 +44,32 @@
         dialogBuilder.setPositiveButton(R.string.restore, this);
         dialogBuilder.setNegativeButton(android.R.string.cancel, this);
 
-        return dialogBuilder.create();
+        mDialog = dialogBuilder.create();
+        return mDialog;
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        if (mDialog != null) {
+            mRestoreButton = ((AlertDialog) mDialog).getButton(DialogInterface.BUTTON_POSITIVE);
+        }
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        if (mRestoreButton != null) {
+            mRestoreButton.setEnabled(false);
+        }
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        if (mRestoreButton != null) {
+            mRestoreButton.setEnabled(true);
+        }
     }
 
     @Override
diff --git a/packages/PrintSpooler/Android.bp b/packages/PrintSpooler/Android.bp
index 6af3c66..000e20f 100644
--- a/packages/PrintSpooler/Android.bp
+++ b/packages/PrintSpooler/Android.bp
@@ -59,6 +59,21 @@
         "android-support-core-ui",
         "android-support-fragment",
         "android-support-annotations",
+        "printspooler_aconfig_flags_java_lib",
     ],
     manifest: "AndroidManifest.xml",
 }
+
+aconfig_declarations {
+    name: "printspooler_aconfig_declarations",
+    package: "com.android.printspooler.flags",
+    container: "system",
+    srcs: [
+        "flags/flags.aconfig",
+    ],
+}
+
+java_aconfig_library {
+    name: "printspooler_aconfig_flags_java_lib",
+    aconfig_declarations: "printspooler_aconfig_declarations",
+}
diff --git a/packages/PrintSpooler/flags/flags.aconfig b/packages/PrintSpooler/flags/flags.aconfig
new file mode 100644
index 0000000..4a76dff
--- /dev/null
+++ b/packages/PrintSpooler/flags/flags.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.printspooler.flags"
+container: "system"
+
+flag {
+  name: "log_print_jobs"
+  namespace: "printing"
+  description: "Log print job creation and state transitions."
+  bug: "385340868"
+}
diff --git a/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java b/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java
index bba57d5..1a9309c 100644
--- a/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java
+++ b/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java
@@ -68,6 +68,7 @@
 import com.android.internal.util.dump.DualDumpOutputStream;
 import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.printspooler.R;
+import com.android.printspooler.flags.Flags;
 import com.android.printspooler.util.ApprovedPrintServices;
 
 import libcore.io.IoUtils;
@@ -493,7 +494,7 @@
             keepAwakeLocked();
         }
 
-        if (DEBUG_PRINT_JOB_LIFECYCLE) {
+        if (Flags.logPrintJobs() || DEBUG_PRINT_JOB_LIFECYCLE) {
             Slog.i(LOG_TAG, "[ADD] " + printJob);
         }
     }
@@ -506,7 +507,7 @@
                 PrintJobInfo printJob = mPrintJobs.get(i);
                 if (isObsoleteState(printJob.getState())) {
                     mPrintJobs.remove(i);
-                    if (DEBUG_PRINT_JOB_LIFECYCLE) {
+                    if (Flags.logPrintJobs() || DEBUG_PRINT_JOB_LIFECYCLE) {
                         Slog.i(LOG_TAG, "[REMOVE] " + printJob.getId().flattenToString());
                     }
                     removePrintJobFileLocked(printJob.getId());
@@ -568,7 +569,7 @@
                     checkIfStillKeepAwakeLocked();
                 }
 
-                if (DEBUG_PRINT_JOB_LIFECYCLE) {
+                if (Flags.logPrintJobs() || DEBUG_PRINT_JOB_LIFECYCLE) {
                     Slog.i(LOG_TAG, "[STATE CHANGED] " + printJob);
                 }
 
diff --git a/packages/SettingsLib/DataStore/OWNERS b/packages/SettingsLib/DataStore/OWNERS
new file mode 100644
index 0000000..1219dc4
--- /dev/null
+++ b/packages/SettingsLib/DataStore/OWNERS
@@ -0,0 +1 @@
+include ../OWNERS_catalyst
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/DataChangeReason.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/DataChangeReason.kt
index 145fabe..ac36b08 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/DataChangeReason.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/DataChangeReason.kt
@@ -39,5 +39,7 @@
         const val RESTORE = 3
         /** Data is synced from another profile (e.g. personal profile to work profile). */
         const val SYNC_ACROSS_PROFILES = 4
+
+        fun isDataChange(reason: Int): Boolean = reason in UNKNOWN..SYNC_ACROSS_PROFILES
     }
 }
diff --git a/packages/SettingsLib/DataStore/tests/Android.bp b/packages/SettingsLib/DataStore/tests/Android.bp
index 2e3b42d..6044eab 100644
--- a/packages/SettingsLib/DataStore/tests/Android.bp
+++ b/packages/SettingsLib/DataStore/tests/Android.bp
@@ -25,6 +25,5 @@
     java_resource_dirs: ["config"],
     instrumentation_for: "SettingsLibDataStoreShell",
     coverage_libs: ["SettingsLibDataStore"],
-    upstream: true,
     strict_mode: false,
 }
diff --git a/packages/SettingsLib/Graph/OWNERS b/packages/SettingsLib/Graph/OWNERS
new file mode 100644
index 0000000..1219dc4
--- /dev/null
+++ b/packages/SettingsLib/Graph/OWNERS
@@ -0,0 +1 @@
+include ../OWNERS_catalyst
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt
index 1ed814a..51813a1c9 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt
@@ -69,7 +69,6 @@
     val visitedScreens: Set<String> = setOf(),
     val locale: Locale? = null,
     val flags: Int = PreferenceGetterFlags.ALL,
-    val includeValue: Boolean = true, // TODO: clean up
     val includeValueDescriptor: Boolean = true,
 )
 
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt
index 2fac545..6fc6b54 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt
@@ -22,6 +22,7 @@
 import com.android.settingslib.ipc.ApiDescriptor
 import com.android.settingslib.ipc.ApiHandler
 import com.android.settingslib.ipc.ApiPermissionChecker
+import com.android.settingslib.metadata.PreferenceCoordinate
 import com.android.settingslib.metadata.PreferenceHierarchyNode
 import com.android.settingslib.metadata.PreferenceScreenRegistry
 
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt
index ff14eb5..70ce62c 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt
@@ -20,6 +20,7 @@
 import android.os.Parcel
 import com.android.settingslib.graph.proto.PreferenceProto
 import com.android.settingslib.ipc.MessageCodec
+import com.android.settingslib.metadata.PreferenceCoordinate
 import java.util.Arrays
 
 /** Message codec for [PreferenceGetterRequest]. */
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt
index 3c870ac..ea79554 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt
@@ -34,6 +34,8 @@
 import com.android.settingslib.metadata.PreferenceScreenRegistry
 import com.android.settingslib.metadata.RangeValue
 import com.android.settingslib.metadata.ReadWritePermit
+import com.android.settingslib.metadata.SensitivityLevel.Companion.HIGH_SENSITIVITY
+import com.android.settingslib.metadata.SensitivityLevel.Companion.UNKNOWN_SENSITIVITY
 
 /** Request to set preference value. */
 data class PreferenceSetterRequest(
@@ -187,6 +189,8 @@
     callingUid: Int,
 ): Int =
     when {
+        sensitivityLevel == UNKNOWN_SENSITIVITY || sensitivityLevel == HIGH_SENSITIVITY ->
+            ReadWritePermit.DISALLOW
         getWritePermissions(context)?.check(context, callingPid, callingUid) == false ->
             ReadWritePermit.REQUIRE_APP_PERMISSION
         else -> getWritePermit(context, value, callingPid, callingUid)
diff --git a/packages/SettingsLib/Ipc/Android.bp b/packages/SettingsLib/Ipc/Android.bp
index 2c7209a..bc5a936 100644
--- a/packages/SettingsLib/Ipc/Android.bp
+++ b/packages/SettingsLib/Ipc/Android.bp
@@ -25,7 +25,7 @@
     name: "SettingsLibIpc-testutils",
     srcs: ["testutils/**/*.kt"],
     static_libs: [
-        "Robolectric_all-target_upstream",
+        "Robolectric_all-target",
         "SettingsLibIpc",
         "androidx.test.core",
         "flag-junit",
diff --git a/packages/SettingsLib/Ipc/OWNERS b/packages/SettingsLib/Ipc/OWNERS
new file mode 100644
index 0000000..1219dc4
--- /dev/null
+++ b/packages/SettingsLib/Ipc/OWNERS
@@ -0,0 +1 @@
+include ../OWNERS_catalyst
diff --git a/packages/SettingsLib/Metadata/OWNERS b/packages/SettingsLib/Metadata/OWNERS
new file mode 100644
index 0000000..1219dc4
--- /dev/null
+++ b/packages/SettingsLib/Metadata/OWNERS
@@ -0,0 +1 @@
+include ../OWNERS_catalyst
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt
index e5bf41f..83725aa 100644
--- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt
@@ -44,6 +44,24 @@
     }
 }
 
+/** The reason of preference change. */
+@IntDef(
+    PreferenceChangeReason.VALUE,
+    PreferenceChangeReason.STATE,
+    PreferenceChangeReason.DEPENDENT,
+)
+@Retention(AnnotationRetention.SOURCE)
+annotation class PreferenceChangeReason {
+    companion object {
+        /** Preference value is changed. */
+        const val VALUE = 1000
+        /** Preference state (title/summary, enable state, etc.) is changed. */
+        const val STATE = 1001
+        /** Dependent preference state is changed. */
+        const val DEPENDENT = 1002
+    }
+}
+
 /** Indicates how sensitive of the data. */
 @Retention(AnnotationRetention.SOURCE)
 @Target(AnnotationTarget.TYPE)
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceCoordinate.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceCoordinate.kt
similarity index 93%
rename from packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceCoordinate.kt
rename to packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceCoordinate.kt
index 68aa2d2..2dd736a 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceCoordinate.kt
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceCoordinate.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2024 The Android Open Source Project
+ * Copyright (C) 2025 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.settingslib.graph
+package com.android.settingslib.metadata
 
 import android.os.Parcel
 import android.os.Parcelable
diff --git a/packages/SettingsLib/OWNERS_catalyst b/packages/SettingsLib/OWNERS_catalyst
new file mode 100644
index 0000000..d44ac68
--- /dev/null
+++ b/packages/SettingsLib/OWNERS_catalyst
@@ -0,0 +1,9 @@
+# OWNERS of Catalyst libraries (DataStore, Metadata, etc.)
+
+# Main developers
+jiannan@google.com
+cechkahn@google.com
+sunnyshao@google.com
+
+# Emergency only
+cipson@google.com
diff --git a/packages/SettingsLib/Preference/OWNERS b/packages/SettingsLib/Preference/OWNERS
new file mode 100644
index 0000000..1219dc4
--- /dev/null
+++ b/packages/SettingsLib/Preference/OWNERS
@@ -0,0 +1 @@
+include ../OWNERS_catalyst
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
index 91abd8b..8358ab9 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
@@ -25,12 +25,14 @@
 import androidx.preference.PreferenceDataStore
 import androidx.preference.PreferenceGroup
 import androidx.preference.PreferenceScreen
+import com.android.settingslib.datastore.DataChangeReason
 import com.android.settingslib.datastore.HandlerExecutor
 import com.android.settingslib.datastore.KeyValueStore
 import com.android.settingslib.datastore.KeyedDataObservable
 import com.android.settingslib.datastore.KeyedObservable
 import com.android.settingslib.datastore.KeyedObserver
 import com.android.settingslib.metadata.PersistentPreference
+import com.android.settingslib.metadata.PreferenceChangeReason
 import com.android.settingslib.metadata.PreferenceHierarchy
 import com.android.settingslib.metadata.PreferenceHierarchyNode
 import com.android.settingslib.metadata.PreferenceLifecycleContext
@@ -73,7 +75,7 @@
                     ?.keyValueStore
 
             override fun notifyPreferenceChange(key: String) =
-                notifyChange(key, CHANGE_REASON_STATE)
+                notifyChange(key, PreferenceChangeReason.STATE)
 
             @Suppress("DEPRECATION")
             override fun startActivityForResult(
@@ -91,7 +93,13 @@
     private val preferenceObserver: KeyedObserver<String?>
 
     private val storageObserver =
-        KeyedObserver<String> { key, _ -> notifyChange(key, CHANGE_REASON_VALUE) }
+        KeyedObserver<String> { key, reason ->
+            if (DataChangeReason.isDataChange(reason)) {
+                notifyChange(key, PreferenceChangeReason.VALUE)
+            } else {
+                notifyChange(key, PreferenceChangeReason.STATE)
+            }
+        }
 
     init {
         val preferencesBuilder = ImmutableMap.builder<String, PreferenceHierarchyNode>()
@@ -148,7 +156,7 @@
         }
 
         // check reason to avoid potential infinite loop
-        if (reason != CHANGE_REASON_DEPENDENT) {
+        if (reason != PreferenceChangeReason.DEPENDENT) {
             notifyDependents(key, mutableSetOf())
         }
     }
@@ -157,7 +165,7 @@
     private fun notifyDependents(key: String, notifiedKeys: MutableSet<String>) {
         if (!notifiedKeys.add(key)) return
         for (dependency in dependencies[key]) {
-            notifyChange(dependency, CHANGE_REASON_DEPENDENT)
+            notifyChange(dependency, PreferenceChangeReason.DEPENDENT)
             notifyDependents(dependency, notifiedKeys)
         }
     }
@@ -210,13 +218,6 @@
     }
 
     companion object {
-        /** Preference value is changed. */
-        const val CHANGE_REASON_VALUE = 0
-        /** Preference state (title/summary, enable state, etc.) is changed. */
-        const val CHANGE_REASON_STATE = 1
-        /** Dependent preference state is changed. */
-        const val CHANGE_REASON_DEPENDENT = 2
-
         /** Updates preference screen that has incomplete hierarchy. */
         @JvmStatic
         fun bind(preferenceScreen: PreferenceScreen) {
diff --git a/packages/SettingsLib/Service/OWNERS b/packages/SettingsLib/Service/OWNERS
new file mode 100644
index 0000000..1219dc4
--- /dev/null
+++ b/packages/SettingsLib/Service/OWNERS
@@ -0,0 +1 @@
+include ../OWNERS_catalyst
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/Android.bp b/packages/SettingsLib/Spa/screenshot/robotests/Android.bp
index f6477e2..dd6743b 100644
--- a/packages/SettingsLib/Spa/screenshot/robotests/Android.bp
+++ b/packages/SettingsLib/Spa/screenshot/robotests/Android.bp
@@ -68,7 +68,6 @@
         "android.test.mock.stubs.system",
         "truth",
     ],
-    upstream: true,
     java_resource_dirs: ["config"],
     instrumentation_for: "SpaRoboApp",
 
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatter.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatter.kt
new file mode 100644
index 0000000..5b7e2a8
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatter.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2025 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.spaprivileged.framework.common
+
+import android.content.Context
+import android.content.res.Resources
+import android.icu.text.DecimalFormat
+import android.icu.text.MeasureFormat
+import android.icu.text.NumberFormat
+import android.icu.text.UnicodeSet
+import android.icu.text.UnicodeSetSpanner
+import android.icu.util.Measure
+import android.text.format.Formatter
+import android.text.format.Formatter.RoundedBytesResult
+import java.math.BigDecimal
+
+class BytesFormatter(resources: Resources) {
+
+    enum class UseCase(val flag: Int) {
+        FileSize(Formatter.FLAG_SI_UNITS),
+        DataUsage(Formatter.FLAG_IEC_UNITS),
+    }
+
+    data class Result(val number: String, val units: String)
+
+    constructor(context: Context) : this(context.resources)
+
+    private val locale = resources.configuration.locales[0]
+
+    fun format(bytes: Long, useCase: UseCase): String {
+        val rounded = RoundedBytesResult.roundBytes(bytes, useCase.flag)
+        val numberFormatter = getNumberFormatter(rounded.fractionDigits)
+        return numberFormatter.formatRoundedBytesResult(rounded)
+    }
+
+    fun formatWithUnits(bytes: Long, useCase: UseCase): Result {
+        val rounded = RoundedBytesResult.roundBytes(bytes, useCase.flag)
+        val numberFormatter = getNumberFormatter(rounded.fractionDigits)
+        val formattedString = numberFormatter.formatRoundedBytesResult(rounded)
+        val formattedNumber = numberFormatter.format(rounded.value)
+        return Result(
+            number = formattedNumber,
+            units = formattedString.removeFirst(formattedNumber),
+        )
+    }
+
+    private fun NumberFormat.formatRoundedBytesResult(rounded: RoundedBytesResult): String {
+        val measureFormatter =
+            MeasureFormat.getInstance(locale, MeasureFormat.FormatWidth.SHORT, this)
+        return measureFormatter.format(Measure(rounded.value, rounded.units))
+    }
+
+    private fun getNumberFormatter(fractionDigits: Int) =
+        NumberFormat.getInstance(locale).apply {
+            minimumFractionDigits = fractionDigits
+            maximumFractionDigits = fractionDigits
+            isGroupingUsed = false
+            if (this is DecimalFormat) {
+                setRoundingMode(BigDecimal.ROUND_HALF_UP)
+            }
+        }
+
+    private companion object {
+        fun String.removeFirst(removed: String): String =
+            SPACES_AND_CONTROLS.trim(replaceFirst(removed, "")).toString()
+
+        val SPACES_AND_CONTROLS = UnicodeSetSpanner(UnicodeSet("[[:Zs:][:Cf:]]").freeze())
+    }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
index e1e1ee5..78d6c31 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
@@ -88,6 +88,7 @@
         matchAnyUserForAdmin: Boolean,
     ): List<ApplicationInfo> = try {
         coroutineScope {
+            // TODO(b/382016780): to be removed after flag cleanup.
             val hiddenSystemModulesDeferred = async { packageManager.getHiddenSystemModules() }
             val hideWhenDisabledPackagesDeferred = async {
                 context.resources.getStringArray(R.array.config_hideWhenDisabled_packageNames)
@@ -95,6 +96,7 @@
             val installedApplicationsAsUser =
                 getInstalledApplications(userId, matchAnyUserForAdmin)
 
+            // TODO(b/382016780): to be removed after flag cleanup.
             val hiddenSystemModules = hiddenSystemModulesDeferred.await()
             val hideWhenDisabledPackages = hideWhenDisabledPackagesDeferred.await()
             installedApplicationsAsUser.filter { app ->
@@ -206,6 +208,7 @@
     private fun isSystemApp(app: ApplicationInfo, homeOrLauncherPackages: Set<String>): Boolean =
         app.isSystemApp && !app.isUpdatedSystemApp && app.packageName !in homeOrLauncherPackages
 
+    // TODO(b/382016780): to be removed after flag cleanup.
     private fun PackageManager.getHiddenSystemModules(): Set<String> {
         val moduleInfos = getInstalledModules(0).filter { it.isHidden }
         val hiddenApps = moduleInfos.mapNotNull { it.packageName }.toMutableSet()
@@ -218,13 +221,14 @@
     companion object {
         private const val TAG = "AppListRepository"
 
+        // TODO(b/382016780): to be removed after flag cleanup.
         private fun ApplicationInfo.isInAppList(
             showInstantApps: Boolean,
             hiddenSystemModules: Set<String>,
             hideWhenDisabledPackages: Array<String>,
         ) = when {
             !showInstantApps && isInstantApp -> false
-            packageName in hiddenSystemModules -> false
+            !Flags.removeHiddenModuleUsage() && (packageName in hiddenSystemModules) -> false
             packageName in hideWhenDisabledPackages -> enabled && !isDisabledUntilUsed
             enabled -> true
             else -> enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatterTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatterTest.kt
new file mode 100644
index 0000000..7220848
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatterTest.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2025 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.spaprivileged.framework.common
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+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 BytesFormatterTest {
+
+    private val context: Context = ApplicationProvider.getApplicationContext()
+
+    private val formatter = BytesFormatter(context)
+
+    @Test
+    fun `Zero bytes`() {
+        // Given a byte value of 0, the formatted output should be "0 byte" for both FileSize
+        // and DataUsage UseCases. This verifies special handling of zero values.
+
+        val fileSizeResult = formatter.format(0, BytesFormatter.UseCase.FileSize)
+        assertThat(fileSizeResult).isEqualTo("0 byte")
+
+        val dataUsageResult = formatter.format(0, BytesFormatter.UseCase.DataUsage)
+        assertThat(dataUsageResult).isEqualTo("0 byte")
+    }
+
+    @Test
+    fun `Positive bytes`() {
+        // Given a positive byte value (e.g., 1000), the formatted output should be correctly
+        // displayed with appropriate units (e.g., '1.00 kB') for both UseCases.
+
+        val fileSizeResult = formatter.format(1000, BytesFormatter.UseCase.FileSize)
+        assertThat(fileSizeResult).isEqualTo("1.00 kB")
+
+        val dataUsageResult = formatter.format(1024, BytesFormatter.UseCase.DataUsage)
+        assertThat(dataUsageResult).isEqualTo("1.00 kB")
+    }
+
+    @Test
+    fun `Large bytes`() {
+        // Given a very large byte value (e.g., Long.MAX_VALUE), the formatted output should be
+        // correctly displayed with the largest unit (e.g., 'PB') for both UseCases.
+
+        val fileSizeResult = formatter.format(Long.MAX_VALUE, BytesFormatter.UseCase.FileSize)
+        assertThat(fileSizeResult).isEqualTo("9223 PB")
+
+        val dataUsageResult = formatter.format(Long.MAX_VALUE, BytesFormatter.UseCase.DataUsage)
+        assertThat(dataUsageResult).isEqualTo("8192 PB")
+    }
+
+    @Test
+    fun `Bytes requiring rounding`() {
+        // Given byte values that require rounding (e.g., 1512), the formatted output should be
+        // rounded to the appropriate number of decimal places (e.g., '1.51 kB').
+
+        val fileSizeResult = formatter.format(1512, BytesFormatter.UseCase.FileSize)
+        assertThat(fileSizeResult).isEqualTo("1.51 kB")
+
+        val dataUsageResult = formatter.format(1512, BytesFormatter.UseCase.DataUsage)
+        assertThat(dataUsageResult).isEqualTo("1.48 kB")
+    }
+
+    @Test
+    fun `FileSize UseCase`() {
+        // When the UseCase is FileSize, the correct units (byte, KB, kB, GB, TB, PB) should
+        // be used.
+        val values =
+            listOf(
+                1L,
+                1024L,
+                1024L * 1024L,
+                1024L * 1024L * 1024L,
+                1024L * 1024L * 1024L * 1024L,
+                1024L * 1024L * 1024L * 1024L * 1024L,
+                1024L * 1024L * 1024L * 1024L * 1024L * 1024L,
+            )
+        val expectedUnits = listOf("byte", "kB", "MB", "GB", "TB", "PB", "PB")
+
+        values.zip(expectedUnits).forEach { (value, expectedUnit) ->
+            val result = formatter.format(value, BytesFormatter.UseCase.FileSize)
+            assertThat(result).contains(expectedUnit)
+        }
+    }
+
+    @Test
+    fun `DataUsage UseCase`() {
+        // When the UseCase is DataUsage, the correct units (byte, kB, MB, GB, TB, PB) should
+        // be used.
+        val values =
+            listOf(
+                1L,
+                1024L,
+                1024L * 1024L,
+                1024L * 1024L * 1024L,
+                1024L * 1024L * 1024L * 1024L,
+                1024L * 1024L * 1024L * 1024L * 1024L,
+                1024L * 1024L * 1024L * 1024L * 1024L * 1024L,
+            )
+        val expectedUnits = listOf("byte", "kB", "MB", "GB", "TB", "PB", "PB")
+
+        values.zip(expectedUnits).forEach { (value, expectedUnit) ->
+            val result = formatter.format(value, BytesFormatter.UseCase.DataUsage)
+            assertThat(result).contains(expectedUnit)
+        }
+    }
+
+    @Test
+    fun `Fraction digits`() {
+        // The number of fraction digits in the output should be correctly determined based on
+        // the rounded byte value.
+
+        assertThat(formatter.format(1500, BytesFormatter.UseCase.FileSize)).isEqualTo("1.50 kB")
+        assertThat(formatter.format(1050, BytesFormatter.UseCase.FileSize)).isEqualTo("1.05 kB")
+        assertThat(formatter.format(999, BytesFormatter.UseCase.FileSize)).isEqualTo("1.00 kB")
+    }
+
+    @Test
+    fun `Rounding mode`() {
+        // The rounding mode used for formatting should be ROUND_HALF_UP.
+
+        val result = formatter.format(1006, BytesFormatter.UseCase.FileSize)
+
+        assertThat(result).isEqualTo("1.01 kB") // Ensure rounding mode is effective
+    }
+
+    @Test
+    fun `Grouping separator`() {
+        // Grouping separators should not be used in the formatted output.
+
+        val result = formatter.format(Long.MAX_VALUE, BytesFormatter.UseCase.FileSize)
+
+        assertThat(result).isEqualTo("9223 PB")
+    }
+
+    @Test
+    fun `Format with units`() {
+        // Verify that the `formatWithUnits` method correctly formats the given bytes with the
+        // specified units.
+
+        val resultByte = formatter.formatWithUnits(0, BytesFormatter.UseCase.FileSize)
+        assertThat(resultByte).isEqualTo(BytesFormatter.Result("0", "byte"))
+
+        val resultKb = formatter.formatWithUnits(1000, BytesFormatter.UseCase.FileSize)
+        assertThat(resultKb).isEqualTo(BytesFormatter.Result("1.00", "kB"))
+
+        val resultMb = formatter.formatWithUnits(479_999_999, BytesFormatter.UseCase.FileSize)
+        assertThat(resultMb).isEqualTo(BytesFormatter.Result("480", "MB"))
+
+        val resultGb = formatter.formatWithUnits(20_100_000_000, BytesFormatter.UseCase.FileSize)
+        assertThat(resultGb).isEqualTo(BytesFormatter.Result("20.10", "GB"))
+
+        val resultTb =
+            formatter.formatWithUnits(300_100_000_000_000, BytesFormatter.UseCase.FileSize)
+        assertThat(resultTb).isEqualTo(BytesFormatter.Result("300", "TB"))
+
+        val resultPb =
+            formatter.formatWithUnits(1000_000_000_000_000, BytesFormatter.UseCase.FileSize)
+        assertThat(resultPb).isEqualTo(BytesFormatter.Result("1.00", "PB"))
+    }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
index b1baa86..fd4b189 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
@@ -281,6 +281,23 @@
         )
     }
 
+    @EnableFlags(Flags.FLAG_REMOVE_HIDDEN_MODULE_USAGE)
+    @Test
+    fun loadApps_shouldIncludeAllSystemModuleApps() = runTest {
+        packageManager.stub {
+            on { getInstalledModules(any()) } doReturn listOf(HIDDEN_MODULE)
+        }
+        mockInstalledApplications(
+            listOf(NORMAL_APP, HIDDEN_APEX_APP, HIDDEN_MODULE_APP),
+            ADMIN_USER_ID
+        )
+
+        val appList = repository.loadApps(userId = ADMIN_USER_ID)
+
+        assertThat(appList).containsExactly(NORMAL_APP, HIDDEN_APEX_APP, HIDDEN_MODULE_APP)
+    }
+
+    @DisableFlags(Flags.FLAG_REMOVE_HIDDEN_MODULE_USAGE)
     @EnableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX)
     @Test
     fun loadApps_hasApkInApexInfo_shouldNotIncludeAllHiddenApps() = runTest {
@@ -297,7 +314,7 @@
         assertThat(appList).containsExactly(NORMAL_APP)
     }
 
-    @DisableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX)
+    @DisableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX, Flags.FLAG_REMOVE_HIDDEN_MODULE_USAGE)
     @Test
     fun loadApps_noApkInApexInfo_shouldNotIncludeHiddenSystemModule() = runTest {
         packageManager.stub {
@@ -456,6 +473,7 @@
             isArchived = true
         }
 
+        // TODO(b/382016780): to be removed after flag cleanup.
         val HIDDEN_APEX_APP = ApplicationInfo().apply {
             packageName = "hidden.apex.package"
         }
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java
index c482995..3390296 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java
@@ -137,6 +137,7 @@
 
     /**
      * Returns a boolean indicating whether the given package is a hidden system module
+     * TODO(b/382016780): to be removed after flag cleanup.
      */
     public static boolean isHiddenSystemModule(Context context, String packageName) {
         return ApplicationsState.getInstance((Application) context.getApplicationContext())
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
index fd9a008..4110d53 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
@@ -157,6 +157,7 @@
     int mCurComputingSizeUserId;
     boolean mSessionsChanged;
     // Maps all installed modules on the system to whether they're hidden or not.
+    // TODO(b/382016780): to be removed after flag cleanup.
     final HashMap<String, Boolean> mSystemModules = new HashMap<>();
 
     // Temporary for dispatching session callbacks.  Only touched by main thread.
@@ -226,12 +227,14 @@
         mRetrieveFlags = PackageManager.MATCH_DISABLED_COMPONENTS |
                 PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
 
-        final List<ModuleInfo> moduleInfos = mPm.getInstalledModules(0 /* flags */);
-        for (ModuleInfo info : moduleInfos) {
-            mSystemModules.put(info.getPackageName(), info.isHidden());
-            if (Flags.provideInfoOfApkInApex()) {
-                for (String apkInApexPackageName : info.getApkInApexPackageNames()) {
-                    mSystemModules.put(apkInApexPackageName, info.isHidden());
+        if (!Flags.removeHiddenModuleUsage()) {
+            final List<ModuleInfo> moduleInfos = mPm.getInstalledModules(0 /* flags */);
+            for (ModuleInfo info : moduleInfos) {
+                mSystemModules.put(info.getPackageName(), info.isHidden());
+                if (Flags.provideInfoOfApkInApex()) {
+                    for (String apkInApexPackageName : info.getApkInApexPackageNames()) {
+                        mSystemModules.put(apkInApexPackageName, info.isHidden());
+                    }
                 }
             }
         }
@@ -336,7 +339,7 @@
                 }
                 mHaveDisabledApps = true;
             }
-            if (isHiddenModule(info.packageName)) {
+            if (!Flags.removeHiddenModuleUsage() && isHiddenModule(info.packageName)) {
                 mApplications.remove(i--);
                 continue;
             }
@@ -453,6 +456,7 @@
         return mHaveInstantApps;
     }
 
+    // TODO(b/382016780): to be removed after flag cleanup.
     boolean isHiddenModule(String packageName) {
         Boolean isHidden = mSystemModules.get(packageName);
         if (isHidden == null) {
@@ -462,6 +466,7 @@
         return isHidden;
     }
 
+    // TODO(b/382016780): to be removed after flag cleanup.
     boolean isSystemModule(String packageName) {
         return mSystemModules.containsKey(packageName);
     }
@@ -755,7 +760,7 @@
             Log.i(TAG, "Looking up entry of pkg " + info.packageName + ": " + entry);
         }
         if (entry == null) {
-            if (isHiddenModule(info.packageName)) {
+            if (!Flags.removeHiddenModuleUsage() && isHiddenModule(info.packageName)) {
                 if (DEBUG) {
                     Log.i(TAG, "No AppEntry for " + info.packageName + " (hidden module)");
                 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index 145b62c..68e9fe7 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -73,6 +73,10 @@
     private static final Set<Integer> SA_PROFILES =
             ImmutableSet.of(
                     BluetoothProfile.A2DP, BluetoothProfile.LE_AUDIO, BluetoothProfile.HEARING_AID);
+    private static final List<Integer> BLUETOOTH_DEVICE_CLASS_HEADSET =
+            List.of(
+                    BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES,
+                    BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET);
 
     private static final String TEMP_BOND_TYPE = "TEMP_BOND_TYPE";
     private static final String TEMP_BOND_DEVICE_METADATA_VALUE = "le_audio_sharing";
@@ -390,6 +394,19 @@
         return false;
     }
 
+    /** Checks whether the bluetooth device is a headset. */
+    public static boolean isHeadset(@NonNull BluetoothDevice bluetoothDevice) {
+        String deviceType =
+                BluetoothUtils.getStringMetaData(
+                        bluetoothDevice, BluetoothDevice.METADATA_DEVICE_TYPE);
+        if (!TextUtils.isEmpty(deviceType)) {
+            return BluetoothDevice.DEVICE_TYPE_HEADSET.equals(deviceType)
+                    || BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET.equals(deviceType);
+        }
+        BluetoothClass btClass = bluetoothDevice.getBluetoothClass();
+        return btClass != null && BLUETOOTH_DEVICE_CLASS_HEADSET.contains(btClass.getDeviceClass());
+    }
+
     /** Create an Icon pointing to a drawable. */
     public static IconCompat createIconWithDrawable(Drawable drawable) {
         Bitmap bitmap;
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
index b2c2794..e05f0a1 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
@@ -483,14 +483,18 @@
 
     void onActiveDeviceChanged(CachedBluetoothDevice device) {
         if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_AUDIO_ROUTING)) {
-            if (device.isConnectedHearingAidDevice()) {
+            if (device.isConnectedHearingAidDevice()
+                    && (device.isActiveDevice(BluetoothProfile.HEARING_AID)
+                    || device.isActiveDevice(BluetoothProfile.LE_AUDIO))) {
                 setAudioRoutingConfig(device);
             } else {
                 clearAudioRoutingConfig();
             }
         }
         if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) {
-            if (device.isConnectedHearingAidDevice()) {
+            if (device.isConnectedHearingAidDevice()
+                    && (device.isActiveDevice(BluetoothProfile.HEARING_AID)
+                    || device.isActiveDevice(BluetoothProfile.LE_AUDIO))) {
                 setMicrophoneForCalls(device);
             } else {
                 clearMicrophoneForCalls();
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
index 6be4336..155c7e6 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
@@ -21,6 +21,7 @@
 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
 
+import android.annotation.CallbackExecutor;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothClass;
 import android.bluetooth.BluetoothCsipSetCoordinator;
@@ -39,6 +40,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.Executor;
 
 public class LeAudioProfile implements LocalBluetoothProfile {
     private static final String TAG = "LeAudioProfile";
@@ -317,6 +319,78 @@
         return mService.getAudioLocation(device);
     }
 
+    /**
+     * Sets the fallback group id when broadcast switches to unicast.
+     *
+     * @param groupId the target fallback group id
+     */
+    public void setBroadcastToUnicastFallbackGroup(int groupId) {
+        if (mService == null) {
+            Log.w(TAG, "Proxy not attached to service. Cannot set fallback group: " + groupId);
+            return;
+        }
+
+        mService.setBroadcastToUnicastFallbackGroup(groupId);
+    }
+
+    /**
+     * Gets the fallback group id when broadcast switches to unicast.
+     *
+     * @return current fallback group id
+     */
+    public int getBroadcastToUnicastFallbackGroup() {
+        if (mService == null) {
+            Log.w(TAG, "Proxy not attached to service. Cannot get fallback group.");
+            return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
+        }
+        return mService.getBroadcastToUnicastFallbackGroup();
+    }
+
+    /**
+     * Registers a {@link BluetoothLeAudio.Callback} that will be invoked during the
+     * operation of this profile.
+     *
+     * Repeated registration of the same <var>callback</var> object after the first call to this
+     * method will result with IllegalArgumentException being thrown, even when the
+     * <var>executor</var> is different. API caller would have to call
+     * {@link #unregisterCallback(BluetoothLeAudio.Callback)} with the same callback object
+     * before registering it again.
+     *
+     * @param executor an {@link Executor} to execute given callback
+     * @param callback user implementation of the {@link BluetoothLeAudio.Callback}
+     * @throws NullPointerException if a null executor, or callback is given, or
+     *                              IllegalArgumentException if the same <var>callback</var> is
+     *                              already registered.
+     */
+    public void registerCallback(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull BluetoothLeAudio.Callback callback) {
+        if (mService == null) {
+            Log.w(TAG, "Proxy not attached to service. Cannot register callback.");
+            return;
+        }
+        mService.registerCallback(executor, callback);
+    }
+
+    /**
+     * Unregisters the specified {@link BluetoothLeAudio.Callback}.
+     * <p>The same {@link BluetoothLeAudio.Callback} object used when calling
+     * {@link #registerCallback(Executor, BluetoothLeAudio.Callback)} must be used.
+     *
+     * <p>Callbacks are automatically unregistered when application process goes away
+     *
+     * @param callback user implementation of the {@link BluetoothLeAudio.Callback}
+     * @throws NullPointerException when callback is null or IllegalArgumentException when no
+     *                              callback is registered
+     */
+    public void unregisterCallback(@NonNull BluetoothLeAudio.Callback callback) {
+        if (mService == null) {
+            Log.w(TAG, "Proxy not attached to service. Cannot unregister callback.");
+            return;
+        }
+        mService.unregisterCallback(callback);
+    }
+
     @RequiresApi(Build.VERSION_CODES.S)
     protected void finalize() {
         if (DEBUG) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
index dc40304..51259e2f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
+++ b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
@@ -16,6 +16,8 @@
 
 package com.android.settingslib.dream;
 
+import static android.service.dreams.Flags.allowDreamWhenPostured;
+
 import android.annotation.IntDef;
 import android.content.ComponentName;
 import android.content.Context;
@@ -78,14 +80,21 @@
     }
 
     @Retention(RetentionPolicy.SOURCE)
-    @IntDef({WHILE_CHARGING, WHILE_DOCKED, EITHER, NEVER})
+    @IntDef({
+            WHILE_CHARGING,
+            WHILE_DOCKED,
+            WHILE_POSTURED,
+            WHILE_CHARGING_OR_DOCKED,
+            NEVER
+    })
     public @interface WhenToDream {
     }
 
     public static final int WHILE_CHARGING = 0;
     public static final int WHILE_DOCKED = 1;
-    public static final int EITHER = 2;
-    public static final int NEVER = 3;
+    public static final int WHILE_POSTURED = 2;
+    public static final int WHILE_CHARGING_OR_DOCKED = 3;
+    public static final int NEVER = 4;
 
     /**
      * The type of dream complications which can be provided by a
@@ -134,6 +143,8 @@
             .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_WHILE_CHARGING_ONLY;
     private static final int WHEN_TO_DREAM_DOCKED = FrameworkStatsLog
             .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_WHILE_DOCKED_ONLY;
+    private static final int WHEN_TO_DREAM_POSTURED = FrameworkStatsLog
+            .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_WHILE_POSTURED_ONLY;
     private static final int WHEN_TO_DREAM_CHARGING_OR_DOCKED = FrameworkStatsLog
             .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_EITHER_CHARGING_OR_DOCKED;
 
@@ -143,6 +154,7 @@
     private final boolean mDreamsEnabledByDefault;
     private final boolean mDreamsActivatedOnSleepByDefault;
     private final boolean mDreamsActivatedOnDockByDefault;
+    private final boolean mDreamsActivatedOnPosturedByDefault;
     private final Set<ComponentName> mDisabledDreams;
     private final List<String> mLoggableDreamPrefixes;
     private Set<Integer> mSupportedComplications;
@@ -168,6 +180,8 @@
                 com.android.internal.R.bool.config_dreamsActivatedOnSleepByDefault);
         mDreamsActivatedOnDockByDefault = resources.getBoolean(
                 com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault);
+        mDreamsActivatedOnPosturedByDefault = resources.getBoolean(
+                com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault);
         mDisabledDreams = Arrays.stream(resources.getStringArray(
                         com.android.internal.R.array.config_disabledDreamComponents))
                 .map(ComponentName::unflattenFromString)
@@ -280,10 +294,11 @@
 
     @WhenToDream
     public int getWhenToDreamSetting() {
-        return isActivatedOnDock() && isActivatedOnSleep() ? EITHER
+        return isActivatedOnDock() && isActivatedOnSleep() ? WHILE_CHARGING_OR_DOCKED
                 : isActivatedOnDock() ? WHILE_DOCKED
-                        : isActivatedOnSleep() ? WHILE_CHARGING
-                                : NEVER;
+                        : isActivatedOnPostured() ? WHILE_POSTURED
+                                : isActivatedOnSleep() ? WHILE_CHARGING
+                                        : NEVER;
     }
 
     public void setWhenToDream(@WhenToDream int whenToDream) {
@@ -293,16 +308,25 @@
             case WHILE_CHARGING:
                 setActivatedOnDock(false);
                 setActivatedOnSleep(true);
+                setActivatedOnPostured(false);
                 break;
 
             case WHILE_DOCKED:
                 setActivatedOnDock(true);
                 setActivatedOnSleep(false);
+                setActivatedOnPostured(false);
                 break;
 
-            case EITHER:
+            case WHILE_CHARGING_OR_DOCKED:
                 setActivatedOnDock(true);
                 setActivatedOnSleep(true);
+                setActivatedOnPostured(false);
+                break;
+
+            case WHILE_POSTURED:
+                setActivatedOnPostured(true);
+                setActivatedOnSleep(false);
+                setActivatedOnDock(false);
                 break;
 
             case NEVER:
@@ -407,6 +431,22 @@
         setBoolean(Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, value);
     }
 
+    public boolean isActivatedOnPostured() {
+        return allowDreamWhenPostured()
+                && getBoolean(Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED,
+                        mDreamsActivatedOnPosturedByDefault);
+    }
+
+    /**
+     * Sets whether dreams should be activated when the device is postured (stationary and upright)
+     */
+    public void setActivatedOnPostured(boolean value) {
+        if (allowDreamWhenPostured()) {
+            logd("setActivatedOnPostured(%s)", value);
+            setBoolean(Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, value);
+        }
+    }
+
     private boolean getBoolean(String key, boolean def) {
         return Settings.Secure.getInt(mContext.getContentResolver(), key, def ? 1 : 0) == 1;
     }
@@ -548,7 +588,9 @@
                 return WHEN_TO_DREAM_CHARGING;
             case WHILE_DOCKED:
                 return WHEN_TO_DREAM_DOCKED;
-            case EITHER:
+            case WHILE_POSTURED:
+                return WHEN_TO_DREAM_POSTURED;
+            case WHILE_CHARGING_OR_DOCKED:
                 return WHEN_TO_DREAM_CHARGING_OR_DOCKED;
             case NEVER:
             default:
diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java
index 7516d2e..e3d7902 100644
--- a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java
+++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java
@@ -22,6 +22,7 @@
 import static com.android.settingslib.enterprise.ManagedDeviceActionDisabledByAdminController.DEFAULT_FOREGROUND_USER_CHECKER;
 
 import android.app.admin.DevicePolicyManager;
+import android.app.supervision.SupervisionManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.hardware.biometrics.BiometricAuthenticator;
@@ -59,12 +60,18 @@
     }
 
     private static boolean isSupervisedDevice(Context context) {
-        DevicePolicyManager devicePolicyManager =
-                context.getSystemService(DevicePolicyManager.class);
-        ComponentName supervisionComponent =
-                devicePolicyManager.getProfileOwnerOrDeviceOwnerSupervisionComponent(
-                        new UserHandle(UserHandle.myUserId()));
-        return supervisionComponent != null;
+        if (android.app.supervision.flags.Flags.deprecateDpmSupervisionApis()) {
+            SupervisionManager supervisionManager =
+                    context.getSystemService(SupervisionManager.class);
+            return supervisionManager.isSupervisionEnabledForUser(UserHandle.myUserId());
+        } else {
+            DevicePolicyManager devicePolicyManager =
+                    context.getSystemService(DevicePolicyManager.class);
+            ComponentName supervisionComponent =
+                    devicePolicyManager.getProfileOwnerOrDeviceOwnerSupervisionComponent(
+                            new UserHandle(UserHandle.myUserId()));
+            return supervisionComponent != null;
+        }
     }
 
     /**
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 496c3e6..9aaefe47 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
@@ -37,7 +37,8 @@
     override val globalZenMode: StateFlow<Int>
         get() = mutableZenMode.asStateFlow()
 
-    private val mutableModesFlow: MutableStateFlow<List<ZenMode>> = MutableStateFlow(listOf())
+    private val mutableModesFlow: MutableStateFlow<List<ZenMode>> =
+        MutableStateFlow(listOf(TestModeBuilder.MANUAL_DND))
     override val modes: Flow<List<ZenMode>>
         get() = mutableModesFlow.asStateFlow()
 
@@ -65,8 +66,11 @@
         mutableModesFlow.value += mode
     }
 
-    fun addMode(id: String, @AutomaticZenRule.Type type: Int = AutomaticZenRule.TYPE_UNKNOWN,
-        active: Boolean = false) {
+    fun addMode(
+        id: String,
+        @AutomaticZenRule.Type type: Int = AutomaticZenRule.TYPE_UNKNOWN,
+        active: Boolean = false,
+    ) {
         mutableModesFlow.value += newMode(id, type, active)
     }
 
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 abc1638..64a2de5 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
@@ -31,7 +31,6 @@
 import android.service.notification.ZenPolicy;
 
 import androidx.annotation.DrawableRes;
-import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import java.util.Random;
@@ -44,22 +43,7 @@
     private boolean mIsManualDnd;
 
     public static final ZenMode EXAMPLE = new TestModeBuilder().build();
-
-    public static final ZenMode MANUAL_DND_ACTIVE = manualDnd(
-            INTERRUPTION_FILTER_PRIORITY, true);
-
-    public static final ZenMode MANUAL_DND_INACTIVE = manualDnd(
-            INTERRUPTION_FILTER_PRIORITY, false);
-
-    @NonNull
-    public static ZenMode manualDnd(@NotificationManager.InterruptionFilter int filter,
-            boolean isActive) {
-        return new TestModeBuilder()
-                .makeManualDnd()
-                .setInterruptionFilter(filter)
-                .setActive(isActive)
-                .build();
-    }
+    public static final ZenMode MANUAL_DND = new TestModeBuilder().makeManualDnd().build();
 
     public TestModeBuilder() {
         // Reasonable defaults
diff --git a/packages/SettingsLib/tests/robotests/Android.bp b/packages/SettingsLib/tests/robotests/Android.bp
index 81358ca..117ca85 100644
--- a/packages/SettingsLib/tests/robotests/Android.bp
+++ b/packages/SettingsLib/tests/robotests/Android.bp
@@ -65,7 +65,6 @@
     test_options: {
         timeout: 36000,
     },
-    upstream: true,
 
     strict_mode: false,
 }
@@ -100,10 +99,10 @@
     plugins: [
         "auto_value_plugin_1.9",
         "auto_value_builder_plugin_1.9",
-        "Robolectric_processor_upstream",
+        "Robolectric_processor",
     ],
     libs: [
-        "Robolectric_all-target_upstream",
+        "Robolectric_all-target",
         "mockito-robolectric-prebuilt",
         "truth",
     ],
diff --git a/packages/SettingsLib/tests/robotests/fragment/Android.bp b/packages/SettingsLib/tests/robotests/fragment/Android.bp
index 3e67156..0214874 100644
--- a/packages/SettingsLib/tests/robotests/fragment/Android.bp
+++ b/packages/SettingsLib/tests/robotests/fragment/Android.bp
@@ -28,13 +28,13 @@
         //"-J-verbose",
     ],
     libs: [
-        "Robolectric_all-target_upstream",
+        "Robolectric_all-target",
         "androidx.fragment_fragment",
     ],
     plugins: [
         "auto_value_plugin_1.9",
         "auto_value_builder_plugin_1.9",
-        "Robolectric_processor_upstream",
+        "Robolectric_processor",
     ],
 
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java
index 3b18aa3..4e821ca 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java
@@ -16,6 +16,7 @@
 
 package com.android.settingslib.applications;
 
+import static android.content.pm.Flags.FLAG_REMOVE_HIDDEN_MODULE_USAGE;
 import static android.content.pm.Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX;
 import static android.os.UserHandle.MU_ENABLED;
 import static android.os.UserHandle.USER_SYSTEM;
@@ -59,6 +60,8 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.text.TextUtils;
 import android.util.IconDrawableFactory;
@@ -204,6 +207,7 @@
             info.setPackageName(packageName);
             info.setApkInApexPackageNames(Collections.singletonList(apexPackageName));
             // will treat any app with package name that contains "hidden" as hidden module
+            // TODO(b/382016780): to be removed after flag cleanup.
             info.setHidden(!TextUtils.isEmpty(packageName) && packageName.contains("hidden"));
             return info;
         }
@@ -414,6 +418,7 @@
     }
 
     @Test
+    @DisableFlags({FLAG_REMOVE_HIDDEN_MODULE_USAGE})
     public void onResume_shouldNotIncludeSystemHiddenModule() {
         mSession.onResume();
 
@@ -424,6 +429,18 @@
     }
 
     @Test
+    @EnableFlags({FLAG_REMOVE_HIDDEN_MODULE_USAGE})
+    public void onResume_shouldIncludeSystemModule() {
+        mSession.onResume();
+
+        final List<ApplicationInfo> mApplications = mApplicationsState.mApplications;
+        assertThat(mApplications).hasSize(3);
+        assertThat(mApplications.get(0).packageName).isEqualTo("test.package.1");
+        assertThat(mApplications.get(1).packageName).isEqualTo("test.hidden.module.2");
+        assertThat(mApplications.get(2).packageName).isEqualTo("test.package.3");
+    }
+
+    @Test
     public void removeAndInstall_noWorkprofile_doResumeIfNeededLocked_shouldClearEntries()
             throws RemoteException {
         // scenario: only owner user
@@ -832,6 +849,7 @@
         mApplicationsState.mEntriesMap.clear();
         ApplicationInfo appInfo = createApplicationInfo(PKG_1, /* uid= */ 0);
         mApplicationsState.mApplications.add(appInfo);
+        // TODO(b/382016780): to be removed after flag cleanup.
         mApplicationsState.mSystemModules.put(PKG_1, /* value= */ false);
 
         assertThat(mApplicationsState.getEntry(PKG_1, /* userId= */ 0).info.packageName)
@@ -839,6 +857,7 @@
     }
 
     @Test
+    @DisableFlags({FLAG_REMOVE_HIDDEN_MODULE_USAGE})
     public void isHiddenModule_hasApkInApexInfo_shouldSupportHiddenApexPackage() {
         mSetFlagsRule.enableFlags(FLAG_PROVIDE_INFO_OF_APK_IN_APEX);
         ApplicationsState.sInstance = null;
@@ -853,6 +872,7 @@
     }
 
     @Test
+    @DisableFlags({FLAG_REMOVE_HIDDEN_MODULE_USAGE})
     public void isHiddenModule_noApkInApexInfo_onlySupportHiddenModule() {
         mSetFlagsRule.disableFlags(FLAG_PROVIDE_INFO_OF_APK_IN_APEX);
         ApplicationsState.sInstance = null;
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
index d49447f..cafe19f 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
@@ -80,7 +80,9 @@
     @Mock(answer = Answers.RETURNS_DEEP_STUBS)
     private CachedBluetoothDevice mCachedBluetoothDevice;
 
-    @Mock private BluetoothDevice mBluetoothDevice;
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private BluetoothDevice mBluetoothDevice;
+
     @Mock private AudioManager mAudioManager;
     @Mock private PackageManager mPackageManager;
     @Mock private LeAudioProfile mA2dpProfile;
@@ -399,6 +401,38 @@
     }
 
     @Test
+    public void isHeadset_metadataMatched_returnTrue() {
+        when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_DEVICE_TYPE))
+                .thenReturn(BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET.getBytes());
+
+        assertThat(BluetoothUtils.isHeadset(mBluetoothDevice)).isTrue();
+    }
+
+    @Test
+    public void isHeadset_metadataNotMatched_returnFalse() {
+        when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_DEVICE_TYPE))
+                .thenReturn(BluetoothDevice.DEVICE_TYPE_CARKIT.getBytes());
+
+        assertThat(BluetoothUtils.isHeadset(mBluetoothDevice)).isFalse();
+    }
+
+    @Test
+    public void isHeadset_btClassMatched_returnTrue() {
+        when(mBluetoothDevice.getBluetoothClass().getDeviceClass())
+                .thenReturn(BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES);
+
+        assertThat(BluetoothUtils.isHeadset(mBluetoothDevice)).isTrue();
+    }
+
+    @Test
+    public void isHeadset_btClassNotMatched_returnFalse() {
+        when(mBluetoothDevice.getBluetoothClass().getDeviceClass())
+                .thenReturn(BluetoothClass.Device.AUDIO_VIDEO_LOUDSPEAKER);
+
+        assertThat(BluetoothUtils.isHeadset(mBluetoothDevice)).isFalse();
+    }
+
+    @Test
     public void isAvailableMediaBluetoothDevice_isConnectedLeAudioDevice_returnTrue() {
         when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true);
         when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
index 21dde1f..a215464 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
@@ -50,6 +50,9 @@
 import android.media.AudioManager;
 import android.media.audiopolicy.AudioProductStrategy;
 import android.os.Parcel;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.util.FeatureFlagUtils;
 
 import androidx.test.core.app.ApplicationProvider;
@@ -72,6 +75,8 @@
 public class HearingAidDeviceManagerTest {
     @Rule
     public MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
 
     private static final long HISYNCID1 = 10;
     private static final long HISYNCID2 = 11;
@@ -736,6 +741,7 @@
 
     @Test
     public void onActiveDeviceChanged_connected_callSetStrategies() {
+        when(mCachedDevice1.isConnectedHearingAidDevice()).thenReturn(true);
         when(mHelper.getMatchedHearingDeviceAttributesForOutput(mCachedDevice1)).thenReturn(
                 mHearingDeviceAttribute);
         when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(true);
@@ -750,6 +756,7 @@
 
     @Test
     public void onActiveDeviceChanged_disconnected_callSetStrategiesWithAutoValue() {
+        when(mCachedDevice1.isConnectedHearingAidDevice()).thenReturn(false);
         when(mHelper.getMatchedHearingDeviceAttributesForOutput(mCachedDevice1)).thenReturn(
                 mHearingDeviceAttribute);
         when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(false);
@@ -952,6 +959,38 @@
                 ConnectionStatus.CONNECTED);
     }
 
+    @Test
+    @RequiresFlagsEnabled(
+            com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICES_INPUT_ROUTING_CONTROL)
+    public void onActiveDeviceChanged_activeHearingAidProfile_callSetInputDeviceForCalls() {
+        when(mCachedDevice1.isConnectedHearingAidDevice()).thenReturn(true);
+        when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(true);
+        when(mDevice1.isMicrophonePreferredForCalls()).thenReturn(true);
+        doReturn(true).when(mHelper).setPreferredDeviceRoutingStrategies(anyList(), any(),
+                anyInt());
+
+        mHearingAidDeviceManager.onActiveDeviceChanged(mCachedDevice1);
+
+        verify(mHelper).setPreferredInputDeviceForCalls(
+                eq(mCachedDevice1), eq(HearingAidAudioRoutingConstants.RoutingValue.AUTO));
+
+    }
+
+    @Test
+    @RequiresFlagsEnabled(
+            com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICES_INPUT_ROUTING_CONTROL)
+    public void onActiveDeviceChanged_notActiveHearingAidProfile_callClearInputDeviceForCalls() {
+        when(mCachedDevice1.isConnectedHearingAidDevice()).thenReturn(true);
+        when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(false);
+        when(mDevice1.isMicrophonePreferredForCalls()).thenReturn(true);
+        doReturn(true).when(mHelper).setPreferredDeviceRoutingStrategies(anyList(), any(),
+                anyInt());
+
+        mHearingAidDeviceManager.onActiveDeviceChanged(mCachedDevice1);
+
+        verify(mHelper).clearPreferredInputDeviceForCalls();
+    }
+
     private HearingAidInfo getLeftAshaHearingAidInfo(long hiSyncId) {
         return new HearingAidInfo.Builder()
                 .setAshaDeviceSide(HearingAidInfo.DeviceSide.SIDE_LEFT)
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
index d08d91d..6b30f15 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
@@ -87,7 +87,7 @@
 
     @Test
     public void testBasicMethods_manualDnd() {
-        ZenMode manualMode = TestModeBuilder.MANUAL_DND_INACTIVE;
+        ZenMode manualMode = TestModeBuilder.MANUAL_DND;
 
         assertThat(manualMode.getId()).isEqualTo(ZenMode.MANUAL_DND_MODE_ID);
         assertThat(manualMode.isManualDnd()).isTrue();
@@ -271,7 +271,7 @@
 
     @Test
     public void setInterruptionFilter_manualDnd_throws() {
-        ZenMode manualDnd = TestModeBuilder.MANUAL_DND_INACTIVE;
+        ZenMode manualDnd = TestModeBuilder.MANUAL_DND;
 
         assertThrows(IllegalStateException.class,
                 () -> manualDnd.setInterruptionFilter(INTERRUPTION_FILTER_ALL));
@@ -280,24 +280,46 @@
     @Test
     public void canEditPolicy_onlyFalseForSpecialDnd() {
         assertThat(TestModeBuilder.EXAMPLE.canEditPolicy()).isTrue();
-        assertThat(TestModeBuilder.MANUAL_DND_ACTIVE.canEditPolicy()).isTrue();
-        assertThat(TestModeBuilder.MANUAL_DND_INACTIVE.canEditPolicy()).isTrue();
 
-        ZenMode dndWithAlarms = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_ALARMS, true);
+        ZenMode inactiveDnd = new TestModeBuilder().makeManualDnd().setActive(false).build();
+        assertThat(inactiveDnd.canEditPolicy()).isTrue();
+
+        ZenMode activeDnd = new TestModeBuilder().makeManualDnd().setActive(true).build();
+        assertThat(activeDnd.canEditPolicy()).isTrue();
+
+        ZenMode dndWithAlarms = new TestModeBuilder()
+                .makeManualDnd()
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+                .setActive(true)
+                .build();
         assertThat(dndWithAlarms.canEditPolicy()).isFalse();
-        ZenMode dndWithNone = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_NONE, true);
+
+        ZenMode dndWithNone = new TestModeBuilder()
+                .makeManualDnd()
+                .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
+                .setActive(true)
+                .build();
         assertThat(dndWithNone.canEditPolicy()).isFalse();
 
         // Note: Backend will never return an inactive manual mode with custom filter.
-        ZenMode badDndWithAlarms = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_ALARMS, false);
+        ZenMode badDndWithAlarms = new TestModeBuilder()
+                .makeManualDnd()
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+                .setActive(false)
+                .build();
         assertThat(badDndWithAlarms.canEditPolicy()).isFalse();
-        ZenMode badDndWithNone = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_NONE, false);
+
+        ZenMode badDndWithNone = new TestModeBuilder()
+                .makeManualDnd()
+                .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
+                .setActive(false)
+                .build();
         assertThat(badDndWithNone.canEditPolicy()).isFalse();
     }
 
     @Test
     public void canEditPolicy_whenTrue_allowsSettingPolicyAndEffects() {
-        ZenMode normalDnd = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_PRIORITY, true);
+        ZenMode normalDnd = new TestModeBuilder().makeManualDnd().setActive(true).build();
 
         assertThat(normalDnd.canEditPolicy()).isTrue();
 
@@ -313,7 +335,11 @@
 
     @Test
     public void canEditPolicy_whenFalse_preventsSettingFilterPolicyOrEffects() {
-        ZenMode specialDnd = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_ALARMS, true);
+        ZenMode specialDnd = new TestModeBuilder()
+                .makeManualDnd()
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+                .setActive(true)
+                .build();
 
         assertThat(specialDnd.canEditPolicy()).isFalse();
         assertThrows(IllegalStateException.class,
@@ -324,7 +350,7 @@
 
     @Test
     public void comparator_prioritizes() {
-        ZenMode manualDnd = TestModeBuilder.MANUAL_DND_INACTIVE;
+        ZenMode manualDnd = TestModeBuilder.MANUAL_DND;
         ZenMode driving1 = new TestModeBuilder().setName("b1").setType(TYPE_DRIVING).build();
         ZenMode driving2 = new TestModeBuilder().setName("b2").setType(TYPE_DRIVING).build();
         ZenMode bedtime1 = new TestModeBuilder().setName("c1").setType(TYPE_BEDTIME).build();
@@ -403,7 +429,7 @@
 
     @Test
     public void getIconKey_manualDnd_isDndIcon() {
-        ZenIcon.Key iconKey = TestModeBuilder.MANUAL_DND_INACTIVE.getIconKey();
+        ZenIcon.Key iconKey = TestModeBuilder.MANUAL_DND.getIconKey();
 
         assertThat(iconKey.resPackage()).isNull();
         assertThat(iconKey.resId()).isEqualTo(
diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml
index 5ddf005..dafcc72 100644
--- a/packages/SettingsProvider/res/values/defaults.xml
+++ b/packages/SettingsProvider/res/values/defaults.xml
@@ -322,9 +322,6 @@
     <!-- Whether vibrate icon is shown in the status bar by default. -->
     <integer name="def_statusBarVibrateIconEnabled">0</integer>
 
-    <!-- Whether predictive back animation is enabled by default. -->
-    <bool name="def_enable_back_animation">false</bool>
-
     <!-- Whether wifi is always requested by default. -->
     <bool name="def_enable_wifi_always_requested">false</bool>
 
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
index 4125a81f..fc61b1e 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
@@ -46,6 +46,7 @@
         Settings.Global.APP_AUTO_RESTRICTION_ENABLED,
         Settings.Global.AUTO_TIME,
         Settings.Global.AUTO_TIME_ZONE,
+        Settings.Global.TIME_ZONE_NOTIFICATIONS,
         Settings.Global.POWER_SOUNDS_ENABLED,
         Settings.Global.DOCK_SOUNDS_ENABLED,
         Settings.Global.CHARGING_SOUNDS_ENABLED,
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index b9f8c71..7b4a2ca 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -91,6 +91,7 @@
         Settings.Secure.KEY_REPEAT_TIMEOUT_MS,
         Settings.Secure.KEY_REPEAT_DELAY_MS,
         Settings.Secure.CAMERA_GESTURE_DISABLED,
+        Settings.Secure.ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE,
         Settings.Secure.ACCESSIBILITY_AUTOCLICK_ENABLED,
         Settings.Secure.ACCESSIBILITY_AUTOCLICK_DELAY,
         Settings.Secure.ACCESSIBILITY_LARGE_POINTER_ICON,
@@ -154,6 +155,7 @@
         Settings.Secure.SCREENSAVER_COMPONENTS,
         Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK,
         Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP,
+        Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED,
         Settings.Secure.SCREENSAVER_HOME_CONTROLS_ENABLED,
         Settings.Secure.SHOW_FIRST_CRASH_DIALOG_DEV_OPTION,
         Settings.Secure.VOLUME_DIALOG_DISMISS_TIMEOUT,
@@ -186,11 +188,6 @@
         Settings.Secure.BACK_GESTURE_INSET_SCALE_LEFT,
         Settings.Secure.BACK_GESTURE_INSET_SCALE_RIGHT,
         Settings.Secure.NAVIGATION_MODE,
-        Settings.Secure.TRACKPAD_GESTURE_BACK_ENABLED,
-        Settings.Secure.TRACKPAD_GESTURE_HOME_ENABLED,
-        Settings.Secure.TRACKPAD_GESTURE_OVERVIEW_ENABLED,
-        Settings.Secure.TRACKPAD_GESTURE_NOTIFICATION_ENABLED,
-        Settings.Secure.TRACKPAD_GESTURE_QUICK_SWITCH_ENABLED,
         Settings.Secure.SKIP_GESTURE_COUNT,
         Settings.Secure.SKIP_TOUCH_COUNT,
         Settings.Secure.SILENCE_ALARMS_GESTURE_COUNT,
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
index 5b4ee8b..1f56f10 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
@@ -109,6 +109,7 @@
                 Settings.System.LOCALE_PREFERENCES,
                 Settings.System.MOUSE_REVERSE_VERTICAL_SCROLLING,
                 Settings.System.MOUSE_SCROLLING_ACCELERATION,
+                Settings.System.MOUSE_SCROLLING_SPEED,
                 Settings.System.MOUSE_SWAP_PRIMARY_BUTTON,
                 Settings.System.MOUSE_POINTER_ACCELERATION_ENABLED,
                 Settings.System.TOUCHPAD_POINTER_SPEED,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index 32d4580..c0e266f 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
@@ -102,6 +102,7 @@
                 });
         VALIDATORS.put(Global.AUTO_TIME, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.AUTO_TIME_ZONE, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(Global.TIME_ZONE_NOTIFICATIONS, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.POWER_SOUNDS_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.DOCK_SOUNDS_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.CHARGING_SOUNDS_ENABLED, BOOLEAN_VALIDATOR);
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 7c5e577..b0309a8f 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -140,6 +140,8 @@
         VALIDATORS.put(Secure.KEY_REPEAT_TIMEOUT_MS, NON_NEGATIVE_INTEGER_VALIDATOR);
         VALIDATORS.put(Secure.KEY_REPEAT_DELAY_MS, NON_NEGATIVE_INTEGER_VALIDATOR);
         VALIDATORS.put(Secure.CAMERA_GESTURE_DISABLED, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(
+                Secure.ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE, NON_NEGATIVE_INTEGER_VALIDATOR);
         VALIDATORS.put(Secure.ACCESSIBILITY_AUTOCLICK_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.ACCESSIBILITY_AUTOCLICK_DELAY, NON_NEGATIVE_INTEGER_VALIDATOR);
         VALIDATORS.put(Secure.ACCESSIBILITY_LARGE_POINTER_ICON, BOOLEAN_VALIDATOR);
@@ -228,6 +230,7 @@
         VALIDATORS.put(Secure.SCREENSAVER_COMPONENTS, COMMA_SEPARATED_COMPONENT_LIST_VALIDATOR);
         VALIDATORS.put(Secure.SCREENSAVER_ACTIVATE_ON_DOCK, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.SCREENSAVER_HOME_CONTROLS_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.SHOW_FIRST_CRASH_DIALOG_DEV_OPTION, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.VOLUME_DIALOG_DISMISS_TIMEOUT, NON_NEGATIVE_INTEGER_VALIDATOR);
@@ -284,11 +287,6 @@
                 new InclusiveFloatRangeValidator(0.0f, Float.MAX_VALUE));
         VALIDATORS.put(Secure.BACK_GESTURE_INSET_SCALE_RIGHT,
                 new InclusiveFloatRangeValidator(0.0f, Float.MAX_VALUE));
-        VALIDATORS.put(Secure.TRACKPAD_GESTURE_BACK_ENABLED, BOOLEAN_VALIDATOR);
-        VALIDATORS.put(Secure.TRACKPAD_GESTURE_HOME_ENABLED, BOOLEAN_VALIDATOR);
-        VALIDATORS.put(Secure.TRACKPAD_GESTURE_OVERVIEW_ENABLED, BOOLEAN_VALIDATOR);
-        VALIDATORS.put(Secure.TRACKPAD_GESTURE_NOTIFICATION_ENABLED, BOOLEAN_VALIDATOR);
-        VALIDATORS.put(Secure.TRACKPAD_GESTURE_QUICK_SWITCH_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.AWARE_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.SKIP_GESTURE_COUNT, NON_NEGATIVE_INTEGER_VALIDATOR);
         VALIDATORS.put(Secure.SKIP_TOUCH_COUNT, NON_NEGATIVE_INTEGER_VALIDATOR);
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
index 0432eea..4d98a11 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
@@ -227,6 +227,7 @@
         VALIDATORS.put(System.MOUSE_SWAP_PRIMARY_BUTTON, BOOLEAN_VALIDATOR);
         VALIDATORS.put(System.MOUSE_SCROLLING_ACCELERATION, BOOLEAN_VALIDATOR);
         VALIDATORS.put(System.MOUSE_POINTER_ACCELERATION_ENABLED, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(System.MOUSE_SCROLLING_SPEED, new InclusiveIntegerRangeValidator(-7, 7));
         VALIDATORS.put(System.TOUCHPAD_POINTER_SPEED, new InclusiveIntegerRangeValidator(-7, 7));
         VALIDATORS.put(System.TOUCHPAD_NATURAL_SCROLLING, BOOLEAN_VALIDATOR);
         VALIDATORS.put(System.TOUCHPAD_TAP_TO_CLICK, BOOLEAN_VALIDATOR);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
index a2cc008..c1c3e04 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
@@ -193,6 +193,7 @@
             "power_button_instantly_locks";
     private static final String KEY_LOCK_SETTINGS_PIN_ENHANCED_PRIVACY =
             "pin_enhanced_privacy";
+    private static final int NUM_LOCK_SETTINGS = 5;
 
     // Error messages for logging metrics.
     private static final String ERROR_COULD_NOT_READ_FROM_CURSOR =
@@ -208,6 +209,11 @@
     private static final String ERROR_SKIPPED_DUE_TO_LARGE_SCREEN =
         "skipped_due_to_large_screen";
     private static final String ERROR_DID_NOT_PASS_VALIDATION = "did_not_pass_validation";
+    private static final String ERROR_IO_EXCEPTION = "io_exception";
+    private static final String ERROR_FAILED_TO_RESTORE_SOFTAP_CONFIG =
+        "failed_to_restore_softap_config";
+    private static final String ERROR_FAILED_TO_RESTORE_WIFI_CONFIG =
+        "failed_to_restore_wifi_config";
 
 
     // Name of the temporary file we use during full backup/restore.  This is
@@ -794,29 +800,44 @@
 
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
         DataOutputStream out = new DataOutputStream(baos);
+        int backedUpSettingsCount = 0;
         try {
             out.writeUTF(KEY_LOCK_SETTINGS_OWNER_INFO_ENABLED);
             out.writeUTF(ownerInfoEnabled ? "1" : "0");
+            backedUpSettingsCount++;
             if (ownerInfo != null) {
                 out.writeUTF(KEY_LOCK_SETTINGS_OWNER_INFO);
                 out.writeUTF(ownerInfo != null ? ownerInfo : "");
+                backedUpSettingsCount++;
             }
             if (lockPatternUtils.isVisiblePatternEverChosen(userId)) {
                 out.writeUTF(KEY_LOCK_SETTINGS_VISIBLE_PATTERN_ENABLED);
                 out.writeUTF(visiblePatternEnabled ? "1" : "0");
+                backedUpSettingsCount++;
             }
             if (lockPatternUtils.isPowerButtonInstantlyLocksEverChosen(userId)) {
                 out.writeUTF(KEY_LOCK_SETTINGS_POWER_BUTTON_INSTANTLY_LOCKS);
                 out.writeUTF(powerButtonInstantlyLocks ? "1" : "0");
+                backedUpSettingsCount++;
             }
             if (lockPatternUtils.isPinEnhancedPrivacyEverChosen(userId)) {
                 out.writeUTF(KEY_LOCK_SETTINGS_PIN_ENHANCED_PRIVACY);
                 out.writeUTF(lockPatternUtils.isPinEnhancedPrivacyEnabled(userId) ? "1" : "0");
+                backedUpSettingsCount++;
             }
             // End marker
             out.writeUTF("");
             out.flush();
+            if (areAgentMetricsEnabled) {
+                numberOfSettingsPerKey.put(KEY_LOCK_SETTINGS, backedUpSettingsCount);
+            }
         } catch (IOException ioe) {
+            if (areAgentMetricsEnabled) {
+                mBackupRestoreEventLogger.logItemsBackupFailed(
+                    KEY_LOCK_SETTINGS,
+                    NUM_LOCK_SETTINGS - backedUpSettingsCount,
+                    ERROR_IO_EXCEPTION);
+            }
         }
         return baos.toByteArray();
     }
@@ -1162,6 +1183,7 @@
 
         ByteArrayInputStream bais = new ByteArrayInputStream(buffer, 0, nBytes);
         DataInputStream in = new DataInputStream(bais);
+        int restoredLockSettingsCount = 0;
         try {
             String key;
             // Read until empty string marker
@@ -1187,9 +1209,20 @@
                         lockPatternUtils.setPinEnhancedPrivacyEnabled("1".equals(value), userId);
                         break;
                 }
+                if (areAgentMetricsEnabled) {
+                    mBackupRestoreEventLogger.logItemsRestored(KEY_LOCK_SETTINGS, /* count= */ 1);
+                    restoredLockSettingsCount++;
+                }
+
             }
             in.close();
         } catch (IOException ioe) {
+            if (areAgentMetricsEnabled) {
+                mBackupRestoreEventLogger.logItemsRestoreFailed(
+                        KEY_LOCK_SETTINGS,
+                        NUM_LOCK_SETTINGS - restoredLockSettingsCount,
+                        ERROR_IO_EXCEPTION);
+            }
         }
     }
 
@@ -1309,12 +1342,31 @@
         mWifiManager.restoreSupplicantBackupData(supplicant_bytes, ipconfig_bytes);
     }
 
-    private byte[] getSoftAPConfiguration() {
-        return mWifiManager.retrieveSoftApBackupData();
+    @VisibleForTesting
+    byte[] getSoftAPConfiguration() {
+        byte[] data = mWifiManager.retrieveSoftApBackupData();
+        if (areAgentMetricsEnabled) {
+            // We're unable to determine how many settings this includes, so we'll just log 1.
+            numberOfSettingsPerKey.put(KEY_SOFTAP_CONFIG, 1);
+        }
+        return data;
     }
 
-    private void restoreSoftApConfiguration(byte[] data) {
-        SoftApConfiguration configInCloud = mWifiManager.restoreSoftApBackupData(data);
+    @VisibleForTesting
+    void restoreSoftApConfiguration(byte[] data) {
+        SoftApConfiguration configInCloud;
+        if (areAgentMetricsEnabled) {
+            try {
+                configInCloud = mWifiManager.restoreSoftApBackupData(data);
+                mBackupRestoreEventLogger.logItemsRestored(KEY_SOFTAP_CONFIG, /* count= */ 1);
+            } catch (Exception e) {
+                configInCloud = null;
+                mBackupRestoreEventLogger.logItemsRestoreFailed(
+                    KEY_SOFTAP_CONFIG, /* count= */ 1, ERROR_FAILED_TO_RESTORE_SOFTAP_CONFIG);
+            }
+        } else {
+            configInCloud = mWifiManager.restoreSoftApBackupData(data);
+        }
         if (configInCloud != null) {
             if (DEBUG) Log.d(TAG, "Successfully unMarshaled SoftApConfiguration ");
             // Depending on device hardware, we may need to notify the user of a setting change
@@ -1405,8 +1457,14 @@
         return baos.toByteArray();
     }
 
-    private byte[] getNewWifiConfigData() {
-        return mWifiManager.retrieveBackupData();
+    @VisibleForTesting
+    byte[] getNewWifiConfigData() {
+        byte[] data = mWifiManager.retrieveBackupData();
+        if (areAgentMetricsEnabled) {
+            // We're unable to determine how many settings this includes, so we'll just log 1.
+            numberOfSettingsPerKey.put(KEY_WIFI_NEW_CONFIG, 1);
+        }
+        return data;
     }
 
     private byte[] getLocaleSettings() {
@@ -1418,11 +1476,22 @@
         return localeList.toLanguageTags().getBytes();
     }
 
-    private void restoreNewWifiConfigData(byte[] bytes) {
+    @VisibleForTesting
+    void restoreNewWifiConfigData(byte[] bytes) {
         if (DEBUG_BACKUP) {
             Log.v(TAG, "Applying restored wifi data");
         }
-        mWifiManager.restoreBackupData(bytes);
+        if (areAgentMetricsEnabled) {
+            try {
+                mWifiManager.restoreBackupData(bytes);
+                mBackupRestoreEventLogger.logItemsRestored(KEY_WIFI_NEW_CONFIG, /* count= */ 1);
+            } catch (Exception e) {
+                mBackupRestoreEventLogger.logItemsRestoreFailed(
+                    KEY_WIFI_NEW_CONFIG, /* count= */ 1, ERROR_FAILED_TO_RESTORE_WIFI_CONFIG);
+            }
+        } else {
+            mWifiManager.restoreBackupData(bytes);
+        }
     }
 
     private void restoreNetworkPolicies(byte[] data) {
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index dedd7eb..1c6d681 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -1715,6 +1715,9 @@
                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
                 SecureSettingsProto.Accessibility.ENABLED_ACCESSIBILITY_SERVICES);
         dumpSetting(s, p,
+                Settings.Secure.ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE,
+                SecureSettingsProto.Accessibility.AUTOCLICK_CURSOR_AREA_SIZE);
+        dumpSetting(s, p,
                 Settings.Secure.ACCESSIBILITY_AUTOCLICK_ENABLED,
                 SecureSettingsProto.Accessibility.AUTOCLICK_ENABLED);
         dumpSetting(s, p,
@@ -2547,6 +2550,9 @@
         dumpSetting(s, p,
                 Settings.Secure.SCREENSAVER_DEFAULT_COMPONENT,
                 SecureSettingsProto.Screensaver.DEFAULT_COMPONENT);
+        dumpSetting(s, p,
+                Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED,
+                SecureSettingsProto.Screensaver.ACTIVATE_ON_POSTURED);
         p.end(screensaverToken);
 
         final long searchToken = p.start(SecureSettingsProto.SEARCH);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index ed19351..cb656bd 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -6122,17 +6122,7 @@
                 }
 
                 if (currentVersion == 220) {
-                    final SettingsState globalSettings = getGlobalSettingsLocked();
-                    final Setting enableBackAnimation =
-                            globalSettings.getSettingLocked(Global.ENABLE_BACK_ANIMATION);
-                    if (enableBackAnimation.isNull()) {
-                        final boolean defEnableBackAnimation =
-                                getContext()
-                                        .getResources()
-                                        .getBoolean(R.bool.def_enable_back_animation);
-                        initGlobalSettingsDefaultValLocked(
-                                Settings.Global.ENABLE_BACK_ANIMATION, defEnableBackAnimation);
-                    }
+                    // Version 221: Removed
                     currentVersion = 221;
                 }
 
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index c88a7fd..cbdb36f 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -564,7 +564,6 @@
                     Settings.Global.WATCHDOG_TIMEOUT_MILLIS,
                     Settings.Global.MANAGED_PROVISIONING_DEFER_PROVISIONING_TO_ROLE_HOLDER,
                     Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
-                    Settings.Global.ENABLE_BACK_ANIMATION, // Temporary for T, dev option only
                     Settings.Global.HEARING_DEVICE_LOCAL_AMBIENT_VOLUME, // cache per hearing device
                     Settings.Global.HEARING_DEVICE_LOCAL_NOTIFICATION, // cache per hearing device
                     Settings.Global.Wearable.COMBINED_LOCATION_ENABLE,
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java
index 18c43a7..6e5b602c 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java
@@ -16,6 +16,9 @@
 
 package com.android.providers.settings;
 
+import static com.android.providers.settings.SettingsBackupRestoreKeys.KEY_WIFI_NEW_CONFIG;
+import static com.android.providers.settings.SettingsBackupRestoreKeys.KEY_SOFTAP_CONFIG;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertNotNull;
 import static junit.framework.Assert.assertNull;
@@ -26,8 +29,11 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.when;
 
+import android.annotation.Nullable;
 import android.app.backup.BackupAnnotations.BackupDestination;
 import android.app.backup.BackupAnnotations.OperationType;
 import android.app.backup.BackupDataInput;
@@ -42,6 +48,8 @@
 import android.database.Cursor;
 import android.database.MatrixCursor;
 import android.net.Uri;
+import android.net.wifi.SoftApConfiguration;
+import android.net.wifi.WifiManager;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.UserHandle;
@@ -64,6 +72,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
@@ -126,6 +135,7 @@
 
     @Mock private BackupDataInput mBackupDataInput;
     @Mock private BackupDataOutput mBackupDataOutput;
+    @Mock private static WifiManager mWifiManager;
 
     private TestFriendlySettingsBackupAgent mAgentUnderTest;
     private Context mContext;
@@ -754,6 +764,148 @@
         assertNull(getLoggingResultForDatatype(TEST_KEY, mAgentUnderTest));
     }
 
+    @Test
+    @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+    public void getSoftAPConfiguration_flagIsEnabled_numberOfSettingsInKeyAreRecorded() {
+        mAgentUnderTest.onCreate(
+            UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.BACKUP);
+        when(mWifiManager.retrieveSoftApBackupData()).thenReturn(null);
+
+        mAgentUnderTest.getSoftAPConfiguration();
+
+        assertEquals(mAgentUnderTest.getNumberOfSettingsPerKey(KEY_SOFTAP_CONFIG), 1);
+    }
+
+    @Test
+    @DisableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+    public void getSoftAPConfiguration_flagIsNotEnabled_numberOfSettingsInKeyAreNotRecorded() {
+        mAgentUnderTest.onCreate(
+            UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.BACKUP);
+        when(mWifiManager.retrieveSoftApBackupData()).thenReturn(null);
+
+        mAgentUnderTest.getSoftAPConfiguration();
+
+        assertEquals(mAgentUnderTest.getNumberOfSettingsPerKey(KEY_SOFTAP_CONFIG), 0);
+    }
+
+    @Test
+    @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+    public void
+        restoreSoftApConfiguration_flagIsEnabled_restoreIsSuccessful_successMetricsAreLogged() {
+        mAgentUnderTest.onCreate(
+            UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+        SoftApConfiguration config = new SoftApConfiguration.Builder().setSsid("test").build();
+        byte[] data = config.toString().getBytes();
+        when(mWifiManager.restoreSoftApBackupData(any())).thenReturn(null);
+
+        mAgentUnderTest.restoreSoftApConfiguration(data);
+
+        DataTypeResult loggingResult =
+            getLoggingResultForDatatype(KEY_SOFTAP_CONFIG, mAgentUnderTest);
+        assertNotNull(loggingResult);
+        assertEquals(loggingResult.getSuccessCount(), 1);
+    }
+
+    @Test
+    @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+    public void
+        restoreSoftApConfiguration_flagIsEnabled_restoreIsNotSuccessful_failureMetricsAreLogged() {
+        mAgentUnderTest.onCreate(
+            UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+        SoftApConfiguration config = new SoftApConfiguration.Builder().setSsid("test").build();
+        byte[] data = config.toString().getBytes();
+        when(mWifiManager.restoreSoftApBackupData(any())).thenThrow(new RuntimeException());
+
+        mAgentUnderTest.restoreSoftApConfiguration(data);
+
+        DataTypeResult loggingResult =
+            getLoggingResultForDatatype(KEY_SOFTAP_CONFIG, mAgentUnderTest);
+        assertNotNull(loggingResult);
+        assertEquals(loggingResult.getFailCount(), 1);
+    }
+
+    @Test
+    @DisableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+    public void restoreSoftApConfiguration_flagIsNotEnabled_metricsAreNotLogged() {
+        mAgentUnderTest.onCreate(
+            UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+        SoftApConfiguration config = new SoftApConfiguration.Builder().setSsid("test").build();
+        byte[] data = config.toString().getBytes();
+        when(mWifiManager.restoreSoftApBackupData(any())).thenReturn(null);
+
+        mAgentUnderTest.restoreSoftApConfiguration(data);
+
+        assertNull(getLoggingResultForDatatype(KEY_SOFTAP_CONFIG, mAgentUnderTest));
+    }
+
+    @Test
+    @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+    public void getNewWifiConfigData_flagIsEnabled_numberOfSettingsInKeyAreRecorded() {
+        mAgentUnderTest.onCreate(
+            UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.BACKUP);
+        when(mWifiManager.retrieveBackupData()).thenReturn(null);
+
+        mAgentUnderTest.getNewWifiConfigData();
+
+        assertEquals(mAgentUnderTest.getNumberOfSettingsPerKey(KEY_WIFI_NEW_CONFIG), 1);
+    }
+
+    @Test
+    @DisableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+    public void getNewWifiConfigData_flagIsNotEnabled_numberOfSettingsInKeyAreNotRecorded() {
+        mAgentUnderTest.onCreate(
+            UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.BACKUP);
+        when(mWifiManager.retrieveBackupData()).thenReturn(null);
+
+        mAgentUnderTest.getNewWifiConfigData();
+
+        assertEquals(mAgentUnderTest.getNumberOfSettingsPerKey(KEY_WIFI_NEW_CONFIG), 0);
+    }
+
+    @Test
+    @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+    public void
+        restoreNewWifiConfigData_flagIsEnabled_restoreIsSuccessful_successMetricsAreLogged() {
+        mAgentUnderTest.onCreate(
+            UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+        doNothing().when(mWifiManager).restoreBackupData(any());
+
+        mAgentUnderTest.restoreNewWifiConfigData(new byte[] {});
+
+        DataTypeResult loggingResult =
+            getLoggingResultForDatatype(KEY_WIFI_NEW_CONFIG, mAgentUnderTest);
+        assertNotNull(loggingResult);
+        assertEquals(loggingResult.getSuccessCount(), 1);
+    }
+
+    @Test
+    @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+    public void
+        restoreNewWifiConfigData_flagIsEnabled_restoreIsNotSuccessful_failureMetricsAreLogged() {
+        mAgentUnderTest.onCreate(
+            UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+        doThrow(new RuntimeException()).when(mWifiManager).restoreBackupData(any());
+
+        mAgentUnderTest.restoreNewWifiConfigData(new byte[] {});
+
+        DataTypeResult loggingResult =
+            getLoggingResultForDatatype(KEY_WIFI_NEW_CONFIG, mAgentUnderTest);
+        assertNotNull(loggingResult);
+        assertEquals(loggingResult.getFailCount(), 1);
+    }
+
+    @Test
+    @DisableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS)
+    public void restoreNewWifiConfigData_flagIsNotEnabled_metricsAreNotLogged() {
+        mAgentUnderTest.onCreate(
+            UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE);
+        doNothing().when(mWifiManager).restoreBackupData(any());
+
+        mAgentUnderTest.restoreNewWifiConfigData(new byte[] {});
+
+        assertNull(getLoggingResultForDatatype(KEY_WIFI_NEW_CONFIG, mAgentUnderTest));
+    }
+
     private byte[] generateBackupData(Map<String, String> keyValueData) {
         int totalBytes = 0;
         for (String key : keyValueData.keySet()) {
@@ -890,6 +1042,13 @@
                 this.numberOfSettingsPerKey.put(key, numberOfSettings);
             }
         }
+
+        int getNumberOfSettingsPerKey(String key) {
+            if (numberOfSettingsPerKey == null || !numberOfSettingsPerKey.containsKey(key)) {
+                return 0;
+            }
+            return numberOfSettingsPerKey.get(key);
+        }
     }
 
     /** The TestSettingsHelper tracks which values have been backed up and/or restored. */
@@ -944,6 +1103,14 @@
         public ContentResolver getContentResolver() {
             return mContentResolver;
         }
+
+        @Override
+        public Object getSystemService(String name) {
+            if (name.equals(Context.WIFI_SERVICE)) {
+                return mWifiManager;
+            }
+            return super.getSystemService(name);
+        }
     }
 
     /** ContentProvider which returns a set of known test values. */
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index b88ae37..6b2449f 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -85,11 +85,13 @@
 filegroup {
     name: "SystemUI-tests-broken-robofiles-run",
     srcs: [
+        "tests/src/**/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt",
+        "tests/src/**/systemui/power/PowerNotificationWarningsTest.java",
+        "tests/src/**/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt",
         "tests/src/**/systemui/dreams/touch/CommunalTouchHandlerTest.java",
         "tests/src/**/systemui/shade/NotificationShadeWindowViewControllerTest.kt",
         "tests/src/**/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt",
         "tests/src/**/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModelParameterizedTest.kt",
-        "tests/src/**/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt",
         "tests/src/**/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt",
         "tests/src/**/systemui/animation/back/FlingOnBackAnimationCallbackTest.kt",
         "tests/src/**/systemui/education/domain/ui/view/ContextualEduDialogTest.kt",
@@ -285,6 +287,7 @@
         "tests/src/**/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java",
         "tests/src/**/systemui/shared/system/RemoteTransitionTest.java",
         "tests/src/**/systemui/qs/tiles/dialog/InternetDetailsContentControllerTest.java",
+        "tests/src/**/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt",
         "tests/src/**/systemui/qs/external/TileLifecycleManagerTest.java",
         "tests/src/**/systemui/ScreenDecorationsTest.java",
         "tests/src/**/systemui/statusbar/policy/BatteryControllerStartableTest.java",
@@ -841,7 +844,6 @@
         "androidx.test.ext.truth",
     ],
 
-    upstream: true,
 
     instrumentation_for: "SystemUIRobo-stub",
     java_resource_dirs: ["tests/robolectric/config"],
@@ -879,7 +881,6 @@
         "androidx.test.ext.truth",
     ],
 
-    upstream: true,
 
     instrumentation_for: "SystemUIRobo-stub",
     java_resource_dirs: ["tests/robolectric/config"],
@@ -916,6 +917,7 @@
         "android.test.mock.impl",
     ],
     auto_gen_config: true,
+    team: "trendy_team_ravenwood",
     plugins: [
         "dagger2-compiler",
     ],
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 99d704f..51ea529 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -413,7 +413,8 @@
         android:defaultToDeviceProtectedStorage="true"
         android:directBootAware="true"
         tools:replace="android:appComponentFactory"
-        android:appComponentFactory=".PhoneSystemUIAppComponentFactory">
+        android:appComponentFactory=".PhoneSystemUIAppComponentFactory"
+        android:enableOnBackInvokedCallback="true">
         <!-- Keep theme in sync with SystemUIApplication.onCreate().
              Setting the theme on the application does not affect views inflated by services.
              The application theme is set again from onCreate to take effect for those views. -->
diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS
index 795b395..c6cc9a9 100644
--- a/packages/SystemUI/OWNERS
+++ b/packages/SystemUI/OWNERS
@@ -119,4 +119,5 @@
 yeinj@google.com
 yuandizhou@google.com
 yurilin@google.com
+yuzhechen@google.com
 zakcohen@google.com
diff --git a/packages/SystemUI/aconfig/predictive_back.aconfig b/packages/SystemUI/aconfig/predictive_back.aconfig
index 46eb9e1..ee918c2 100644
--- a/packages/SystemUI/aconfig/predictive_back.aconfig
+++ b/packages/SystemUI/aconfig/predictive_back.aconfig
@@ -2,29 +2,8 @@
 container: "system"
 
 flag {
-    name: "predictive_back_sysui"
-    namespace: "systemui"
-    description: "Predictive Back Dispatching for SysUI"
-    bug: "327737297"
-}
-
-flag {
     name: "predictive_back_animate_shade"
     namespace: "systemui"
     description: "Enable Shade Animations"
     bug: "327732946"
 }
-
-flag {
-    name: "predictive_back_animate_bouncer"
-    namespace: "systemui"
-    description: "Enable Predictive Back Animation in Bouncer"
-    bug: "327733487"
-}
-
-flag {
-    name: "predictive_back_animate_dialogs"
-    namespace: "systemui"
-    description: "Enable Predictive Back Animation for SysUI dialogs"
-    bug: "327721544"
-}
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 715d223..70d4cc2 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -133,14 +133,6 @@
 }
 
 flag {
-    name: "notifications_footer_view_refactor"
-    namespace: "systemui"
-    description: "Enables the refactored version of the footer view in the notification shade "
-        "(containing the \"Clear all\" button). Should not bring any behavior changes"
-    bug: "293167744"
-}
-
-flag {
     name: "notifications_icon_container_refactor"
     namespace: "systemui"
     description: "Enables the refactored version of the notification icon container in StatusBar, "
@@ -432,24 +424,6 @@
 }
 
 flag {
-    name: "status_bar_use_repos_for_call_chip"
-    namespace: "systemui"
-    description: "Use repositories as the source of truth for call notifications shown as a chip in"
-        "the status bar"
-    bug: "328584859"
-    metadata {
-        purpose: PURPOSE_BUGFIX
-    }
-}
-
-flag {
-    name: "status_bar_call_chip_notification_icon"
-    namespace: "systemui"
-    description: "Use the small icon set on the notification for the status bar call chip"
-    bug: "354930838"
-}
-
-flag {
    name: "status_bar_signal_policy_refactor"
    namespace: "systemui"
    description: "Use a settings observer for airplane mode and make StatusBarSignalPolicy startable"
@@ -1352,6 +1326,13 @@
 }
 
 flag {
+  name: "output_switcher_redesign"
+  namespace: "systemui"
+  description: "Enables visual update for Media Output Switcher"
+  bug: "388296370"
+}
+
+flag {
   namespace: "systemui"
   name: "enable_view_capture_tracing"
   description: "Enables view capture tracing in System UI."
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java
index 91fad4f..56d85ab 100644
--- a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java
@@ -107,7 +107,7 @@
             IBinder mergeTarget,
             IRemoteTransitionFinishedCallback finishCallback) {
         logD("mergeAnimation - " + info);
-        mHandler.post(this::cancel);
+        cancel();
     }
 
     @Override
@@ -129,7 +129,7 @@
     @Override
     public void onTransitionConsumed(IBinder transition, boolean aborted) {
         logD("onTransitionConsumed - aborted: " + aborted);
-        mHandler.post(this::cancel);
+        cancel();
     }
 
     private void startAnimationInternal(
@@ -342,11 +342,14 @@
         mFinishCallback = null;
     }
 
-    private void cancel() {
+    public void cancel() {
         logD("cancel()");
-        if (mAnimator != null) {
-            mAnimator.cancel();
-        }
+        mHandler.post(
+                () -> {
+                    if (mAnimator != null) {
+                        mAnimator.cancel();
+                    }
+                });
     }
 
     private static void logD(String msg) {
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java
index 6d6aa88..cb3dfb9 100644
--- a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java
@@ -43,6 +43,7 @@
 /**
  * A session object that holds origin transition states for starting an activity from an on-screen
  * UI component and smoothly transitioning back from the activity to the same UI component.
+ *
  * @hide
  */
 public class OriginTransitionSession {
@@ -143,6 +144,12 @@
                 logE("Unable to cancel origin transition!", e);
             }
         }
+        if (mEntryTransition instanceof OriginRemoteTransition) {
+            ((OriginRemoteTransition) mEntryTransition).cancel();
+        }
+        if (mExitTransition instanceof OriginRemoteTransition) {
+            ((OriginRemoteTransition) mExitTransition).cancel();
+        }
     }
 
     private boolean hasEntryTransition() {
@@ -182,6 +189,7 @@
 
     /**
      * A builder to build a {@link OriginTransitionSession}.
+     *
      * @hide
      */
     public static class Builder {
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java
index 7c219c6..2e8f928 100644
--- a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java
@@ -24,13 +24,15 @@
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
 import android.util.Log;
 import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewRootImpl;
-import android.view.ViewTreeObserver.OnDrawListener;
+import android.view.ViewTreeObserver;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -51,8 +53,9 @@
     private final Path mClippingPath = new Path();
     private final Outline mClippingOutline = new Outline();
 
-    private final OnDrawListener mOnDrawListener = this::postDraw;
+    private final LifecycleListener mLifecycleListener = new LifecycleListener();
     private final View mView;
+    private final Handler mMainHandler;
 
     @Nullable private SurfaceControl mSurfaceControl;
     @Nullable private Surface mSurface;
@@ -62,6 +65,7 @@
 
     public ViewUIComponent(View view) {
         mView = view;
+        mMainHandler = new Handler(Looper.getMainLooper());
     }
 
     /**
@@ -110,11 +114,11 @@
         t.reparent(mSurfaceControl, transitionLeash).show(mSurfaceControl);
 
         // Make sure view draw triggers surface draw.
-        mView.getViewTreeObserver().addOnDrawListener(mOnDrawListener);
+        mLifecycleListener.register();
 
         // Make the view invisible AFTER the surface is shown.
         t.addTransactionCommittedListener(
-                        mView::post,
+                        this::post,
                         () -> {
                             logD("Surface attached!");
                             forceDraw();
@@ -129,14 +133,14 @@
         SurfaceControl sc = mSurfaceControl;
         mSurface = null;
         mSurfaceControl = null;
-        mView.getViewTreeObserver().removeOnDrawListener(mOnDrawListener);
+        mLifecycleListener.unregister();
         // Restore view visibility
         mView.setVisibility(mVisibleOverride ? View.VISIBLE : View.INVISIBLE);
         // Clean up surfaces.
         SurfaceControl.Transaction t = new SurfaceControl.Transaction();
         t.reparent(sc, null)
                 .addTransactionCommittedListener(
-                        mView::post,
+                        this::post,
                         () -> {
                             s.release();
                             sc.release();
@@ -269,7 +273,66 @@
             return;
         }
         mDirty = true;
-        mView.post(this::draw);
+        post(this::draw);
+    }
+
+    private void post(Runnable r) {
+        if (mView.isAttachedToWindow()) {
+            mView.post(r);
+        } else {
+            // If the view is detached from window, {@code View.post()} will postpone the action
+            // until the view is attached again. However, we don't know if the view will be attached
+            // again, so we post the action to the main thread in this case. This could lead to race
+            // condition if the attachment change caused a thread switching, and it's the caller's
+            // responsibility to ensure the window attachment state doesn't change unexpectedly.
+            if (DEBUG) {
+                Log.w(TAG, mView + " is not attached. Posting action to main thread!");
+            }
+            mMainHandler.post(r);
+        }
+    }
+
+    /** A listener for monitoring view life cycles. */
+    private class LifecycleListener
+            implements ViewTreeObserver.OnDrawListener, View.OnAttachStateChangeListener {
+        private boolean mRegistered;
+
+        @Override
+        public void onDraw() {
+            // View draw should trigger surface draw.
+            postDraw();
+        }
+
+        @Override
+        public void onViewAttachedToWindow(View v) {
+            // empty
+        }
+
+        @Override
+        public void onViewDetachedFromWindow(View v) {
+            Log.w(
+                    TAG,
+                    v + " is detached from the window. Unregistering the life cycle listener ...");
+            unregister();
+        }
+
+        public void register() {
+            if (mRegistered) {
+                return;
+            }
+            mRegistered = true;
+            mView.getViewTreeObserver().addOnDrawListener(this);
+            mView.addOnAttachStateChangeListener(this);
+        }
+
+        public void unregister() {
+            if (!mRegistered) {
+                return;
+            }
+            mRegistered = false;
+            mView.getViewTreeObserver().removeOnDrawListener(this);
+            mView.removeOnAttachStateChangeListener(this);
+        }
     }
 
     /** @hide */
@@ -278,34 +341,33 @@
 
         @Override
         public Transaction setAlpha(ViewUIComponent ui, float alpha) {
-            mChanges.add(() -> ui.mView.post(() -> ui.setAlpha(alpha)));
+            mChanges.add(() -> ui.post(() -> ui.setAlpha(alpha)));
             return this;
         }
 
         @Override
         public Transaction setVisible(ViewUIComponent ui, boolean visible) {
-            mChanges.add(() -> ui.mView.post(() -> ui.setVisible(visible)));
+            mChanges.add(() -> ui.post(() -> ui.setVisible(visible)));
             return this;
         }
 
         @Override
         public Transaction setBounds(ViewUIComponent ui, Rect bounds) {
-            mChanges.add(() -> ui.mView.post(() -> ui.setBounds(bounds)));
+            mChanges.add(() -> ui.post(() -> ui.setBounds(bounds)));
             return this;
         }
 
         @Override
         public Transaction attachToTransitionLeash(
                 ViewUIComponent ui, SurfaceControl transitionLeash, int w, int h) {
-            mChanges.add(
-                    () -> ui.mView.post(() -> ui.attachToTransitionLeash(transitionLeash, w, h)));
+            mChanges.add(() -> ui.post(() -> ui.attachToTransitionLeash(transitionLeash, w, h)));
             return this;
         }
 
         @Override
         public Transaction detachFromTransitionLeash(
                 ViewUIComponent ui, Executor executor, Runnable onDone) {
-            mChanges.add(() -> ui.mView.post(() -> ui.detachFromTransitionLeash(executor, onDone)));
+            mChanges.add(() -> ui.post(() -> ui.detachFromTransitionLeash(executor, onDone)));
             return this;
         }
 
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/AnimationFeatureFlags.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/AnimationFeatureFlags.kt
deleted file mode 100644
index 1c9dabb..0000000
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/AnimationFeatureFlags.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.android.systemui.animation
-
-interface AnimationFeatureFlags {
-    val isPredictiveBackQsDialogAnim: Boolean
-        get() = false
-}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
index 907c39d..c88c4ebb 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
@@ -59,13 +59,8 @@
     private val mainExecutor: Executor,
     private val callback: Callback,
     private val interactionJankMonitor: InteractionJankMonitor,
-    private val featureFlags: AnimationFeatureFlags,
     private val transitionAnimator: TransitionAnimator =
-        TransitionAnimator(
-            mainExecutor,
-            TIMINGS,
-            INTERPOLATORS,
-        ),
+        TransitionAnimator(mainExecutor, TIMINGS, INTERPOLATORS),
     private val isForTesting: Boolean = false,
 ) {
     private companion object {
@@ -219,7 +214,7 @@
         dialog: Dialog,
         view: View,
         cuj: DialogCuj? = null,
-        animateBackgroundBoundsChange: Boolean = false
+        animateBackgroundBoundsChange: Boolean = false,
     ) {
         val controller = Controller.fromView(view, cuj)
         if (controller == null) {
@@ -245,7 +240,7 @@
     fun show(
         dialog: Dialog,
         controller: Controller,
-        animateBackgroundBoundsChange: Boolean = false
+        animateBackgroundBoundsChange: Boolean = false,
     ) {
         if (Looper.myLooper() != Looper.getMainLooper()) {
             throw IllegalStateException(
@@ -263,15 +258,14 @@
         val controller =
             animatedParent?.dialogContentWithBackground?.let {
                 Controller.fromView(it, controller.cuj)
-            }
-                ?: controller
+            } ?: controller
 
         // Make sure we don't run the launch animation from the same source twice at the same time.
         if (openedDialogs.any { it.controller.sourceIdentity == controller.sourceIdentity }) {
             Log.e(
                 TAG,
                 "Not running dialog launch animation from source as it is already expanded into a" +
-                    " dialog"
+                    " dialog",
             )
             dialog.show()
             return
@@ -288,7 +282,6 @@
                 animateBackgroundBoundsChange = animateBackgroundBoundsChange,
                 parentAnimatedDialog = animatedParent,
                 forceDisableSynchronization = isForTesting,
-                featureFlags = featureFlags,
             )
 
         openedDialogs.add(animatedDialog)
@@ -305,7 +298,7 @@
         dialog: Dialog,
         animateFrom: Dialog,
         cuj: DialogCuj? = null,
-        animateBackgroundBoundsChange: Boolean = false
+        animateBackgroundBoundsChange: Boolean = false,
     ) {
         val view =
             openedDialogs.firstOrNull { it.dialog == animateFrom }?.dialogContentWithBackground
@@ -313,7 +306,7 @@
             Log.w(
                 TAG,
                 "Showing dialog $dialog normally as the dialog it is shown from was not shown " +
-                    "using DialogTransitionAnimator"
+                    "using DialogTransitionAnimator",
             )
             dialog.show()
             return
@@ -323,7 +316,7 @@
             dialog,
             view,
             animateBackgroundBoundsChange = animateBackgroundBoundsChange,
-            cuj = cuj
+            cuj = cuj,
         )
     }
 
@@ -346,8 +339,7 @@
         val animatedDialog =
             openedDialogs.firstOrNull {
                 it.dialog.window?.decorView?.viewRootImpl == view.viewRootImpl
-            }
-                ?: return null
+            } ?: return null
         return createActivityTransitionController(animatedDialog, cujType)
     }
 
@@ -373,7 +365,7 @@
 
     private fun createActivityTransitionController(
         animatedDialog: AnimatedDialog,
-        cujType: Int? = null
+        cujType: Int? = null,
     ): ActivityTransitionAnimator.Controller? {
         // At this point, we know that the intent of the caller is to dismiss the dialog to show
         // an app, so we disable the exit animation into the source because we will never want to
@@ -440,7 +432,7 @@
             }
 
             private fun disableDialogDismiss() {
-                dialog.setDismissOverride { /* Do nothing */}
+                dialog.setDismissOverride { /* Do nothing */ }
             }
 
             private fun enableDialogDismiss() {
@@ -530,7 +522,6 @@
      * Whether synchronization should be disabled, which can be useful if we are running in a test.
      */
     private val forceDisableSynchronization: Boolean,
-    private val featureFlags: AnimationFeatureFlags,
 ) {
     /**
      * The DecorView of this dialog window.
@@ -643,8 +634,7 @@
         originalDialogBackgroundColor =
             GhostedViewTransitionAnimatorController.findGradientDrawable(background)
                 ?.color
-                ?.defaultColor
-                ?: Color.BLACK
+                ?.defaultColor ?: Color.BLACK
 
         // Make the background view invisible until we start the animation. We use the transition
         // visibility like GhostView does so that we don't mess up with the accessibility tree (see
@@ -700,7 +690,7 @@
                     oldLeft: Int,
                     oldTop: Int,
                     oldRight: Int,
-                    oldBottom: Int
+                    oldBottom: Int,
                 ) {
                     dialogContentWithBackground.removeOnLayoutChangeListener(this)
 
@@ -717,9 +707,7 @@
         // the dialog.
         dialog.setDismissOverride(this::onDialogDismissed)
 
-        if (featureFlags.isPredictiveBackQsDialogAnim) {
-            dialog.registerAnimationOnBackInvoked(targetView = dialogContentWithBackground)
-        }
+        dialog.registerAnimationOnBackInvoked(targetView = dialogContentWithBackground)
 
         // Show the dialog.
         dialog.show()
@@ -815,7 +803,7 @@
                 if (hasInstrumentedJank) {
                     interactionJankMonitor.end(controller.cuj!!.cujType)
                 }
-            }
+            },
         )
     }
 
@@ -888,14 +876,14 @@
                     onAnimationFinished(true /* instantDismiss */)
                     onDialogDismissed(this@AnimatedDialog)
                 }
-            }
+            },
         )
     }
 
     private fun startAnimation(
         isLaunching: Boolean,
         onLaunchAnimationStart: () -> Unit = {},
-        onLaunchAnimationEnd: () -> Unit = {}
+        onLaunchAnimationEnd: () -> Unit = {},
     ) {
         // Create 2 controllers to animate both the dialog and the source.
         val startController =
@@ -969,7 +957,7 @@
                 override fun onTransitionAnimationProgress(
                     state: TransitionAnimator.State,
                     progress: Float,
-                    linearProgress: Float
+                    linearProgress: Float,
                 ) {
                     startController.onTransitionAnimationProgress(state, progress, linearProgress)
 
@@ -1026,7 +1014,7 @@
             oldLeft: Int,
             oldTop: Int,
             oldRight: Int,
-            oldBottom: Int
+            oldBottom: Int,
         ) {
             // Don't animate if bounds didn't actually change.
             if (left == oldLeft && top == oldTop && right == oldRight && bottom == oldBottom) {
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt
index e02e8b4..a27bf8a 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt
@@ -34,9 +34,11 @@
 import androidx.compose.ui.input.pointer.AwaitPointerEventScope
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerEventType
 import androidx.compose.ui.input.pointer.PointerId
 import androidx.compose.ui.input.pointer.PointerInputChange
 import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.PointerType
 import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
 import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed
 import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
@@ -52,7 +54,6 @@
 import androidx.compose.ui.platform.LocalViewConfiguration
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.Velocity
-import androidx.compose.ui.util.fastSumBy
 import com.android.compose.modifiers.thenIf
 import kotlin.math.sign
 import kotlinx.coroutines.CompletableDeferred
@@ -81,7 +82,13 @@
      * in the direction given by [sign], with the given number of [pointersDown] when the touch slop
      * was detected.
      */
-    fun onDragStarted(position: Offset, sign: Float, pointersDown: Int): Controller
+    fun onDragStarted(
+        position: Offset,
+        sign: Float,
+        pointersDown: Int,
+        // TODO(b/382665591): Make this non-nullable.
+        pointerType: PointerType?,
+    ): Controller
 
     /**
      * Whether this draggable should consume any scroll amount with the given [sign] coming from a
@@ -140,21 +147,66 @@
     private val orientation: Orientation,
     private val overscrollEffect: OverscrollEffect?,
     private val enabled: Boolean,
-) : ModifierNodeElement<NestedDraggableNode>() {
-    override fun create(): NestedDraggableNode {
-        return NestedDraggableNode(draggable, orientation, overscrollEffect, enabled)
+) : ModifierNodeElement<NestedDraggableRootNode>() {
+    override fun create(): NestedDraggableRootNode {
+        return NestedDraggableRootNode(draggable, orientation, overscrollEffect, enabled)
     }
 
-    override fun update(node: NestedDraggableNode) {
+    override fun update(node: NestedDraggableRootNode) {
         node.update(draggable, orientation, overscrollEffect, enabled)
     }
 }
 
+/**
+ * A root node on top of [NestedDraggableNode] so that no [PointerInputModifierNode] is installed
+ * when this draggable is disabled.
+ */
+private class NestedDraggableRootNode(
+    draggable: NestedDraggable,
+    orientation: Orientation,
+    overscrollEffect: OverscrollEffect?,
+    enabled: Boolean,
+) : DelegatingNode() {
+    private var delegateNode =
+        if (enabled) create(draggable, orientation, overscrollEffect) else null
+
+    fun update(
+        draggable: NestedDraggable,
+        orientation: Orientation,
+        overscrollEffect: OverscrollEffect?,
+        enabled: Boolean,
+    ) {
+        // Disabled.
+        if (!enabled) {
+            delegateNode?.let { undelegate(it) }
+            delegateNode = null
+            return
+        }
+
+        // Disabled => Enabled.
+        val nullableDelegate = delegateNode
+        if (nullableDelegate == null) {
+            delegateNode = create(draggable, orientation, overscrollEffect)
+            return
+        }
+
+        // Enabled => Enabled (update).
+        nullableDelegate.update(draggable, orientation, overscrollEffect)
+    }
+
+    private fun create(
+        draggable: NestedDraggable,
+        orientation: Orientation,
+        overscrollEffect: OverscrollEffect?,
+    ): NestedDraggableNode {
+        return delegate(NestedDraggableNode(draggable, orientation, overscrollEffect))
+    }
+}
+
 private class NestedDraggableNode(
     private var draggable: NestedDraggable,
     override var orientation: Orientation,
     private var overscrollEffect: OverscrollEffect?,
-    private var enabled: Boolean,
 ) :
     DelegatingNode(),
     PointerInputModifierNode,
@@ -162,17 +214,11 @@
     CompositionLocalConsumerModifierNode,
     OrientationAware {
     private val nestedScrollDispatcher = NestedScrollDispatcher()
-    private var trackDownPositionDelegate: SuspendingPointerInputModifierNode? = null
-        set(value) {
-            field?.let { undelegate(it) }
-            field = value?.also { delegate(it) }
-        }
-
-    private var detectDragsDelegate: SuspendingPointerInputModifierNode? = null
-        set(value) {
-            field?.let { undelegate(it) }
-            field = value?.also { delegate(it) }
-        }
+    private val trackWheelScroll =
+        delegate(SuspendingPointerInputModifierNode { trackWheelScroll() })
+    private val trackDownPositionDelegate =
+        delegate(SuspendingPointerInputModifierNode { trackDownPosition() })
+    private val detectDragsDelegate = delegate(SuspendingPointerInputModifierNode { detectDrags() })
 
     /** The controller created by the nested scroll logic (and *not* the drag logic). */
     private var nestedScrollController: NestedScrollController? = null
@@ -183,9 +229,10 @@
      * This is use to track the started position of a drag started on a nested scrollable.
      */
     private var lastFirstDown: Offset? = null
+    private var lastEventWasScrollWheel: Boolean = false
 
-    /** The number of pointers down. */
-    private var pointersDownCount = 0
+    /** The pointers currently down, in order of which they were done and mapping to their type. */
+    private val pointersDown = linkedMapOf<PointerId, PointerType>()
 
     init {
         delegate(nestedScrollModifierNode(this, nestedScrollDispatcher))
@@ -200,23 +247,25 @@
         draggable: NestedDraggable,
         orientation: Orientation,
         overscrollEffect: OverscrollEffect?,
-        enabled: Boolean,
     ) {
+        if (
+            draggable == this.draggable &&
+                orientation == this.orientation &&
+                overscrollEffect == this.overscrollEffect
+        ) {
+            return
+        }
+
         this.draggable = draggable
         this.orientation = orientation
         this.overscrollEffect = overscrollEffect
-        this.enabled = enabled
 
-        trackDownPositionDelegate?.resetPointerInputHandler()
-        detectDragsDelegate?.resetPointerInputHandler()
+        trackWheelScroll.resetPointerInputHandler()
+        trackDownPositionDelegate.resetPointerInputHandler()
+        detectDragsDelegate.resetPointerInputHandler()
+
         nestedScrollController?.ensureOnDragStoppedIsCalled()
         nestedScrollController = null
-
-        if (!enabled && trackDownPositionDelegate != null) {
-            check(detectDragsDelegate != null)
-            trackDownPositionDelegate = null
-            detectDragsDelegate = null
-        }
     }
 
     override fun onPointerEvent(
@@ -224,21 +273,15 @@
         pass: PointerEventPass,
         bounds: IntSize,
     ) {
-        if (!enabled) return
-
-        if (trackDownPositionDelegate == null) {
-            check(detectDragsDelegate == null)
-            trackDownPositionDelegate = SuspendingPointerInputModifierNode { trackDownPosition() }
-            detectDragsDelegate = SuspendingPointerInputModifierNode { detectDrags() }
-        }
-
-        checkNotNull(trackDownPositionDelegate).onPointerEvent(pointerEvent, pass, bounds)
-        checkNotNull(detectDragsDelegate).onPointerEvent(pointerEvent, pass, bounds)
+        trackWheelScroll.onPointerEvent(pointerEvent, pass, bounds)
+        trackDownPositionDelegate.onPointerEvent(pointerEvent, pass, bounds)
+        detectDragsDelegate.onPointerEvent(pointerEvent, pass, bounds)
     }
 
     override fun onCancelPointerInput() {
-        trackDownPositionDelegate?.onCancelPointerInput()
-        detectDragsDelegate?.onCancelPointerInput()
+        trackWheelScroll.onCancelPointerInput()
+        trackDownPositionDelegate.onCancelPointerInput()
+        detectDragsDelegate.onCancelPointerInput()
     }
 
     /*
@@ -256,7 +299,9 @@
             check(down.position == lastFirstDown) {
                 "Position from detectDrags() is not the same as position in trackDownPosition()"
             }
-            check(pointersDownCount == 1) { "pointersDownCount is equal to $pointersDownCount" }
+            check(pointersDown.size == 1 && pointersDown.keys.first() == down.id) {
+                "pointersDown should only contain $down but it contains $pointersDown"
+            }
 
             var overSlop = 0f
             val onTouchSlopReached = { change: PointerInputChange, over: Float ->
@@ -295,8 +340,9 @@
                     }
                 }
 
-                check(pointersDownCount > 0) { "pointersDownCount is equal to $pointersDownCount" }
-                val controller = draggable.onDragStarted(down.position, sign, pointersDownCount)
+                check(pointersDown.size > 0) { "pointersDown is empty" }
+                val controller =
+                    draggable.onDragStarted(down.position, sign, pointersDown.size, drag.type)
                 if (overSlop != 0f) {
                     onDrag(controller, drag, overSlop, velocityTracker)
                 }
@@ -398,7 +444,7 @@
         val left = available - consumed
         val postConsumed =
             nestedScrollDispatcher.dispatchPostScroll(
-                consumed = preConsumed + consumed,
+                consumed = consumed,
                 available = left,
                 source = NestedScrollSource.UserInput,
             )
@@ -436,10 +482,9 @@
         val available = velocity - preConsumed
         val consumed = performFling(available)
         val left = available - consumed
-        return nestedScrollDispatcher.dispatchPostFling(
-            consumed = consumed + preConsumed,
-            available = left,
-        )
+        val postConsumed =
+            nestedScrollDispatcher.dispatchPostFling(consumed = consumed, available = left)
+        return preConsumed + consumed + postConsumed
     }
 
     /*
@@ -448,22 +493,33 @@
      * ===============================
      */
 
+    private suspend fun PointerInputScope.trackWheelScroll() {
+        awaitEachGesture {
+            val event = awaitPointerEvent(pass = PointerEventPass.Initial)
+            lastEventWasScrollWheel = event.type == PointerEventType.Scroll
+        }
+    }
+
     private suspend fun PointerInputScope.trackDownPosition() {
         awaitEachGesture {
-            val down = awaitFirstDown(requireUnconsumed = false)
-            lastFirstDown = down.position
-            pointersDownCount = 1
+            try {
+                val down = awaitFirstDown(requireUnconsumed = false)
+                lastFirstDown = down.position
+                pointersDown[down.id] = down.type
 
-            do {
-                pointersDownCount +=
-                    awaitPointerEvent().changes.fastSumBy { change ->
+                do {
+                    awaitPointerEvent().changes.forEach { change ->
                         when {
-                            change.changedToDownIgnoreConsumed() -> 1
-                            change.changedToUpIgnoreConsumed() -> -1
-                            else -> 0
+                            change.changedToDownIgnoreConsumed() -> {
+                                pointersDown[change.id] = change.type
+                            }
+                            change.changedToUpIgnoreConsumed() -> pointersDown.remove(change.id)
                         }
                     }
-            } while (pointersDownCount > 0)
+                } while (pointersDown.size > 0)
+            } finally {
+                pointersDown.clear()
+            }
         }
     }
 
@@ -488,15 +544,22 @@
         }
 
         val sign = offset.sign
-        if (nestedScrollController == null && draggable.shouldConsumeNestedScroll(sign)) {
-            val startedPosition = checkNotNull(lastFirstDown) { "lastFirstDown is not set" }
+        if (
+            nestedScrollController == null &&
+                // TODO(b/388231324): Remove this.
+                !lastEventWasScrollWheel &&
+                draggable.shouldConsumeNestedScroll(sign) &&
+                lastFirstDown != null
+        ) {
+            val startedPosition = checkNotNull(lastFirstDown)
 
-            // TODO(b/382665591): Replace this by check(pointersDownCount > 0).
-            val pointersDown = pointersDownCount.coerceAtLeast(1)
+            // TODO(b/382665591): Ensure that there is at least one pointer down.
+            val pointersDownCount = pointersDown.size.coerceAtLeast(1)
+            val pointerType = pointersDown.entries.firstOrNull()?.value
             nestedScrollController =
                 NestedScrollController(
                     overscrollEffect,
-                    draggable.onDragStarted(startedPosition, sign, pointersDown),
+                    draggable.onDragStarted(startedPosition, sign, pointersDownCount, pointerType),
                 )
         }
 
diff --git a/packages/SystemUI/compose/core/tests/AndroidManifest.xml b/packages/SystemUI/compose/core/tests/AndroidManifest.xml
index 28f80d4..7c721b9 100644
--- a/packages/SystemUI/compose/core/tests/AndroidManifest.xml
+++ b/packages/SystemUI/compose/core/tests/AndroidManifest.xml
@@ -15,6 +15,7 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
     package="com.android.compose.core.tests" >
 
     <application>
@@ -23,7 +24,8 @@
         <activity
             android:name="androidx.activity.ComponentActivity"
             android:theme="@android:style/Theme.DeviceDefault.DayNight"
-            android:exported="true" />
+            android:exported="true"
+            tools:replace="android:theme" />
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt
index 9c49090..19d28cc 100644
--- a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt
@@ -18,28 +18,42 @@
 
 import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.rememberScrollableState
+import androidx.compose.foundation.gestures.scrollable
 import androidx.compose.foundation.horizontalScroll
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
 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.nestedScroll
 import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.PointerType
 import androidx.compose.ui.platform.LocalViewConfiguration
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ScrollWheel
+import androidx.compose.ui.test.assertTextEquals
 import androidx.compose.ui.test.junit4.ComposeContentTestRule
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performMouseInput
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.test.swipeDown
 import androidx.compose.ui.test.swipeLeft
+import androidx.compose.ui.test.swipeWithVelocity
 import androidx.compose.ui.unit.Velocity
 import com.google.common.truth.Truth.assertThat
 import kotlin.math.ceil
@@ -653,6 +667,289 @@
         assertThat(flingIsDone).isTrue()
     }
 
+    @Test
+    fun pointerType() {
+        val draggable = TestDraggable()
+        val touchSlop =
+            rule.setContentWithTouchSlop {
+                Box(Modifier.fillMaxSize().nestedDraggable(draggable, orientation))
+            }
+
+        rule.onRoot().performTouchInput {
+            down(center)
+            moveBy(touchSlop.toOffset())
+        }
+
+        assertThat(draggable.onDragStartedPointerType).isEqualTo(PointerType.Touch)
+    }
+
+    @Test
+    fun pointerType_mouse() {
+        val draggable = TestDraggable()
+        val touchSlop =
+            rule.setContentWithTouchSlop {
+                Box(Modifier.fillMaxSize().nestedDraggable(draggable, orientation))
+            }
+
+        rule.onRoot().performMouseInput {
+            moveTo(center)
+            press()
+            moveBy(touchSlop.toOffset())
+            release()
+        }
+
+        assertThat(draggable.onDragStartedPointerType).isEqualTo(PointerType.Mouse)
+    }
+
+    @Test
+    @Ignore("b/388507816: re-enable this when the crash in HitPath is fixed")
+    fun pointersDown_clearedWhenDisabled() {
+        val draggable = TestDraggable()
+        var enabled by mutableStateOf(true)
+        rule.setContent {
+            Box(Modifier.fillMaxSize().nestedDraggable(draggable, orientation, enabled = enabled))
+        }
+
+        rule.onRoot().performTouchInput { down(center) }
+
+        enabled = false
+        rule.waitForIdle()
+
+        rule.onRoot().performTouchInput { up() }
+
+        enabled = true
+        rule.waitForIdle()
+
+        rule.onRoot().performTouchInput { down(center) }
+    }
+
+    @Test
+    // TODO(b/388231324): Remove this.
+    fun nestedScrollWithMouseWheelIsIgnored() {
+        val draggable = TestDraggable()
+        val touchSlop =
+            rule.setContentWithTouchSlop {
+                Box(
+                    Modifier.fillMaxSize()
+                        .nestedDraggable(draggable, orientation)
+                        .scrollable(rememberScrollableState { 0f }, orientation)
+                )
+            }
+
+        rule.onRoot().performMouseInput {
+            enter(center)
+            scroll(
+                touchSlop + 1f,
+                when (orientation) {
+                    Orientation.Horizontal -> ScrollWheel.Horizontal
+                    Orientation.Vertical -> ScrollWheel.Vertical
+                },
+            )
+        }
+
+        assertThat(draggable.onDragStartedCalled).isFalse()
+    }
+
+    @Test
+    fun doesNotConsumeGesturesWhenDisabled() {
+        val buttonTag = "button"
+        rule.setContent {
+            Box {
+                var count by remember { mutableStateOf(0) }
+                Button(onClick = { count++ }, Modifier.testTag(buttonTag).align(Alignment.Center)) {
+                    Text("Count: $count")
+                }
+
+                Box(
+                    Modifier.fillMaxSize()
+                        .nestedDraggable(remember { TestDraggable() }, orientation, enabled = false)
+                )
+            }
+        }
+
+        rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 0")
+
+        // Click on the root at its center, where the button is located. Clicks should go through
+        // the draggable and reach the button given that it is disabled.
+        repeat(3) { rule.onRoot().performClick() }
+        rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 3")
+    }
+
+    @Test
+    fun nestedDragNotStartedWhenEnabledAfterDragStarted() {
+        val draggable = TestDraggable()
+        var enabled by mutableStateOf(false)
+        val touchSlop =
+            rule.setContentWithTouchSlop {
+                Box(
+                    Modifier.fillMaxSize()
+                        .nestedDraggable(draggable, orientation, enabled = enabled)
+                        .scrollable(rememberScrollableState { 0f }, orientation)
+                )
+            }
+
+        rule.onRoot().performTouchInput { down(center) }
+
+        enabled = true
+        rule.waitForIdle()
+
+        rule.onRoot().performTouchInput { moveBy((touchSlop + 1f).toOffset()) }
+
+        assertThat(draggable.onDragStartedCalled).isFalse()
+    }
+
+    @Test
+    fun availableAndConsumedScrollDeltas() {
+        val totalScroll = 200f
+        val consumedByEffectPreScroll = 10f // 200f => 190f
+        val consumedByConnectionPreScroll = 20f // 190f => 170f
+        val consumedByScroll = 30f // 170f => 140f
+        val consumedByConnectionPostScroll = 40f // 140f => 100f
+
+        // Available scroll values that we will check later.
+        var availableToEffectPreScroll = 0f
+        var availableToConnectionPreScroll = 0f
+        var availableToScroll = 0f
+        var availableToConnectionPostScroll = 0f
+        var availableToEffectPostScroll = 0f
+
+        val effect =
+            TestOverscrollEffect(
+                orientation,
+                onPreScroll = {
+                    availableToEffectPreScroll = it
+                    consumedByEffectPreScroll
+                },
+                onPostScroll = {
+                    availableToEffectPostScroll = it
+                    it
+                },
+            )
+
+        val connection =
+            object : NestedScrollConnection {
+                override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+                    availableToConnectionPreScroll = available.toFloat()
+                    return consumedByConnectionPreScroll.toOffset()
+                }
+
+                override fun onPostScroll(
+                    consumed: Offset,
+                    available: Offset,
+                    source: NestedScrollSource,
+                ): Offset {
+                    assertThat(consumed.toFloat()).isEqualTo(consumedByScroll)
+                    availableToConnectionPostScroll = available.toFloat()
+                    return consumedByConnectionPostScroll.toOffset()
+                }
+            }
+
+        val draggable =
+            TestDraggable(
+                onDrag = {
+                    availableToScroll = it
+                    consumedByScroll
+                }
+            )
+
+        val touchSlop =
+            rule.setContentWithTouchSlop {
+                Box(
+                    Modifier.fillMaxSize()
+                        .nestedScroll(connection)
+                        .nestedDraggable(draggable, orientation, effect)
+                )
+            }
+
+        rule.onRoot().performTouchInput {
+            down(center)
+            moveBy((touchSlop + totalScroll).toOffset())
+        }
+
+        assertThat(availableToEffectPreScroll).isEqualTo(200f)
+        assertThat(availableToConnectionPreScroll).isEqualTo(190f)
+        assertThat(availableToScroll).isEqualTo(170f)
+        assertThat(availableToConnectionPostScroll).isEqualTo(140f)
+        assertThat(availableToEffectPostScroll).isEqualTo(100f)
+    }
+
+    @Test
+    fun availableAndConsumedVelocities() {
+        val totalVelocity = 200f
+        val consumedByEffectPreFling = 10f // 200f => 190f
+        val consumedByConnectionPreFling = 20f // 190f => 170f
+        val consumedByFling = 30f // 170f => 140f
+        val consumedByConnectionPostFling = 40f // 140f => 100f
+
+        // Available velocities that we will check later.
+        var availableToEffectPreFling = 0f
+        var availableToConnectionPreFling = 0f
+        var availableToFling = 0f
+        var availableToConnectionPostFling = 0f
+        var availableToEffectPostFling = 0f
+
+        val effect =
+            TestOverscrollEffect(
+                orientation,
+                onPreFling = {
+                    availableToEffectPreFling = it
+                    consumedByEffectPreFling
+                },
+                onPostFling = {
+                    availableToEffectPostFling = it
+                    it
+                },
+                onPostScroll = { 0f },
+            )
+
+        val connection =
+            object : NestedScrollConnection {
+                override suspend fun onPreFling(available: Velocity): Velocity {
+                    availableToConnectionPreFling = available.toFloat()
+                    return consumedByConnectionPreFling.toVelocity()
+                }
+
+                override suspend fun onPostFling(
+                    consumed: Velocity,
+                    available: Velocity,
+                ): Velocity {
+                    assertThat(consumed.toFloat()).isEqualTo(consumedByFling)
+                    availableToConnectionPostFling = available.toFloat()
+                    return consumedByConnectionPostFling.toVelocity()
+                }
+            }
+
+        val draggable =
+            TestDraggable(
+                onDragStopped = { velocity, _ ->
+                    availableToFling = velocity
+                    consumedByFling
+                },
+                onDrag = { 0f },
+            )
+
+        rule.setContent {
+            Box(
+                Modifier.fillMaxSize()
+                    .nestedScroll(connection)
+                    .nestedDraggable(draggable, orientation, effect)
+            )
+        }
+
+        rule.onRoot().performTouchInput {
+            when (orientation) {
+                Orientation.Horizontal -> swipeWithVelocity(topLeft, topRight, totalVelocity)
+                Orientation.Vertical -> swipeWithVelocity(topLeft, bottomLeft, totalVelocity)
+            }
+        }
+
+        assertThat(availableToEffectPreFling).isWithin(1f).of(200f)
+        assertThat(availableToConnectionPreFling).isWithin(1f).of(190f)
+        assertThat(availableToFling).isWithin(1f).of(170f)
+        assertThat(availableToConnectionPostFling).isWithin(1f).of(140f)
+        assertThat(availableToEffectPostFling).isWithin(1f).of(100f)
+    }
+
     private fun ComposeContentTestRule.setContentWithTouchSlop(
         content: @Composable () -> Unit
     ): Float {
@@ -688,6 +985,7 @@
         var onDragStartedPosition = Offset.Zero
         var onDragStartedSign = 0f
         var onDragStartedPointersDown = 0
+        var onDragStartedPointerType: PointerType? = null
         var onDragDelta = 0f
 
         override fun shouldStartDrag(change: PointerInputChange): Boolean = shouldStartDrag
@@ -696,11 +994,13 @@
             position: Offset,
             sign: Float,
             pointersDown: Int,
+            pointerType: PointerType?,
         ): NestedDraggable.Controller {
             onDragStartedCalled = true
             onDragStartedPosition = position
             onDragStartedSign = sign
             onDragStartedPointersDown = pointersDown
+            onDragStartedPointerType = pointerType
             onDragDelta = 0f
 
             onDragStarted.invoke(position, sign)
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt
index 8bf9c21..0659f919 100644
--- a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt
@@ -24,6 +24,8 @@
 
 class TestOverscrollEffect(
     override val orientation: Orientation,
+    private val onPreScroll: (Float) -> Float = { 0f },
+    private val onPreFling: suspend (Float) -> Float = { 0f },
     private val onPostFling: suspend (Float) -> Float = { it },
     private val onPostScroll: (Float) -> Float,
 ) : OverscrollEffect, OrientationAware {
@@ -36,19 +38,23 @@
         source: NestedScrollSource,
         performScroll: (Offset) -> Offset,
     ): Offset {
-        val consumedByScroll = performScroll(delta)
-        val available = delta - consumedByScroll
-        val consumedByEffect = onPostScroll(available.toFloat()).toOffset()
-        return consumedByScroll + consumedByEffect
+        val consumedByPreScroll = onPreScroll(delta.toFloat()).toOffset()
+        val availableToScroll = delta - consumedByPreScroll
+        val consumedByScroll = performScroll(availableToScroll)
+        val availableToPostScroll = availableToScroll - consumedByScroll
+        val consumedByPostScroll = onPostScroll(availableToPostScroll.toFloat()).toOffset()
+        return consumedByPreScroll + consumedByScroll + consumedByPostScroll
     }
 
     override suspend fun applyToFling(
         velocity: Velocity,
         performFling: suspend (Velocity) -> Velocity,
     ) {
-        val consumedByFling = performFling(velocity)
-        val available = velocity - consumedByFling
-        onPostFling(available.toFloat())
+        val consumedByPreFling = onPreFling(velocity.toFloat()).toVelocity()
+        val availableToFling = velocity - consumedByPreFling
+        val consumedByFling = performFling(availableToFling)
+        val availableToPostFling = availableToFling - consumedByFling
+        onPostFling(availableToPostFling.toFloat())
         applyToFlingDone = true
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
index 1a8c7f8..0054a4c8 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
@@ -14,8 +14,6 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalFoundationApi::class)
-
 package com.android.systemui.bouncer.ui.composable
 
 import android.app.AlertDialog
@@ -26,7 +24,6 @@
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.animation.core.snap
 import androidx.compose.animation.core.tween
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
 import androidx.compose.foundation.combinedClickable
@@ -99,7 +96,6 @@
 import com.android.compose.windowsizeclass.LocalWindowSizeClass
 import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel
 import com.android.systemui.bouncer.ui.BouncerDialogFactory
-import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout
 import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.BouncerMessageViewModel
 import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayout.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayout.kt
index eb62d33..328fec5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayout.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayout.kt
@@ -16,13 +16,11 @@
 
 package com.android.systemui.bouncer.ui.composable
 
+import androidx.annotation.VisibleForTesting
 import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
 import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
 import androidx.compose.runtime.Composable
 import com.android.compose.windowsizeclass.LocalWindowSizeClass
-import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout
-import com.android.systemui.bouncer.ui.helper.SizeClass
-import com.android.systemui.bouncer.ui.helper.calculateLayoutInternal
 
 /**
  * Returns the [BouncerSceneLayout] that should be used by the bouncer scene. If
@@ -57,3 +55,50 @@
         else -> error("Unsupported WindowHeightSizeClass \"$this\"")
     }
 }
+
+/** Enumerates all known adaptive layout configurations. */
+enum class BouncerSceneLayout {
+    /** The default UI with the bouncer laid out normally. */
+    STANDARD_BOUNCER,
+    /** The bouncer is displayed vertically stacked with the user switcher. */
+    BELOW_USER_SWITCHER,
+    /** The bouncer is displayed side-by-side with the user switcher or an empty space. */
+    BESIDE_USER_SWITCHER,
+    /** The bouncer is split in two with both sides shown side-by-side. */
+    SPLIT_BOUNCER,
+}
+
+/** Enumerates the supported window size classes. */
+enum class SizeClass {
+    COMPACT,
+    MEDIUM,
+    EXPANDED,
+}
+
+/**
+ * Internal version of `calculateLayout` in the System UI Compose library, extracted here to allow
+ * for testing that's not dependent on Compose.
+ */
+@VisibleForTesting
+fun calculateLayoutInternal(
+    width: SizeClass,
+    height: SizeClass,
+    isOneHandedModeSupported: Boolean,
+): BouncerSceneLayout {
+    return when (height) {
+        SizeClass.COMPACT -> BouncerSceneLayout.SPLIT_BOUNCER
+        SizeClass.MEDIUM ->
+            when (width) {
+                SizeClass.COMPACT -> BouncerSceneLayout.STANDARD_BOUNCER
+                SizeClass.MEDIUM -> BouncerSceneLayout.STANDARD_BOUNCER
+                SizeClass.EXPANDED -> BouncerSceneLayout.BESIDE_USER_SWITCHER
+            }
+        SizeClass.EXPANDED ->
+            when (width) {
+                SizeClass.COMPACT -> BouncerSceneLayout.STANDARD_BOUNCER
+                SizeClass.MEDIUM -> BouncerSceneLayout.BELOW_USER_SWITCHER
+                SizeClass.EXPANDED -> BouncerSceneLayout.BESIDE_USER_SWITCHER
+            }
+    }.takeIf { it != BouncerSceneLayout.BESIDE_USER_SWITCHER || isOneHandedModeSupported }
+        ?: BouncerSceneLayout.STANDARD_BOUNCER
+}
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 9c53afe..a2a91fc 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
@@ -78,6 +78,18 @@
     const val EDIT_MODE_TO_HUB_GRID_END_MS =
         EDIT_MODE_TO_HUB_GRID_DELAY_MS + EDIT_MODE_TO_HUB_CONTENT_MS
     const val HUB_TO_EDIT_MODE_CONTENT_MS = 250
+    const val TO_GLANCEABLE_HUB_DURATION_MS = 1000
+}
+
+val sceneTransitionsV2 = transitions {
+    to(CommunalScenes.Communal) {
+        spec = tween(durationMillis = TransitionDuration.TO_GLANCEABLE_HUB_DURATION_MS)
+        fade(AllElements)
+    }
+    to(CommunalScenes.Blank) {
+        spec = tween(durationMillis = TO_GONE_DURATION.toInt(DurationUnit.MILLISECONDS))
+        fade(AllElements)
+    }
 }
 
 val sceneTransitions = transitions {
@@ -157,7 +169,7 @@
         MutableSceneTransitionLayoutState(
             initialScene = currentSceneKey,
             canChangeScene = { _ -> viewModel.canChangeScene() },
-            transitions = sceneTransitions,
+            transitions = if (viewModel.v2FlagEnabled()) sceneTransitionsV2 else sceneTransitions,
         )
     }
     val isUiBlurred by viewModel.isUiBlurred.collectAsStateWithLifecycle()
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 46e0efa..183929c 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
@@ -43,6 +43,7 @@
 import androidx.compose.foundation.layout.imeAnimationTarget
 import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawing
 import androidx.compose.foundation.layout.systemBars
 import androidx.compose.foundation.layout.windowInsetsBottomHeight
 import androidx.compose.foundation.overscroll
@@ -180,10 +181,12 @@
     stackScrollView: NotificationScrollView,
     viewModel: NotificationsPlaceholderViewModel,
 ) {
+
     val isHeadsUp by viewModel.isHeadsUpOrAnimatingAway.collectAsStateWithLifecycle(false)
 
     var scrollOffset by remember { mutableFloatStateOf(0f) }
-    val minScrollOffset = -(stackScrollView.getHeadsUpInset().toFloat())
+    val headsUpInset = with(LocalDensity.current) { headsUpTopInset().toPx() }
+    val minScrollOffset = -headsUpInset
     val maxScrollOffset = 0f
 
     val scrollableState = rememberScrollableState { delta ->
@@ -241,6 +244,12 @@
     )
 }
 
+/** Y position of the HUNs at rest, when the shade is closed. */
+@Composable
+fun headsUpTopInset(): Dp =
+    WindowInsets.safeDrawing.asPaddingValues().calculateTopPadding() +
+        dimensionResource(R.dimen.heads_up_status_bar_padding)
+
 /** Adds the space where notification stack should appear in the scene. */
 @Composable
 fun ContentScope.ConstrainedNotificationStack(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
index 75226b3..2e1100a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
@@ -19,6 +19,7 @@
 import android.annotation.StringRes
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
@@ -31,7 +32,6 @@
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.verticalScroll
-import androidx.compose.material3.HorizontalDivider
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Surface
 import androidx.compose.material3.Text
@@ -45,6 +45,7 @@
 import androidx.compose.ui.res.dimensionResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.systemui.compose.modifiers.sysuiResTag
@@ -60,7 +61,11 @@
  *   the Activity/Fragment/View hosting this Composable once a result is available.
  */
 @Composable
-fun PeopleScreen(viewModel: PeopleViewModel, onResult: (PeopleViewModel.Result) -> Unit) {
+fun PeopleScreen(
+    viewModel: PeopleViewModel,
+    onResult: (PeopleViewModel.Result) -> Unit,
+    modifier: Modifier = Modifier,
+) {
     val priorityTiles by viewModel.priorityTiles.collectAsStateWithLifecycle()
     val recentTiles by viewModel.recentTiles.collectAsStateWithLifecycle()
 
@@ -74,7 +79,7 @@
         }
     }
 
-    Surface(color = MaterialTheme.colorScheme.background, modifier = Modifier.fillMaxSize()) {
+    Surface(color = MaterialTheme.colorScheme.background, modifier = modifier.fillMaxSize()) {
         if (priorityTiles.isNotEmpty() || recentTiles.isNotEmpty()) {
             PeopleScreenWithConversations(priorityTiles, recentTiles, viewModel.onTileClicked)
         } else {
@@ -88,9 +93,10 @@
     priorityTiles: List<PeopleTileViewModel>,
     recentTiles: List<PeopleTileViewModel>,
     onTileClicked: (PeopleTileViewModel) -> Unit,
+    modifier: Modifier = Modifier,
 ) {
     Column(
-        Modifier.fillMaxSize().safeDrawingPadding().sysuiResTag("top_level_with_conversations")
+        modifier.fillMaxSize().safeDrawingPadding().sysuiResTag("top_level_with_conversations")
     ) {
         Column(
             Modifier.fillMaxWidth().padding(PeopleSpacePadding),
@@ -139,28 +145,32 @@
     @StringRes headerTextResource: Int,
     tiles: List<PeopleTileViewModel>,
     onTileClicked: (PeopleTileViewModel) -> Unit,
+    modifier: Modifier = Modifier,
 ) {
-    Text(
-        stringResource(headerTextResource),
-        Modifier.padding(start = 16.dp),
-        style = MaterialTheme.typography.labelLarge,
-        color = MaterialTheme.colorScheme.primary,
-    )
+    val largeCornerRadius = dimensionResource(R.dimen.people_space_widget_radius)
+    val smallCornerRadius = 4.dp
 
-    Spacer(Modifier.height(10.dp))
+    fun topRadius(i: Int): Dp = if (i == 0) largeCornerRadius else smallCornerRadius
+    fun bottomRadius(i: Int): Dp =
+        if (i == tiles.lastIndex) largeCornerRadius else smallCornerRadius
 
-    tiles.forEachIndexed { index, tile ->
-        if (index > 0) {
-            HorizontalDivider(color = MaterialTheme.colorScheme.background, thickness = 2.dp)
-        }
+    Column(modifier, verticalArrangement = Arrangement.spacedBy(2.dp)) {
+        Text(
+            stringResource(headerTextResource),
+            Modifier.padding(start = 16.dp, bottom = 8.dp),
+            style = MaterialTheme.typography.labelLarge,
+            color = MaterialTheme.colorScheme.primary,
+        )
 
-        key(tile.key.toString()) {
-            Tile(
-                tile,
-                onTileClicked,
-                withTopCornerRadius = index == 0,
-                withBottomCornerRadius = index == tiles.lastIndex,
-            )
+        tiles.forEachIndexed { index, tile ->
+            key(tile.key.toString()) {
+                Tile(
+                    tile,
+                    onTileClicked,
+                    topCornerRadius = topRadius(index),
+                    bottomCornerRadius = bottomRadius(index),
+                )
+            }
         }
     }
 }
@@ -169,14 +179,12 @@
 private fun Tile(
     tile: PeopleTileViewModel,
     onTileClicked: (PeopleTileViewModel) -> Unit,
-    withTopCornerRadius: Boolean,
-    withBottomCornerRadius: Boolean,
+    topCornerRadius: Dp,
+    bottomCornerRadius: Dp,
+    modifier: Modifier = Modifier,
 ) {
-    val cornerRadius = dimensionResource(R.dimen.people_space_widget_radius)
-    val topCornerRadius = if (withTopCornerRadius) cornerRadius else 0.dp
-    val bottomCornerRadius = if (withBottomCornerRadius) cornerRadius else 0.dp
-
     Surface(
+        modifier,
         color = MaterialTheme.colorScheme.secondaryContainer,
         shape =
             RoundedCornerShape(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt
index 527314d..d4dea65 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt
@@ -44,9 +44,9 @@
 import com.android.systemui.res.R
 
 @Composable
-internal fun PeopleScreenEmpty(onGotItClicked: () -> Unit) {
+internal fun PeopleScreenEmpty(onGotItClicked: () -> Unit, modifier: Modifier = Modifier) {
     Column(
-        Modifier.fillMaxSize().safeDrawingPadding().padding(PeopleSpacePadding),
+        modifier.fillMaxSize().safeDrawingPadding().padding(PeopleSpacePadding),
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
         Text(
@@ -74,8 +74,9 @@
 }
 
 @Composable
-private fun ExampleTile() {
+private fun ExampleTile(modifier: Modifier = Modifier) {
     Surface(
+        modifier,
         shape = RoundedCornerShape(28.dp),
         color = MaterialTheme.colorScheme.secondaryContainer,
     ) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
index e4f4df3..9ee25c3 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
@@ -24,6 +24,7 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
@@ -34,6 +35,7 @@
 import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.lifecycle.rememberViewModel
 import com.android.systemui.notifications.ui.composable.SnoozeableHeadsUpNotificationSpace
+import com.android.systemui.notifications.ui.composable.headsUpTopInset
 import com.android.systemui.qs.ui.composable.QuickSettings
 import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaLandscapeTopOffset
 import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaOffset.Default
@@ -78,6 +80,8 @@
             }
         }
 
+        val headsUpInset = with(LocalDensity.current) { headsUpTopInset().toPx() }
+
         LaunchedEffect(isIdleAndNotOccluded) {
             // Wait for being Idle on this Scene, otherwise LaunchedEffect would fire too soon,
             // and another transition could override the NSSL stack bounds.
@@ -86,7 +90,7 @@
                 // and not to confuse the StackScrollAlgorithm when it displays a HUN over GONE.
                 notificationStackScrolLView.get().apply {
                     // use -headsUpInset to allow HUN translation outside bounds for snoozing
-                    setStackTop(-getHeadsUpInset().toFloat())
+                    setStackTop(-headsUpInset)
                     setStackCutoff(0f)
                 }
             }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index c3dc84d..a6a6362 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -33,6 +33,7 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
@@ -43,11 +44,13 @@
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
 import com.android.compose.animation.scene.observableTransitionState
+import com.android.systemui.lifecycle.rememberActivated
 import com.android.systemui.qs.ui.adapter.QSSceneAdapter
 import com.android.systemui.qs.ui.composable.QuickSettingsTheme
 import com.android.systemui.ribbon.ui.composable.BottomRightCornerRibbon
 import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
 import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.ui.view.SceneJankMonitor
 import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
 import javax.inject.Provider
 
@@ -82,16 +85,38 @@
     sceneTransitions: SceneTransitions,
     dataSourceDelegator: SceneDataSourceDelegator,
     qsSceneAdapter: Provider<QSSceneAdapter>,
+    sceneJankMonitorFactory: SceneJankMonitor.Factory,
     modifier: Modifier = Modifier,
 ) {
     val coroutineScope = rememberCoroutineScope()
-    val state: MutableSceneTransitionLayoutState = remember {
-        MutableSceneTransitionLayoutState(
-            initialScene = initialSceneKey,
-            canChangeScene = { toScene -> viewModel.canChangeScene(toScene) },
-            transitions = sceneTransitions,
-        )
-    }
+
+    val view = LocalView.current
+    val sceneJankMonitor =
+        rememberActivated(traceName = "sceneJankMonitor") { sceneJankMonitorFactory.create() }
+
+    val state: MutableSceneTransitionLayoutState =
+        remember(view, sceneJankMonitor) {
+            MutableSceneTransitionLayoutState(
+                initialScene = initialSceneKey,
+                canChangeScene = { toScene -> viewModel.canChangeScene(toScene) },
+                transitions = sceneTransitions,
+                onTransitionStart = { transition ->
+                    sceneJankMonitor.onTransitionStart(
+                        view = view,
+                        from = transition.fromContent,
+                        to = transition.toContent,
+                        cuj = transition.cuj,
+                    )
+                },
+                onTransitionEnd = { transition ->
+                    sceneJankMonitor.onTransitionEnd(
+                        from = transition.fromContent,
+                        to = transition.toContent,
+                        cuj = transition.cuj,
+                    )
+                },
+            )
+        }
 
     DisposableEffect(state) {
         val dataSource = SceneTransitionLayoutDataSource(state, coroutineScope)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
index a27bb0c..aa8b4ae 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
@@ -3,6 +3,7 @@
 import androidx.compose.animation.core.spring
 import com.android.compose.animation.scene.TransitionKey
 import com.android.compose.animation.scene.transitions
+import com.android.internal.jank.Cuj
 import com.android.systemui.notifications.ui.composable.Notifications
 import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.Scenes
@@ -56,14 +57,41 @@
     from(Scenes.Dream, to = Scenes.Bouncer) { dreamToBouncerTransition() }
     from(Scenes.Dream, to = Scenes.Communal) { dreamToCommunalTransition() }
     from(Scenes.Dream, to = Scenes.Gone) { dreamToGoneTransition() }
-    from(Scenes.Dream, to = Scenes.Shade) { dreamToShadeTransition() }
-    from(Scenes.Gone, to = Scenes.Shade) { goneToShadeTransition() }
-    from(Scenes.Gone, to = Scenes.Shade, key = ToSplitShade) { goneToSplitShadeTransition() }
-    from(Scenes.Gone, to = Scenes.Shade, key = SlightlyFasterShadeCollapse) {
+    from(Scenes.Dream, to = Scenes.Shade, cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE) {
+        dreamToShadeTransition()
+    }
+    from(Scenes.Gone, to = Scenes.Shade, cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE) {
+        goneToShadeTransition()
+    }
+    from(
+        Scenes.Gone,
+        to = Scenes.Shade,
+        key = ToSplitShade,
+        cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE,
+    ) {
+        goneToSplitShadeTransition()
+    }
+    from(
+        Scenes.Gone,
+        to = Scenes.Shade,
+        key = SlightlyFasterShadeCollapse,
+        cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE,
+    ) {
         goneToShadeTransition(durationScale = 0.9)
     }
-    from(Scenes.Gone, to = Scenes.QuickSettings) { goneToQuickSettingsTransition() }
-    from(Scenes.Gone, to = Scenes.QuickSettings, key = SlightlyFasterShadeCollapse) {
+    from(
+        Scenes.Gone,
+        to = Scenes.QuickSettings,
+        cuj = Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE,
+    ) {
+        goneToQuickSettingsTransition()
+    }
+    from(
+        Scenes.Gone,
+        to = Scenes.QuickSettings,
+        key = SlightlyFasterShadeCollapse,
+        cuj = Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE,
+    ) {
         goneToQuickSettingsTransition(durationScale = 0.9)
     }
 
@@ -78,49 +106,112 @@
     }
     from(Scenes.Lockscreen, to = Scenes.Communal) { lockscreenToCommunalTransition() }
     from(Scenes.Lockscreen, to = Scenes.Dream) { lockscreenToDreamTransition() }
-    from(Scenes.Lockscreen, to = Scenes.Shade) { lockscreenToShadeTransition() }
-    from(Scenes.Lockscreen, to = Scenes.Shade, key = ToSplitShade) {
+    from(Scenes.Lockscreen, to = Scenes.Shade, cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE) {
+        lockscreenToShadeTransition()
+    }
+    from(
+        Scenes.Lockscreen,
+        to = Scenes.Shade,
+        key = ToSplitShade,
+        cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE,
+    ) {
         lockscreenToSplitShadeTransition()
         sharedElement(Shade.Elements.BackgroundScrim, enabled = false)
     }
-    from(Scenes.Lockscreen, to = Scenes.Shade, key = SlightlyFasterShadeCollapse) {
+    from(
+        Scenes.Lockscreen,
+        to = Scenes.Shade,
+        key = SlightlyFasterShadeCollapse,
+        cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE,
+    ) {
         lockscreenToShadeTransition(durationScale = 0.9)
     }
-    from(Scenes.Lockscreen, to = Scenes.QuickSettings) { lockscreenToQuickSettingsTransition() }
+    from(
+        Scenes.Lockscreen,
+        to = Scenes.QuickSettings,
+        cuj = Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE,
+    ) {
+        lockscreenToQuickSettingsTransition()
+    }
     from(Scenes.Lockscreen, to = Scenes.Gone) { lockscreenToGoneTransition() }
-    from(Scenes.QuickSettings, to = Scenes.Shade) {
+    from(
+        Scenes.QuickSettings,
+        to = Scenes.Shade,
+        cuj = Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE,
+    ) {
         reversed { shadeToQuickSettingsTransition() }
         sharedElement(Notifications.Elements.HeadsUpNotificationPlaceholder, enabled = false)
     }
-    from(Scenes.Shade, to = Scenes.QuickSettings) { shadeToQuickSettingsTransition() }
-    from(Scenes.Shade, to = Scenes.Lockscreen) {
+    from(
+        Scenes.Shade,
+        to = Scenes.QuickSettings,
+        cuj = Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE,
+    ) {
+        shadeToQuickSettingsTransition()
+    }
+    from(Scenes.Shade, to = Scenes.Lockscreen, cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE) {
         reversed { lockscreenToShadeTransition() }
         sharedElement(Notifications.Elements.NotificationStackPlaceholder, enabled = false)
         sharedElement(Notifications.Elements.HeadsUpNotificationPlaceholder, enabled = false)
     }
-    from(Scenes.Shade, to = Scenes.Lockscreen, key = ToSplitShade) {
+    from(
+        Scenes.Shade,
+        to = Scenes.Lockscreen,
+        key = ToSplitShade,
+        cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE,
+    ) {
         reversed { lockscreenToSplitShadeTransition() }
     }
-    from(Scenes.Communal, to = Scenes.Shade) { communalToShadeTransition() }
+    from(Scenes.Communal, to = Scenes.Shade, cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE) {
+        communalToShadeTransition()
+    }
     from(Scenes.Communal, to = Scenes.Bouncer) { communalToBouncerTransition() }
 
     // Overlay transitions
 
-    to(Overlays.NotificationsShade) { toNotificationsShadeTransition() }
-    to(Overlays.QuickSettingsShade) { toQuickSettingsShadeTransition() }
-    from(Overlays.NotificationsShade, to = Overlays.QuickSettingsShade) {
+    to(Overlays.NotificationsShade, cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE) {
+        toNotificationsShadeTransition()
+    }
+    to(Overlays.QuickSettingsShade, cuj = Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE) {
+        toQuickSettingsShadeTransition()
+    }
+    from(
+        Overlays.NotificationsShade,
+        to = Overlays.QuickSettingsShade,
+        cuj = Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE,
+    ) {
         notificationsShadeToQuickSettingsShadeTransition()
     }
-    from(Scenes.Gone, to = Overlays.NotificationsShade, key = SlightlyFasterShadeCollapse) {
+    from(
+        Scenes.Gone,
+        to = Overlays.NotificationsShade,
+        key = SlightlyFasterShadeCollapse,
+        cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE,
+    ) {
         toNotificationsShadeTransition(durationScale = 0.9)
     }
-    from(Scenes.Gone, to = Overlays.QuickSettingsShade, key = SlightlyFasterShadeCollapse) {
+    from(
+        Scenes.Gone,
+        to = Overlays.QuickSettingsShade,
+        key = SlightlyFasterShadeCollapse,
+        cuj = Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE,
+    ) {
         toQuickSettingsShadeTransition(durationScale = 0.9)
     }
-    from(Scenes.Lockscreen, to = Overlays.NotificationsShade, key = SlightlyFasterShadeCollapse) {
+    from(
+        Scenes.Lockscreen,
+        to = Overlays.NotificationsShade,
+        key = SlightlyFasterShadeCollapse,
+        cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE,
+    ) {
         toNotificationsShadeTransition(durationScale = 0.9)
     }
-    from(Scenes.Lockscreen, to = Overlays.QuickSettingsShade, key = SlightlyFasterShadeCollapse) {
+    from(
+        Scenes.Lockscreen,
+        to = Overlays.QuickSettingsShade,
+        key = SlightlyFasterShadeCollapse,
+        cuj = Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE,
+    ) {
         toQuickSettingsShadeTransition(durationScale = 0.9)
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt
index 93c10b6..6ca9c79 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt
@@ -17,17 +17,12 @@
 package com.android.systemui.scene.ui.composable.transitions
 
 import androidx.compose.animation.core.tween
-import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.TransitionBuilder
 import com.android.systemui.communal.ui.compose.AllElements
-import com.android.systemui.communal.ui.compose.Communal
 
 fun TransitionBuilder.dreamToCommunalTransition() {
     spec = tween(durationMillis = 1000)
 
-    // Translate communal hub grid from the end direction.
-    translate(Communal.Elements.Grid, Edge.End)
-
-    // Fade all communal hub elements.
-    timestampRange(startMillis = 167, endMillis = 334) { fade(AllElements) }
+    // Fade in all communal hub elements.
+    fade(AllElements)
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt
index 826a255..de9a78c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt
@@ -17,21 +17,12 @@
 package com.android.systemui.scene.ui.composable.transitions
 
 import androidx.compose.animation.core.tween
-import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.TransitionBuilder
 import com.android.systemui.communal.ui.compose.AllElements
-import com.android.systemui.communal.ui.compose.Communal
-import com.android.systemui.scene.shared.model.Scenes
 
 fun TransitionBuilder.lockscreenToCommunalTransition() {
     spec = tween(durationMillis = 1000)
 
-    // Translate lockscreen to the start direction.
-    translate(Scenes.Lockscreen.rootElementKey, Edge.Start)
-
-    // Translate communal hub grid from the end direction.
-    translate(Communal.Elements.Grid, Edge.End)
-
-    // Fade all communal hub elements.
-    timestampRange(startMillis = 167, endMillis = 334) { fade(AllElements) }
+    // Fade all communal hub elements in.
+    fade(AllElements)
 }
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 6bb579d..2ca8464 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
@@ -60,17 +60,12 @@
      * Stop the current drag with the given [velocity].
      *
      * @param velocity The velocity of the drag when it stopped.
-     * @param canChangeContent Whether the content can be changed as a result of this drag.
      * @return the consumed [velocity] when the animation complete
      */
-    suspend fun onStop(velocity: Float, canChangeContent: Boolean): Float
+    suspend fun onStop(velocity: Float): Float
 
-    /**
-     * Cancels the current drag.
-     *
-     * @param canChangeContent Whether the content can be changed as a result of this drag.
-     */
-    fun onCancel(canChangeContent: Boolean)
+    /** Cancels the current drag. */
+    fun onCancel()
 }
 
 internal class DraggableHandlerImpl(
@@ -295,17 +290,16 @@
         return newOffset - previousOffset
     }
 
-    override suspend fun onStop(velocity: Float, canChangeContent: Boolean): Float {
+    override suspend fun onStop(velocity: Float): Float {
         // To ensure that any ongoing animation completes gracefully and avoids an undefined state,
         // we execute the actual `onStop` logic in a non-cancellable context. This prevents the
         // coroutine from being cancelled prematurely, which could interrupt the animation.
         // TODO(b/378470603) Remove this check once NestedDraggable is used to handle drags.
-        return withContext(NonCancellable) { onStop(velocity, canChangeContent, swipeAnimation) }
+        return withContext(NonCancellable) { onStop(velocity, swipeAnimation) }
     }
 
     private suspend fun <T : ContentKey> onStop(
         velocity: Float,
-        canChangeContent: Boolean,
 
         // Important: Make sure that this has the same name as [this.swipeAnimation] so that all the
         // code here references the current animation when [onDragStopped] is called, otherwise the
@@ -319,35 +313,27 @@
         }
 
         val fromContent = swipeAnimation.fromContent
+        // If we are halfway between two contents, we check what the target will be based on
+        // the velocity and offset of the transition, then we launch the animation.
+
+        val toContent = swipeAnimation.toContent
+
+        // Compute the destination content (and therefore offset) to settle in.
+        val offset = swipeAnimation.dragOffset
+        val distance = swipeAnimation.distance()
         val targetContent =
-            if (canChangeContent) {
-                // If we are halfway between two contents, we check what the target will be based on
-                // the velocity and offset of the transition, then we launch the animation.
-
-                val toContent = swipeAnimation.toContent
-
-                // Compute the destination content (and therefore offset) to settle in.
-                val offset = swipeAnimation.dragOffset
-                val distance = swipeAnimation.distance()
-                if (
-                    distance != DistanceUnspecified &&
-                        shouldCommitSwipe(
-                            offset = offset,
-                            distance = distance,
-                            velocity = velocity,
-                            wasCommitted = swipeAnimation.currentContent == toContent,
-                            requiresFullDistanceSwipe = swipeAnimation.requiresFullDistanceSwipe,
-                        )
-                ) {
-                    toContent
-                } else {
-                    fromContent
-                }
+            if (
+                distance != DistanceUnspecified &&
+                    shouldCommitSwipe(
+                        offset = offset,
+                        distance = distance,
+                        velocity = velocity,
+                        wasCommitted = swipeAnimation.currentContent == toContent,
+                        requiresFullDistanceSwipe = swipeAnimation.requiresFullDistanceSwipe,
+                    )
+            ) {
+                toContent
             } else {
-                // We are doing an overscroll preview animation between scenes.
-                check(fromContent == swipeAnimation.currentContent) {
-                    "canChangeContent is false but currentContent != fromContent"
-                }
                 fromContent
             }
 
@@ -423,10 +409,8 @@
         }
     }
 
-    override fun onCancel(canChangeContent: Boolean) {
-        swipeAnimation.contentTransition.coroutineScope.launch {
-            onStop(velocity = 0f, canChangeContent = canChangeContent)
-        }
+    override fun onCancel() {
+        swipeAnimation.contentTransition.coroutineScope.launch { onStop(velocity = 0f) }
     }
 }
 
@@ -445,6 +429,58 @@
     }
 
     /**
+     * Finds the best matching [UserActionResult] for the given [swipe] within this [Content].
+     * Prioritizes actions with matching [Swipe.Resolved.fromSource].
+     *
+     * @param swipe The swipe to match against.
+     * @return The best matching [UserActionResult], or `null` if no match is found.
+     */
+    private fun Content.findActionResultBestMatch(swipe: Swipe.Resolved): UserActionResult? {
+        if (!areSwipesAllowed()) {
+            return null
+        }
+
+        var bestPoints = Int.MIN_VALUE
+        var bestMatch: UserActionResult? = null
+        userActions.forEach { (actionSwipe, actionResult) ->
+            if (
+                actionSwipe !is Swipe.Resolved ||
+                    // The direction must match.
+                    actionSwipe.direction != swipe.direction ||
+                    // The number of pointers down must match.
+                    actionSwipe.pointerCount != swipe.pointerCount ||
+                    // The action requires a specific fromSource.
+                    (actionSwipe.fromSource != null &&
+                        actionSwipe.fromSource != swipe.fromSource) ||
+                    // The action requires a specific pointerType.
+                    (actionSwipe.pointersType != null &&
+                        actionSwipe.pointersType != swipe.pointersType)
+            ) {
+                // This action is not eligible.
+                return@forEach
+            }
+
+            val sameFromSource = actionSwipe.fromSource == swipe.fromSource
+            val samePointerType = actionSwipe.pointersType == swipe.pointersType
+            // Prioritize actions with a perfect match.
+            if (sameFromSource && samePointerType) {
+                return actionResult
+            }
+
+            var points = 0
+            if (sameFromSource) points++
+            if (samePointerType) points++
+
+            // Otherwise, keep track of the best eligible action.
+            if (points > bestPoints) {
+                bestPoints = points
+                bestMatch = actionResult
+            }
+        }
+        return bestMatch
+    }
+
+    /**
      * Update the swipes results.
      *
      * Usually we don't want to update them while doing a drag, because this could change the target
@@ -519,11 +555,11 @@
         }
 
         override suspend fun OnStopScope.onStop(initialVelocity: Float): Float {
-            return dragController.onStop(velocity = initialVelocity, canChangeContent = true)
+            return dragController.onStop(velocity = initialVelocity)
         }
 
         override fun onCancel() {
-            dragController.onCancel(canChangeContent = true)
+            dragController.onCancel()
         }
 
         /**
@@ -547,9 +583,9 @@
 private object NoOpDragController : DragController {
     override fun onDrag(delta: Float) = 0f
 
-    override suspend fun onStop(velocity: Float, canChangeContent: Boolean) = 0f
+    override suspend fun onStop(velocity: Float) = 0f
 
-    override fun onCancel(canChangeContent: Boolean) {
+    override fun onCancel() {
         /* do nothing */
     }
 }
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 f5f01d4..89320f13 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
@@ -307,13 +307,13 @@
                                             velocityTracker.calculateVelocity(maxVelocity)
                                         }
                                         .toFloat(),
-                                onFling = { controller.onStop(it, canChangeContent = true) },
+                                onFling = { controller.onStop(it) },
                             )
                         },
                         onDragCancel = { controller ->
                             startFlingGesture(
                                 initialVelocity = 0f,
-                                onFling = { controller.onStop(it, canChangeContent = true) },
+                                onFling = { controller.onStop(it) },
                             )
                         },
                         swipeDetector = swipeDetector,
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 c704a3e..de428a7 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
@@ -35,6 +35,7 @@
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
+import com.android.compose.gesture.NestedScrollableBound
 import com.android.compose.gesture.effect.ContentOverscrollEffect
 
 /**
@@ -238,6 +239,18 @@
     fun Modifier.noResizeDuringTransitions(): Modifier
 
     /**
+     * Temporarily disable this content swipe actions when any scrollable below this modifier has
+     * consumed any amount of scroll delta, until the scroll gesture is finished.
+     *
+     * This can for instance be used to ensure that a scrollable list is overscrolled once it
+     * reached its bounds instead of directly starting a scene transition from the same scroll
+     * gesture.
+     */
+    fun Modifier.disableSwipesWhenScrolling(
+        bounds: NestedScrollableBound = NestedScrollableBound.Any
+    ): Modifier
+
+    /**
      * A [NestedSceneTransitionLayout] will share its elements with its ancestor STLs therefore
      * enabling sharedElement transitions between them.
      */
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 568a358..8153586 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
@@ -232,6 +232,8 @@
     canShowOverlay: (OverlayKey) -> Boolean = { true },
     canHideOverlay: (OverlayKey) -> Boolean = { true },
     canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ -> true },
+    onTransitionStart: (TransitionState.Transition) -> Unit = {},
+    onTransitionEnd: (TransitionState.Transition) -> Unit = {},
 ): MutableSceneTransitionLayoutState {
     return MutableSceneTransitionLayoutStateImpl(
         initialScene,
@@ -241,6 +243,8 @@
         canShowOverlay,
         canHideOverlay,
         canReplaceOverlay,
+        onTransitionStart,
+        onTransitionEnd,
     )
 }
 
@@ -252,7 +256,11 @@
     internal val canChangeScene: (SceneKey) -> Boolean = { true },
     internal val canShowOverlay: (OverlayKey) -> Boolean = { true },
     internal val canHideOverlay: (OverlayKey) -> Boolean = { true },
-    internal val canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ -> true },
+    internal val canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ ->
+        true
+    },
+    private val onTransitionStart: (TransitionState.Transition) -> Unit = {},
+    private val onTransitionEnd: (TransitionState.Transition) -> Unit = {},
 ) : MutableSceneTransitionLayoutState {
     private val creationThread: Thread = Thread.currentThread()
 
@@ -367,9 +375,11 @@
             startTransitionInternal(transition, chain)
 
             // Run the transition until it is finished.
+            onTransitionStart(transition)
             transition.runInternal()
         } finally {
             finishTransition(transition)
+            onTransitionEnd(transition)
         }
     }
 
@@ -384,14 +394,10 @@
         val toContent = transition.toContent
 
         // Update the transition specs.
-        transition.transformationSpec =
-            transitions
-                .transitionSpec(fromContent, toContent, key = transition.key)
-                .transformationSpec(transition)
-        transition.previewTransformationSpec =
-            transitions
-                .transitionSpec(fromContent, toContent, key = transition.key)
-                .previewTransformationSpec(transition)
+        val spec = transitions.transitionSpec(fromContent, toContent, key = transition.key)
+        transition._cuj = spec.cuj
+        transition.transformationSpec = spec.transformationSpec(transition)
+        transition.previewTransformationSpec = spec.previewTransformationSpec(transition)
     }
 
     private fun startTransitionInternal(transition: TransitionState.Transition, chain: Boolean) {
@@ -411,9 +417,7 @@
                     if (tooManyTransitions) logTooManyTransitions()
 
                     // Force finish all transitions.
-                    while (currentTransitions.isNotEmpty()) {
-                        finishTransition(transitionStates[0] as TransitionState.Transition)
-                    }
+                    currentTransitions.fastForEach { finishTransition(it) }
 
                     // We finished all transitions, so we are now idle. We remove this state so that
                     // we end up only with the new transition after appending it.
@@ -475,46 +479,36 @@
         // Mark this transition as finished.
         finishedTransitions.add(transition)
 
-        // Keep a reference to the last transition, in case we remove all transitions and should
-        // settle to Idle.
+        if (finishedTransitions.size != transitionStates.size) {
+            // Some transitions were not finished, so we won't settle to idle.
+            return
+        }
+
+        // Keep a reference to the last transition, in case all transitions are finished and we
+        // should settle to Idle.
         val lastTransition = transitionStates.last()
 
-        // Remove all first n finished transitions.
-        var i = 0
-        val nStates = transitionStates.size
-        while (i < nStates) {
-            val t = transitionStates[i]
-            if (!finishedTransitions.contains(t)) {
-                // Stop here.
-                break
+        transitionStates.fastForEach { state ->
+            if (!finishedTransitions.contains(state)) {
+                // Some transitions were not finished, so we won't settle to idle.
+                return
             }
-
-            // Remove the transition from the set of finished transitions.
-            finishedTransitions.remove(t)
-            i++
         }
 
-        // If all transitions are finished, we are idle.
-        if (i == nStates) {
-            check(finishedTransitions.isEmpty())
-            val idle =
-                TransitionState.Idle(lastTransition.currentScene, lastTransition.currentOverlays)
-            Log.i(TAG, "all transitions finished. idle=$idle")
-            this.transitionStates = listOf(idle)
-        } else if (i > 0) {
-            this.transitionStates = transitionStates.subList(fromIndex = i, toIndex = nStates)
-        }
+        val idle = TransitionState.Idle(lastTransition.currentScene, lastTransition.currentOverlays)
+        Log.i(TAG, "all transitions finished. idle=$idle")
+        finishedTransitions.clear()
+        this.transitionStates = listOf(idle)
     }
 
     override fun snapToScene(scene: SceneKey, currentOverlays: Set<OverlayKey>) {
         checkThread()
 
         // Force finish all transitions.
-        while (currentTransitions.isNotEmpty()) {
-            finishTransition(transitionStates[0] as TransitionState.Transition)
-        }
+        currentTransitions.fastForEach { finishTransition(it) }
 
         check(transitionStates.size == 1)
+        check(currentTransitions.isEmpty())
         transitionStates = listOf(TransitionState.Idle(scene, currentOverlays))
     }
 
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
index 294022a..d50304d4 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
@@ -29,6 +29,7 @@
 import com.android.compose.animation.scene.transformation.SharedElementTransformation
 import com.android.compose.animation.scene.transformation.TransformationMatcher
 import com.android.compose.animation.scene.transformation.TransformationWithRange
+import com.android.internal.jank.Cuj.CujType
 
 /** The transitions configuration of a [SceneTransitionLayout]. */
 class SceneTransitions
@@ -111,7 +112,15 @@
     }
 
     private fun defaultTransition(from: ContentKey, to: ContentKey) =
-        TransitionSpecImpl(key = null, from, to, null, null, TransformationSpec.EmptyProvider)
+        TransitionSpecImpl(
+            key = null,
+            from,
+            to,
+            cuj = null,
+            previewTransformationSpec = null,
+            reversePreviewTransformationSpec = null,
+            TransformationSpec.EmptyProvider,
+        )
 
     companion object {
         internal val DefaultSwipeSpec =
@@ -147,6 +156,9 @@
      */
     val to: ContentKey?
 
+    /** The CUJ covered by this transition. */
+    @CujType val cuj: Int?
+
     /**
      * Return a reversed version of this [TransitionSpec] for a transition going from [to] to
      * [from].
@@ -213,6 +225,7 @@
     override val key: TransitionKey?,
     override val from: ContentKey?,
     override val to: ContentKey?,
+    override val cuj: Int?,
     private val previewTransformationSpec:
         ((TransitionState.Transition) -> TransformationSpecImpl)? =
         null,
@@ -226,6 +239,7 @@
             key = key,
             from = to,
             to = from,
+            cuj = cuj,
             previewTransformationSpec = reversePreviewTransformationSpec,
             reversePreviewTransformationSpec = previewTransformationSpec,
             transformationSpec = { transition ->
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 c5b3df2..e221211 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
@@ -37,11 +37,7 @@
     draggableHandler: DraggableHandlerImpl,
     swipeDetector: SwipeDetector,
 ): Modifier {
-    return if (draggableHandler.enabled()) {
-        this.then(SwipeToSceneElement(draggableHandler, swipeDetector))
-    } else {
-        this
-    }
+    return then(SwipeToSceneElement(draggableHandler, swipeDetector, draggableHandler.enabled()))
 }
 
 private fun DraggableHandlerImpl.enabled(): Boolean {
@@ -54,87 +50,69 @@
 
 /** Whether swipe should be enabled in the given [orientation]. */
 internal fun Content.shouldEnableSwipes(orientation: Orientation): Boolean {
-    if (userActions.isEmpty()) {
+    if (userActions.isEmpty() || !areSwipesAllowed()) {
         return false
     }
 
     return userActions.keys.any { it is Swipe.Resolved && it.direction.orientation == orientation }
 }
 
-/**
- * Finds the best matching [UserActionResult] for the given [swipe] within this [Content].
- * Prioritizes actions with matching [Swipe.Resolved.fromSource].
- *
- * @param swipe The swipe to match against.
- * @return The best matching [UserActionResult], or `null` if no match is found.
- */
-internal fun Content.findActionResultBestMatch(swipe: Swipe.Resolved): UserActionResult? {
-    var bestPoints = Int.MIN_VALUE
-    var bestMatch: UserActionResult? = null
-    userActions.forEach { (actionSwipe, actionResult) ->
-        if (
-            actionSwipe !is Swipe.Resolved ||
-                // The direction must match.
-                actionSwipe.direction != swipe.direction ||
-                // The number of pointers down must match.
-                actionSwipe.pointerCount != swipe.pointerCount ||
-                // The action requires a specific fromSource.
-                (actionSwipe.fromSource != null && actionSwipe.fromSource != swipe.fromSource) ||
-                // The action requires a specific pointerType.
-                (actionSwipe.pointersType != null && actionSwipe.pointersType != swipe.pointersType)
-        ) {
-            // This action is not eligible.
-            return@forEach
-        }
-
-        val sameFromSource = actionSwipe.fromSource == swipe.fromSource
-        val samePointerType = actionSwipe.pointersType == swipe.pointersType
-        // Prioritize actions with a perfect match.
-        if (sameFromSource && samePointerType) {
-            return actionResult
-        }
-
-        var points = 0
-        if (sameFromSource) points++
-        if (samePointerType) points++
-
-        // Otherwise, keep track of the best eligible action.
-        if (points > bestPoints) {
-            bestPoints = points
-            bestMatch = actionResult
-        }
-    }
-    return bestMatch
-}
-
 private data class SwipeToSceneElement(
     val draggableHandler: DraggableHandlerImpl,
     val swipeDetector: SwipeDetector,
+    val enabled: Boolean,
 ) : ModifierNodeElement<SwipeToSceneRootNode>() {
     override fun create(): SwipeToSceneRootNode =
-        SwipeToSceneRootNode(draggableHandler, swipeDetector)
+        SwipeToSceneRootNode(draggableHandler, swipeDetector, enabled)
 
     override fun update(node: SwipeToSceneRootNode) {
-        node.update(draggableHandler, swipeDetector)
+        node.update(draggableHandler, swipeDetector, enabled)
     }
 }
 
 private class SwipeToSceneRootNode(
     draggableHandler: DraggableHandlerImpl,
     swipeDetector: SwipeDetector,
+    enabled: Boolean,
 ) : DelegatingNode() {
-    private var delegateNode = delegate(SwipeToSceneNode(draggableHandler, swipeDetector))
+    private var delegateNode = if (enabled) create(draggableHandler, swipeDetector) else null
 
-    fun update(draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector) {
-        if (draggableHandler == delegateNode.draggableHandler) {
+    fun update(
+        draggableHandler: DraggableHandlerImpl,
+        swipeDetector: SwipeDetector,
+        enabled: Boolean,
+    ) {
+        // Disabled.
+        if (!enabled) {
+            delegateNode?.let { undelegate(it) }
+            delegateNode = null
+            return
+        }
+
+        // Disabled => Enabled.
+        val nullableDelegate = delegateNode
+        if (nullableDelegate == null) {
+            delegateNode = create(draggableHandler, swipeDetector)
+            return
+        }
+
+        // Enabled => Enabled (update).
+        if (draggableHandler == nullableDelegate.draggableHandler) {
             // Simple update, just update the swipe detector directly and keep the node.
-            delegateNode.swipeDetector = swipeDetector
+            nullableDelegate.swipeDetector = swipeDetector
         } else {
             // The draggableHandler changed, force recreate the underlying SwipeToSceneNode.
-            undelegate(delegateNode)
-            delegateNode = delegate(SwipeToSceneNode(draggableHandler, swipeDetector))
+            undelegate(nullableDelegate)
+            delegateNode = create(draggableHandler, swipeDetector)
         }
     }
+
+    private fun create(
+        draggableHandler: DraggableHandlerImpl,
+        swipeDetector: SwipeDetector,
+    ): SwipeToSceneNode {
+        return delegate(SwipeToSceneNode(draggableHandler, swipeDetector))
+    }
 }
 
 private class SwipeToSceneNode(
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
index 89f900c..776d553 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -25,6 +25,7 @@
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.content.state.TransitionState
 import com.android.compose.animation.scene.transformation.Transformation
+import com.android.internal.jank.Cuj.CujType
 
 /** Define the [transitions][SceneTransitions] to be used with a [SceneTransitionLayout]. */
 fun transitions(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions {
@@ -64,6 +65,7 @@
     fun to(
         to: ContentKey,
         key: TransitionKey? = null,
+        @CujType cuj: Int? = null,
         preview: (TransitionBuilder.() -> Unit)? = null,
         reversePreview: (TransitionBuilder.() -> Unit)? = null,
         builder: TransitionBuilder.() -> Unit = {},
@@ -90,6 +92,7 @@
         from: ContentKey,
         to: ContentKey? = null,
         key: TransitionKey? = null,
+        @CujType cuj: Int? = null,
         preview: (TransitionBuilder.() -> Unit)? = null,
         reversePreview: (TransitionBuilder.() -> Unit)? = null,
         builder: TransitionBuilder.() -> Unit = {},
@@ -146,6 +149,9 @@
      */
     var motionSpatialSpec: SpringSpec<Float>?
 
+    /** The CUJ associated to this transitions. */
+    @CujType var cuj: Int?
+
     /**
      * Define a timestamp-based range for the transformations inside [builder].
      *
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
index fc5da0f..9a9b05e 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
@@ -37,6 +37,7 @@
 import com.android.compose.animation.scene.transformation.TransformationMatcher
 import com.android.compose.animation.scene.transformation.TransformationRange
 import com.android.compose.animation.scene.transformation.Translate
+import com.android.internal.jank.Cuj.CujType
 
 internal fun transitionsImpl(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions {
     val impl = SceneTransitionsBuilderImpl().apply(builder)
@@ -56,28 +57,47 @@
     override fun to(
         to: ContentKey,
         key: TransitionKey?,
+        @CujType cuj: Int?,
         preview: (TransitionBuilder.() -> Unit)?,
         reversePreview: (TransitionBuilder.() -> Unit)?,
         builder: TransitionBuilder.() -> Unit,
     ) {
-        transition(from = null, to = to, key = key, preview, reversePreview, builder)
+        transition(
+            from = null,
+            to = to,
+            key = key,
+            cuj = cuj,
+            preview = preview,
+            reversePreview = reversePreview,
+            builder = builder,
+        )
     }
 
     override fun from(
         from: ContentKey,
         to: ContentKey?,
         key: TransitionKey?,
+        @CujType cuj: Int?,
         preview: (TransitionBuilder.() -> Unit)?,
         reversePreview: (TransitionBuilder.() -> Unit)?,
         builder: TransitionBuilder.() -> Unit,
     ) {
-        transition(from = from, to = to, key = key, preview, reversePreview, builder)
+        transition(
+            from = from,
+            to = to,
+            key = key,
+            cuj = cuj,
+            preview = preview,
+            reversePreview = reversePreview,
+            builder = builder,
+        )
     }
 
     private fun transition(
         from: ContentKey?,
         to: ContentKey?,
         key: TransitionKey?,
+        @CujType cuj: Int?,
         preview: (TransitionBuilder.() -> Unit)?,
         reversePreview: (TransitionBuilder.() -> Unit)?,
         builder: TransitionBuilder.() -> Unit,
@@ -97,9 +117,10 @@
 
         val spec =
             TransitionSpecImpl(
-                key,
-                from,
-                to,
+                key = key,
+                from = from,
+                to = to,
+                cuj = cuj,
                 previewTransformationSpec = preview?.let { { t -> transformationSpec(t, it) } },
                 reversePreviewTransformationSpec =
                     reversePreview?.let { { t -> transformationSpec(t, it) } },
@@ -194,6 +215,7 @@
     override var spec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessLow)
     override var motionSpatialSpec: SpringSpec<Float>? = null
     override var distance: UserActionDistance? = null
+    override var cuj: Int? = null
     private val durationMillis: Int by lazy {
         val spec = spec
         if (spec !is DurationBasedAnimationSpec) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
index 4c15f7a..59b4a09 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
@@ -56,7 +56,10 @@
 import com.android.compose.animation.scene.effect.VisualEffect
 import com.android.compose.animation.scene.element
 import com.android.compose.animation.scene.modifiers.noResizeDuringTransitions
+import com.android.compose.gesture.NestedScrollControlState
+import com.android.compose.gesture.NestedScrollableBound
 import com.android.compose.gesture.effect.OffsetOverscrollEffect
+import com.android.compose.gesture.nestedScrollController
 import com.android.compose.modifiers.thenIf
 import com.android.compose.ui.graphics.ContainerState
 import com.android.compose.ui.graphics.container
@@ -70,7 +73,8 @@
     actions: Map<UserAction.Resolved, UserActionResult>,
     zIndex: Float,
 ) {
-    internal val scope = ContentScopeImpl(layoutImpl, content = this)
+    private val nestedScrollControlState = NestedScrollControlState()
+    internal val scope = ContentScopeImpl(layoutImpl, content = this, nestedScrollControlState)
     val containerState = ContainerState()
 
     var content by mutableStateOf(content)
@@ -101,11 +105,14 @@
             scope.content()
         }
     }
+
+    fun areSwipesAllowed(): Boolean = nestedScrollControlState.isOuterScrollAllowed
 }
 
 internal class ContentScopeImpl(
     private val layoutImpl: SceneTransitionLayoutImpl,
     private val content: Content,
+    private val nestedScrollControlState: NestedScrollControlState,
 ) : ContentScope, ElementStateScope by layoutImpl.elementStateScope {
     override val contentKey: ContentKey
         get() = content.key
@@ -176,6 +183,10 @@
         return noResizeDuringTransitions(layoutState = layoutImpl.state)
     }
 
+    override fun Modifier.disableSwipesWhenScrolling(bounds: NestedScrollableBound): Modifier {
+        return nestedScrollController(nestedScrollControlState, bounds)
+    }
+
     @Composable
     override fun NestedSceneTransitionLayout(
         state: SceneTransitionLayoutState,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
index 161e832..0977226 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
@@ -32,6 +32,7 @@
 import com.android.compose.animation.scene.TransformationSpec
 import com.android.compose.animation.scene.TransformationSpecImpl
 import com.android.compose.animation.scene.TransitionKey
+import com.android.internal.jank.Cuj.CujType
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
@@ -237,6 +238,11 @@
         /** Whether user input is currently driving the transition. */
         abstract val isUserInputOngoing: Boolean
 
+        /** The CUJ covered by this transition. */
+        @CujType
+        val cuj: Int?
+            get() = _cuj
+
         /**
          * The progress of the preview transition. This is usually in the `[0; 1]` range, but it can
          * also be less than `0` or greater than `1` when using transitions with a spring
@@ -251,13 +257,15 @@
         internal open val isInPreviewStage: Boolean = false
 
         /**
-         * The current [TransformationSpecImpl] associated to this transition.
+         * The current [TransformationSpecImpl] and other values associated to this transition from
+         * the spec.
          *
          * Important: These will be set exactly once, when this transition is
          * [started][MutableSceneTransitionLayoutStateImpl.startTransition].
          */
         internal var transformationSpec: TransformationSpecImpl = TransformationSpec.Empty
         internal var previewTransformationSpec: TransformationSpecImpl? = null
+        internal var _cuj: Int? = null
 
         /**
          * An animatable that animates from 1f to 0f. This will be used to nicely animate the sudden
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ContentTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ContentTest.kt
new file mode 100644
index 0000000..06a9735
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ContentTest.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2025 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
+import androidx.compose.foundation.gestures.rememberScrollableState
+import androidx.compose.foundation.gestures.scrollable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performTouchInput
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestScenes.SceneA
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ContentTest {
+    @get:Rule val rule = createComposeRule()
+
+    @Test
+    fun disableSwipesWhenScrolling() {
+        lateinit var layoutImpl: SceneTransitionLayoutImpl
+        rule.setContent {
+            SceneTransitionLayoutForTesting(
+                remember { MutableSceneTransitionLayoutState(SceneA) },
+                onLayoutImpl = { layoutImpl = it },
+            ) {
+                scene(SceneA) {
+                    Box(
+                        Modifier.fillMaxSize()
+                            .disableSwipesWhenScrolling()
+                            .scrollable(rememberScrollableState { it }, Orientation.Vertical)
+                    )
+                }
+            }
+        }
+
+        val content = layoutImpl.content(SceneA)
+        assertThat(content.areSwipesAllowed()).isTrue()
+        rule.onRoot().performTouchInput {
+            down(topLeft)
+            moveBy(bottomLeft)
+        }
+
+        assertThat(content.areSwipesAllowed()).isFalse()
+        rule.onRoot().performTouchInput { up() }
+        assertThat(content.areSwipesAllowed()).isTrue()
+    }
+}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index 6106aed..def8323 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -247,32 +247,26 @@
 
         suspend fun DragController.onDragStoppedAnimateNow(
             velocity: Float,
-            canChangeScene: Boolean = true,
             onAnimationStart: () -> Unit,
             onAnimationEnd: (Float) -> Unit,
         ) {
-            val velocityConsumed = onDragStoppedAnimateLater(velocity, canChangeScene)
+            val velocityConsumed = onDragStoppedAnimateLater(velocity)
             onAnimationStart()
             onAnimationEnd(velocityConsumed.await())
         }
 
         suspend fun DragController.onDragStoppedAnimateNow(
             velocity: Float,
-            canChangeScene: Boolean = true,
             onAnimationStart: () -> Unit,
         ) =
             onDragStoppedAnimateNow(
                 velocity = velocity,
-                canChangeScene = canChangeScene,
                 onAnimationStart = onAnimationStart,
                 onAnimationEnd = {},
             )
 
-        fun DragController.onDragStoppedAnimateLater(
-            velocity: Float,
-            canChangeScene: Boolean = true,
-        ): Deferred<Float> {
-            val velocityConsumed = testScope.async { onStop(velocity, canChangeScene) }
+        fun DragController.onDragStoppedAnimateLater(velocity: Float): Deferred<Float> {
+            val velocityConsumed = testScope.async { onStop(velocity) }
             testScope.testScheduler.runCurrent()
             return velocityConsumed
         }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
index 4153350..5c6f91b 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
@@ -72,12 +72,12 @@
             return delta
         }
 
-        override suspend fun onStop(velocity: Float, canChangeContent: Boolean): Float {
+        override suspend fun onStop(velocity: Float): Float {
             onStop.invoke(velocity)
             return velocity
         }
 
-        override fun onCancel(canChangeContent: Boolean) {
+        override fun onCancel() {
             error("MultiPointerDraggable never calls onCancel()")
         }
     }
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 d1bd52b..f3be5e4 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
@@ -198,23 +198,30 @@
         assertThat(state.currentTransitions).containsExactly(aToB, bToC).inOrder()
 
         // C => A. This should automatically call freezeAndAnimateToCurrentState() on bToC.
-        state.startTransitionImmediately(animationScope = backgroundScope, cToA)
+        val cToAJob = state.startTransitionImmediately(animationScope = backgroundScope, cToA)
         assertThat(frozenTransitions).containsExactly(aToB, bToC)
         assertThat(state.finishedTransitions).isEmpty()
         assertThat(state.currentTransitions).containsExactly(aToB, bToC, cToA).inOrder()
 
-        // Mark bToC as finished. The list of current transitions does not change because aToB is
-        // still not marked as finished.
-        bToC.finish()
-        bToCJob.join()
-        assertThat(state.finishedTransitions).containsExactly(bToC)
-        assertThat(state.currentTransitions).containsExactly(aToB, bToC, cToA).inOrder()
-
-        // Mark aToB as finished. This will remove both aToB and bToC from the list of transitions.
+        // Mark aToB and bToC as finished. The list of current transitions does not change because
+        // cToA is still running.
         aToB.finish()
         aToBJob.join()
+        assertThat(state.finishedTransitions).containsExactly(aToB)
+        assertThat(state.currentTransitions).containsExactly(aToB, bToC, cToA).inOrder()
+
+        bToC.finish()
+        bToCJob.join()
+        assertThat(state.finishedTransitions).containsExactly(aToB, bToC)
+        assertThat(state.currentTransitions).containsExactly(aToB, bToC, cToA).inOrder()
+
+        // Mark cToA as finished. This should clear all transitions and settle to idle.
+        cToA.finish()
+        cToAJob.join()
         assertThat(state.finishedTransitions).isEmpty()
-        assertThat(state.currentTransitions).containsExactly(cToA).inOrder()
+        assertThat(state.currentTransitions).isEmpty()
+        assertThat(state.transitionState).isIdle()
+        assertThat(state.transitionState).hasCurrentScene(SceneA)
     }
 
     @Test
@@ -473,4 +480,77 @@
                     "SceneKey(debugName=SceneB)"
             )
     }
+
+    @Test
+    fun snapToScene_multipleTransitions() = runMonotonicClockTest {
+        val state = MutableSceneTransitionLayoutState(SceneA)
+        state.startTransitionImmediately(this, transition(SceneA, SceneB))
+        state.startTransitionImmediately(this, transition(SceneB, SceneC))
+        state.snapToScene(SceneC)
+
+        assertThat(state.transitionState).isIdle()
+        assertThat(state.transitionState).hasCurrentScene(SceneC)
+    }
+
+    @Test
+    fun trackTransitionCujs() = runTest {
+        val started = mutableSetOf<TransitionState.Transition>()
+        val finished = mutableSetOf<TransitionState.Transition>()
+        val cujWhenStarting = mutableMapOf<TransitionState.Transition, Int?>()
+        val state =
+            MutableSceneTransitionLayoutState(
+                SceneA,
+                transitions {
+                    // A <=> B.
+                    from(SceneA, to = SceneB, cuj = 1)
+
+                    // A <=> C.
+                    from(SceneA, to = SceneC, cuj = 2)
+                    from(SceneC, to = SceneA, cuj = 3)
+                },
+                onTransitionStart = { transition ->
+                    started.add(transition)
+                    cujWhenStarting[transition] = transition.cuj
+                },
+                onTransitionEnd = { finished.add(it) },
+            )
+
+        val aToB = transition(SceneA, SceneB)
+        val bToA = transition(SceneB, SceneA)
+        val aToC = transition(SceneA, SceneC)
+        val cToA = transition(SceneC, SceneA)
+
+        val animationScope = this
+        state.startTransitionImmediately(animationScope, aToB)
+        assertThat(started).containsExactly(aToB)
+        assertThat(finished).isEmpty()
+
+        state.startTransitionImmediately(animationScope, bToA)
+        assertThat(started).containsExactly(aToB, bToA)
+        assertThat(finished).isEmpty()
+
+        aToB.finish()
+        runCurrent()
+        assertThat(finished).containsExactly(aToB)
+
+        state.startTransitionImmediately(animationScope, aToC)
+        assertThat(started).containsExactly(aToB, bToA, aToC)
+        assertThat(finished).containsExactly(aToB)
+
+        state.startTransitionImmediately(animationScope, cToA)
+        assertThat(started).containsExactly(aToB, bToA, aToC, cToA)
+        assertThat(finished).containsExactly(aToB)
+
+        bToA.finish()
+        aToC.finish()
+        cToA.finish()
+        runCurrent()
+        assertThat(started).containsExactly(aToB, bToA, aToC, cToA)
+        assertThat(finished).containsExactly(aToB, bToA, aToC, cToA)
+
+        assertThat(cujWhenStarting[aToB]).isEqualTo(1)
+        assertThat(cujWhenStarting[bToA]).isEqualTo(1)
+        assertThat(cujWhenStarting[aToC]).isEqualTo(2)
+        assertThat(cujWhenStarting[cToA]).isEqualTo(3)
+    }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
index 2d57680..e580e3c 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -384,8 +384,8 @@
         assertThat(transition).hasProgress(0.5f)
         rule.waitForIdle()
 
-        // B and C are composed.
-        rule.onNodeWithTag("aRoot").assertDoesNotExist()
+        // A, B and C are still composed given that B => C is not finished yet.
+        rule.onNodeWithTag("aRoot").assertExists()
         rule.onNodeWithTag("bRoot").assertExists()
         rule.onNodeWithTag("cRoot").assertExists()
 
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index 9135fdd..e80805a 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -936,4 +936,45 @@
         assertThat(state.transitionState).isIdle()
         assertThat(state.transitionState).hasCurrentScene(SceneC)
     }
+
+    @Test
+    fun swipeToSceneNodeIsKeptWhenDisabled() {
+        var hasHorizontalActions by mutableStateOf(false)
+        val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
+        var touchSlop = 0f
+        rule.setContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            SceneTransitionLayout(state) {
+                scene(
+                    SceneA,
+                    userActions =
+                        buildList {
+                                add(Swipe.Down to SceneB)
+
+                                if (hasHorizontalActions) {
+                                    add(Swipe.Left to SceneC)
+                                }
+                            }
+                            .toMap(),
+                ) {
+                    Box(Modifier.fillMaxSize())
+                }
+                scene(SceneB) { Box(Modifier.fillMaxSize()) }
+            }
+        }
+
+        // Swipe down to start a transition to B.
+        rule.onRoot().performTouchInput {
+            down(middle)
+            moveBy(Offset(0f, touchSlop))
+        }
+
+        assertThat(state.transitionState).isSceneTransition()
+
+        // Add new horizontal user actions. This should not stop the current transition, even if a
+        // new horizontal Modifier.swipeToScene() handler is introduced where the vertical one was.
+        hasHorizontalActions = true
+        rule.waitForIdle()
+        assertThat(state.transitionState).isSceneTransition()
+    }
 }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt
deleted file mode 100644
index 15373d3..0000000
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt
+++ /dev/null
@@ -1,282 +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.shared.clocks
-
-import android.graphics.Point
-import android.view.animation.Interpolator
-import com.android.app.animation.Interpolators
-import com.android.internal.annotations.Keep
-import com.android.systemui.monet.Style as MonetStyle
-import com.android.systemui.shared.clocks.view.HorizontalAlignment
-import com.android.systemui.shared.clocks.view.VerticalAlignment
-
-/** Data format for a simple asset-defined clock */
-@Keep
-data class ClockDesign(
-    val id: String,
-    val name: String? = null,
-    val description: String? = null,
-    val thumbnail: String? = null,
-    val large: ClockFace? = null,
-    val small: ClockFace? = null,
-    @MonetStyle.Type val colorPalette: Int? = null,
-)
-
-/** Describes a clock using layers */
-@Keep
-data class ClockFace(
-    val layers: List<ClockLayer> = listOf<ClockLayer>(),
-    val layerBounds: LayerBounds = LayerBounds.FIT,
-    val wallpaper: String? = null,
-    val faceLayout: DigitalFaceLayout? = null,
-    val pickerScale: ClockFaceScaleInPicker? = ClockFaceScaleInPicker(1.0f, 1.0f),
-)
-
-@Keep data class ClockFaceScaleInPicker(val scaleX: Float, val scaleY: Float)
-
-/** Base Type for a Clock Layer */
-@Keep
-interface ClockLayer {
-    /** Override of face LayerBounds setting for this layer */
-    val layerBounds: LayerBounds?
-}
-
-/** Clock layer that renders a static asset */
-@Keep
-data class AssetLayer(
-    /** Asset to render in this layer */
-    val asset: AssetReference,
-    override val layerBounds: LayerBounds? = null,
-) : ClockLayer
-
-/** Clock layer that renders the time (or a component of it) using numerals */
-@Keep
-data class DigitalHandLayer(
-    /** See SimpleDateFormat for timespec format info */
-    val timespec: DigitalTimespec,
-    val style: TextStyle,
-    // adoStyle concrete type must match style,
-    // cause styles will transition between style and aodStyle
-    val aodStyle: TextStyle?,
-    val timer: Int? = null,
-    override val layerBounds: LayerBounds? = null,
-    var faceLayout: DigitalFaceLayout? = null,
-    // we pass 12-hour format from json, which will be converted to 24-hour format in codes
-    val dateTimeFormat: String,
-    val alignment: DigitalAlignment?,
-    // ratio of margins to measured size, currently used for handwritten clocks
-    val marginRatio: DigitalMarginRatio? = DigitalMarginRatio(),
-) : ClockLayer
-
-/** Clock layer that renders the time (or a component of it) using numerals */
-@Keep
-data class ComposedDigitalHandLayer(
-    val customizedView: String? = null,
-    /** See SimpleDateFormat for timespec format info */
-    val digitalLayers: List<DigitalHandLayer> = listOf<DigitalHandLayer>(),
-    override val layerBounds: LayerBounds? = null,
-) : ClockLayer
-
-@Keep
-data class DigitalAlignment(
-    val horizontalAlignment: HorizontalAlignment?,
-    val verticalAlignment: VerticalAlignment?,
-)
-
-@Keep
-data class DigitalMarginRatio(
-    val left: Float = 0F,
-    val top: Float = 0F,
-    val right: Float = 0F,
-    val bottom: Float = 0F,
-)
-
-/** Clock layer which renders a component of the time using an analog hand */
-@Keep
-data class AnalogHandLayer(
-    val timespec: AnalogTimespec,
-    val tickMode: AnalogTickMode,
-    val asset: AssetReference,
-    val timer: Int? = null,
-    val clock_pivot: Point = Point(0, 0),
-    val asset_pivot: Point? = null,
-    val length: Float = 1f,
-    override val layerBounds: LayerBounds? = null,
-) : ClockLayer
-
-/** Clock layer which renders the time using an AVD */
-@Keep
-data class AnimatedHandLayer(
-    val timespec: AnalogTimespec,
-    val asset: AssetReference,
-    val timer: Int? = null,
-    override val layerBounds: LayerBounds? = null,
-) : ClockLayer
-
-/** A collection of asset references for use in different device modes */
-@Keep
-data class AssetReference(
-    val light: String,
-    val dark: String,
-    val doze: String? = null,
-    val lightTint: String? = null,
-    val darkTint: String? = null,
-    val dozeTint: String? = null,
-)
-
-/**
- * Core TextStyling attributes for text clocks. Both color and sizing information can be applied to
- * either subtype.
- */
-@Keep
-interface TextStyle {
-    // fontSizeScale is a scale factor applied to the default clock's font size.
-    val fontSizeScale: Float?
-}
-
-/**
- * This specifies a font and styling parameters for that font. This is rendered using a text view
- * and the text animation classes used by the default clock. To ensure default value take effects,
- * all parameters MUST have a default value
- */
-@Keep
-data class FontTextStyle(
-    // Font to load and use in the TextView
-    val fontFamily: String? = null,
-    val lineHeight: Float? = null,
-    val borderWidth: String? = null,
-    // ratio of borderWidth / fontSize
-    val borderWidthScale: Float? = null,
-    // A color literal like `#FF00FF` or a color resource like `@android:color/system_accent1_100`
-    val fillColorLight: String? = null,
-    // A color literal like `#FF00FF` or a color resource like `@android:color/system_accent1_100`
-    val fillColorDark: String? = null,
-    override val fontSizeScale: Float? = null,
-    // used when alternate in one font file is needed
-    var fontFeatureSettings: String? = null,
-    val renderType: RenderType = RenderType.STROKE_TEXT,
-    val outlineColor: String? = null,
-    val transitionDuration: Long = -1L,
-    val transitionInterpolator: InterpolatorEnum? = null,
-) : TextStyle
-
-/**
- * As an alternative to using a font, we can instead render a digital clock using a set of drawables
- * for each numeral, and optionally a colon. These drawables will be rendered directly after sizing
- * and placing them. This may be easier than generating a font file in some cases, and is provided
- * for ease of use. Unlike fonts, these are not localizable to other numeric systems (like Burmese).
- */
-@Keep
-data class LottieTextStyle(
-    val numbers: List<String> = listOf(),
-    // Spacing between numbers, dimension string
-    val spacing: String = "0dp",
-    // Colon drawable may be omitted if unused in format spec
-    val colon: String? = null,
-    // key is keypath name to get strokes from lottie, value is the color name to query color in
-    // palette, e.g. @android:color/system_accent1_100
-    val fillColorLightMap: Map<String, String>? = null,
-    val fillColorDarkMap: Map<String, String>? = null,
-    override val fontSizeScale: Float? = null,
-    val paddingVertical: String = "0dp",
-    val paddingHorizontal: String = "0dp",
-) : TextStyle
-
-/** Layer sizing mode for the clockface or layer */
-enum class LayerBounds {
-    /**
-     * Sized so the larger dimension matches the allocated space. This results in some of the
-     * allocated space being unused.
-     */
-    FIT,
-
-    /**
-     * Sized so the smaller dimension matches the allocated space. This will clip some content to
-     * the edges of the space.
-     */
-    FILL,
-
-    /** Fills the allocated space exactly by stretching the layer */
-    STRETCH,
-}
-
-/** Ticking mode for analog hands. */
-enum class AnalogTickMode {
-    SWEEP,
-    TICK,
-}
-
-/** Timspec options for Analog Hands. Named for tick interval. */
-enum class AnalogTimespec {
-    SECONDS,
-    MINUTES,
-    HOURS,
-    HOURS_OF_DAY,
-    DAY_OF_WEEK,
-    DAY_OF_MONTH,
-    DAY_OF_YEAR,
-    WEEK,
-    MONTH,
-    TIMER,
-}
-
-enum class DigitalTimespec {
-    TIME_FULL_FORMAT,
-    DIGIT_PAIR,
-    FIRST_DIGIT,
-    SECOND_DIGIT,
-    DATE_FORMAT,
-}
-
-enum class DigitalFaceLayout {
-    // can only use HH_PAIR, MM_PAIR from DigitalTimespec
-    TWO_PAIRS_VERTICAL,
-    TWO_PAIRS_HORIZONTAL,
-    // can only use HOUR_FIRST_DIGIT, HOUR_SECOND_DIGIT, MINUTE_FIRST_DIGIT, MINUTE_SECOND_DIGIT
-    // from DigitalTimespec, used for tabular layout when the font doesn't support tnum
-    FOUR_DIGITS_ALIGN_CENTER,
-    FOUR_DIGITS_HORIZONTAL,
-}
-
-enum class RenderType {
-    CHANGE_WEIGHT,
-    HOLLOW_TEXT,
-    STROKE_TEXT,
-    OUTER_OUTLINE_TEXT,
-}
-
-enum class InterpolatorEnum(factory: () -> Interpolator) {
-    STANDARD({ Interpolators.STANDARD }),
-    EMPHASIZED({ Interpolators.EMPHASIZED });
-
-    val interpolator: Interpolator by lazy(factory)
-}
-
-fun generateDigitalLayerIdString(layer: DigitalHandLayer): String {
-    return if (
-        layer.timespec == DigitalTimespec.TIME_FULL_FORMAT ||
-            layer.timespec == DigitalTimespec.DATE_FORMAT
-    ) {
-        layer.timespec.toString()
-    } else {
-        if ("h" in layer.dateTimeFormat) {
-            "HOUR" + "_" + layer.timespec.toString()
-        } else {
-            "MINUTE" + "_" + layer.timespec.toString()
-        }
-    }
-}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
index d0a32dc..9fb60c7 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
@@ -18,6 +18,7 @@
 
 import android.graphics.Rect
 import androidx.annotation.VisibleForTesting
+import com.android.app.animation.Interpolators
 import com.android.systemui.log.core.Logger
 import com.android.systemui.plugins.clocks.AlarmData
 import com.android.systemui.plugins.clocks.ClockAnimations
@@ -29,14 +30,13 @@
 import com.android.systemui.plugins.clocks.WeatherData
 import com.android.systemui.plugins.clocks.ZenData
 import com.android.systemui.shared.clocks.view.FlexClockView
-import com.android.systemui.shared.clocks.view.SimpleDigitalClockTextView
+import com.android.systemui.shared.clocks.view.HorizontalAlignment
+import com.android.systemui.shared.clocks.view.VerticalAlignment
 import java.util.Locale
 import java.util.TimeZone
 
-class ComposedDigitalLayerController(
-    private val clockCtx: ClockContext,
-    private val layer: ComposedDigitalHandLayer,
-) : SimpleClockLayerController {
+class ComposedDigitalLayerController(private val clockCtx: ClockContext) :
+    SimpleClockLayerController {
     private val logger =
         Logger(clockCtx.messageBuffer, ComposedDigitalLayerController::class.simpleName!!)
 
@@ -46,14 +46,40 @@
     override val view = FlexClockView(clockCtx)
 
     init {
-        layer.digitalLayers.forEach {
-            val childView = SimpleDigitalClockTextView(clockCtx)
-            val controller =
-                SimpleDigitalHandLayerController(clockCtx, it as DigitalHandLayer, childView)
-
-            view.addView(childView)
+        fun createController(cfg: LayerConfig) {
+            val controller = SimpleDigitalHandLayerController(clockCtx, cfg)
+            view.addView(controller.view)
             layerControllers.add(controller)
         }
+
+        val layerCfg =
+            LayerConfig(
+                style = FontTextStyle(lineHeight = 147.25f),
+                aodStyle =
+                    FontTextStyle(
+                        transitionInterpolator = Interpolators.EMPHASIZED,
+                        transitionDuration = 750,
+                    ),
+                alignment =
+                    DigitalAlignment(HorizontalAlignment.CENTER, VerticalAlignment.BASELINE),
+
+                // Placeholders
+                timespec = DigitalTimespec.TIME_FULL_FORMAT,
+                dateTimeFormat = "hh:mm",
+            )
+
+        createController(
+            layerCfg.copy(timespec = DigitalTimespec.FIRST_DIGIT, dateTimeFormat = "hh")
+        )
+        createController(
+            layerCfg.copy(timespec = DigitalTimespec.SECOND_DIGIT, dateTimeFormat = "hh")
+        )
+        createController(
+            layerCfg.copy(timespec = DigitalTimespec.FIRST_DIGIT, dateTimeFormat = "mm")
+        )
+        createController(
+            layerCfg.copy(timespec = DigitalTimespec.SECOND_DIGIT, dateTimeFormat = "mm")
+        )
     }
 
     private fun refreshTime() {
@@ -79,17 +105,11 @@
                 refreshTime()
             }
 
-            override fun onWeatherDataChanged(data: WeatherData) {
-                view.onWeatherDataChanged(data)
-            }
+            override fun onWeatherDataChanged(data: WeatherData) {}
 
-            override fun onAlarmDataChanged(data: AlarmData) {
-                view.onAlarmDataChanged(data)
-            }
+            override fun onAlarmDataChanged(data: AlarmData) {}
 
-            override fun onZenDataChanged(data: ZenData) {
-                view.onZenDataChanged(data)
-            }
+            override fun onZenDataChanged(data: ZenData) {}
 
             override fun onFontAxesChanged(axes: List<ClockFontAxisSetting>) {
                 view.updateAxes(axes)
@@ -123,15 +143,11 @@
                 view.animateCharge()
             }
 
-            override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {
-                view.onPositionUpdated(fromLeft, direction, fraction)
-            }
+            override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {}
 
             override fun onPositionUpdated(distance: Float, fraction: Float) {}
 
-            override fun onPickerCarouselSwiping(swipingFraction: Float) {
-                view.onPickerCarouselSwiping(swipingFraction)
-            }
+            override fun onPickerCarouselSwiping(swipingFraction: Float) {}
         }
 
     override val faceEvents =
@@ -163,9 +179,8 @@
 
     override val config =
         ClockFaceConfig(
-            hasCustomWeatherDataDisplay = view.hasCustomWeatherDataDisplay,
-            hasCustomPositionUpdatedAnimation = view.hasCustomPositionUpdatedAnimation,
-            useCustomClockScene = view.useCustomClockScene,
+            hasCustomWeatherDataDisplay = false,
+            hasCustomPositionUpdatedAnimation = true,
         )
 
     @VisibleForTesting
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index c73e1c3..f6ff3268 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -27,8 +27,6 @@
 import com.android.systemui.plugins.clocks.ClockPickerConfig
 import com.android.systemui.plugins.clocks.ClockProvider
 import com.android.systemui.plugins.clocks.ClockSettings
-import com.android.systemui.shared.clocks.view.HorizontalAlignment
-import com.android.systemui.shared.clocks.view.VerticalAlignment
 
 private val TAG = DefaultClockProvider::class.simpleName
 const val DEFAULT_CLOCK_ID = "DEFAULT"
@@ -78,8 +76,7 @@
                     typefaceCache,
                     buffers,
                     buffers.infraMessageBuffer,
-                ),
-                FLEX_DESIGN,
+                )
             )
         } else {
             DefaultClockController(ctx, layoutInflater, resources, settings, messageBuffers)
@@ -128,119 +125,5 @@
             // TODO(b/364680873): Move constant to config_clockFontFamily when shipping
             Typeface.create("google-sans-flex-clock", Typeface.NORMAL)
         }
-
-        val FLEX_DESIGN = run {
-            val largeLayer =
-                listOf(
-                    ComposedDigitalHandLayer(
-                        layerBounds = LayerBounds.FIT,
-                        customizedView = "FlexClockView",
-                        digitalLayers =
-                            listOf(
-                                DigitalHandLayer(
-                                    layerBounds = LayerBounds.FIT,
-                                    timespec = DigitalTimespec.FIRST_DIGIT,
-                                    style = FontTextStyle(lineHeight = 147.25f),
-                                    aodStyle =
-                                        FontTextStyle(
-                                            fillColorLight = "#FFFFFFFF",
-                                            outlineColor = "#00000000",
-                                            renderType = RenderType.CHANGE_WEIGHT,
-                                            transitionInterpolator = InterpolatorEnum.EMPHASIZED,
-                                            transitionDuration = 750,
-                                        ),
-                                    alignment =
-                                        DigitalAlignment(
-                                            HorizontalAlignment.CENTER,
-                                            VerticalAlignment.BASELINE,
-                                        ),
-                                    dateTimeFormat = "hh",
-                                ),
-                                DigitalHandLayer(
-                                    layerBounds = LayerBounds.FIT,
-                                    timespec = DigitalTimespec.SECOND_DIGIT,
-                                    style = FontTextStyle(lineHeight = 147.25f),
-                                    aodStyle =
-                                        FontTextStyle(
-                                            fillColorLight = "#FFFFFFFF",
-                                            outlineColor = "#00000000",
-                                            renderType = RenderType.CHANGE_WEIGHT,
-                                            transitionInterpolator = InterpolatorEnum.EMPHASIZED,
-                                            transitionDuration = 750,
-                                        ),
-                                    alignment =
-                                        DigitalAlignment(
-                                            HorizontalAlignment.CENTER,
-                                            VerticalAlignment.BASELINE,
-                                        ),
-                                    dateTimeFormat = "hh",
-                                ),
-                                DigitalHandLayer(
-                                    layerBounds = LayerBounds.FIT,
-                                    timespec = DigitalTimespec.FIRST_DIGIT,
-                                    style = FontTextStyle(lineHeight = 147.25f),
-                                    aodStyle =
-                                        FontTextStyle(
-                                            fillColorLight = "#FFFFFFFF",
-                                            outlineColor = "#00000000",
-                                            renderType = RenderType.CHANGE_WEIGHT,
-                                            transitionInterpolator = InterpolatorEnum.EMPHASIZED,
-                                            transitionDuration = 750,
-                                        ),
-                                    alignment =
-                                        DigitalAlignment(
-                                            HorizontalAlignment.CENTER,
-                                            VerticalAlignment.BASELINE,
-                                        ),
-                                    dateTimeFormat = "mm",
-                                ),
-                                DigitalHandLayer(
-                                    layerBounds = LayerBounds.FIT,
-                                    timespec = DigitalTimespec.SECOND_DIGIT,
-                                    style = FontTextStyle(lineHeight = 147.25f),
-                                    aodStyle =
-                                        FontTextStyle(
-                                            fillColorLight = "#FFFFFFFF",
-                                            outlineColor = "#00000000",
-                                            renderType = RenderType.CHANGE_WEIGHT,
-                                            transitionInterpolator = InterpolatorEnum.EMPHASIZED,
-                                            transitionDuration = 750,
-                                        ),
-                                    alignment =
-                                        DigitalAlignment(
-                                            HorizontalAlignment.CENTER,
-                                            VerticalAlignment.BASELINE,
-                                        ),
-                                    dateTimeFormat = "mm",
-                                ),
-                            ),
-                    )
-                )
-
-            val smallLayer =
-                listOf(
-                    DigitalHandLayer(
-                        layerBounds = LayerBounds.FIT,
-                        timespec = DigitalTimespec.TIME_FULL_FORMAT,
-                        style = FontTextStyle(fontSizeScale = 0.98f),
-                        aodStyle =
-                            FontTextStyle(
-                                fillColorLight = "#FFFFFFFF",
-                                outlineColor = "#00000000",
-                                renderType = RenderType.CHANGE_WEIGHT,
-                            ),
-                        alignment = DigitalAlignment(HorizontalAlignment.LEFT, null),
-                        dateTimeFormat = "h:mm",
-                    )
-                )
-
-            ClockDesign(
-                id = DEFAULT_CLOCK_ID,
-                name = "@string/clock_default_name",
-                description = "@string/clock_default_description",
-                large = ClockFace(layers = largeLayer),
-                small = ClockFace(layers = smallLayer),
-            )
-        }
     }
 }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
index 7f01fd7..e69fa99 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
@@ -32,21 +32,16 @@
 import java.util.TimeZone
 
 /** Controller for the default flex clock */
-class FlexClockController(
-    private val clockCtx: ClockContext,
-    val design: ClockDesign, // TODO(b/364680879): Remove when done inlining
-) : ClockController {
+class FlexClockController(private val clockCtx: ClockContext) : ClockController {
     override val smallClock =
         FlexClockFaceController(
             clockCtx.copy(messageBuffer = clockCtx.messageBuffers.smallClockMessageBuffer),
-            design.small ?: design.large!!,
             isLargeClock = false,
         )
 
     override val largeClock =
         FlexClockFaceController(
             clockCtx.copy(messageBuffer = clockCtx.messageBuffers.largeClockMessageBuffer),
-            design.large ?: design.small!!,
             isLargeClock = true,
         )
 
@@ -55,7 +50,6 @@
             DEFAULT_CLOCK_ID,
             clockCtx.resources.getString(R.string.clock_default_name),
             clockCtx.resources.getString(R.string.clock_default_description),
-            isReactiveToTone = true,
         )
     }
 
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
index 4a47f1b..827bd68 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
@@ -35,17 +35,14 @@
 import com.android.systemui.plugins.clocks.WeatherData
 import com.android.systemui.plugins.clocks.ZenData
 import com.android.systemui.shared.clocks.view.FlexClockView
-import com.android.systemui.shared.clocks.view.SimpleDigitalClockTextView
+import com.android.systemui.shared.clocks.view.HorizontalAlignment
 import java.util.Locale
 import java.util.TimeZone
 import kotlin.math.max
 
 // TODO(b/364680879): Merge w/ ComposedDigitalLayerController
-class FlexClockFaceController(
-    clockCtx: ClockContext,
-    face: ClockFace,
-    private val isLargeClock: Boolean,
-) : ClockFaceController {
+class FlexClockFaceController(clockCtx: ClockContext, private val isLargeClock: Boolean) :
+    ClockFaceController {
     override val view: View
         get() = layerController.view
 
@@ -59,19 +56,12 @@
     val timespecHandler = DigitalTimespecHandler(DigitalTimespec.TIME_FULL_FORMAT, "hh:mm")
 
     init {
-        val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
-        lp.gravity = Gravity.CENTER
-
-        val layer = face.layers[0]
-
         layerController =
-            if (isLargeClock) {
-                ComposedDigitalLayerController(clockCtx, layer as ComposedDigitalHandLayer)
-            } else {
-                val childView = SimpleDigitalClockTextView(clockCtx)
-                SimpleDigitalHandLayerController(clockCtx, layer as DigitalHandLayer, childView)
-            }
-        layerController.view.layoutParams = lp
+            if (isLargeClock) ComposedDigitalLayerController(clockCtx)
+            else SimpleDigitalHandLayerController(clockCtx, SMALL_LAYER_CONFIG)
+
+        layerController.view.layoutParams =
+            FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT).apply { gravity = Gravity.CENTER }
     }
 
     /** See documentation at [FlexClockView.offsetGlyphsForStepClockAnimation]. */
@@ -227,10 +217,6 @@
             }
 
             override fun onPickerCarouselSwiping(swipingFraction: Float) {
-                face.pickerScale?.let {
-                    view.scaleX = swipingFraction * (1 - it.scaleX) + it.scaleX
-                    view.scaleY = swipingFraction * (1 - it.scaleY) + it.scaleY
-                }
                 if (isLargeClock && !(view as FlexClockView).isAlignedWithScreen()) {
                     view.translationY = keyguardLargeClockTopMargin / 2F * swipingFraction
                 }
@@ -248,4 +234,15 @@
                 // TODO(b/378128811) port stepping animation
             }
         }
+
+    companion object {
+        val SMALL_LAYER_CONFIG =
+            LayerConfig(
+                timespec = DigitalTimespec.TIME_FULL_FORMAT,
+                style = FontTextStyle(fontSizeScale = 0.98f),
+                aodStyle = FontTextStyle(),
+                alignment = DigitalAlignment(HorizontalAlignment.LEFT, null),
+                dateTimeFormat = "h:mm",
+            )
+    }
 }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockRelativeLayout.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockRelativeLayout.kt
deleted file mode 100644
index 6e1b9aa..0000000
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockRelativeLayout.kt
+++ /dev/null
@@ -1,47 +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.shared.clocks
-
-import android.content.Context
-import android.view.View.MeasureSpec.EXACTLY
-import android.widget.RelativeLayout
-import androidx.core.view.children
-import com.android.systemui.shared.clocks.view.SimpleDigitalClockView
-
-class SimpleClockRelativeLayout(context: Context, val faceLayout: DigitalFaceLayout?) :
-    RelativeLayout(context) {
-    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
-        // For migrate_clocks_to_blueprint, mode is EXACTLY
-        // when the flag is turned off, we won't execute this codes
-        if (MeasureSpec.getMode(heightMeasureSpec) == EXACTLY) {
-            if (
-                faceLayout == DigitalFaceLayout.TWO_PAIRS_VERTICAL ||
-                    faceLayout == DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER
-            ) {
-                val constrainedHeight = MeasureSpec.getSize(heightMeasureSpec) / 2F
-                children.forEach {
-                    // The assumption here is the height of text view is linear to font size
-                    (it as SimpleDigitalClockView).applyTextSize(
-                        constrainedHeight,
-                        constrainedByHeight = true,
-                    )
-                }
-            }
-        }
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
-    }
-}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
index ebac4b24..82fc3501 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
@@ -17,8 +17,8 @@
 package com.android.systemui.shared.clocks
 
 import android.graphics.Rect
-import android.view.View
 import android.view.ViewGroup
+import android.view.animation.Interpolator
 import android.widget.RelativeLayout
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.customization.R
@@ -32,22 +32,56 @@
 import com.android.systemui.plugins.clocks.ThemeConfig
 import com.android.systemui.plugins.clocks.WeatherData
 import com.android.systemui.plugins.clocks.ZenData
-import com.android.systemui.shared.clocks.view.SimpleDigitalClockView
+import com.android.systemui.shared.clocks.view.HorizontalAlignment
+import com.android.systemui.shared.clocks.view.SimpleDigitalClockTextView
+import com.android.systemui.shared.clocks.view.VerticalAlignment
 import java.util.Locale
 import java.util.TimeZone
 
 private val TAG = SimpleDigitalHandLayerController::class.simpleName!!
 
-open class SimpleDigitalHandLayerController<T>(
-    private val clockCtx: ClockContext,
-    private val layer: DigitalHandLayer,
-    override val view: T,
-) : SimpleClockLayerController where T : View, T : SimpleDigitalClockView {
-    private val logger = Logger(clockCtx.messageBuffer, TAG)
-    val timespec = DigitalTimespecHandler(layer.timespec, layer.dateTimeFormat)
+// TODO(b/364680879): The remains of ClockDesign. Cut further.
+data class LayerConfig(
+    val style: FontTextStyle,
+    val aodStyle: FontTextStyle,
+    val alignment: DigitalAlignment,
+    val timespec: DigitalTimespec,
+    val dateTimeFormat: String,
+) {
+    fun generateDigitalLayerIdString(): String {
+        return when {
+            timespec == DigitalTimespec.TIME_FULL_FORMAT -> "$timespec"
+            "h" in dateTimeFormat -> "HOUR_$timespec"
+            else -> "MINUTE_$timespec"
+        }
+    }
+}
 
-    @VisibleForTesting
-    fun hasLeadingZero() = layer.dateTimeFormat.startsWith("hh") || timespec.is24Hr
+data class DigitalAlignment(
+    val horizontalAlignment: HorizontalAlignment?,
+    val verticalAlignment: VerticalAlignment?,
+)
+
+data class FontTextStyle(
+    val lineHeight: Float? = null,
+    val fontSizeScale: Float? = null,
+    val transitionDuration: Long = -1L,
+    val transitionInterpolator: Interpolator? = null,
+)
+
+enum class DigitalTimespec {
+    TIME_FULL_FORMAT,
+    FIRST_DIGIT,
+    SECOND_DIGIT,
+}
+
+open class SimpleDigitalHandLayerController(
+    private val clockCtx: ClockContext,
+    private val layerCfg: LayerConfig,
+) : SimpleClockLayerController {
+    override val view = SimpleDigitalClockTextView(clockCtx)
+    private val logger = Logger(clockCtx.messageBuffer, TAG)
+    val timespec = DigitalTimespecHandler(layerCfg.timespec, layerCfg.dateTimeFormat)
 
     @VisibleForTesting
     override var fakeTimeMills: Long?
@@ -65,145 +99,17 @@
                 ViewGroup.LayoutParams.WRAP_CONTENT,
                 ViewGroup.LayoutParams.WRAP_CONTENT,
             )
-        if (layer.alignment != null) {
-            layer.alignment.verticalAlignment?.let { view.verticalAlignment = it }
-            layer.alignment.horizontalAlignment?.let { view.horizontalAlignment = it }
-        }
-        view.applyStyles(layer.style, layer.aodStyle)
+        layerCfg.alignment.verticalAlignment?.let { view.verticalAlignment = it }
+        layerCfg.alignment.horizontalAlignment?.let { view.horizontalAlignment = it }
+        view.applyStyles(layerCfg.style, layerCfg.aodStyle)
         view.id =
             clockCtx.resources.getIdentifier(
-                generateDigitalLayerIdString(layer),
+                layerCfg.generateDigitalLayerIdString(),
                 "id",
                 clockCtx.context.getPackageName(),
             )
     }
 
-    fun applyLayout(layout: DigitalFaceLayout?) {
-        when (layout) {
-            DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER,
-            DigitalFaceLayout.FOUR_DIGITS_HORIZONTAL -> applyFourDigitsLayout(layout)
-            DigitalFaceLayout.TWO_PAIRS_HORIZONTAL,
-            DigitalFaceLayout.TWO_PAIRS_VERTICAL -> applyTwoPairsLayout(layout)
-            else -> {
-                // one view always use FrameLayout
-                // no need to change here
-            }
-        }
-        applyMargin()
-    }
-
-    private fun applyMargin() {
-        if (view.layoutParams is RelativeLayout.LayoutParams) {
-            val lp = view.layoutParams as RelativeLayout.LayoutParams
-            layer.marginRatio?.let {
-                lp.setMargins(
-                    /* left = */ (it.left * view.measuredWidth).toInt(),
-                    /* top = */ (it.top * view.measuredHeight).toInt(),
-                    /* right = */ (it.right * view.measuredWidth).toInt(),
-                    /* bottom = */ (it.bottom * view.measuredHeight).toInt(),
-                )
-            }
-            view.layoutParams = lp
-        }
-    }
-
-    private fun applyTwoPairsLayout(twoPairsLayout: DigitalFaceLayout) {
-        val lp = view.layoutParams as RelativeLayout.LayoutParams
-        lp.addRule(RelativeLayout.TEXT_ALIGNMENT_CENTER)
-        if (twoPairsLayout == DigitalFaceLayout.TWO_PAIRS_HORIZONTAL) {
-            when (view.id) {
-                R.id.HOUR_DIGIT_PAIR -> {
-                    lp.addRule(RelativeLayout.CENTER_VERTICAL)
-                    lp.addRule(RelativeLayout.ALIGN_PARENT_START)
-                }
-                R.id.MINUTE_DIGIT_PAIR -> {
-                    lp.addRule(RelativeLayout.CENTER_VERTICAL)
-                    lp.addRule(RelativeLayout.END_OF, R.id.HOUR_DIGIT_PAIR)
-                }
-                else -> {
-                    throw Exception("cannot apply two pairs layout to view ${view.id}")
-                }
-            }
-        } else {
-            when (view.id) {
-                R.id.HOUR_DIGIT_PAIR -> {
-                    lp.addRule(RelativeLayout.CENTER_HORIZONTAL)
-                    lp.addRule(RelativeLayout.ALIGN_PARENT_TOP)
-                }
-                R.id.MINUTE_DIGIT_PAIR -> {
-                    lp.addRule(RelativeLayout.CENTER_HORIZONTAL)
-                    lp.addRule(RelativeLayout.BELOW, R.id.HOUR_DIGIT_PAIR)
-                }
-                else -> {
-                    throw Exception("cannot apply two pairs layout to view ${view.id}")
-                }
-            }
-        }
-        view.layoutParams = lp
-    }
-
-    private fun applyFourDigitsLayout(fourDigitsfaceLayout: DigitalFaceLayout) {
-        val lp = view.layoutParams as RelativeLayout.LayoutParams
-        when (fourDigitsfaceLayout) {
-            DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER -> {
-                when (view.id) {
-                    R.id.HOUR_FIRST_DIGIT -> {
-                        lp.addRule(RelativeLayout.ALIGN_PARENT_START)
-                        lp.addRule(RelativeLayout.ALIGN_PARENT_TOP)
-                    }
-                    R.id.HOUR_SECOND_DIGIT -> {
-                        lp.addRule(RelativeLayout.END_OF, R.id.HOUR_FIRST_DIGIT)
-                        lp.addRule(RelativeLayout.ALIGN_TOP, R.id.HOUR_FIRST_DIGIT)
-                    }
-                    R.id.MINUTE_FIRST_DIGIT -> {
-                        lp.addRule(RelativeLayout.ALIGN_START, R.id.HOUR_FIRST_DIGIT)
-                        lp.addRule(RelativeLayout.BELOW, R.id.HOUR_FIRST_DIGIT)
-                    }
-                    R.id.MINUTE_SECOND_DIGIT -> {
-                        lp.addRule(RelativeLayout.ALIGN_START, R.id.HOUR_SECOND_DIGIT)
-                        lp.addRule(RelativeLayout.BELOW, R.id.HOUR_SECOND_DIGIT)
-                    }
-                    else -> {
-                        throw Exception("cannot apply four digits layout to view ${view.id}")
-                    }
-                }
-            }
-            DigitalFaceLayout.FOUR_DIGITS_HORIZONTAL -> {
-                when (view.id) {
-                    R.id.HOUR_FIRST_DIGIT -> {
-                        lp.addRule(RelativeLayout.CENTER_VERTICAL)
-                        lp.addRule(RelativeLayout.ALIGN_PARENT_START)
-                    }
-                    R.id.HOUR_SECOND_DIGIT -> {
-                        lp.addRule(RelativeLayout.CENTER_VERTICAL)
-                        lp.addRule(RelativeLayout.END_OF, R.id.HOUR_FIRST_DIGIT)
-                    }
-                    R.id.MINUTE_FIRST_DIGIT -> {
-                        lp.addRule(RelativeLayout.CENTER_VERTICAL)
-                        lp.addRule(RelativeLayout.END_OF, R.id.HOUR_SECOND_DIGIT)
-                    }
-                    R.id.MINUTE_SECOND_DIGIT -> {
-                        lp.addRule(RelativeLayout.CENTER_VERTICAL)
-                        lp.addRule(RelativeLayout.END_OF, R.id.MINUTE_FIRST_DIGIT)
-                    }
-                    else -> {
-                        throw Exception("cannot apply FOUR_DIGITS_HORIZONTAL to view ${view.id}")
-                    }
-                }
-            }
-            else -> {
-                throw IllegalArgumentException(
-                    "applyFourDigitsLayout function should not " +
-                        "have parameters as ${layer.faceLayout}"
-                )
-            }
-        }
-        if (lp == view.layoutParams) {
-            return
-        }
-        view.layoutParams = lp
-    }
-
     fun refreshTime() {
         timespec.updateTime()
         val text = timespec.getDigitString()
@@ -248,7 +154,6 @@
     override val animations =
         object : ClockAnimations {
             override fun enter() {
-                applyLayout(layer.faceLayout)
                 refreshTime()
             }
 
@@ -264,7 +169,6 @@
             }
 
             override fun fold(fraction: Float) {
-                applyLayout(layer.faceLayout)
                 refreshTime()
             }
 
@@ -283,17 +187,13 @@
         object : ClockFaceEvents {
             override fun onTimeTick() {
                 refreshTime()
-                if (
-                    layer.timespec == DigitalTimespec.TIME_FULL_FORMAT ||
-                        layer.timespec == DigitalTimespec.DATE_FORMAT
-                ) {
+                if (layerCfg.timespec == DigitalTimespec.TIME_FULL_FORMAT) {
                     view.contentDescription = timespec.getContentDescription()
                 }
             }
 
             override fun onFontSettingChanged(fontSizePx: Float) {
                 view.applyTextSize(fontSizePx)
-                applyMargin()
             }
 
             override fun onThemeChanged(theme: ThemeConfig) {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt
index ed6a403..37db783 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt
@@ -25,9 +25,7 @@
 import java.util.Locale
 import java.util.TimeZone
 
-open class TimespecHandler(
-    val cal: Calendar,
-) {
+open class TimespecHandler(val cal: Calendar) {
     var timeZone: TimeZone
         get() = cal.timeZone
         set(value) {
@@ -82,10 +80,7 @@
     }
 
     private fun updateSimpleDateFormat(locale: Locale): DateFormat {
-        if (
-            locale.language.equals(Locale.ENGLISH.language) ||
-                timespec != DigitalTimespec.DATE_FORMAT
-        ) {
+        if (locale.language.equals(Locale.ENGLISH.language)) {
             // force date format in English, and time format to use format defined in json
             return SimpleDateFormat(timeFormat, timeFormat, ULocale.forLocale(locale))
         } else {
@@ -97,24 +92,18 @@
         return when (timespec) {
             DigitalTimespec.TIME_FULL_FORMAT ->
                 SimpleDateFormat.getInstanceForSkeleton("hh:mm", locale)
-            DigitalTimespec.DATE_FORMAT ->
-                SimpleDateFormat.getInstanceForSkeleton("EEEE MMMM d", locale)
-            else -> {
-                null
-            }
+            else -> null
         }
     }
 
     private fun applyPattern() {
         val timeFormat24Hour = timeFormat.replace("hh", "h").replace("h", "HH")
         val format = if (is24Hr) timeFormat24Hour else timeFormat
-        if (timespec != DigitalTimespec.DATE_FORMAT) {
-            (dateFormat as SimpleDateFormat).applyPattern(format)
-            (contentDescriptionFormat as? SimpleDateFormat)?.applyPattern(
-                if (is24Hr) CONTENT_DESCRIPTION_TIME_FORMAT_24_HOUR
-                else CONTENT_DESCRIPTION_TIME_FORMAT_12_HOUR
-            )
-        }
+        (dateFormat as SimpleDateFormat).applyPattern(format)
+        (contentDescriptionFormat as? SimpleDateFormat)?.applyPattern(
+            if (is24Hr) CONTENT_DESCRIPTION_TIME_FORMAT_24_HOUR
+            else CONTENT_DESCRIPTION_TIME_FORMAT_12_HOUR
+        )
     }
 
     private fun getSingleDigit(): String {
@@ -122,7 +111,7 @@
         val text = dateFormat.format(cal.time).toString()
         return text.substring(
             if (isFirstDigit) 0 else text.length - 1,
-            if (isFirstDigit) text.length - 1 else text.length
+            if (isFirstDigit) text.length - 1 else text.length,
         )
     }
 
@@ -130,27 +119,16 @@
         return when (timespec) {
             DigitalTimespec.FIRST_DIGIT,
             DigitalTimespec.SECOND_DIGIT -> getSingleDigit()
-            DigitalTimespec.DIGIT_PAIR -> {
-                dateFormat.format(cal.time).toString()
-            }
-            DigitalTimespec.TIME_FULL_FORMAT -> {
-                dateFormat.format(cal.time).toString()
-            }
-            DigitalTimespec.DATE_FORMAT -> {
-                dateFormat.format(cal.time).toString().uppercase()
-            }
+            DigitalTimespec.TIME_FULL_FORMAT -> dateFormat.format(cal.time).toString()
         }
     }
 
     fun getContentDescription(): String? {
         return when (timespec) {
-            DigitalTimespec.TIME_FULL_FORMAT,
-            DigitalTimespec.DATE_FORMAT -> {
+            DigitalTimespec.TIME_FULL_FORMAT -> {
                 contentDescriptionFormat?.format(cal.time).toString()
             }
-            else -> {
-                return null
-            }
+            else -> return null
         }
     }
 
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt
deleted file mode 100644
index d4eb767..0000000
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt
+++ /dev/null
@@ -1,184 +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.shared.clocks.view
-
-import android.graphics.Canvas
-import android.graphics.Point
-import android.view.View
-import android.widget.FrameLayout
-import androidx.annotation.VisibleForTesting
-import com.android.systemui.log.core.Logger
-import com.android.systemui.plugins.clocks.AlarmData
-import com.android.systemui.plugins.clocks.ClockFontAxisSetting
-import com.android.systemui.plugins.clocks.WeatherData
-import com.android.systemui.plugins.clocks.ZenData
-import com.android.systemui.shared.clocks.ClockContext
-import com.android.systemui.shared.clocks.LogUtil
-import java.util.Locale
-
-// TODO(b/364680879): Merge w/ only subclass FlexClockView
-abstract class DigitalClockFaceView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) {
-    protected val logger = Logger(clockCtx.messageBuffer, this::class.simpleName!!)
-        get() = field ?: LogUtil.FALLBACK_INIT_LOGGER
-
-    abstract var digitalClockTextViewMap: MutableMap<Int, SimpleDigitalClockTextView>
-
-    @VisibleForTesting
-    var isAnimationEnabled = true
-        set(value) {
-            field = value
-            digitalClockTextViewMap.forEach { _, view -> view.isAnimationEnabled = value }
-        }
-
-    var dozeFraction: Float = 0F
-        set(value) {
-            field = value
-            digitalClockTextViewMap.forEach { _, view -> view.dozeFraction = field }
-        }
-
-    val dozeControlState = DozeControlState()
-
-    var isReactiveTouchInteractionEnabled = false
-        set(value) {
-            field = value
-        }
-
-    open val text: String?
-        get() = null
-
-    open fun refreshTime() = logger.d("refreshTime()")
-
-    override fun invalidate() {
-        logger.d("invalidate()")
-        super.invalidate()
-    }
-
-    override fun requestLayout() {
-        logger.d("requestLayout()")
-        super.requestLayout()
-    }
-
-    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
-        logger.d("onMeasure()")
-        calculateSize(widthMeasureSpec, heightMeasureSpec)?.let { setMeasuredDimension(it.x, it.y) }
-            ?: run { super.onMeasure(widthMeasureSpec, heightMeasureSpec) }
-        calculateLeftTopPosition()
-        dozeControlState.animateReady = true
-    }
-
-    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
-        logger.d("onLayout()")
-        super.onLayout(changed, left, top, right, bottom)
-    }
-
-    override fun onDraw(canvas: Canvas) {
-        text?.let { logger.d({ "onDraw($str1)" }) { str1 = it } } ?: run { logger.d("onDraw()") }
-        super.onDraw(canvas)
-    }
-
-    /*
-     * Called in onMeasure to generate width/height overrides to the normal measuring logic. A null
-     * result causes the normal view measuring logic to execute.
-     */
-    protected open fun calculateSize(widthMeasureSpec: Int, heightMeasureSpec: Int): Point? = null
-
-    protected open fun calculateLeftTopPosition() {}
-
-    override fun addView(child: View?) {
-        if (child == null) return
-        logger.d({ "addView($str1 @$int1)" }) {
-            str1 = child::class.simpleName!!
-            int1 = child.id
-        }
-        super.addView(child)
-        if (child is SimpleDigitalClockTextView) {
-            digitalClockTextViewMap[child.id] = child
-        }
-        child.setWillNotDraw(true)
-    }
-
-    open fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
-        digitalClockTextViewMap.forEach { _, view -> view.animateDoze(isDozing, isAnimated) }
-    }
-
-    open fun animateCharge() {
-        digitalClockTextViewMap.forEach { _, view -> view.animateCharge() }
-    }
-
-    open fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {}
-
-    fun updateColor(color: Int) {
-        digitalClockTextViewMap.forEach { _, view -> view.updateColor(color) }
-        invalidate()
-    }
-
-    fun updateAxes(axes: List<ClockFontAxisSetting>) {
-        digitalClockTextViewMap.forEach { _, view -> view.updateAxes(axes) }
-        requestLayout()
-    }
-
-    fun onFontSettingChanged(fontSizePx: Float) {
-        digitalClockTextViewMap.forEach { _, view -> view.applyTextSize(fontSizePx) }
-    }
-
-    open val hasCustomWeatherDataDisplay
-        get() = false
-
-    open val hasCustomPositionUpdatedAnimation
-        get() = false
-
-    /** True if it's large weather clock, will use weatherBlueprint in compose */
-    open val useCustomClockScene
-        get() = false
-
-    open fun onLocaleChanged(locale: Locale) {}
-
-    open fun onWeatherDataChanged(data: WeatherData) {}
-
-    open fun onAlarmDataChanged(data: AlarmData) {}
-
-    open fun onZenDataChanged(data: ZenData) {}
-
-    open fun onPickerCarouselSwiping(swipingFraction: Float) {}
-
-    open fun isAlignedWithScreen(): Boolean = false
-
-    /**
-     * animateDoze needs correct translate value, which is calculated in onMeasure so we need to
-     * delay this animation when we get correct values
-     */
-    class DozeControlState {
-        var animateDoze: () -> Unit = {}
-            set(value) {
-                if (animateReady) {
-                    value()
-                    field = {}
-                } else {
-                    field = value
-                }
-            }
-
-        var animateReady = false
-            set(value) {
-                if (value) {
-                    animateDoze()
-                    animateDoze = {}
-                }
-                field = value
-            }
-    }
-}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
index faef18c..c40bb9a 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
@@ -22,11 +22,16 @@
 import android.util.MathUtils.constrainedMap
 import android.view.View
 import android.view.ViewGroup
+import android.widget.FrameLayout
 import android.widget.RelativeLayout
+import androidx.annotation.VisibleForTesting
 import com.android.app.animation.Interpolators
 import com.android.systemui.customization.R
+import com.android.systemui.log.core.Logger
+import com.android.systemui.plugins.clocks.ClockFontAxisSetting
 import com.android.systemui.shared.clocks.ClockContext
 import com.android.systemui.shared.clocks.DigitTranslateAnimator
+import com.android.systemui.shared.clocks.LogUtil
 import java.util.Locale
 import kotlin.math.abs
 import kotlin.math.max
@@ -34,14 +39,38 @@
 
 fun clamp(value: Float, minVal: Float, maxVal: Float): Float = max(min(value, maxVal), minVal)
 
-class FlexClockView(clockCtx: ClockContext) : DigitalClockFaceView(clockCtx) {
-    override var digitalClockTextViewMap = mutableMapOf<Int, SimpleDigitalClockTextView>()
+class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) {
+    protected val logger = Logger(clockCtx.messageBuffer, this::class.simpleName!!)
+        get() = field ?: LogUtil.FALLBACK_INIT_LOGGER
+
+    @VisibleForTesting
+    var isAnimationEnabled = true
+        set(value) {
+            field = value
+            digitalClockTextViewMap.forEach { _, view -> view.isAnimationEnabled = value }
+        }
+
+    var dozeFraction: Float = 0F
+        set(value) {
+            field = value
+            digitalClockTextViewMap.forEach { _, view -> view.dozeFraction = field }
+        }
+
+    var isReactiveTouchInteractionEnabled = false
+        set(value) {
+            field = value
+        }
+
+    var digitalClockTextViewMap = mutableMapOf<Int, SimpleDigitalClockTextView>()
     private val digitLeftTopMap = mutableMapOf<Int, Point>()
 
     private var maxSingleDigitSize = Point(-1, -1)
     private val lockscreenTranslate = Point(0, 0)
     private var aodTranslate = Point(0, 0)
 
+    private var onAnimateDoze: (() -> Unit)? = null
+    private var isDozeReadyToAnimate = false
+
     // Does the current language have mono vertical size when displaying numerals
     private var isMonoVerticalNumericLineSpacing = true
 
@@ -57,13 +86,7 @@
 
     private val digitOffsets = mutableMapOf<Int, Float>()
 
-    override fun addView(child: View?) {
-        super.addView(child)
-        (child as SimpleDigitalClockTextView).digitTranslateAnimator =
-            DigitTranslateAnimator(::invalidate)
-    }
-
-    protected override fun calculateSize(widthMeasureSpec: Int, heightMeasureSpec: Int): Point {
+    protected fun calculateSize(widthMeasureSpec: Int, heightMeasureSpec: Int): Point? {
         maxSingleDigitSize = Point(-1, -1)
         val bottomLocation: (textView: SimpleDigitalClockTextView) -> Int = { textView ->
             if (isMonoVerticalNumericLineSpacing) {
@@ -85,7 +108,7 @@
         )
     }
 
-    protected override fun calculateLeftTopPosition() {
+    protected fun calculateLeftTopPosition() {
         digitLeftTopMap[R.id.HOUR_FIRST_DIGIT] = Point(0, 0)
         digitLeftTopMap[R.id.HOUR_SECOND_DIGIT] = Point(maxSingleDigitSize.x, 0)
         digitLeftTopMap[R.id.MINUTE_FIRST_DIGIT] = Point(0, maxSingleDigitSize.y)
@@ -96,13 +119,57 @@
         }
     }
 
-    override fun refreshTime() {
-        super.refreshTime()
+    override fun addView(child: View?) {
+        if (child == null) return
+        logger.d({ "addView($str1 @$int1)" }) {
+            str1 = child::class.simpleName!!
+            int1 = child.id
+        }
+
+        super.addView(child)
+        (child as? SimpleDigitalClockTextView)?.let {
+            it.digitTranslateAnimator = DigitTranslateAnimator(::invalidate)
+            digitalClockTextViewMap[child.id] = child
+        }
+        child.setWillNotDraw(true)
+    }
+
+    fun refreshTime() {
+        logger.d("refreshTime()")
         digitalClockTextViewMap.forEach { (_, textView) -> textView.refreshText() }
     }
 
+    override fun invalidate() {
+        logger.d("invalidate()")
+        super.invalidate()
+    }
+
+    override fun requestLayout() {
+        logger.d("requestLayout()")
+        super.requestLayout()
+    }
+
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        logger.d("onMeasure()")
+        calculateSize(widthMeasureSpec, heightMeasureSpec)?.let { size ->
+            setMeasuredDimension(size.x, size.y)
+        } ?: run { super.onMeasure(widthMeasureSpec, heightMeasureSpec) }
+        calculateLeftTopPosition()
+
+        isDozeReadyToAnimate = true
+        onAnimateDoze?.invoke()
+        onAnimateDoze = null
+    }
+
+    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+        logger.d("onLayout()")
+        super.onLayout(changed, left, top, right, bottom)
+    }
+
     override fun onDraw(canvas: Canvas) {
+        logger.d("onDraw()")
         super.onDraw(canvas)
+
         digitalClockTextViewMap.forEach { (id, textView) ->
             // save canvas location in anticipation of restoration later
             canvas.save()
@@ -117,14 +184,30 @@
         }
     }
 
-    override fun onLocaleChanged(locale: Locale) {
+    fun isAlignedWithScreen(): Boolean = false
+
+    fun onLocaleChanged(locale: Locale) {
         updateLocale(locale)
         requestLayout()
     }
 
-    override fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
-        dozeControlState.animateDoze = {
-            super.animateDoze(isDozing, isAnimated)
+    fun updateColor(color: Int) {
+        digitalClockTextViewMap.forEach { _, view -> view.updateColor(color) }
+        invalidate()
+    }
+
+    fun updateAxes(axes: List<ClockFontAxisSetting>) {
+        digitalClockTextViewMap.forEach { _, view -> view.updateAxes(axes) }
+        requestLayout()
+    }
+
+    fun onFontSettingChanged(fontSizePx: Float) {
+        digitalClockTextViewMap.forEach { _, view -> view.applyTextSize(fontSizePx) }
+    }
+
+    fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
+        fun executeDozeAnimation() {
+            digitalClockTextViewMap.forEach { _, view -> view.animateDoze(isDozing, isAnimated) }
             if (maxSingleDigitSize.x < 0 || maxSingleDigitSize.y < 0) {
                 measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
             }
@@ -150,10 +233,13 @@
                 }
             }
         }
+
+        if (isDozeReadyToAnimate) executeDozeAnimation()
+        else onAnimateDoze = { executeDozeAnimation() }
     }
 
-    override fun animateCharge() {
-        super.animateCharge()
+    fun animateCharge() {
+        digitalClockTextViewMap.forEach { _, view -> view.animateCharge() }
         digitalClockTextViewMap.forEach { (id, textView) ->
             textView.digitTranslateAnimator?.let {
                 it.animatePosition(
@@ -301,7 +387,7 @@
         // Add language tags below that do not have vertically mono spaced numerals
         private val NON_MONO_VERTICAL_NUMERIC_LINE_SPACING_LANGUAGES =
             setOf(
-                "my", // Burmese
+                "my" // Burmese
             )
 
         // Use the sign of targetTranslation to control the direction of digit translation
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
index cef24e9..2b0825f 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
@@ -30,7 +30,6 @@
 import android.util.Log
 import android.util.MathUtils
 import android.util.TypedValue
-import android.view.View.MeasureSpec.AT_MOST
 import android.view.View.MeasureSpec.EXACTLY
 import android.view.animation.Interpolator
 import android.widget.TextView
@@ -44,19 +43,30 @@
 import com.android.systemui.shared.clocks.DimensionParser
 import com.android.systemui.shared.clocks.FontTextStyle
 import com.android.systemui.shared.clocks.LogUtil
-import com.android.systemui.shared.clocks.RenderType
-import com.android.systemui.shared.clocks.TextStyle
 import java.lang.Thread
 import kotlin.math.max
 import kotlin.math.min
 
 private val TAG = SimpleDigitalClockTextView::class.simpleName!!
 
+enum class VerticalAlignment {
+    TOP,
+    BOTTOM,
+    BASELINE, // default
+    CENTER,
+}
+
+enum class HorizontalAlignment {
+    LEFT,
+    RIGHT,
+    CENTER, // default
+}
+
 @SuppressLint("AppCompatCustomView")
 open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSet? = null) :
-    TextView(clockCtx.context, attrs), SimpleDigitalClockView {
+    TextView(clockCtx.context, attrs) {
     val lockScreenPaint = TextPaint()
-    override lateinit var textStyle: FontTextStyle
+    lateinit var textStyle: FontTextStyle
     lateinit var aodStyle: FontTextStyle
 
     private var lsFontVariation = ClockFontAxisSetting.toFVar(DEFAULT_LS_VARIATION)
@@ -66,7 +76,6 @@
     var maxSingleDigitWidth = -1
     var digitTranslateAnimator: DigitTranslateAnimator? = null
     var aodFontSizePx: Float = -1F
-    var isVertical: Boolean = false
 
     // Store the font size when there's no height constraint as a reference when adjusting font size
     private var lastUnconstrainedTextSize: Float = Float.MAX_VALUE
@@ -98,25 +107,20 @@
         TextAnimator(layout, typefaceCache, invalidateCb)
     }
 
-    override var verticalAlignment: VerticalAlignment = VerticalAlignment.BASELINE
-    override var horizontalAlignment: HorizontalAlignment = HorizontalAlignment.LEFT
-    override var isAnimationEnabled = true
-    override var dozeFraction: Float = 0F
+    var verticalAlignment: VerticalAlignment = VerticalAlignment.BASELINE
+    var horizontalAlignment: HorizontalAlignment = HorizontalAlignment.LEFT
+    var isAnimationEnabled = true
+    var dozeFraction: Float = 0F
         set(value) {
             field = value
             invalidate()
         }
 
-    // Have to passthrough to unify View with SimpleDigitalClockView
-    override var text: String
-        get() = super.getText().toString()
-        set(value) = super.setText(value)
-
     var textBorderWidth = 0F
     var baselineFromMeasure = 0
     var lockscreenColor = Color.WHITE
 
-    override fun updateColor(color: Int) {
+    fun updateColor(color: Int) {
         lockscreenColor = color
         lockScreenPaint.color = lockscreenColor
         if (dozeFraction < 1f) {
@@ -125,7 +129,7 @@
         invalidate()
     }
 
-    override fun updateAxes(axes: List<ClockFontAxisSetting>) {
+    fun updateAxes(axes: List<ClockFontAxisSetting>) {
         lsFontVariation = ClockFontAxisSetting.toFVar(axes + OPTICAL_SIZE_AXIS)
         lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation)
         typeface = lockScreenPaint.typeface
@@ -142,16 +146,7 @@
 
     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
         logger.d("onMeasure()")
-        if (isVertical) {
-            // use at_most to avoid apply measuredWidth from last measuring to measuredHeight
-            // cause we use max to setMeasuredDimension
-            super.onMeasure(
-                MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), AT_MOST),
-                MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec), AT_MOST),
-            )
-        } else {
-            super.onMeasure(widthMeasureSpec, heightMeasureSpec)
-        }
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
 
         val layout = this.layout
         if (layout != null) {
@@ -207,18 +202,10 @@
                 )
         }
 
-        if (isVertical) {
-            expectedWidth = expectedHeight.also { expectedHeight = expectedWidth }
-        }
         setMeasuredDimension(expectedWidth, expectedHeight)
     }
 
     override fun onDraw(canvas: Canvas) {
-        if (isVertical) {
-            canvas.save()
-            canvas.translate(0F, measuredHeight.toFloat())
-            canvas.rotate(-90F)
-        }
         logger.d({ "onDraw(); ls: $str1" }) { str1 = textAnimator.textInterpolator.shapedText }
         val translation = getLocalTranslation()
         canvas.translate(translation.x.toFloat(), translation.y.toFloat())
@@ -226,47 +213,26 @@
             canvas.translate(it.updatedTranslate.x.toFloat(), it.updatedTranslate.y.toFloat())
         }
 
-        if (aodStyle.renderType == RenderType.HOLLOW_TEXT) {
-            canvas.saveLayer(
-                -translation.x.toFloat(),
-                -translation.y.toFloat(),
-                (-translation.x + measuredWidth).toFloat(),
-                (-translation.y + measuredHeight).toFloat(),
-                null,
-            )
-            canvas.saveLayer(
-                -translation.x.toFloat(),
-                -translation.y.toFloat(),
-                (-translation.x + measuredWidth).toFloat(),
-                (-translation.y + measuredHeight).toFloat(),
-                PORTER_DUFF_XFER_MODE_PAINT,
-            )
-            canvas.restore()
-            canvas.restore()
-        }
         textAnimator.draw(canvas)
 
         digitTranslateAnimator?.let {
             canvas.translate(-it.updatedTranslate.x.toFloat(), -it.updatedTranslate.y.toFloat())
         }
         canvas.translate(-translation.x.toFloat(), -translation.y.toFloat())
-        if (isVertical) {
-            canvas.restore()
-        }
     }
 
     override fun invalidate() {
         logger.d("invalidate()")
         super.invalidate()
-        (parent as? DigitalClockFaceView)?.invalidate()
+        (parent as? FlexClockView)?.invalidate()
     }
 
-    override fun refreshTime() {
+    fun refreshTime() {
         logger.d("refreshTime()")
         refreshText()
     }
 
-    override fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
+    fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
         if (!this::textAnimator.isInitialized) return
         textAnimator.setTextStyle(
             animate = isAnimated && isAnimationEnabled,
@@ -279,7 +245,7 @@
         updateTextBoundsForTextAnimator()
     }
 
-    override fun animateCharge() {
+    fun animateCharge() {
         if (!this::textAnimator.isInitialized || textAnimator.isRunning()) {
             // Skip charge animation if dozing animation is already playing.
             return
@@ -365,18 +331,20 @@
     }
 
     private fun updateXtranslation(inPoint: Point, interpolatedTextBounds: Rect): Point {
-        val viewWidth = if (isVertical) measuredHeight else measuredWidth
         when (horizontalAlignment) {
             HorizontalAlignment.LEFT -> {
                 inPoint.x = lockScreenPaint.strokeWidth.toInt() - interpolatedTextBounds.left
             }
             HorizontalAlignment.RIGHT -> {
                 inPoint.x =
-                    viewWidth - interpolatedTextBounds.right - lockScreenPaint.strokeWidth.toInt()
+                    measuredWidth -
+                        interpolatedTextBounds.right -
+                        lockScreenPaint.strokeWidth.toInt()
             }
             HorizontalAlignment.CENTER -> {
                 inPoint.x =
-                    (viewWidth - interpolatedTextBounds.width()) / 2 - interpolatedTextBounds.left
+                    (measuredWidth - interpolatedTextBounds.width()) / 2 -
+                        interpolatedTextBounds.left
             }
         }
         return inPoint
@@ -385,7 +353,6 @@
     // translation of reference point of text
     // used for translation when calling textInterpolator
     private fun getLocalTranslation(): Point {
-        val viewHeight = if (isVertical) measuredWidth else measuredHeight
         val interpolatedTextBounds = updateInterpolatedTextBounds()
         val localTranslation = Point(0, 0)
         val correctedBaseline = if (baseline != -1) baseline else baselineFromMeasure
@@ -393,7 +360,7 @@
         when (verticalAlignment) {
             VerticalAlignment.CENTER -> {
                 localTranslation.y =
-                    ((viewHeight - interpolatedTextBounds.height()) / 2 -
+                    ((measuredHeight - interpolatedTextBounds.height()) / 2 -
                         interpolatedTextBounds.top -
                         correctedBaseline)
             }
@@ -404,7 +371,7 @@
             }
             VerticalAlignment.BOTTOM -> {
                 localTranslation.y =
-                    viewHeight -
+                    measuredHeight -
                         interpolatedTextBounds.bottom -
                         lockScreenPaint.strokeWidth.toInt() -
                         correctedBaseline
@@ -419,27 +386,15 @@
         return updateXtranslation(localTranslation, interpolatedTextBounds)
     }
 
-    override fun applyStyles(textStyle: TextStyle, aodStyle: TextStyle?) {
-        this.textStyle = textStyle as FontTextStyle
-        val typefaceName = "fonts/" + textStyle.fontFamily
+    fun applyStyles(textStyle: FontTextStyle, aodStyle: FontTextStyle?) {
+        this.textStyle = textStyle
         lockScreenPaint.strokeJoin = Paint.Join.ROUND
         lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation)
-        textStyle.fontFeatureSettings?.let {
-            lockScreenPaint.fontFeatureSettings = it
-            fontFeatureSettings = it
-        }
         typeface = lockScreenPaint.typeface
         textStyle.lineHeight?.let { lineHeight = it.toInt() }
-        // borderWidth in textStyle and aodStyle is used to draw,
-        // strokeWidth in lockScreenPaint is used to measure and get enough space for the text
-        textStyle.borderWidth?.let { textBorderWidth = parser.convert(it) }
 
-        if (aodStyle != null && aodStyle is FontTextStyle) {
-            this.aodStyle = aodStyle
-        } else {
-            this.aodStyle = textStyle.copy()
-        }
-        this.aodStyle.transitionInterpolator?.let { aodDozingInterpolator = it.interpolator }
+        this.aodStyle = aodStyle ?: textStyle.copy()
+        this.aodStyle.transitionInterpolator?.let { aodDozingInterpolator = it }
         lockScreenPaint.strokeWidth = textBorderWidth
         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
         setInterpolatorPaint()
@@ -448,7 +403,7 @@
     }
 
     // When constrainedByHeight is on, targetFontSizePx is the constrained height of textView
-    override fun applyTextSize(targetFontSizePx: Float?, constrainedByHeight: Boolean) {
+    fun applyTextSize(targetFontSizePx: Float?, constrainedByHeight: Boolean = false) {
         val adjustedFontSizePx = adjustFontSize(targetFontSizePx, constrainedByHeight)
         val fontSizePx = adjustedFontSizePx * (textStyle.fontSizeScale ?: 1f)
         aodFontSizePx =
@@ -463,7 +418,6 @@
             val lastUnconstrainedHeight = textBounds.height() + lockScreenPaint.strokeWidth * 2
             fontSizeAdjustFactor = lastUnconstrainedHeight / lastUnconstrainedTextSize
         }
-        textStyle.borderWidthScale?.let { textBorderWidth = fontSizePx * it }
 
         lockScreenPaint.strokeWidth = textBorderWidth
         recomputeMaxSingleDigitSizes()
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt
deleted file mode 100644
index e8be28f..0000000
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt
+++ /dev/null
@@ -1,57 +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.shared.clocks.view
-
-import androidx.annotation.VisibleForTesting
-import com.android.systemui.plugins.clocks.ClockFontAxisSetting
-import com.android.systemui.shared.clocks.TextStyle
-
-interface SimpleDigitalClockView {
-    var text: String
-    var verticalAlignment: VerticalAlignment
-    var horizontalAlignment: HorizontalAlignment
-    var dozeFraction: Float
-    val textStyle: TextStyle
-    @VisibleForTesting var isAnimationEnabled: Boolean
-
-    fun applyStyles(textStyle: TextStyle, aodStyle: TextStyle?)
-
-    fun applyTextSize(targetFontSizePx: Float?, constrainedByHeight: Boolean = false)
-
-    fun updateColor(color: Int)
-
-    fun updateAxes(axes: List<ClockFontAxisSetting>)
-
-    fun refreshTime()
-
-    fun animateCharge()
-
-    fun animateDoze(isDozing: Boolean, isAnimated: Boolean)
-}
-
-enum class VerticalAlignment {
-    TOP,
-    BOTTOM,
-    BASELINE, // default
-    CENTER,
-}
-
-enum class HorizontalAlignment {
-    LEFT,
-    RIGHT,
-    CENTER, // default
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
index 2c1dacd..4d2a6d9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
@@ -232,7 +232,6 @@
     @Test
     fun testOnViewAttached_withAutoPinConfirmationFailedPasswordAttemptsLessThan5() {
         val pinViewController = constructPinViewController(mockKeyguardPinView)
-        `when`(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true)
         `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6)
         `when`(lockPatternUtils.isAutoPinConfirmEnabled(anyInt())).thenReturn(true)
         `when`(lockPatternUtils.getCurrentFailedPasswordAttempts(anyInt())).thenReturn(3)
@@ -249,7 +248,6 @@
     @Test
     fun testOnViewAttached_withAutoPinConfirmationFailedPasswordAttemptsMoreThan5() {
         val pinViewController = constructPinViewController(mockKeyguardPinView)
-        `when`(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true)
         `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6)
         `when`(lockPatternUtils.isAutoPinConfirmEnabled(anyInt())).thenReturn(true)
         `when`(lockPatternUtils.getCurrentFailedPasswordAttempts(anyInt())).thenReturn(6)
@@ -275,7 +273,6 @@
     @Test
     fun onUserInput_autoConfirmation_attemptsUnlock() {
         val pinViewController = constructPinViewController(mockKeyguardPinView)
-        whenever(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true)
         whenever(lockPatternUtils.getPinLength(anyInt())).thenReturn(6)
         whenever(lockPatternUtils.isAutoPinConfirmEnabled(anyInt())).thenReturn(true)
         whenever(passwordTextView.text).thenReturn("000000")
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java
index 2665910..6edf949 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java
@@ -65,7 +65,7 @@
     @Before
     public void setUp() throws Exception {
         final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
-        final SecureSettings mockSecureSettings = TestUtils.mockSecureSettings();
+        final SecureSettings mockSecureSettings = TestUtils.mockSecureSettings(mContext);
         final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager,
                 mockSecureSettings, mHearingAidDeviceManager);
         final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
index 241da5f..15afd25 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
@@ -71,7 +71,7 @@
     private AccessibilityManager mAccessibilityManager;
     @Mock
     private HearingAidDeviceManager mHearingAidDeviceManager;
-    private final SecureSettings mSecureSettings = TestUtils.mockSecureSettings();
+    private final SecureSettings mSecureSettings = TestUtils.mockSecureSettings(mContext);
     private RecyclerView mStubListView;
     private MenuView mMenuView;
     private MenuViewLayer mMenuViewLayer;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java
index 715c40a..56a97bb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java
@@ -89,7 +89,7 @@
     @Before
     public void setUp() throws Exception {
         final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
-        final SecureSettings secureSettings = TestUtils.mockSecureSettings();
+        final SecureSettings secureSettings = TestUtils.mockSecureSettings(mContext);
         final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager,
                 secureSettings, mHearingAidDeviceManager);
         final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java
index cb7c205..5ff7bd0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java
@@ -91,7 +91,7 @@
         mSpyContext = spy(mContext);
         doNothing().when(mSpyContext).startActivity(any());
 
-        final SecureSettings secureSettings = TestUtils.mockSecureSettings();
+        final SecureSettings secureSettings = TestUtils.mockSecureSettings(mContext);
         final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager,
                 secureSettings, mHearingAidDeviceManager);
         final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/utils/TestUtils.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/utils/TestUtils.java
index 8399fa8..aafb212 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/utils/TestUtils.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/utils/TestUtils.java
@@ -22,6 +22,7 @@
 import static org.mockito.Mockito.when;
 
 import android.content.ComponentName;
+import android.content.Context;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -76,8 +77,10 @@
      * Returns a mock secure settings configured to return information needed for tests.
      * Currently, this only includes button targets.
      */
-    public static SecureSettings mockSecureSettings() {
+    public static SecureSettings mockSecureSettings(Context context) {
         SecureSettings secureSettings = mock(SecureSettings.class);
+        when(secureSettings.getRealUserHandle(UserHandle.USER_CURRENT))
+                .thenReturn(context.getUserId());
 
         final String targets = getShortcutTargets(
                 Set.of(TEST_COMPONENT_A, TEST_COMPONENT_B));
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt
index a11dace..4c329dc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt
@@ -18,12 +18,20 @@
 
 import android.bluetooth.BluetoothLeBroadcast
 import android.bluetooth.BluetoothLeBroadcastMetadata
+import android.content.ContentResolver
+import android.content.applicationContext
 import android.testing.TestableLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.BluetoothEventManager
 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager
+import com.android.settingslib.bluetooth.VolumeControlProfile
+import com.android.settingslib.volume.shared.AudioSharingLogger
 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.testKosmos
 import com.google.common.truth.Truth.assertThat
@@ -38,10 +46,16 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
 import org.mockito.Mock
+import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.junit.MockitoJUnit
 import org.mockito.junit.MockitoRule
 import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.times
+import org.mockito.kotlin.whenever
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -50,8 +64,11 @@
 class AudioSharingInteractorTest : SysuiTestCase() {
     @get:Rule val mockito: MockitoRule = MockitoJUnit.rule()
     private val kosmos = testKosmos()
+
     @Mock private lateinit var localBluetoothLeBroadcast: LocalBluetoothLeBroadcast
+
     @Mock private lateinit var bluetoothLeBroadcastMetadata: BluetoothLeBroadcastMetadata
+
     @Captor private lateinit var callbackCaptor: ArgumentCaptor<BluetoothLeBroadcast.Callback>
     private lateinit var underTest: AudioSharingInteractor
 
@@ -157,13 +174,15 @@
     fun testHandleAudioSourceWhenReady_hasProfileButAudioSharingOff_sourceNotAdded() =
         with(kosmos) {
             testScope.runTest {
-                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
+                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
                 bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
                 bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
                     localBluetoothLeBroadcast
                 )
                 val job = launch { underTest.handleAudioSourceWhenReady() }
                 runCurrent()
+                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
+                runCurrent()
 
                 assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse()
                 job.cancel()
@@ -174,15 +193,14 @@
     fun testHandleAudioSourceWhenReady_audioSharingOnButNoPlayback_sourceNotAdded() =
         with(kosmos) {
             testScope.runTest {
-                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
+                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
                 bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
                 bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
                     localBluetoothLeBroadcast
                 )
                 val job = launch { underTest.handleAudioSourceWhenReady() }
                 runCurrent()
-                verify(localBluetoothLeBroadcast)
-                    .registerServiceCallBack(any(), callbackCaptor.capture())
+                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
                 runCurrent()
 
                 assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse()
@@ -194,13 +212,15 @@
     fun testHandleAudioSourceWhenReady_audioSharingOnAndPlaybackStarts_sourceAdded() =
         with(kosmos) {
             testScope.runTest {
-                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
+                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
                 bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
                 bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
                     localBluetoothLeBroadcast
                 )
                 val job = launch { underTest.handleAudioSourceWhenReady() }
                 runCurrent()
+                bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
+                runCurrent()
                 verify(localBluetoothLeBroadcast)
                     .registerServiceCallBack(any(), callbackCaptor.capture())
                 runCurrent()
@@ -211,4 +231,100 @@
                 job.cancel()
             }
         }
+
+    @Test
+    fun testHandleAudioSourceWhenReady_skipInitialValue_noAudioSharing_sourceNotAdded() =
+        with(kosmos) {
+            testScope.runTest {
+                val (broadcast, repository) = setupRepositoryImpl()
+                val interactor =
+                    object :
+                        AudioSharingInteractorImpl(
+                            applicationContext,
+                            localBluetoothManager,
+                            repository,
+                            testDispatcher,
+                        ) {
+                        override suspend fun audioSharingAvailable() = true
+                    }
+                val job = launch { interactor.handleAudioSourceWhenReady() }
+                runCurrent()
+                // Verify callback registered for onBroadcastStartedOrStopped
+                verify(broadcast).registerServiceCallBack(any(), callbackCaptor.capture())
+                runCurrent()
+                // Verify source is not added
+                verify(repository, never()).addSource()
+                job.cancel()
+            }
+        }
+
+    @Test
+    fun testHandleAudioSourceWhenReady_skipInitialValue_newAudioSharing_sourceAdded() =
+        with(kosmos) {
+            testScope.runTest {
+                val (broadcast, repository) = setupRepositoryImpl()
+                val interactor =
+                    object :
+                        AudioSharingInteractorImpl(
+                            applicationContext,
+                            localBluetoothManager,
+                            repository,
+                            testDispatcher,
+                        ) {
+                        override suspend fun audioSharingAvailable() = true
+                    }
+                val job = launch { interactor.handleAudioSourceWhenReady() }
+                runCurrent()
+                // Verify callback registered for onBroadcastStartedOrStopped
+                verify(broadcast).registerServiceCallBack(any(), callbackCaptor.capture())
+                // Audio sharing started, trigger onBroadcastStarted
+                whenever(broadcast.isEnabled(null)).thenReturn(true)
+                callbackCaptor.value.onBroadcastStarted(0, 0)
+                runCurrent()
+                // Verify callback registered for onBroadcastMetadataChanged
+                verify(broadcast, times(2)).registerServiceCallBack(any(), callbackCaptor.capture())
+                runCurrent()
+                // Trigger onBroadcastMetadataChanged (ready to add source)
+                callbackCaptor.value.onBroadcastMetadataChanged(0, bluetoothLeBroadcastMetadata)
+                runCurrent()
+                // Verify source added
+                verify(repository).addSource()
+                job.cancel()
+            }
+        }
+
+    private fun setupRepositoryImpl(): Pair<LocalBluetoothLeBroadcast, AudioSharingRepositoryImpl> {
+        with(kosmos) {
+            val broadcast =
+                mock<LocalBluetoothLeBroadcast> {
+                    on { isProfileReady } doReturn true
+                    on { isEnabled(null) } doReturn false
+                }
+            val assistant =
+                mock<LocalBluetoothLeBroadcastAssistant> { on { isProfileReady } doReturn true }
+            val volumeControl = mock<VolumeControlProfile> { on { isProfileReady } doReturn true }
+            val profileManager =
+                mock<LocalBluetoothProfileManager> {
+                    on { leAudioBroadcastProfile } doReturn broadcast
+                    on { leAudioBroadcastAssistantProfile } doReturn assistant
+                    on { volumeControlProfile } doReturn volumeControl
+                }
+            whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
+            whenever(localBluetoothManager.eventManager).thenReturn(mock<BluetoothEventManager> {})
+
+            val repository =
+                AudioSharingRepositoryImpl(
+                    localBluetoothManager,
+                    com.android.settingslib.volume.data.repository.AudioSharingRepositoryImpl(
+                        mock<ContentResolver> {},
+                        localBluetoothManager,
+                        testScope.backgroundScope,
+                        testScope.testScheduler,
+                        mock<AudioSharingLogger> {},
+                    ),
+                    testDispatcher,
+                )
+            return Pair(broadcast, spy(repository))
+        }
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt
index acfe9dd..f074606 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt
@@ -111,6 +111,28 @@
         }
 
     @Test
+    fun testStopAudioSharing() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
+                whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile)
+                audioSharingRepository.setAudioSharingAvailable(true)
+                underTest.stopAudioSharing()
+                verify(leAudioBroadcastProfile).stopLatestBroadcast()
+            }
+        }
+
+    @Test
+    fun testStopAudioSharing_flagOff_doNothing() =
+        with(kosmos) {
+            testScope.runTest {
+                audioSharingRepository.setAudioSharingAvailable(false)
+                underTest.stopAudioSharing()
+                verify(leAudioBroadcastProfile, never()).stopLatestBroadcast()
+            }
+        }
+
+    @Test
     fun testAddSource_flagOff_doesNothing() =
         with(kosmos) {
             testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt
index 44f9720..ad0337e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt
@@ -15,14 +15,15 @@
  */
 package com.android.systemui.bluetooth.qsdialog
 
-import androidx.test.ext.junit.runners.AndroidJUnit4
 import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
@@ -48,6 +49,7 @@
     private lateinit var notConnectedDeviceItem: DeviceItem
     private lateinit var connectedMediaDeviceItem: DeviceItem
     private lateinit var connectedOtherDeviceItem: DeviceItem
+    private lateinit var audioSharingDeviceItem: DeviceItem
     @Mock private lateinit var dialog: SystemUIDialog
 
     @Before
@@ -59,7 +61,7 @@
                 deviceName = DEVICE_NAME,
                 connectionSummary = DEVICE_CONNECTION_SUMMARY,
                 iconWithDescription = null,
-                background = null
+                background = null,
             )
         notConnectedDeviceItem =
             DeviceItem(
@@ -68,7 +70,7 @@
                 deviceName = DEVICE_NAME,
                 connectionSummary = DEVICE_CONNECTION_SUMMARY,
                 iconWithDescription = null,
-                background = null
+                background = null,
             )
         connectedMediaDeviceItem =
             DeviceItem(
@@ -77,7 +79,7 @@
                 deviceName = DEVICE_NAME,
                 connectionSummary = DEVICE_CONNECTION_SUMMARY,
                 iconWithDescription = null,
-                background = null
+                background = null,
             )
         connectedOtherDeviceItem =
             DeviceItem(
@@ -86,7 +88,16 @@
                 deviceName = DEVICE_NAME,
                 connectionSummary = DEVICE_CONNECTION_SUMMARY,
                 iconWithDescription = null,
-                background = null
+                background = null,
+            )
+        audioSharingDeviceItem =
+            DeviceItem(
+                type = DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE,
+                cachedBluetoothDevice = kosmos.cachedBluetoothDevice,
+                deviceName = DEVICE_NAME,
+                connectionSummary = DEVICE_CONNECTION_SUMMARY,
+                iconWithDescription = null,
+                background = null,
             )
         actionInteractorImpl = kosmos.deviceItemActionInteractorImpl
     }
@@ -135,6 +146,29 @@
         }
     }
 
+    @Test
+    fun onActionIconClick_onIntent() {
+        with(kosmos) {
+            testScope.runTest {
+                var onIntentCalledOnAddress = ""
+                whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
+                actionInteractorImpl.onActionIconClick(connectedMediaDeviceItem) {
+                    onIntentCalledOnAddress = connectedMediaDeviceItem.cachedBluetoothDevice.address
+                }
+                assertThat(onIntentCalledOnAddress).isEqualTo(DEVICE_ADDRESS)
+            }
+        }
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun onActionIconClick_audioSharingDeviceType_throwException() {
+        with(kosmos) {
+            testScope.runTest {
+                actionInteractorImpl.onActionIconClick(audioSharingDeviceItem) {}
+            }
+        }
+    }
+
     private companion object {
         const val DEVICE_NAME = "device"
         const val DEVICE_CONNECTION_SUMMARY = "active"
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
index b33a83c..a654155 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
@@ -69,6 +69,7 @@
 import com.android.systemui.scene.ui.composable.Scene
 import com.android.systemui.scene.ui.composable.SceneContainer
 import com.android.systemui.scene.ui.composable.SceneContainerTransitions
+import com.android.systemui.scene.ui.view.sceneJankMonitorFactory
 import com.android.systemui.testKosmos
 import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.awaitCancellation
@@ -193,6 +194,7 @@
                                 overlayByKey = emptyMap(),
                                 dataSourceDelegator = kosmos.sceneDataSourceDelegator,
                                 qsSceneAdapter = { kosmos.fakeQsSceneAdapter },
+                                sceneJankMonitorFactory = kosmos.sceneJankMonitorFactory,
                             )
                         }
                     },
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayoutTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayoutTest.kt
similarity index 95%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayoutTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayoutTest.kt
index 3ede841..b4b4178 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayoutTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerSceneLayoutTest.kt
@@ -14,14 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.systemui.bouncer.ui.helper
+package com.android.systemui.bouncer.ui.composable
 
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout.BELOW_USER_SWITCHER
-import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout.BESIDE_USER_SWITCHER
-import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout.SPLIT_BOUNCER
-import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout.STANDARD_BOUNCER
+import com.android.systemui.bouncer.ui.composable.BouncerSceneLayout.BELOW_USER_SWITCHER
+import com.android.systemui.bouncer.ui.composable.BouncerSceneLayout.BESIDE_USER_SWITCHER
+import com.android.systemui.bouncer.ui.composable.BouncerSceneLayout.SPLIT_BOUNCER
+import com.android.systemui.bouncer.ui.composable.BouncerSceneLayout.STANDARD_BOUNCER
 import com.google.common.truth.Truth.assertThat
 import java.util.Locale
 import org.junit.Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt
index 20d6615..6c955bf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt
@@ -256,6 +256,22 @@
 
     @EnableSceneContainer
     @Test
+    fun playSuccessHaptic_onFaceAuthSuccess_whenBypassDisabled_sceneContainer() =
+        testScope.runTest {
+            underTest = kosmos.deviceEntryHapticsInteractor
+            val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic)
+
+            enrollFace()
+            kosmos.configureKeyguardBypass(isBypassAvailable = false)
+            runCurrent()
+            configureDeviceEntryFromBiometricSource(isFaceUnlock = true, bypassEnabled = false)
+            kosmos.fakeDeviceEntryFaceAuthRepository.isAuthenticated.value = true
+
+            assertThat(playSuccessHaptic).isNotNull()
+        }
+
+    @EnableSceneContainer
+    @Test
     fun skipSuccessHaptic_onDeviceEntryFromSfps_whenPowerDown_sceneContainer() =
         testScope.runTest {
             kosmos.configureKeyguardBypass(isBypassAvailable = false)
@@ -299,6 +315,7 @@
     private fun configureDeviceEntryFromBiometricSource(
         isFpUnlock: Boolean = false,
         isFaceUnlock: Boolean = false,
+        bypassEnabled: Boolean = true,
     ) {
         // Mock DeviceEntrySourceInteractor#deviceEntryBiometricAuthSuccessState
         if (isFpUnlock) {
@@ -314,11 +331,14 @@
             )
 
             // Mock DeviceEntrySourceInteractor#faceWakeAndUnlockMode = MODE_UNLOCK_COLLAPSING
-            kosmos.sceneInteractor.setTransitionState(
-                MutableStateFlow<ObservableTransitionState>(
-                    ObservableTransitionState.Idle(Scenes.Lockscreen)
+            // if the successful face authentication will bypass keyguard
+            if (bypassEnabled) {
+                kosmos.sceneInteractor.setTransitionState(
+                    MutableStateFlow<ObservableTransitionState>(
+                        ObservableTransitionState.Idle(Scenes.Lockscreen)
+                    )
                 )
-            )
+            }
         }
         underTest = kosmos.deviceEntryHapticsInteractor
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt
similarity index 81%
rename from packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt
index 74e8257..5e023a2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt
@@ -292,8 +292,7 @@
 
             val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
             val originalValue = model!!.signalCount
-            val listener = getOverviewProxyListener()
-            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+            updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
 
             assertThat(model?.signalCount).isEqualTo(originalValue + 1)
         }
@@ -306,8 +305,7 @@
 
             val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
             val originalValue = model!!.signalCount
-            val listener = getOverviewProxyListener()
-            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+            updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
 
             assertThat(model?.signalCount).isEqualTo(originalValue)
         }
@@ -321,8 +319,7 @@
 
             val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
             val originalValue = model!!.signalCount
-            val listener = getOverviewProxyListener()
-            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+            updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
 
             assertThat(model?.signalCount).isEqualTo(originalValue + 1)
         }
@@ -335,8 +332,7 @@
 
             val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
             val originalValue = model!!.signalCount
-            val listener = getOverviewProxyListener()
-            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+            updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
 
             assertThat(model?.signalCount).isEqualTo(originalValue)
         }
@@ -347,8 +343,7 @@
             val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
             assertThat(model?.lastShortcutTriggeredTime).isNull()
 
-            val listener = getOverviewProxyListener()
-            listener.updateContextualEduStats(/* isTrackpadGesture= */ true, gestureType)
+            updateContextualEduStats(/* isTrackpadGesture= */ true, gestureType)
 
             assertThat(model?.lastShortcutTriggeredTime).isEqualTo(kosmos.fakeEduClock.instant())
         }
@@ -358,15 +353,14 @@
         testScope.runTest {
             setUpForDeviceConnection()
             tutorialSchedulerRepository.setScheduledTutorialLaunchTime(
-                DeviceType.TOUCHPAD,
+                getTargetDevice(gestureType),
                 eduClock.instant(),
             )
 
             val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
             val originalValue = model!!.signalCount
             eduClock.offset(initialDelayElapsedDuration)
-            val listener = getOverviewProxyListener()
-            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+            updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
 
             assertThat(model?.signalCount).isEqualTo(originalValue + 1)
         }
@@ -376,33 +370,92 @@
         testScope.runTest {
             setUpForDeviceConnection()
             tutorialSchedulerRepository.setScheduledTutorialLaunchTime(
-                DeviceType.TOUCHPAD,
+                getTargetDevice(gestureType),
                 eduClock.instant(),
             )
 
             val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
             val originalValue = model!!.signalCount
             // No offset to the clock to simulate update before initial delay
-            val listener = getOverviewProxyListener()
-            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+            updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
 
             assertThat(model?.signalCount).isEqualTo(originalValue)
         }
 
     @Test
-    fun dataUnchangedOnIncrementSignalCountWithoutOobeLaunchTime() =
+    fun dataUnchangedOnIncrementSignalCountWithoutOobeLaunchOrNotifyTime() =
         testScope.runTest {
-            // No update to OOBE launch time to simulate no OOBE is launched yet
+            // No update to OOBE launch/notify time to simulate no OOBE is launched yet
             setUpForDeviceConnection()
 
             val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
             val originalValue = model!!.signalCount
-            val listener = getOverviewProxyListener()
-            listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+            updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
 
             assertThat(model?.signalCount).isEqualTo(originalValue)
         }
 
+    @Test
+    fun dataUpdatedOnIncrementSignalCountAfterNotifyTimeDelayWithoutLaunchTime() =
+        testScope.runTest {
+            setUpForDeviceConnection()
+            tutorialSchedulerRepository.setNotifiedTime(
+                getTargetDevice(gestureType),
+                eduClock.instant(),
+            )
+
+            val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+            val originalValue = model!!.signalCount
+            eduClock.offset(initialDelayElapsedDuration)
+            updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+
+            assertThat(model?.signalCount).isEqualTo(originalValue + 1)
+        }
+
+    @Test
+    fun dataUnchangedOnIncrementSignalCountBeforeLaunchTimeDelayWithNotifyTime() =
+        testScope.runTest {
+            setUpForDeviceConnection()
+            tutorialSchedulerRepository.setNotifiedTime(
+                getTargetDevice(gestureType),
+                eduClock.instant(),
+            )
+            eduClock.offset(initialDelayElapsedDuration)
+
+            tutorialSchedulerRepository.setScheduledTutorialLaunchTime(
+                getTargetDevice(gestureType),
+                eduClock.instant(),
+            )
+            val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+            val originalValue = model!!.signalCount
+            // No offset to the clock to simulate update before initial delay of launch time
+            updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+
+            assertThat(model?.signalCount).isEqualTo(originalValue)
+        }
+
+    @Test
+    fun dataUpdatedOnIncrementSignalCountAfterLaunchTimeDelayWithNotifyTime() =
+        testScope.runTest {
+            setUpForDeviceConnection()
+            tutorialSchedulerRepository.setNotifiedTime(
+                getTargetDevice(gestureType),
+                eduClock.instant(),
+            )
+            eduClock.offset(initialDelayElapsedDuration)
+
+            tutorialSchedulerRepository.setScheduledTutorialLaunchTime(
+                getTargetDevice(gestureType),
+                eduClock.instant(),
+            )
+            val model by collectLastValue(repository.readGestureEduModelFlow(gestureType))
+            val originalValue = model!!.signalCount
+            eduClock.offset(initialDelayElapsedDuration)
+            updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType)
+
+            assertThat(model?.signalCount).isEqualTo(originalValue + 1)
+        }
+
     private suspend fun setUpForInitialDelayElapse() {
         tutorialSchedulerRepository.setScheduledTutorialLaunchTime(
             DeviceType.TOUCHPAD,
@@ -465,12 +518,18 @@
         keyboardRepository.setIsAnyKeyboardConnected(true)
     }
 
-    private fun getOverviewProxyListener(): OverviewProxyListener {
+    private fun updateContextualEduStats(isTrackpadGesture: Boolean, gestureType: GestureType) {
         val listenerCaptor = argumentCaptor<OverviewProxyListener>()
         verify(overviewProxyService).addCallback(listenerCaptor.capture())
-        return listenerCaptor.firstValue
+        listenerCaptor.firstValue.updateContextualEduStats(isTrackpadGesture, gestureType)
     }
 
+    private fun getTargetDevice(gestureType: GestureType) =
+        when (gestureType) {
+            ALL_APPS -> DeviceType.KEYBOARD
+            else -> DeviceType.TOUCHPAD
+        }
+
     companion object {
         private val USER_INFOS = listOf(UserInfo(101, "Second User", 0))
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt
index e659ef2..698fac1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt
@@ -18,7 +18,9 @@
 
 import android.content.Context
 import android.content.Context.INPUT_SERVICE
+import android.content.Intent
 import android.hardware.input.InputGestureData
+import android.hardware.input.InputManager
 import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS
 import android.hardware.input.fakeInputManager
 import android.platform.test.annotations.EnableFlags
@@ -27,9 +29,12 @@
 import com.android.hardware.input.Flags.FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES
 import com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.broadcastDispatcher
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyboard.shortcut.customInputGesturesRepository
 import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.allAppsInputGestureData
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.goHomeInputGestureData
+import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.settings.FakeUserTracker
 import com.android.systemui.settings.userTracker
@@ -48,18 +53,41 @@
 @EnableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
 class CustomInputGesturesRepositoryTest : SysuiTestCase() {
 
-    private val mockUserContext: Context = mock()
+    private val primaryUserContext: Context = mock()
+    private val secondaryUserContext: Context = mock()
+    private var activeUserContext: Context = primaryUserContext
+
     private val kosmos = testKosmos().also {
-        it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { mockUserContext })
+        it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { activeUserContext })
     }
 
     private val inputManager = kosmos.fakeInputManager.inputManager
+    private val broadcastDispatcher = kosmos.broadcastDispatcher
+    private val inputManagerForSecondaryUser: InputManager = mock()
     private val testScope = kosmos.testScope
+    private val testHelper = kosmos.shortcutHelperTestHelper
     private val customInputGesturesRepository = kosmos.customInputGesturesRepository
 
     @Before
-    fun setup(){
-        whenever(mockUserContext.getSystemService(INPUT_SERVICE)).thenReturn(inputManager)
+    fun setup() {
+        activeUserContext = primaryUserContext
+        whenever(primaryUserContext.getSystemService(INPUT_SERVICE)).thenReturn(inputManager)
+        whenever(secondaryUserContext.getSystemService(INPUT_SERVICE))
+            .thenReturn(inputManagerForSecondaryUser)
+    }
+
+    @Test
+    fun customInputGestures_emitsNewUsersInputGesturesWhenUserIsSwitch() {
+        testScope.runTest {
+            setCustomInputGesturesForPrimaryUser(allAppsInputGestureData)
+            setCustomInputGesturesForSecondaryUser(goHomeInputGestureData)
+
+            val inputGestures by collectLastValue(customInputGesturesRepository.customInputGestures)
+            assertThat(inputGestures).containsExactly(allAppsInputGestureData)
+
+            switchToSecondaryUser()
+            assertThat(inputGestures).containsExactly(goHomeInputGestureData)
+        }
     }
 
     @Test
@@ -115,4 +143,24 @@
         }
     }
 
+    private fun setCustomInputGesturesForPrimaryUser(vararg inputGesture: InputGestureData) {
+        whenever(
+            inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY)
+        ).thenReturn(inputGesture.toList())
+    }
+
+    private fun setCustomInputGesturesForSecondaryUser(vararg inputGesture: InputGestureData) {
+        whenever(
+            inputManagerForSecondaryUser.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY)
+        ).thenReturn(inputGesture.toList())
+    }
+
+    private fun switchToSecondaryUser() {
+        activeUserContext = secondaryUserContext
+        broadcastDispatcher.sendIntentToMatchingReceiversOnly(
+            context,
+            Intent(Intent.ACTION_USER_SWITCHED)
+        )
+    }
+
 }
\ No newline at end of file
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt
index 7c88d76..183e4d6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt
@@ -24,6 +24,7 @@
 import android.os.SystemClock
 import android.view.KeyEvent
 import android.view.KeyEvent.ACTION_DOWN
+import android.view.KeyEvent.ACTION_UP
 import android.view.KeyEvent.KEYCODE_A
 import android.view.KeyEvent.META_ALT_ON
 import android.view.KeyEvent.META_CTRL_ON
@@ -540,11 +541,7 @@
             simpleShortcutCategory(System, "System apps", "Take a note"),
             simpleShortcutCategory(System, "System controls", "Take screenshot"),
             simpleShortcutCategory(System, "System controls", "Go back"),
-            simpleShortcutCategory(
-                MultiTasking,
-                "Split screen",
-                "Switch to full screen",
-            ),
+            simpleShortcutCategory(MultiTasking, "Split screen", "Switch to full screen"),
             simpleShortcutCategory(
                 MultiTasking,
                 "Split screen",
@@ -704,7 +701,7 @@
             android.view.KeyEvent(
                 /* downTime = */ SystemClock.uptimeMillis(),
                 /* eventTime = */ SystemClock.uptimeMillis(),
-                /* action = */ ACTION_DOWN,
+                /* action = */ ACTION_UP,
                 /* code = */ KEYCODE_A,
                 /* repeat = */ 0,
                 /* metaState = */ 0,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt
index 755c218..d9d34f5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt
@@ -92,13 +92,14 @@
             viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
             val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
 
-            assertThat(uiState).isEqualTo(
-                AddShortcutDialog(
-                    shortcutLabel = "Standard shortcut",
-                    defaultCustomShortcutModifierKey =
-                    ShortcutKey.Icon.ResIdIcon(R.drawable.ic_ksh_key_meta),
+            assertThat(uiState)
+                .isEqualTo(
+                    AddShortcutDialog(
+                        shortcutLabel = "Standard shortcut",
+                        defaultCustomShortcutModifierKey =
+                            ShortcutKey.Icon.ResIdIcon(R.drawable.ic_ksh_key_meta),
+                    )
                 )
-            )
         }
     }
 
@@ -137,8 +138,7 @@
         testScope.runTest {
             val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
             viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
-            assertThat((uiState as AddShortcutDialog).pressedKeys)
-                .isEmpty()
+            assertThat((uiState as AddShortcutDialog).pressedKeys).isEmpty()
         }
     }
 
@@ -161,8 +161,7 @@
             val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
             viewModel.onShortcutCustomizationRequested(allAppsShortcutAddRequest)
 
-            assertThat((uiState as AddShortcutDialog).errorMessage)
-                .isEmpty()
+            assertThat((uiState as AddShortcutDialog).errorMessage).isEmpty()
         }
     }
 
@@ -244,32 +243,34 @@
     }
 
     @Test
-    fun onKeyPressed_handlesKeyEvents_whereActionKeyIsAlsoPressed() {
+    fun onShortcutKeyCombinationSelected_handlesKeyEvents_whereActionKeyIsAlsoPressed() {
         testScope.runTest {
             viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
-            val isHandled = viewModel.onKeyPressed(keyDownEventWithActionKeyPressed)
+            val isHandled =
+                viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed)
 
             assertThat(isHandled).isTrue()
         }
     }
 
     @Test
-    fun onKeyPressed_doesNotHandleKeyEvents_whenActionKeyIsNotAlsoPressed() {
+    fun onShortcutKeyCombinationSelected_doesNotHandleKeyEvents_whenActionKeyIsNotAlsoPressed() {
         testScope.runTest {
             viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
-            val isHandled = viewModel.onKeyPressed(keyDownEventWithoutActionKeyPressed)
+            val isHandled =
+                viewModel.onShortcutKeyCombinationSelected(keyDownEventWithoutActionKeyPressed)
 
             assertThat(isHandled).isFalse()
         }
     }
 
     @Test
-    fun onKeyPressed_convertsKeyEventsAndUpdatesUiStatesPressedKey() {
+    fun onShortcutKeyCombinationSelected_convertsKeyEventsAndUpdatesUiStatesPressedKey() {
         testScope.runTest {
             val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
             viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
-            viewModel.onKeyPressed(keyDownEventWithActionKeyPressed)
-            viewModel.onKeyPressed(keyUpEventWithActionKeyPressed)
+            viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed)
+            viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed)
 
             // Note that Action Key is excluded as it's already displayed on the UI
             assertThat((uiState as AddShortcutDialog).pressedKeys)
@@ -282,8 +283,8 @@
         testScope.runTest {
             val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
             viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
-            viewModel.onKeyPressed(keyDownEventWithActionKeyPressed)
-            viewModel.onKeyPressed(keyUpEventWithActionKeyPressed)
+            viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed)
+            viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed)
 
             // Note that Action Key is excluded as it's already displayed on the UI
             assertThat((uiState as AddShortcutDialog).pressedKeys)
@@ -292,16 +293,15 @@
             // Close the dialog and show it again
             viewModel.onDialogDismissed()
             viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
-            assertThat((uiState as AddShortcutDialog).pressedKeys)
-                .isEmpty()
+            assertThat((uiState as AddShortcutDialog).pressedKeys).isEmpty()
         }
     }
 
     private suspend fun openAddShortcutDialogAndSetShortcut() {
         viewModel.onShortcutCustomizationRequested(allAppsShortcutAddRequest)
 
-        viewModel.onKeyPressed(keyDownEventWithActionKeyPressed)
-        viewModel.onKeyPressed(keyUpEventWithActionKeyPressed)
+        viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed)
+        viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed)
 
         viewModel.onSetShortcut()
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt
index fde9b8c..bf49186 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt
@@ -28,7 +28,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.settingslib.notification.modes.EnableZenModeDialog
-import com.android.settingslib.notification.modes.TestModeBuilder
+import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.ContentDescription
@@ -187,7 +187,7 @@
         testScope.runTest {
             val currentModes by collectLastValue(zenModeRepository.modes)
 
-            zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_ACTIVE)
+            zenModeRepository.activateMode(MANUAL_DND)
             secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, -2)
             collectLastValue(underTest.lockScreenState)
             runCurrent()
@@ -233,7 +233,6 @@
         testScope.runTest {
             val currentModes by collectLastValue(zenModeRepository.modes)
 
-            zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE)
             secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_FOREVER)
             collectLastValue(underTest.lockScreenState)
             runCurrent()
@@ -278,7 +277,6 @@
     fun onTriggered_dndModeIsOff_settingNotFOREVERorPROMPT_dndWithDuration() =
         testScope.runTest {
             val currentModes by collectLastValue(zenModeRepository.modes)
-            zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE)
             secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, -900)
             runCurrent()
 
@@ -323,7 +321,6 @@
     fun onTriggered_dndModeIsOff_settingIsPROMPT_showDialog() =
         testScope.runTest {
             val expandable: Expandable = mock()
-            zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE)
             secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_PROMPT)
             whenever(enableZenModeDialog.createDialog()).thenReturn(mock())
             collectLastValue(underTest.lockScreenState)
@@ -405,10 +402,6 @@
         testScope.runTest {
             val lockScreenState by collectLastValue(underTest.lockScreenState)
 
-            val manualDnd = TestModeBuilder.MANUAL_DND_INACTIVE
-            zenModeRepository.addMode(manualDnd)
-            runCurrent()
-
             assertThat(lockScreenState)
                 .isEqualTo(
                     KeyguardQuickAffordanceConfig.LockScreenState.Visible(
@@ -420,7 +413,7 @@
                     )
                 )
 
-            zenModeRepository.activateMode(manualDnd)
+            zenModeRepository.activateMode(MANUAL_DND)
             runCurrent()
 
             assertThat(lockScreenState)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
index b29a5f4..9e8713b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
@@ -34,6 +34,7 @@
 import com.android.systemui.keyguard.shared.model.StatusBarState.KEYGUARD
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat as assertThatRepository
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.shade.data.repository.FlingInfo
 import com.android.systemui.shade.data.repository.fakeShadeRepository
@@ -47,7 +48,6 @@
 import org.junit.runner.RunWith
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.spy
-import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat as assertThatRepository
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -55,9 +55,8 @@
 class FromLockscreenTransitionInteractorTest : SysuiTestCase() {
     private val kosmos =
         testKosmos().apply {
-            this.fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository(
-                testScope = testScope,
-            ))
+            this.fakeKeyguardTransitionRepository =
+                spy(FakeKeyguardTransitionRepository(testScope = testScope))
         }
 
     private val testScope = kosmos.testScope
@@ -181,6 +180,12 @@
             underTest.start()
             assertThatRepository(transitionRepository).noTransitionsStarted()
 
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.DOZING,
+                to = KeyguardState.LOCKSCREEN,
+                testScope = testScope,
+            )
+
             keyguardRepository.setKeyguardDismissible(true)
             runCurrent()
             shadeRepository.setCurrentFling(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt
index e60d971..282bebc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt
@@ -25,13 +25,14 @@
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.keyguardClockRepository
 import com.android.systemui.keyguard.data.repository.keyguardRepository
 import com.android.systemui.keyguard.shared.model.ClockSize
+import com.android.systemui.keyguard.shared.model.DozeStateModel
+import com.android.systemui.keyguard.shared.model.DozeTransitionModel
 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.testScope
 import com.android.systemui.media.controls.data.repository.mediaFilterRepository
@@ -75,25 +76,16 @@
         }
 
     @Test
-    @DisableSceneContainer
-    fun clockShouldBeCentered_sceneContainerFlagOff_basedOnRepository() =
-        testScope.runTest {
-            val value by collectLastValue(underTest.clockShouldBeCentered)
-            kosmos.keyguardInteractor.setClockShouldBeCentered(true)
-            assertThat(value).isTrue()
-
-            kosmos.keyguardInteractor.setClockShouldBeCentered(false)
-            assertThat(value).isFalse()
-        }
-
-    @Test
     @EnableSceneContainer
     fun clockSize_forceSmallClock_SMALL() =
         testScope.runTest {
             val value by collectLastValue(underTest.clockSize)
             kosmos.fakeKeyguardClockRepository.setShouldForceSmallClock(true)
             kosmos.fakeFeatureFlagsClassic.set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, true)
-            transitionTo(KeyguardState.AOD, KeyguardState.LOCKSCREEN)
+            kosmos.fakeKeyguardTransitionRepository.transitionTo(
+                KeyguardState.AOD,
+                KeyguardState.LOCKSCREEN,
+            )
             assertThat(value).isEqualTo(ClockSize.SMALL)
         }
 
@@ -190,7 +182,10 @@
             val value by collectLastValue(underTest.clockShouldBeCentered)
             kosmos.shadeRepository.setShadeLayoutWide(true)
             kosmos.activeNotificationListRepository.setActiveNotifs(1)
-            transitionTo(KeyguardState.LOCKSCREEN, KeyguardState.AOD)
+            kosmos.fakeKeyguardTransitionRepository.transitionTo(
+                KeyguardState.LOCKSCREEN,
+                KeyguardState.AOD,
+            )
             assertThat(value).isTrue()
         }
 
@@ -201,15 +196,187 @@
             val value by collectLastValue(underTest.clockShouldBeCentered)
             kosmos.shadeRepository.setShadeLayoutWide(true)
             kosmos.activeNotificationListRepository.setActiveNotifs(1)
-            transitionTo(KeyguardState.AOD, KeyguardState.LOCKSCREEN)
+            kosmos.fakeKeyguardTransitionRepository.transitionTo(
+                KeyguardState.AOD,
+                KeyguardState.LOCKSCREEN,
+            )
             assertThat(value).isFalse()
         }
 
-    private suspend fun transitionTo(from: KeyguardState, to: KeyguardState) {
-        with(kosmos.fakeKeyguardTransitionRepository) {
-            sendTransitionStep(TransitionStep(from, to, 0f, TransitionState.STARTED))
-            sendTransitionStep(TransitionStep(from, to, 0.5f, TransitionState.RUNNING))
-            sendTransitionStep(TransitionStep(from, to, 1f, TransitionState.FINISHED))
+    @Test
+    @DisableSceneContainer
+    fun clockShouldBeCentered_sceneContainerFlagOff_notSplitMode_true() =
+        testScope.runTest {
+            val value by collectLastValue(underTest.clockShouldBeCentered)
+            kosmos.shadeRepository.setShadeLayoutWide(false)
+            assertThat(value).isTrue()
         }
-    }
+
+    @Test
+    @DisableSceneContainer
+    fun clockShouldBeCentered_sceneContainerFlagOff_splitMode_lockscreen_withNotifs_false() =
+        testScope.runTest {
+            val value by collectLastValue(underTest.clockShouldBeCentered)
+            kosmos.shadeRepository.setShadeLayoutWide(true)
+            kosmos.activeNotificationListRepository.setActiveNotifs(1)
+            kosmos.fakeKeyguardTransitionRepository.transitionTo(
+                KeyguardState.AOD,
+                KeyguardState.LOCKSCREEN,
+            )
+            assertThat(value).isFalse()
+        }
+
+    @Test
+    @DisableSceneContainer
+    fun clockShouldBeCentered_sceneContainerFlagOff_splitMode_lockscreen_withoutNotifs_true() =
+        testScope.runTest {
+            val value by collectLastValue(underTest.clockShouldBeCentered)
+            kosmos.shadeRepository.setShadeLayoutWide(true)
+            kosmos.activeNotificationListRepository.setActiveNotifs(0)
+            kosmos.fakeKeyguardTransitionRepository.transitionTo(
+                KeyguardState.AOD,
+                KeyguardState.LOCKSCREEN,
+            )
+            assertThat(value).isTrue()
+        }
+
+    @Test
+    @DisableSceneContainer
+    fun clockShouldBeCentered_sceneContainerFlagOff_splitMode_LsToAod_withNotifs_true() =
+        testScope.runTest {
+            val value by collectLastValue(underTest.clockShouldBeCentered)
+            kosmos.shadeRepository.setShadeLayoutWide(true)
+            kosmos.activeNotificationListRepository.setActiveNotifs(1)
+            kosmos.fakeKeyguardTransitionRepository.transitionTo(
+                KeyguardState.OFF,
+                KeyguardState.LOCKSCREEN,
+            )
+            assertThat(value).isFalse()
+            kosmos.fakeKeyguardTransitionRepository.transitionTo(
+                KeyguardState.LOCKSCREEN,
+                KeyguardState.AOD,
+            )
+            assertThat(value).isTrue()
+        }
+
+    @Test
+    @DisableSceneContainer
+    fun clockShouldBeCentered_sceneContainerFlagOff_splitMode_AodToLs_withNotifs_false() =
+        testScope.runTest {
+            val value by collectLastValue(underTest.clockShouldBeCentered)
+            kosmos.shadeRepository.setShadeLayoutWide(true)
+            kosmos.activeNotificationListRepository.setActiveNotifs(1)
+            kosmos.fakeKeyguardTransitionRepository.transitionTo(
+                KeyguardState.LOCKSCREEN,
+                KeyguardState.AOD,
+            )
+            assertThat(value).isTrue()
+            kosmos.fakeKeyguardTransitionRepository.transitionTo(
+                KeyguardState.AOD,
+                KeyguardState.LOCKSCREEN,
+            )
+            assertThat(value).isFalse()
+        }
+
+    @Test
+    @DisableSceneContainer
+    fun clockShouldBeCentered_sceneContainerFlagOff_splitMode_Aod_withPulsingNotifs_false() =
+        testScope.runTest {
+            val value by collectLastValue(underTest.clockShouldBeCentered)
+            kosmos.shadeRepository.setShadeLayoutWide(true)
+            kosmos.fakeKeyguardTransitionRepository.transitionTo(
+                KeyguardState.LOCKSCREEN,
+                KeyguardState.AOD,
+            )
+            assertThat(value).isTrue()
+            kosmos.fakeKeyguardRepository.setDozeTransitionModel(
+                DozeTransitionModel(
+                    from = DozeStateModel.DOZE_AOD,
+                    to = DozeStateModel.DOZE_PULSING,
+                )
+            )
+            assertThat(value).isFalse()
+        }
+
+    @Test
+    @DisableSceneContainer
+    fun clockShouldBeCentered_sceneContainerFlagOff_splitMode_LStoGone_withoutNotifs_true() =
+        testScope.runTest {
+            val value by collectLastValue(underTest.clockShouldBeCentered)
+            kosmos.shadeRepository.setShadeLayoutWide(true)
+            kosmos.activeNotificationListRepository.setActiveNotifs(0)
+            kosmos.fakeKeyguardTransitionRepository.transitionTo(
+                KeyguardState.OFF,
+                KeyguardState.LOCKSCREEN,
+            )
+            assertThat(value).isTrue()
+            kosmos.fakeKeyguardTransitionRepository.transitionTo(
+                KeyguardState.LOCKSCREEN,
+                KeyguardState.GONE,
+            )
+            kosmos.activeNotificationListRepository.setActiveNotifs(1)
+            assertThat(value).isTrue()
+        }
+
+    @Test
+    @DisableSceneContainer
+    fun clockShouldBeCentered_sceneContainerFlagOff_splitMode_AodOn_GoneToAOD() =
+        testScope.runTest {
+            val value by collectLastValue(underTest.clockShouldBeCentered)
+            kosmos.fakeKeyguardTransitionRepository.transitionTo(
+                KeyguardState.AOD,
+                KeyguardState.LOCKSCREEN,
+            )
+            kosmos.shadeRepository.setShadeLayoutWide(true)
+            kosmos.activeNotificationListRepository.setActiveNotifs(0)
+            assertThat(value).isTrue()
+
+            kosmos.fakeKeyguardTransitionRepository.transitionTo(
+                KeyguardState.LOCKSCREEN,
+                KeyguardState.GONE,
+            )
+            kosmos.activeNotificationListRepository.setActiveNotifs(1)
+            assertThat(value).isTrue()
+
+            kosmos.fakeKeyguardTransitionRepository.transitionTo(
+                KeyguardState.GONE,
+                KeyguardState.AOD,
+            )
+            assertThat(value).isTrue()
+        }
+
+    @Test
+    @DisableSceneContainer
+    fun clockShouldBeCentered_sceneContainerFlagOff_splitMode_AodOff_GoneToDoze() =
+        testScope.runTest {
+            val value by collectLastValue(underTest.clockShouldBeCentered)
+            kosmos.shadeRepository.setShadeLayoutWide(true)
+            kosmos.fakeKeyguardTransitionRepository.transitionTo(
+                KeyguardState.DOZING,
+                KeyguardState.LOCKSCREEN,
+            )
+            kosmos.activeNotificationListRepository.setActiveNotifs(0)
+            assertThat(value).isTrue()
+
+            kosmos.fakeKeyguardTransitionRepository.transitionTo(
+                KeyguardState.LOCKSCREEN,
+                KeyguardState.GONE,
+            )
+            kosmos.activeNotificationListRepository.setActiveNotifs(1)
+            assertThat(value).isTrue()
+
+            kosmos.fakeKeyguardTransitionRepository.transitionTo(
+                KeyguardState.GONE,
+                KeyguardState.DOZING,
+            )
+            kosmos.activeNotificationListRepository.setActiveNotifs(1)
+            assertThat(value).isTrue()
+
+            kosmos.fakeKeyguardTransitionRepository.transitionTo(
+                KeyguardState.DOZING,
+                KeyguardState.LOCKSCREEN,
+            )
+            kosmos.activeNotificationListRepository.setActiveNotifs(0)
+            assertThat(value).isTrue()
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
index b3417b9..c44f27e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
@@ -46,8 +46,6 @@
 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.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
-import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.res.R
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
@@ -76,7 +74,6 @@
     private val configRepository by lazy { kosmos.fakeConfigurationRepository }
     private val bouncerRepository by lazy { kosmos.keyguardBouncerRepository }
     private val shadeRepository by lazy { kosmos.shadeRepository }
-    private val powerInteractor by lazy { kosmos.powerInteractor }
     private val keyguardRepository by lazy { kosmos.keyguardRepository }
     private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
 
@@ -444,7 +441,6 @@
             repository.setDozeTransitionModel(
                 DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
             )
-            powerInteractor.setAwakeForTest()
             advanceTimeBy(1000L)
 
             assertThat(isAbleToDream).isEqualTo(false)
@@ -460,9 +456,6 @@
             repository.setDozeTransitionModel(
                 DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
             )
-            powerInteractor.setAwakeForTest()
-            runCurrent()
-
             // After some delay, still false
             advanceTimeBy(300L)
             assertThat(isAbleToDream).isEqualTo(false)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorTest.kt
index b069855..98e3c68 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorTest.kt
@@ -36,6 +36,7 @@
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.BiometricUnlockMode
 import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
@@ -406,4 +407,48 @@
             // It should not have any effect.
             assertEquals(listOf(false, true, false, true), canWake)
         }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testCanWakeDirectlyToGone_falseAsSoonAsTransitionsAwayFromGone() =
+        testScope.runTest {
+            val canWake by collectValues(underTest.canWakeDirectlyToGone)
+
+            assertEquals(
+                listOf(
+                    false // Defaults to false.
+                ),
+                canWake,
+            )
+
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.GONE,
+                testScope,
+            )
+
+            assertEquals(
+                listOf(
+                    false,
+                    true, // Because we're GONE.
+                ),
+                canWake,
+            )
+
+            transitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.GONE,
+                testScope = testScope,
+                throughTransitionState = TransitionState.RUNNING,
+            )
+
+            assertEquals(
+                listOf(
+                    false,
+                    true,
+                    false, // False as soon as we start a transition away from GONE.
+                ),
+                canWake,
+            )
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt
index ea2b3cd..605a5d2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt
@@ -21,6 +21,7 @@
 import android.platform.test.annotations.RequiresFlagsEnabled
 import android.platform.test.flag.junit.CheckFlagsRule
 import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import android.view.IRemoteAnimationFinishedCallback
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -38,10 +39,13 @@
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mock
 import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
 import org.mockito.MockitoAnnotations
 import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -222,4 +226,22 @@
         underTest.setSurfaceBehindVisibility(false)
         verify(keyguardTransitions).startKeyguardTransition(eq(true), any())
     }
+
+    @Test
+    fun remoteAnimationInstantlyFinished_ifDismissTransitionNotStarted() {
+        val mockedCallback = mock<IRemoteAnimationFinishedCallback>()
+        whenever(keyguardDismissTransitionInteractor.startDismissKeyguardTransition(any()))
+            .thenReturn(false)
+
+        underTest.onKeyguardGoingAwayRemoteAnimationStart(
+            transit = 0,
+            apps = emptyArray(),
+            wallpapers = emptyArray(),
+            nonApps = emptyArray(),
+            finishedCallback = mockedCallback,
+        )
+
+        verify(mockedCallback).onAnimationFinished()
+        verifyNoMoreInteractions(mockedCallback)
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
index 87ab3c8..1cf45f8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
@@ -27,7 +27,6 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.customization.R as customR
-import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.keyguardBlueprintInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardSmartspaceInteractor
@@ -156,7 +155,6 @@
 
                 shadeRepository.setShadeLayoutWide(false)
                 keyguardClockInteractor.setClockSize(ClockSize.LARGE)
-                fakeKeyguardRepository.setClockShouldBeCentered(true)
                 notificationsKeyguardInteractor.setNotificationsFullyHidden(true)
                 keyguardSmartspaceInteractor.setBcSmartspaceVisibility(VISIBLE)
                 fakeConfigurationController.notifyConfigurationChanged()
@@ -181,7 +179,6 @@
 
                 shadeRepository.setShadeLayoutWide(true)
                 keyguardClockInteractor.setClockSize(ClockSize.LARGE)
-                fakeKeyguardRepository.setClockShouldBeCentered(true)
                 notificationsKeyguardInteractor.setNotificationsFullyHidden(true)
                 keyguardSmartspaceInteractor.setBcSmartspaceVisibility(VISIBLE)
                 fakeConfigurationController.notifyConfigurationChanged()
@@ -206,7 +203,6 @@
 
                 shadeRepository.setShadeLayoutWide(false)
                 keyguardClockInteractor.setClockSize(ClockSize.LARGE)
-                fakeKeyguardRepository.setClockShouldBeCentered(true)
                 notificationsKeyguardInteractor.setNotificationsFullyHidden(true)
                 keyguardSmartspaceInteractor.setBcSmartspaceVisibility(VISIBLE)
                 fakeConfigurationController.notifyConfigurationChanged()
@@ -230,7 +226,6 @@
 
                 shadeRepository.setShadeLayoutWide(true)
                 keyguardClockInteractor.setClockSize(ClockSize.SMALL)
-                fakeKeyguardRepository.setClockShouldBeCentered(true)
                 notificationsKeyguardInteractor.setNotificationsFullyHidden(true)
                 keyguardSmartspaceInteractor.setBcSmartspaceVisibility(VISIBLE)
                 fakeConfigurationController.notifyConfigurationChanged()
@@ -254,7 +249,6 @@
 
                 shadeRepository.setShadeLayoutWide(false)
                 keyguardClockInteractor.setClockSize(ClockSize.SMALL)
-                fakeKeyguardRepository.setClockShouldBeCentered(true)
                 notificationsKeyguardInteractor.setNotificationsFullyHidden(true)
                 keyguardSmartspaceInteractor.setBcSmartspaceVisibility(VISIBLE)
                 fakeConfigurationController.notifyConfigurationChanged()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt
index feaf06a..ade7614 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt
@@ -16,10 +16,13 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_BOUNCER_UI_REVAMP
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectValues
+import com.android.systemui.flags.BrokenWithSceneContainer
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
@@ -72,6 +75,28 @@
         }
 
     @Test
+    @EnableFlags(FLAG_BOUNCER_UI_REVAMP)
+    @BrokenWithSceneContainer(388068805)
+    fun notifications_areFullyVisible_whenShadeIsOpen() =
+        testScope.runTest {
+            val values by collectValues(underTest.notificationAlpha)
+            kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true)
+
+            keyguardTransitionRepository.sendTransitionSteps(
+                listOf(
+                    step(0f, TransitionState.STARTED),
+                    step(0.1f),
+                    step(0.2f),
+                    step(0.3f),
+                    step(1f),
+                ),
+                testScope,
+            )
+
+            values.forEach { assertThat(it).isEqualTo(1f) }
+        }
+
+    @Test
     fun blurRadiusGoesToMaximumWhenShadeIsExpanded() =
         testScope.runTest {
             val values by collectValues(underTest.windowBlurRadius)
@@ -88,6 +113,25 @@
         }
 
     @Test
+    @EnableFlags(FLAG_BOUNCER_UI_REVAMP)
+    @BrokenWithSceneContainer(388068805)
+    fun notificationBlur_isNonZero_whenShadeIsExpanded() =
+        testScope.runTest {
+            val values by collectValues(underTest.notificationBlurRadius)
+
+            kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true)
+
+            kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius(
+                transitionProgress = listOf(0f, 0f, 0.1f, 0.2f, 0.3f, 1f),
+                startValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f,
+                endValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f,
+                transitionFactory = ::step,
+                actualValuesProvider = { values },
+                checkInterpolatedValues = false,
+            )
+        }
+
+    @Test
     fun blurRadiusGoesFromMinToMaxWhenShadeIsNotExpanded() =
         testScope.runTest {
             val values by collectValues(underTest.windowBlurRadius)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
index 05a6b87..8a599a1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
@@ -20,15 +20,15 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.flags.BrokenWithSceneContainer
 import com.android.systemui.flags.DisableSceneContainer
 import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.flags.andSceneContainer
 import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.keyguardClockRepository
-import com.android.systemui.keyguard.data.repository.keyguardRepository
 import com.android.systemui.keyguard.shared.model.ClockSize
 import com.android.systemui.keyguard.shared.model.ClockSizeSetting
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel.ClockLayout
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.plugins.clocks.ClockConfig
@@ -37,6 +37,8 @@
 import com.android.systemui.plugins.clocks.ClockFaceController
 import com.android.systemui.res.R
 import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
 import com.android.systemui.statusbar.ui.fakeSystemBarUtilsProxy
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.whenever
@@ -87,7 +89,11 @@
 
             with(kosmos) {
                 shadeRepository.setShadeLayoutWide(true)
-                keyguardRepository.setClockShouldBeCentered(true)
+                kosmos.activeNotificationListRepository.setActiveNotifs(0)
+                fakeKeyguardTransitionRepository.transitionTo(
+                    KeyguardState.AOD,
+                    KeyguardState.LOCKSCREEN,
+                )
                 keyguardClockRepository.setClockSize(ClockSize.LARGE)
             }
 
@@ -95,14 +101,18 @@
         }
 
     @Test
-    @BrokenWithSceneContainer(339465026)
+    @EnableSceneContainer
     fun currentClockLayout_splitShadeOn_clockNotCentered_largeClock_splitShadeLargeClock() =
         testScope.runTest {
             val currentClockLayout by collectLastValue(underTest.currentClockLayout)
 
             with(kosmos) {
                 shadeRepository.setShadeLayoutWide(true)
-                keyguardRepository.setClockShouldBeCentered(false)
+                activeNotificationListRepository.setActiveNotifs(1)
+                fakeKeyguardTransitionRepository.transitionTo(
+                    KeyguardState.AOD,
+                    KeyguardState.LOCKSCREEN,
+                )
                 keyguardClockRepository.setClockSize(ClockSize.LARGE)
             }
 
@@ -110,42 +120,46 @@
         }
 
     @Test
-    @BrokenWithSceneContainer(339465026)
-    fun currentClockLayout_splitShadeOn_clockNotCentered_smallClock_splitShadeSmallClock() =
+    @EnableSceneContainer
+    fun currentClockLayout_splitShadeOn_clockNotCentered_forceSmallClock_splitShadeSmallClock() =
         testScope.runTest {
             val currentClockLayout by collectLastValue(underTest.currentClockLayout)
 
             with(kosmos) {
                 shadeRepository.setShadeLayoutWide(true)
-                keyguardRepository.setClockShouldBeCentered(false)
-                keyguardClockRepository.setClockSize(ClockSize.SMALL)
+                activeNotificationListRepository.setActiveNotifs(1)
+                fakeKeyguardTransitionRepository.transitionTo(
+                    KeyguardState.AOD,
+                    KeyguardState.LOCKSCREEN,
+                )
+                fakeKeyguardClockRepository.setShouldForceSmallClock(true)
             }
 
             assertThat(currentClockLayout).isEqualTo(ClockLayout.SPLIT_SHADE_SMALL_CLOCK)
         }
 
     @Test
-    @BrokenWithSceneContainer(339465026)
-    fun currentClockLayout_singleShade_smallClock_smallClock() =
+    @EnableSceneContainer
+    fun currentClockLayout_singleShade_withNotifs_smallClock() =
         testScope.runTest {
             val currentClockLayout by collectLastValue(underTest.currentClockLayout)
 
             with(kosmos) {
                 shadeRepository.setShadeLayoutWide(false)
-                keyguardClockRepository.setClockSize(ClockSize.SMALL)
+                activeNotificationListRepository.setActiveNotifs(1)
             }
 
             assertThat(currentClockLayout).isEqualTo(ClockLayout.SMALL_CLOCK)
         }
 
     @Test
-    fun currentClockLayout_singleShade_largeClock_largeClock() =
+    fun currentClockLayout_singleShade_withoutNotifs_largeClock() =
         testScope.runTest {
             val currentClockLayout by collectLastValue(underTest.currentClockLayout)
 
             with(kosmos) {
                 shadeRepository.setShadeLayoutWide(false)
-                keyguardClockRepository.setClockSize(ClockSize.LARGE)
+                activeNotificationListRepository.setActiveNotifs(0)
             }
 
             assertThat(currentClockLayout).isEqualTo(ClockLayout.LARGE_CLOCK)
@@ -195,7 +209,7 @@
         }
 
     @Test
-    @BrokenWithSceneContainer(339465026)
+    @DisableSceneContainer
     fun testClockSize_dynamicClockSize() =
         testScope.runTest {
             with(kosmos) {
@@ -219,7 +233,7 @@
         }
 
     @Test
-    @BrokenWithSceneContainer(339465026)
+    @DisableSceneContainer
     fun isLargeClockVisible_whenSmallClockSize_isFalse() =
         testScope.runTest {
             val value by collectLastValue(underTest.isLargeClockVisible)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
index d909c5a..914094f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
@@ -16,9 +16,11 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.FlagsParameterization
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.systemui.Flags.FLAG_BOUNCER_UI_REVAMP
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
@@ -153,7 +155,7 @@
         }
 
     @Test
-    @BrokenWithSceneContainer(330311871)
+    @BrokenWithSceneContainer(388068805)
     fun blurRadiusIsMaxWhenShadeIsExpanded() =
         testScope.runTest {
             val values by collectValues(underTest.windowBlurRadius)
@@ -170,7 +172,7 @@
         }
 
     @Test
-    @BrokenWithSceneContainer(330311871)
+    @BrokenWithSceneContainer(388068805)
     fun blurRadiusGoesFromMinToMaxWhenShadeIsNotExpanded() =
         testScope.runTest {
             val values by collectValues(underTest.windowBlurRadius)
@@ -185,6 +187,44 @@
             )
         }
 
+    @Test
+    @EnableFlags(FLAG_BOUNCER_UI_REVAMP)
+    @BrokenWithSceneContainer(388068805)
+    fun notificationBlur_isNonZero_whenShadeIsExpanded() =
+        testScope.runTest {
+            val values by collectValues(underTest.notificationBlurRadius)
+            kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true)
+            runCurrent()
+
+            kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius(
+                transitionProgress = listOf(0f, 0f, 0.1f, 0.2f, 0.3f, 1f),
+                startValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f,
+                endValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f,
+                transitionFactory = ::step,
+                actualValuesProvider = { values },
+                checkInterpolatedValues = false,
+            )
+        }
+
+    @Test
+    @EnableFlags(FLAG_BOUNCER_UI_REVAMP)
+    @BrokenWithSceneContainer(388068805)
+    fun notifications_areFullyVisible_whenShadeIsExpanded() =
+        testScope.runTest {
+            val values by collectValues(underTest.notificationAlpha)
+            kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true)
+            runCurrent()
+
+            kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius(
+                transitionProgress = listOf(0f, 0f, 0.1f, 0.2f, 0.3f, 1f),
+                startValue = 1.0f,
+                endValue = 1.0f,
+                transitionFactory = ::step,
+                actualValuesProvider = { values },
+                checkInterpolatedValues = false,
+            )
+        }
+
     private fun step(
         value: Float,
         state: TransitionState = TransitionState.RUNNING,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt
index 5798e07..338b068 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt
@@ -24,8 +24,9 @@
 import java.util.function.Consumer
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
 import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.launch
@@ -36,12 +37,10 @@
  * Gives direct control over ValueAnimator, in order to make transition tests deterministic. See
  * [AnimationHandler]. Animators are required to be run on the main thread, so dispatch accordingly.
  */
-class KeyguardTransitionRunner(val repository: KeyguardTransitionRepository) :
-    AnimationFrameCallbackProvider {
-
-    private var frameCount = 1L
-    private var frames = MutableStateFlow(Pair<Long, FrameCallback?>(0L, null))
-    private var job: Job? = null
+class KeyguardTransitionRunner(
+    val frames: Flow<Long>,
+    val repository: KeyguardTransitionRepository,
+) {
     @Volatile private var isTerminated = false
 
     /**
@@ -54,21 +53,12 @@
         maxFrames: Int = 100,
         frameCallback: Consumer<Long>? = null,
     ) {
-        // AnimationHandler uses ThreadLocal storage, and ValueAnimators MUST start from main
-        // thread
-        withContext(Dispatchers.Main) {
-            info.animator!!.getAnimationHandler().setProvider(this@KeyguardTransitionRunner)
-        }
-
-        job =
+        val job =
             scope.launch {
-                frames.collect {
-                    val (frameNumber, callback) = it
-
+                frames.collect { frameNumber ->
                     isTerminated = frameNumber >= maxFrames
                     if (!isTerminated) {
                         try {
-                            withContext(Dispatchers.Main) { callback?.doFrame(frameNumber) }
                             frameCallback?.accept(frameNumber)
                         } catch (e: IllegalStateException) {
                             e.printStackTrace()
@@ -78,27 +68,46 @@
             }
         withContext(Dispatchers.Main) { repository.startTransition(info) }
 
-        waitUntilComplete(info.animator!!)
+        waitUntilComplete(info, info.animator!!)
+        job.cancel()
     }
 
-    private suspend fun waitUntilComplete(animator: ValueAnimator) {
+    private suspend fun waitUntilComplete(info: TransitionInfo, animator: ValueAnimator) {
         withContext(Dispatchers.Main) {
             val startTime = System.currentTimeMillis()
             while (!isTerminated && animator.isRunning()) {
                 delay(1)
                 if (System.currentTimeMillis() - startTime > MAX_TEST_DURATION) {
-                    fail("Failed test due to excessive runtime of: $MAX_TEST_DURATION")
+                    fail("Failed due to excessive runtime of: $MAX_TEST_DURATION, info: $info")
                 }
             }
-
-            animator.getAnimationHandler().setProvider(null)
         }
+    }
 
-        job?.cancel()
+    companion object {
+        private const val MAX_TEST_DURATION = 300L
+    }
+}
+
+class FrameCallbackProvider(val scope: CoroutineScope) : AnimationFrameCallbackProvider {
+    private val callback = MutableSharedFlow<FrameCallback?>(replay = 2)
+    private var frameCount = 0L
+    val frames = MutableStateFlow(frameCount)
+
+    init {
+        scope.launch {
+            callback.collect {
+                withContext(Dispatchers.Main) {
+                    delay(1)
+                    it?.doFrame(frameCount)
+                }
+            }
+        }
     }
 
     override fun postFrameCallback(cb: FrameCallback) {
-        frames.value = Pair(frameCount++, cb)
+        frames.value = ++frameCount
+        callback.tryEmit(cb)
     }
 
     override fun postCommitCallback(runnable: Runnable) {}
@@ -108,8 +117,4 @@
     override fun getFrameDelay() = 1L
 
     override fun setFrameDelay(delay: Long) {}
-
-    companion object {
-        private const val MAX_TEST_DURATION = 200L
-    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
index a36e0ea..9bae7bd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
@@ -1,6 +1,7 @@
 package com.android.systemui.navigationbar
 
 import android.app.ActivityManager
+import android.os.Handler
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -61,6 +62,7 @@
     @Mock lateinit var mStatusBarKeyguardViewManager: StatusBarKeyguardViewManager
     @Mock lateinit var mStatusBarStateController: StatusBarStateController
     @Mock lateinit var mDisplayTracker: DisplayTracker
+    @Mock lateinit var mHandler: Handler
 
     @Before
     fun setup() {
@@ -69,6 +71,11 @@
         `when`(mLightBarControllerFactory.create(any())).thenReturn(mLightBarTransitionController)
         `when`(mNavBarHelper.currentSysuiState).thenReturn(mCurrentSysUiState)
         `when`(mSysUiState.setFlag(anyLong(), anyBoolean())).thenReturn(mSysUiState)
+        `when`(mHandler.post(any())).thenAnswer {
+            (it.arguments[0] as Runnable).run()
+            true
+        }
+
         mTaskStackChangeListeners = TaskStackChangeListeners.getTestInstance()
         mTaskbarDelegate =
             TaskbarDelegate(
@@ -76,6 +83,7 @@
                 mLightBarControllerFactory,
                 mStatusBarKeyguardViewManager,
                 mStatusBarStateController,
+                mHandler,
             )
         mTaskbarDelegate.setDependencies(
             mCommandQueue,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt
index eeccbdf..79556ba 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.qs.tiles
 
 import android.os.Handler
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.FlagsParameterization
 import android.platform.test.flag.junit.FlagsParameterization.allCombinationsOf
 import android.service.quicksettings.Tile
@@ -24,18 +26,26 @@
 import android.testing.TestableLooper.RunWithLooper
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.MetricsLogger
+import com.android.systemui.Flags
+import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingManagerFake
+import com.android.systemui.flags.setFlagValue
+import com.android.systemui.keyguard.KeyguardWmStateRefactor
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.qs.QSHost
 import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.flags.QSComposeFragment
+import com.android.systemui.qs.flags.QsDetailedView
 import com.android.systemui.qs.logging.QSLogger
 import com.android.systemui.qs.tiles.dialog.InternetDialogManager
 import com.android.systemui.qs.tiles.dialog.WifiStateWorker
 import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.shade.shared.flag.DualShade
 import com.android.systemui.statusbar.connectivity.AccessPointController
+import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
 import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteractor
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
@@ -256,6 +266,41 @@
         verify(wifiStateWorker, times(1)).isWifiEnabled = eq(true)
     }
 
+    @Test
+    @DisableFlags(QsDetailedView.FLAG_NAME)
+    fun click_withQsDetailedViewDisabled() {
+        underTest.click(null)
+        looper.processAllMessages()
+
+        verify(dialogManager, times(1)).create(
+            aboveStatusBar = true,
+            accessPointController.canConfigMobileData(),
+            accessPointController.canConfigWifi(),
+            null,
+        )
+    }
+
+    @Test
+    @EnableFlags(
+        value = [
+            QsDetailedView.FLAG_NAME,
+            FLAG_SCENE_CONTAINER,
+            KeyguardWmStateRefactor.FLAG_NAME,
+            NotificationThrottleHun.FLAG_NAME,
+            DualShade.FLAG_NAME]
+    )
+    fun click_withQsDetailedViewEnabled() {
+        underTest.click(null)
+        looper.processAllMessages()
+
+        verify(dialogManager, times(0)).create(
+            aboveStatusBar = true,
+            accessPointController.canConfigMobileData(),
+            accessPointController.canConfigWifi(),
+            null,
+        )
+    }
+
     companion object {
         const val WIFI_SSID = "test ssid"
         val ACTIVE_WIFI =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
index fc1d73b..3a3f537 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
@@ -35,6 +35,7 @@
 import android.app.Dialog;
 import android.media.projection.StopReason;
 import android.os.Handler;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.FlagsParameterization;
 import android.service.quicksettings.Tile;
 import android.testing.TestableLooper;
@@ -52,6 +53,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.QsEventLogger;
+import com.android.systemui.qs.flags.QsDetailedView;
 import com.android.systemui.qs.flags.QsInCompose;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor;
@@ -63,6 +65,7 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
 import org.junit.After;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -70,11 +73,11 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.util.List;
-
 import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
 import platform.test.runner.parameterized.Parameters;
 
+import java.util.List;
+
 @RunWith(ParameterizedAndroidJunit4.class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 @SmallTest
@@ -82,7 +85,8 @@
 
     @Parameters(name = "{0}")
     public static List<FlagsParameterization> getParams() {
-        return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX);
+        return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX,
+                QsDetailedView.FLAG_NAME);
     }
 
     @Mock
@@ -336,6 +340,30 @@
                 .notifyPermissionRequestDisplayed(mContext.getUserId());
     }
 
+    @Test
+    @EnableFlags(QsDetailedView.FLAG_NAME)
+    public void testNotStartingAndRecording_returnDetailsViewModel() {
+        when(mController.isStarting()).thenReturn(false);
+        when(mController.isRecording()).thenReturn(false);
+        mTile.getDetailsViewModel(Assert::assertNotNull);
+    }
+
+    @Test
+    @EnableFlags(QsDetailedView.FLAG_NAME)
+    public void testStarting_notReturnDetailsViewModel() {
+        when(mController.isStarting()).thenReturn(true);
+        when(mController.isRecording()).thenReturn(false);
+        mTile.getDetailsViewModel(Assert::assertNull);
+    }
+
+    @Test
+    @EnableFlags(QsDetailedView.FLAG_NAME)
+    public void testRecording_notReturnDetailsViewModel() {
+        when(mController.isStarting()).thenReturn(false);
+        when(mController.isRecording()).thenReturn(true);
+        mTile.getDetailsViewModel(Assert::assertNull);
+    }
+
     private QSTile.Icon createExpectedIcon(int resId) {
         if (QsInCompose.isEnabled()) {
             return new QSTileImpl.DrawableIconWithRes(mContext.getDrawable(resId), resId);
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 04e094f..c8b3aba 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
@@ -24,6 +24,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.settingslib.notification.modes.TestModeBuilder
+import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND
 import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.Expandable
@@ -87,9 +88,9 @@
         testScope.runTest {
             val activeModes by collectLastValue(zenModeInteractor.activeModes)
 
+            zenModeRepository.activateMode(MANUAL_DND)
             zenModeRepository.addModes(
                 listOf(
-                    TestModeBuilder.MANUAL_DND_ACTIVE,
                     TestModeBuilder().setName("Mode 1").setActive(true).build(),
                     TestModeBuilder().setName("Mode 2").setActive(true).build(),
                 )
@@ -111,7 +112,7 @@
         testScope.runTest {
             val dndMode by collectLastValue(zenModeInteractor.dndMode)
 
-            zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_ACTIVE)
+            zenModeRepository.activateMode(MANUAL_DND)
             assertThat(dndMode?.isActive).isTrue()
 
             underTest.handleInput(
@@ -127,7 +128,6 @@
         testScope.runTest {
             val dndMode by collectLastValue(zenModeInteractor.dndMode)
 
-            zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE)
             assertThat(dndMode?.isActive).isFalse()
 
             underTest.handleInput(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index 48edded..de54e75 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -575,4 +575,50 @@
 
             assertThat(currentScene).isNotEqualTo(disabledScene)
         }
+
+    @Test
+    fun transitionAnimations() =
+        kosmos.runTest {
+            val isVisible by collectLastValue(underTest.isVisible)
+            assertThat(isVisible).isTrue()
+
+            underTest.setVisible(false, "test")
+            assertThat(isVisible).isFalse()
+
+            underTest.onTransitionAnimationStart()
+            // One animation is active, forced visible.
+            assertThat(isVisible).isTrue()
+
+            underTest.onTransitionAnimationEnd()
+            // No more active animations, not forced visible.
+            assertThat(isVisible).isFalse()
+
+            underTest.onTransitionAnimationStart()
+            // One animation is active, forced visible.
+            assertThat(isVisible).isTrue()
+
+            underTest.onTransitionAnimationCancelled()
+            // No more active animations, not forced visible.
+            assertThat(isVisible).isFalse()
+
+            underTest.setVisible(true, "test")
+            assertThat(isVisible).isTrue()
+
+            underTest.onTransitionAnimationStart()
+            underTest.onTransitionAnimationStart()
+            // Two animations are active, forced visible.
+            assertThat(isVisible).isTrue()
+
+            underTest.setVisible(false, "test")
+            // Two animations are active, forced visible.
+            assertThat(isVisible).isTrue()
+
+            underTest.onTransitionAnimationEnd()
+            // One animation is still active, forced visible.
+            assertThat(isVisible).isTrue()
+
+            underTest.onTransitionAnimationEnd()
+            // No more active animations, not forced visible.
+            assertThat(isVisible).isFalse()
+        }
 }
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 06dd046..51f056a 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
@@ -36,6 +36,8 @@
 import com.android.keyguard.keyguardUpdateMonitor
 import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.animation.activityTransitionAnimator
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
 import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
 import com.android.systemui.authentication.domain.interactor.authenticationInteractor
@@ -141,6 +143,7 @@
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.whenever
 
 @SmallTest
@@ -169,6 +172,7 @@
     private val uiEventLoggerFake = kosmos.uiEventLoggerFake
     private val msdlPlayer = kosmos.fakeMSDLPlayer
     private val authInteractionProperties = AuthInteractionProperties()
+    private val mockActivityTransitionAnimator = mock<ActivityTransitionAnimator>()
 
     private lateinit var underTest: SceneContainerStartable
 
@@ -177,6 +181,8 @@
         MockitoAnnotations.initMocks(this)
         whenever(kosmos.keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
             .thenReturn(true)
+        kosmos.activityTransitionAnimator = mockActivityTransitionAnimator
+
         underTest = kosmos.sceneContainerStartable
     }
 
@@ -2716,6 +2722,27 @@
             assertThat(currentOverlays).isEmpty()
         }
 
+    @Test
+    fun hydrateActivityTransitionAnimationState() =
+        kosmos.runTest {
+            underTest.start()
+
+            val isVisible by collectLastValue(sceneInteractor.isVisible)
+            assertThat(isVisible).isTrue()
+
+            sceneInteractor.setVisible(false, "reason")
+            assertThat(isVisible).isFalse()
+
+            val argumentCaptor = argumentCaptor<ActivityTransitionAnimator.Listener>()
+            verify(mockActivityTransitionAnimator).addListener(argumentCaptor.capture())
+
+            val listeners = argumentCaptor.allValues
+            listeners.forEach { it.onTransitionAnimationStart() }
+            assertThat(isVisible).isTrue()
+            listeners.forEach { it.onTransitionAnimationEnd() }
+            assertThat(isVisible).isFalse()
+        }
+
     private fun TestScope.emulateSceneTransition(
         transitionStateFlow: MutableStateFlow<ObservableTransitionState>,
         toScene: SceneKey,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/view/SceneJankMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/view/SceneJankMonitorTest.kt
new file mode 100644
index 0000000..984f8fd
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/view/SceneJankMonitorTest.kt
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2025 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.scene.ui.view
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.jank.Cuj
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
+import com.android.systemui.jank.interactionJankMonitor
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.runCurrent
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SceneJankMonitorTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val underTest: SceneJankMonitor = kosmos.sceneJankMonitorFactory.create()
+
+    @Before
+    fun setUp() {
+        underTest.activateIn(kosmos.testScope)
+    }
+
+    @Test
+    fun onTransitionStart_withProvidedCuj_beginsThatCuj() =
+        kosmos.runTest {
+            val cuj = 1337
+            underTest.onTransitionStart(
+                view = mock(),
+                from = Scenes.Communal,
+                to = Scenes.Dream,
+                cuj = cuj,
+            )
+            verify(interactionJankMonitor).begin(any(), eq(cuj))
+            verify(interactionJankMonitor, never()).end(anyInt())
+        }
+
+    @Test
+    fun onTransitionEnd_withProvidedCuj_endsThatCuj() =
+        kosmos.runTest {
+            val cuj = 1337
+            underTest.onTransitionEnd(from = Scenes.Communal, to = Scenes.Dream, cuj = cuj)
+            verify(interactionJankMonitor, never()).begin(any(), anyInt())
+            verify(interactionJankMonitor).end(cuj)
+        }
+
+    @Test
+    fun bouncer_authMethodPin() =
+        kosmos.runTest {
+            bouncer(
+                authenticationMethod = AuthenticationMethodModel.Pin,
+                appearCuj = Cuj.CUJ_LOCKSCREEN_PIN_APPEAR,
+                disappearCuj = Cuj.CUJ_LOCKSCREEN_PIN_DISAPPEAR,
+            )
+        }
+
+    @Test
+    fun bouncer_authMethodSim() =
+        kosmos.runTest {
+            bouncer(
+                authenticationMethod = AuthenticationMethodModel.Sim,
+                appearCuj = Cuj.CUJ_LOCKSCREEN_PIN_APPEAR,
+                disappearCuj = Cuj.CUJ_LOCKSCREEN_PIN_DISAPPEAR,
+                // When the auth method is SIM, unlocking doesn't work like normal. Instead of
+                // leaving the bouncer, the bouncer is switched over to the real authentication
+                // method when the SIM is unlocked.
+                //
+                // Therefore, there's no point in testing this code path and it will, in fact, fail
+                // to unlock.
+                testUnlockedDisappearance = false,
+            )
+        }
+
+    @Test
+    fun bouncer_authMethodPattern() =
+        kosmos.runTest {
+            bouncer(
+                authenticationMethod = AuthenticationMethodModel.Pattern,
+                appearCuj = Cuj.CUJ_LOCKSCREEN_PATTERN_APPEAR,
+                disappearCuj = Cuj.CUJ_LOCKSCREEN_PATTERN_DISAPPEAR,
+            )
+        }
+
+    @Test
+    fun bouncer_authMethodPassword() =
+        kosmos.runTest {
+            bouncer(
+                authenticationMethod = AuthenticationMethodModel.Password,
+                appearCuj = Cuj.CUJ_LOCKSCREEN_PASSWORD_APPEAR,
+                disappearCuj = Cuj.CUJ_LOCKSCREEN_PASSWORD_DISAPPEAR,
+            )
+        }
+
+    private fun Kosmos.bouncer(
+        authenticationMethod: AuthenticationMethodModel,
+        appearCuj: Int,
+        disappearCuj: Int,
+        testUnlockedDisappearance: Boolean = true,
+    ) {
+        // Set up state:
+        fakeAuthenticationRepository.setAuthenticationMethod(authenticationMethod)
+        runCurrent()
+
+        fun verifyCujCounts(
+            beginAppearCount: Int = 0,
+            beginDisappearCount: Int = 0,
+            endAppearCount: Int = 0,
+            endDisappearCount: Int = 0,
+        ) {
+            verify(interactionJankMonitor, times(beginAppearCount)).begin(any(), eq(appearCuj))
+            verify(interactionJankMonitor, times(beginDisappearCount))
+                .begin(any(), eq(disappearCuj))
+            verify(interactionJankMonitor, times(endAppearCount)).end(appearCuj)
+            verify(interactionJankMonitor, times(endDisappearCount)).end(disappearCuj)
+        }
+
+        // Precondition checks:
+        assertThat(deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked).isFalse()
+        verifyCujCounts()
+
+        // Bouncer appears CUJ:
+        underTest.onTransitionStart(
+            view = mock(),
+            from = Scenes.Lockscreen,
+            to = Scenes.Bouncer,
+            cuj = null,
+        )
+        verifyCujCounts(beginAppearCount = 1)
+        underTest.onTransitionEnd(from = Scenes.Lockscreen, to = Scenes.Bouncer, cuj = null)
+        verifyCujCounts(beginAppearCount = 1, endAppearCount = 1)
+
+        // Bouncer disappear CUJ but it doesn't log because the device isn't unlocked.
+        underTest.onTransitionStart(
+            view = mock(),
+            from = Scenes.Bouncer,
+            to = Scenes.Lockscreen,
+            cuj = null,
+        )
+        verifyCujCounts(beginAppearCount = 1, endAppearCount = 1)
+        underTest.onTransitionEnd(from = Scenes.Bouncer, to = Scenes.Lockscreen, cuj = null)
+        verifyCujCounts(beginAppearCount = 1, endAppearCount = 1)
+
+        if (!testUnlockedDisappearance) {
+            return
+        }
+
+        // Unlock the device and transition away from the bouncer.
+        fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+            SuccessFingerprintAuthenticationStatus(0, true)
+        )
+        runCurrent()
+        assertThat(deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked).isTrue()
+
+        // Bouncer disappear CUJ and it doeslog because the device is unlocked.
+        underTest.onTransitionStart(
+            view = mock(),
+            from = Scenes.Bouncer,
+            to = Scenes.Gone,
+            cuj = null,
+        )
+        verifyCujCounts(beginAppearCount = 1, endAppearCount = 1, beginDisappearCount = 1)
+        underTest.onTransitionEnd(from = Scenes.Bouncer, to = Scenes.Gone, cuj = null)
+        verifyCujCounts(
+            beginAppearCount = 1,
+            endAppearCount = 1,
+            beginDisappearCount = 1,
+            endDisappearCount = 1,
+        )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 0e93167..62c3604 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -85,7 +85,6 @@
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingObserver;
 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
@@ -335,16 +334,14 @@
         mFeatureFlags.set(Flags.QS_USER_DETAIL_SHORTCUT, false);
 
         mMainDispatcher = getMainDispatcher();
-        KeyguardInteractorFactory.WithDependencies keyguardInteractorDeps =
-                KeyguardInteractorFactory.create();
-        mFakeKeyguardRepository = keyguardInteractorDeps.getRepository();
+        mFakeKeyguardRepository = mKosmos.getKeyguardRepository();
         mFakeKeyguardClockRepository = new FakeKeyguardClockRepository();
         mKeyguardClockInteractor = mKosmos.getKeyguardClockInteractor();
-        mKeyguardInteractor = keyguardInteractorDeps.getKeyguardInteractor();
+        mKeyguardInteractor = mKosmos.getKeyguardInteractor();
         mShadeRepository = new FakeShadeRepository();
         mShadeAnimationInteractor = new ShadeAnimationInteractorLegacyImpl(
                 new ShadeAnimationRepository(), mShadeRepository);
-        mPowerInteractor = keyguardInteractorDeps.getPowerInteractor();
+        mPowerInteractor = mKosmos.getPowerInteractor();
         when(mKeyguardTransitionInteractor.isInTransitionWhere(any(), any())).thenReturn(
                 MutableStateFlow(false));
         when(mKeyguardTransitionInteractor.isInTransition(any(), any()))
@@ -531,9 +528,6 @@
 
         mNotificationPanelViewController = new NotificationPanelViewController(
                 mView,
-                mMainHandler,
-                mLayoutInflater,
-                mFeatureFlags,
                 coordinator, expansionHandler, mDynamicPrivacyController, mKeyguardBypassController,
                 mFalsingManager, new FalsingCollectorFake(),
                 mKeyguardStateController,
@@ -553,7 +547,6 @@
                 mKeyguardStatusBarViewComponentFactory,
                 mLockscreenShadeTransitionController,
                 mScrimController,
-                mUserManager,
                 mMediaDataManager,
                 mNotificationShadeDepthController,
                 mAmbientState,
@@ -564,7 +557,6 @@
                 mQsController,
                 mFragmentService,
                 mStatusBarService,
-                mContentResolver,
                 mShadeHeaderController,
                 mScreenOffAnimationController,
                 mLockscreenGestureLogger,
@@ -575,7 +567,6 @@
                 mKeyguardUnlockAnimationController,
                 mKeyguardIndicationController,
                 mNotificationListContainer,
-                mNotificationStackSizeCalculator,
                 mUnlockedScreenOffAnimationController,
                 systemClock,
                 mKeyguardClockInteractor,
@@ -594,7 +585,6 @@
                 new ResourcesSplitShadeStateController(),
                 mPowerInteractor,
                 mKeyguardClockPositionAlgorithm,
-                mNaturalScrollingSettingObserver,
                 mMSDLPlayer,
                 mBrightnessMirrorShowingInteractor);
         mNotificationPanelViewController.initDependencies(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java
index 2e9d6e8..49cbb5a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java
@@ -53,7 +53,6 @@
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.qs.flags.QSComposeFragment;
 import com.android.systemui.res.R;
-import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -365,7 +364,6 @@
     }
 
     @Test
-    @EnableFlags(FooterViewRefactor.FLAG_NAME)
     public void updateExpansion_partiallyExpanded_fullscreenFalse() {
         // WHEN QS are only partially expanded
         mQsController.setExpanded(true);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt
index b5043ce..fe44c3e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt
@@ -39,7 +39,7 @@
 
     @Before
     fun setUp() {
-        underTest = ShadeRepositoryImpl()
+        underTest = ShadeRepositoryImpl(testScope)
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt
index a98d1a2..d3ba3dc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt
@@ -22,10 +22,13 @@
 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.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.scene.ui.view.mockShadeRootView
 import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository
 import com.android.systemui.testKosmos
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -41,6 +44,7 @@
 class ShadeDisplaysInteractorTest : SysuiTestCase() {
     val kosmos = testKosmos().useUnconfinedTestDispatcher()
 
+    private val testScope = kosmos.testScope
     private val shadeRootview = kosmos.mockShadeRootView
     private val positionRepository = kosmos.fakeShadeDisplaysRepository
     private val shadeContext = kosmos.mockedWindowContext
@@ -49,7 +53,7 @@
     private val configuration = mock<Configuration>()
     private val display = mock<Display>()
 
-    private val underTest = kosmos.shadeDisplaysInteractor
+    private val underTest by lazy { kosmos.shadeDisplaysInteractor }
 
     @Before
     fun setup() {
@@ -84,12 +88,14 @@
     }
 
     @Test
-    fun start_shadeInWrongPosition_logsStartToLatencyTracker() {
-        whenever(display.displayId).thenReturn(0)
-        positionRepository.setDisplayId(1)
+    fun start_shadeInWrongPosition_logsStartToLatencyTracker() =
+        testScope.runTest {
+            whenever(display.displayId).thenReturn(0)
+            positionRepository.setDisplayId(1)
 
-        underTest.start()
+            underTest.start()
+            advanceUntilIdle()
 
-        verify(latencyTracker).onShadeDisplayChanging(eq(1))
-    }
+            verify(latencyTracker).onShadeDisplayChanging(eq(1))
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractorTest.kt
new file mode 100644
index 0000000..58396e7
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractorTest.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractorImpl.NotificationElement
+import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractorImpl.QSElement
+import com.android.systemui.shade.shadeTestUtil
+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
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@EnableSceneContainer
+class ShadeExpandedStateInteractorTest : SysuiTestCase() {
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+
+    private val testScope = kosmos.testScope
+    private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
+
+    private val underTest: ShadeExpandedStateInteractor by lazy {
+        kosmos.shadeExpandedStateInteractor
+    }
+
+    @Test
+    fun expandedElement_qsExpanded_returnsQSElement() =
+        testScope.runTest {
+            shadeTestUtil.setShadeAndQsExpansion(shadeExpansion = 0f, qsExpansion = 1f)
+            val currentlyExpandedElement = underTest.currentlyExpandedElement
+
+            val element = currentlyExpandedElement.value
+
+            assertThat(element).isInstanceOf(QSElement::class.java)
+        }
+
+    @Test
+    fun expandedElement_shadeExpanded_returnsShade() =
+        testScope.runTest {
+            shadeTestUtil.setShadeAndQsExpansion(shadeExpansion = 1f, qsExpansion = 0f)
+
+            val element = underTest.currentlyExpandedElement.value
+
+            assertThat(element).isInstanceOf(NotificationElement::class.java)
+        }
+
+    @Test
+    fun expandedElement_noneExpanded_returnsNull() =
+        testScope.runTest {
+            shadeTestUtil.setShadeAndQsExpansion(shadeExpansion = 0f, qsExpansion = 0f)
+
+            val element = underTest.currentlyExpandedElement.value
+
+            assertThat(element).isNull()
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionExtensionsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionExtensionsTest.kt
index 83fb14a..6b2c4b2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionExtensionsTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionExtensionsTest.kt
@@ -9,9 +9,8 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -25,7 +24,7 @@
 
     @Before
     fun setUp() {
-        testScope = TestScope(StandardTestDispatcher())
+        testScope = TestScope(UnconfinedTestDispatcher())
     }
 
     @Test
@@ -34,11 +33,9 @@
             val flow = flowOf(true)
             val condition = flow.toCondition(scope = this, Condition.START_EAGERLY)
 
-            runCurrent()
             assertThat(condition.isConditionSet).isFalse()
 
             condition.start()
-            runCurrent()
             assertThat(condition.isConditionSet).isTrue()
             assertThat(condition.isConditionMet).isTrue()
         }
@@ -49,11 +46,9 @@
             val flow = flowOf(false)
             val condition = flow.toCondition(scope = this, Condition.START_EAGERLY)
 
-            runCurrent()
             assertThat(condition.isConditionSet).isFalse()
 
             condition.start()
-            runCurrent()
             assertThat(condition.isConditionSet).isTrue()
             assertThat(condition.isConditionMet).isFalse()
         }
@@ -65,7 +60,6 @@
             val condition = flow.toCondition(scope = this, Condition.START_EAGERLY)
             condition.start()
 
-            runCurrent()
             assertThat(condition.isConditionSet).isFalse()
             assertThat(condition.isConditionMet).isFalse()
         }
@@ -78,11 +72,10 @@
                 flow.toCondition(
                     scope = this,
                     strategy = Condition.START_EAGERLY,
-                    initialValue = true
+                    initialValue = true,
                 )
             condition.start()
 
-            runCurrent()
             assertThat(condition.isConditionSet).isTrue()
             assertThat(condition.isConditionMet).isTrue()
         }
@@ -95,11 +88,10 @@
                 flow.toCondition(
                     scope = this,
                     strategy = Condition.START_EAGERLY,
-                    initialValue = false
+                    initialValue = false,
                 )
             condition.start()
 
-            runCurrent()
             assertThat(condition.isConditionSet).isTrue()
             assertThat(condition.isConditionMet).isFalse()
         }
@@ -111,16 +103,13 @@
             val condition = flow.toCondition(scope = this, strategy = Condition.START_EAGERLY)
             condition.start()
 
-            runCurrent()
             assertThat(condition.isConditionSet).isTrue()
             assertThat(condition.isConditionMet).isFalse()
 
             flow.value = true
-            runCurrent()
             assertThat(condition.isConditionMet).isTrue()
 
             flow.value = false
-            runCurrent()
             assertThat(condition.isConditionMet).isFalse()
 
             condition.stop()
@@ -131,15 +120,12 @@
         testScope.runTest {
             val flow = MutableSharedFlow<Boolean>()
             val condition = flow.toCondition(scope = this, strategy = Condition.START_EAGERLY)
-            runCurrent()
             assertThat(flow.subscriptionCount.value).isEqualTo(0)
 
             condition.start()
-            runCurrent()
             assertThat(flow.subscriptionCount.value).isEqualTo(1)
 
             condition.stop()
-            runCurrent()
             assertThat(flow.subscriptionCount.value).isEqualTo(0)
         }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index 4a3be44..deaf579 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -81,6 +81,7 @@
 import com.android.systemui.flags.DisableSceneContainer;
 import com.android.systemui.flags.EnableSceneContainer;
 import com.android.systemui.flags.FakeFeatureFlagsClassic;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.log.LogWtfHandlerRule;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.recents.OverviewProxyService;
@@ -90,6 +91,7 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
+import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.concurrency.FakeExecutor;
@@ -115,6 +117,7 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
 
 import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
 import platform.test.runner.parameterized.Parameters;
@@ -169,6 +172,10 @@
     @Mock
     private DeviceUnlockedInteractor mDeviceUnlockedInteractor;
     @Mock
+    private Lazy<KeyguardInteractor> mKeyguardInteractorLazy;
+    @Mock
+    private KeyguardInteractor mKeyguardInteractor;
+    @Mock
     private StateFlow<DeviceUnlockStatus> mDeviceUnlockStatusStateFlow;
 
     private UserInfo mCurrentUser;
@@ -181,6 +188,7 @@
     private NotificationEntry mSecondaryUserNotif;
     private NotificationEntry mWorkProfileNotif;
     private NotificationEntry mSensitiveContentNotif;
+    private long mSensitiveNotifPostTime;
     private final FakeFeatureFlagsClassic mFakeFeatureFlags = new FakeFeatureFlagsClassic();
     private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
     private final FakeExecutor mBackgroundExecutor = new FakeExecutor(mFakeSystemClock);
@@ -246,13 +254,17 @@
         mSensitiveContentNotif = new NotificationEntryBuilder()
                 .setNotification(notifWithPrivateVisibility)
                 .setUser(new UserHandle(mCurrentUser.id))
+                .setPostTime(System.currentTimeMillis())
                 .build();
         mSensitiveContentNotif.setRanking(new RankingBuilder(mCurrentUserNotif.getRanking())
                 .setChannel(channel)
                 .setSensitiveContent(true)
                 .setVisibilityOverride(VISIBILITY_NO_OVERRIDE).build());
+        mSensitiveNotifPostTime = mSensitiveContentNotif.getSbn().getPostTime();
         when(mNotifCollection.getEntry(mWorkProfileNotif.getKey())).thenReturn(mWorkProfileNotif);
-
+        when(mKeyguardInteractorLazy.get()).thenReturn(mKeyguardInteractor);
+        when(mKeyguardInteractor.isKeyguardDismissible())
+                .thenReturn(mock(StateFlow.class));
         mLockscreenUserManager = new TestNotificationLockscreenUserManager(mContext);
         mLockscreenUserManager.setUpWithPresenter(mPresenter);
 
@@ -504,11 +516,85 @@
     }
 
     @Test
+    @EnableFlags(LockscreenOtpRedaction.FLAG_NAME)
+    public void testHasSensitiveContent_notRedactedIfNotLocked() {
+        // Allow private notifications for this user
+        mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
+                mCurrentUser.id);
+        changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
+        // Claim the device was last locked 1 day ago
+        mLockscreenUserManager.mLastLockTime
+                .set(mSensitiveNotifPostTime - TimeUnit.DAYS.toMillis(1));
+        // Device is not currently locked
+        mLockscreenUserManager.mLocked.set(false);
+
+        // Sensitive Content notifications are always redacted
+        assertEquals(REDACTION_TYPE_NONE,
+                mLockscreenUserManager.getRedactionType(mSensitiveContentNotif));
+    }
+
+    @Test
+    @EnableFlags(LockscreenOtpRedaction.FLAG_NAME)
+    public void testHasSensitiveContent_notRedactedIfUnlockedSinceReceipt() {
+        // Allow private notifications for this user
+        mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
+                mCurrentUser.id);
+        changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
+        mLockscreenUserManager.mLocked.set(true);
+        // Device was locked after this notification arrived
+        mLockscreenUserManager.mLastLockTime
+                .set(mSensitiveNotifPostTime + TimeUnit.DAYS.toMillis(1));
+
+        // Sensitive Content notifications are always redacted
+        assertEquals(REDACTION_TYPE_NONE,
+                mLockscreenUserManager.getRedactionType(mSensitiveContentNotif));
+    }
+
+    @Test
+    @EnableFlags(LockscreenOtpRedaction.FLAG_NAME)
+    public void testHasSensitiveContent_notRedactedIfNotLockedForLongEnough() {
+        // Allow private notifications for this user
+        mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
+                mCurrentUser.id);
+        changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
+        // Device has been locked for 1 second before the notification came in, which is too short
+        mLockscreenUserManager.mLastLockTime
+                .set(mSensitiveNotifPostTime - TimeUnit.SECONDS.toMillis(1));
+        mLockscreenUserManager.mLocked.set(true);
+
+        // Sensitive Content notifications are always redacted
+        assertEquals(REDACTION_TYPE_NONE,
+                mLockscreenUserManager.getRedactionType(mSensitiveContentNotif));
+    }
+
+    @Test
+    @DisableFlags(LockscreenOtpRedaction.FLAG_NAME)
+    public void testHasSensitiveContent_notRedactedFlagDisabled() {
+        // Allow private notifications for this user
+        mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
+                mCurrentUser.id);
+        changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
+        // Claim the device was last locked 1 day ago
+        mLockscreenUserManager.mLastLockTime
+                .set(mSensitiveNotifPostTime - TimeUnit.DAYS.toMillis(1));
+        mLockscreenUserManager.mLocked.set(true);
+
+        // Sensitive Content notifications are always redacted
+        assertEquals(REDACTION_TYPE_NONE,
+                mLockscreenUserManager.getRedactionType(mSensitiveContentNotif));
+    }
+
+    @Test
+    @EnableFlags(LockscreenOtpRedaction.FLAG_NAME)
     public void testHasSensitiveContent_redacted() {
         // Allow private notifications for this user
         mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
                 mCurrentUser.id);
         changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
+        when(mKeyguardManager.isDeviceLocked()).thenReturn(true);
+        // Claim the device was last unlocked 1 day ago
+        mLockscreenUserManager.mLastLockTime
+                .set(mSensitiveNotifPostTime - TimeUnit.DAYS.toMillis(1));
 
         // Sensitive Content notifications are always redacted
         assertEquals(REDACTION_TYPE_SENSITIVE_CONTENT,
@@ -1066,7 +1152,9 @@
                     mock(DumpManager.class),
                     mock(LockPatternUtils.class),
                     mFakeFeatureFlags,
-                    mDeviceUnlockedInteractorLazy
+                    mDeviceUnlockedInteractorLazy,
+                    mKeyguardInteractorLazy,
+                    null //CoroutineScope
             );
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
index 2d7dc2e..0a05649 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
@@ -43,6 +43,7 @@
 import com.android.systemui.util.WallpaperController
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor
+import com.android.wm.shell.appzoomout.AppZoomOut
 import com.google.common.truth.Truth.assertThat
 import java.util.function.Consumer
 import org.junit.Before
@@ -65,6 +66,7 @@
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.junit.MockitoJUnit
+import java.util.Optional
 
 @RunWith(AndroidJUnit4::class)
 @RunWithLooper
@@ -82,6 +84,7 @@
     @Mock private lateinit var wallpaperController: WallpaperController
     @Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController
     @Mock private lateinit var dumpManager: DumpManager
+    @Mock private lateinit var appZoomOutOptional: Optional<AppZoomOut>
     @Mock private lateinit var root: View
     @Mock private lateinit var viewRootImpl: ViewRootImpl
     @Mock private lateinit var windowToken: IBinder
@@ -128,6 +131,7 @@
                 ResourcesSplitShadeStateController(),
                 windowRootViewBlurInteractor,
                 applicationScope,
+                appZoomOutOptional,
                 dumpManager,
                 configurationController,
             )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarSignalPolicyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarSignalPolicyTest.kt
index bb9141a..5f73ac4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarSignalPolicyTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarSignalPolicyTest.kt
@@ -45,8 +45,11 @@
 import org.junit.Before
 import org.junit.runner.RunWith
 import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
 import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
 import org.mockito.kotlin.verifyNoMoreInteractions
 
 @SmallTest
@@ -106,10 +109,10 @@
             // Make sure the legacy code path does not change airplane mode when the refactor
             // flag is enabled.
             underTest.setIsAirplaneMode(IconState(true, TelephonyIcons.FLIGHT_MODE_ICON, ""))
-            verifyNoMoreInteractions(statusBarIconController)
+            verify(statusBarIconController, never()).setIconVisibility(eq(slotAirplane), any())
 
             underTest.setIsAirplaneMode(IconState(false, TelephonyIcons.FLIGHT_MODE_ICON, ""))
-            verifyNoMoreInteractions(statusBarIconController)
+            verify(statusBarIconController, never()).setIconVisibility(eq(slotAirplane), any())
         }
 
     @Test
@@ -144,10 +147,10 @@
             // Make sure changing airplane mode from airplaneModeRepository does nothing
             // if the StatusBarSignalPolicyRefactor is not enabled.
             airplaneModeInteractor.setIsAirplaneMode(true)
-            verifyNoMoreInteractions(statusBarIconController)
+            verify(statusBarIconController, never()).setIconVisibility(eq(slotAirplane), any())
 
             airplaneModeInteractor.setIsAirplaneMode(false)
-            verifyNoMoreInteractions(statusBarIconController)
+            verify(statusBarIconController, never()).setIconVisibility(eq(slotAirplane), any())
         }
 
     @Test
@@ -196,7 +199,7 @@
             underTest.setEthernetIndicators(
                 IconState(/* visible= */ true, /* icon= */ 1, /* contentDescription= */ "Ethernet")
             )
-            verifyNoMoreInteractions(statusBarIconController)
+            verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any())
 
             underTest.setEthernetIndicators(
                 IconState(
@@ -205,7 +208,7 @@
                     /* contentDescription= */ "No ethernet",
                 )
             )
-            verifyNoMoreInteractions(statusBarIconController)
+            verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any())
         }
 
     @Test
@@ -217,13 +220,13 @@
             clearInvocations(statusBarIconController)
 
             connectivityRepository.fake.setEthernetConnected(default = true, validated = true)
-            verifyNoMoreInteractions(statusBarIconController)
+            verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any())
 
             connectivityRepository.fake.setEthernetConnected(default = false, validated = false)
-            verifyNoMoreInteractions(statusBarIconController)
+            verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any())
 
             connectivityRepository.fake.setEthernetConnected(default = true, validated = false)
-            verifyNoMoreInteractions(statusBarIconController)
+            verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any())
         }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
index a62d9d5..75d000b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
@@ -22,7 +22,6 @@
 import android.view.View
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.coroutines.collectLastValue
@@ -126,46 +125,8 @@
         }
 
     @Test
-    @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
-    fun chip_positiveStartTime_notifIconFlagOff_iconIsPhone() =
-        testScope.runTest {
-            val latest by collectLastValue(underTest.chip)
-
-            repo.setOngoingCallState(
-                inCallModel(startTimeMs = 1000, notificationIcon = mock<StatusBarIconView>())
-            )
-
-            assertThat((latest as OngoingActivityChipModel.Shown).icon)
-                .isInstanceOf(OngoingActivityChipModel.ChipIcon.SingleColorIcon::class.java)
-            val icon =
-                (((latest as OngoingActivityChipModel.Shown).icon)
-                        as OngoingActivityChipModel.ChipIcon.SingleColorIcon)
-                    .impl as Icon.Resource
-            assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone)
-            assertThat(icon.contentDescription).isNotNull()
-        }
-
-    @Test
-    @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
-    fun chip_positiveStartTime_notifIconFlagOn_iconIsNotifIcon() =
-        testScope.runTest {
-            val latest by collectLastValue(underTest.chip)
-
-            val notifIcon = mock<StatusBarIconView>()
-            repo.setOngoingCallState(inCallModel(startTimeMs = 1000, notificationIcon = notifIcon))
-
-            assertThat((latest as OngoingActivityChipModel.Shown).icon)
-                .isInstanceOf(OngoingActivityChipModel.ChipIcon.StatusBarView::class.java)
-            val actualIcon =
-                (((latest as OngoingActivityChipModel.Shown).icon)
-                        as OngoingActivityChipModel.ChipIcon.StatusBarView)
-                    .impl
-            assertThat(actualIcon).isEqualTo(notifIcon)
-        }
-
-    @Test
-    @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME)
-    fun chip_positiveStartTime_notifIconAndConnectedDisplaysFlagOn_iconIsNotifIcon() =
+    @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+    fun chip_positiveStartTime_connectedDisplaysFlagOn_iconIsNotifIcon() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
@@ -186,32 +147,12 @@
         }
 
     @Test
-    @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
-    fun chip_zeroStartTime_notifIconFlagOff_iconIsPhone() =
+    @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+    fun chip_zeroStartTime_cdFlagOff_iconIsNotifIcon() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
-            repo.setOngoingCallState(
-                inCallModel(startTimeMs = 0, notificationIcon = mock<StatusBarIconView>())
-            )
-
-            assertThat((latest as OngoingActivityChipModel.Shown).icon)
-                .isInstanceOf(OngoingActivityChipModel.ChipIcon.SingleColorIcon::class.java)
-            val icon =
-                (((latest as OngoingActivityChipModel.Shown).icon)
-                        as OngoingActivityChipModel.ChipIcon.SingleColorIcon)
-                    .impl as Icon.Resource
-            assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone)
-            assertThat(icon.contentDescription).isNotNull()
-        }
-
-    @Test
-    @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
-    fun chip_zeroStartTime_notifIconFlagOn_iconIsNotifIcon() =
-        testScope.runTest {
-            val latest by collectLastValue(underTest.chip)
-
-            val notifIcon = mock<StatusBarIconView>()
+            val notifIcon = createStatusBarIconViewOrNull()
             repo.setOngoingCallState(inCallModel(startTimeMs = 0, notificationIcon = notifIcon))
 
             assertThat((latest as OngoingActivityChipModel.Shown).icon)
@@ -224,8 +165,26 @@
         }
 
     @Test
-    @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
-    fun chip_notifIconFlagOn_butNullNotifIcon_iconIsPhone() =
+    @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+    fun chip_zeroStartTime_cdFlagOn_iconIsNotifKeyIcon() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            repo.setOngoingCallState(
+                inCallModel(
+                    startTimeMs = 0,
+                    notificationIcon = createStatusBarIconViewOrNull(),
+                    notificationKey = "notifKey",
+                )
+            )
+
+            assertThat((latest as OngoingActivityChipModel.Shown).icon)
+                .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon("notifKey"))
+        }
+
+    @Test
+    @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+    fun chip_notifIconFlagOn_butNullNotifIcon_cdFlagOff_iconIsPhone() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
@@ -242,6 +201,24 @@
         }
 
     @Test
+    @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+    fun chip_notifIconFlagOn_butNullNotifIcon_iconNotifKey() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            repo.setOngoingCallState(
+                inCallModel(
+                    startTimeMs = 1000,
+                    notificationIcon = null,
+                    notificationKey = "notifKey",
+                )
+            )
+
+            assertThat((latest as OngoingActivityChipModel.Shown).icon)
+                .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon("notifKey"))
+        }
+
+    @Test
     fun chip_positiveStartTime_colorsAreThemed() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
@@ -330,4 +307,13 @@
 
             verify(kosmos.activityStarter).postStartActivityDismissingKeyguard(intent, null)
         }
+
+    companion object {
+        fun createStatusBarIconViewOrNull(): StatusBarIconView? =
+            if (StatusBarConnectedDisplays.isEnabled) {
+                null
+            } else {
+                mock<StatusBarIconView>()
+            }
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
index 0d033a4..fe15eac 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.chips.notification.domain.interactor
 
+import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -148,7 +149,8 @@
         }
 
     @Test
-    fun notificationChip_missingStatusBarIconChipView_inConstructor_emitsNull() =
+    @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+    fun notificationChip_missingStatusBarIconChipView_cdFlagDisabled_inConstructor_emitsNull() =
         kosmos.runTest {
             val underTest =
                 factory.create(
@@ -167,6 +169,25 @@
 
     @Test
     @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+    fun notificationChip_missingStatusBarIconChipView_cdFlagEnabled_inConstructor_emitsNotNull() =
+        kosmos.runTest {
+            val underTest =
+                factory.create(
+                    activeNotificationModel(
+                        key = "notif1",
+                        statusBarChipIcon = null,
+                        promotedContent = PROMOTED_CONTENT,
+                    ),
+                    32L,
+                )
+
+            val latest by collectLastValue(underTest.notificationChip)
+
+            assertThat(latest).isNotNull()
+        }
+
+    @Test
+    @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
     fun notificationChip_cdEnabled_missingStatusBarIconChipView_inConstructor_emitsNotNull() =
         kosmos.runTest {
             val underTest =
@@ -186,7 +207,8 @@
         }
 
     @Test
-    fun notificationChip_missingStatusBarIconChipView_inSet_emitsNull() =
+    @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+    fun notificationChip_cdFlagDisabled_missingStatusBarIconChipView_inSet_emitsNull() =
         kosmos.runTest {
             val startingNotif =
                 activeNotificationModel(
@@ -211,6 +233,31 @@
 
     @Test
     @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+    fun notificationChip_cdFlagEnabled_missingStatusBarIconChipView_inSet_emitsNotNull() =
+        kosmos.runTest {
+            val startingNotif =
+                activeNotificationModel(
+                    key = "notif1",
+                    statusBarChipIcon = mock(),
+                    promotedContent = PROMOTED_CONTENT,
+                )
+            val underTest = factory.create(startingNotif, 123L)
+            val latest by collectLastValue(underTest.notificationChip)
+            assertThat(latest).isNotNull()
+
+            underTest.setNotification(
+                activeNotificationModel(
+                    key = "notif1",
+                    statusBarChipIcon = null,
+                    promotedContent = PROMOTED_CONTENT,
+                )
+            )
+
+            assertThat(latest).isNotNull()
+        }
+
+    @Test
+    @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
     fun notificationChip_missingStatusBarIconChipView_inSet_cdEnabled_emitsNotNull() =
         kosmos.runTest {
             val startingNotif =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
index f703d78..ee4a52d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
 import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
@@ -83,7 +84,8 @@
 
     @Test
     @EnableFlags(StatusBarNotifChips.FLAG_NAME)
-    fun notificationChips_notifMissingStatusBarChipIconView_empty() =
+    @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+    fun notificationChips_notifMissingStatusBarChipIconView_cdFlagOff_empty() =
         kosmos.runTest {
             val latest by collectLastValue(underTest.notificationChips)
 
@@ -101,6 +103,25 @@
         }
 
     @Test
+    @EnableFlags(StatusBarNotifChips.FLAG_NAME, StatusBarConnectedDisplays.FLAG_NAME)
+    fun notificationChips_notifMissingStatusBarChipIconView_cdFlagOn_notEmpty() =
+        kosmos.runTest {
+            val latest by collectLastValue(underTest.notificationChips)
+
+            setNotifs(
+                listOf(
+                    activeNotificationModel(
+                        key = "notif",
+                        statusBarChipIcon = null,
+                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                    )
+                )
+            )
+
+            assertThat(latest).isNotEmpty()
+        }
+
+    @Test
     @EnableFlags(StatusBarNotifChips.FLAG_NAME)
     fun notificationChips_onePromotedNotif_statusBarIconViewMatches() =
         kosmos.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
index 17076b4..902db5e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
@@ -23,7 +23,6 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.Flags.FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.kosmos.collectLastValue
@@ -31,6 +30,7 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.chips.call.ui.viewmodel.CallChipViewModelTest.Companion.createStatusBarIconViewOrNull
 import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor
 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
 import com.android.systemui.statusbar.chips.ui.model.ColorsModel
@@ -48,7 +48,6 @@
 import com.google.common.truth.Truth.assertThat
 import kotlin.test.Test
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.runner.RunWith
 import org.mockito.kotlin.mock
@@ -84,8 +83,8 @@
         }
 
     @Test
-    @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
-    fun chips_notifMissingStatusBarChipIconView_empty() =
+    @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY, StatusBarConnectedDisplays.FLAG_NAME)
+    fun chips_notifMissingStatusBarChipIconView_cdFlagDisabled_empty() =
         kosmos.runTest {
             val latest by collectLastValue(underTest.chips)
 
@@ -104,11 +103,31 @@
 
     @Test
     @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
+    @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+    fun chips_notifMissingStatusBarChipIconView_cdFlagEnabled_notEmpty() =
+        kosmos.runTest {
+            val latest by collectLastValue(underTest.chips)
+
+            setNotifs(
+                listOf(
+                    activeNotificationModel(
+                        key = "notif",
+                        statusBarChipIcon = null,
+                        promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+                    )
+                )
+            )
+
+            assertThat(latest).isNotEmpty()
+        }
+
+    @Test
+    @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
     fun chips_onePromotedNotif_statusBarIconViewMatches() =
         kosmos.runTest {
             val latest by collectLastValue(underTest.chips)
 
-            val icon = mock<StatusBarIconView>()
+            val icon = createStatusBarIconViewOrNull()
             setNotifs(
                 listOf(
                     activeNotificationModel(
@@ -121,8 +140,7 @@
 
             assertThat(latest).hasSize(1)
             val chip = latest!![0]
-            assertThat(chip).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
-            assertThat(chip.icon).isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarView(icon))
+            assertIsNotifChip(chip, icon, "notif")
         }
 
     @Test
@@ -168,7 +186,7 @@
                 listOf(
                     activeNotificationModel(
                         key = "notif",
-                        statusBarChipIcon = mock<StatusBarIconView>(),
+                        statusBarChipIcon = createStatusBarIconViewOrNull(),
                         promotedContent = promotedContentBuilder.build(),
                     )
                 )
@@ -187,8 +205,8 @@
         kosmos.runTest {
             val latest by collectLastValue(underTest.chips)
 
-            val firstIcon = mock<StatusBarIconView>()
-            val secondIcon = mock<StatusBarIconView>()
+            val firstIcon = createStatusBarIconViewOrNull()
+            val secondIcon = createStatusBarIconViewOrNull()
             setNotifs(
                 listOf(
                     activeNotificationModel(
@@ -203,15 +221,15 @@
                     ),
                     activeNotificationModel(
                         key = "notif3",
-                        statusBarChipIcon = mock<StatusBarIconView>(),
+                        statusBarChipIcon = createStatusBarIconViewOrNull(),
                         promotedContent = null,
                     ),
                 )
             )
 
             assertThat(latest).hasSize(2)
-            assertIsNotifChip(latest!![0], firstIcon)
-            assertIsNotifChip(latest!![1], secondIcon)
+            assertIsNotifChip(latest!![0], firstIcon, "notif1")
+            assertIsNotifChip(latest!![1], secondIcon, "notif2")
         }
 
     @Test
@@ -269,7 +287,7 @@
                 listOf(
                     activeNotificationModel(
                         key = "notif",
-                        statusBarChipIcon = mock<StatusBarIconView>(),
+                        statusBarChipIcon = createStatusBarIconViewOrNull(),
                         promotedContent = promotedContentBuilder.build(),
                     )
                 )
@@ -293,7 +311,7 @@
                 listOf(
                     activeNotificationModel(
                         key = "notif",
-                        statusBarChipIcon = mock<StatusBarIconView>(),
+                        statusBarChipIcon = createStatusBarIconViewOrNull(),
                         promotedContent = promotedContentBuilder.build(),
                     )
                 )
@@ -323,7 +341,7 @@
                 listOf(
                     activeNotificationModel(
                         key = "notif",
-                        statusBarChipIcon = mock<StatusBarIconView>(),
+                        statusBarChipIcon = createStatusBarIconViewOrNull(),
                         promotedContent = promotedContentBuilder.build(),
                     )
                 )
@@ -353,7 +371,7 @@
                 listOf(
                     activeNotificationModel(
                         key = "notif",
-                        statusBarChipIcon = mock<StatusBarIconView>(),
+                        statusBarChipIcon = createStatusBarIconViewOrNull(),
                         promotedContent = promotedContentBuilder.build(),
                     )
                 )
@@ -382,7 +400,7 @@
                 listOf(
                     activeNotificationModel(
                         key = "notif",
-                        statusBarChipIcon = mock<StatusBarIconView>(),
+                        statusBarChipIcon = createStatusBarIconViewOrNull(),
                         promotedContent = promotedContentBuilder.build(),
                     )
                 )
@@ -411,7 +429,7 @@
                 listOf(
                     activeNotificationModel(
                         key = "notif",
-                        statusBarChipIcon = mock<StatusBarIconView>(),
+                        statusBarChipIcon = createStatusBarIconViewOrNull(),
                         promotedContent = promotedContentBuilder.build(),
                     )
                 )
@@ -439,7 +457,7 @@
                 listOf(
                     activeNotificationModel(
                         key = "notif",
-                        statusBarChipIcon = mock<StatusBarIconView>(),
+                        statusBarChipIcon = createStatusBarIconViewOrNull(),
                         promotedContent = promotedContentBuilder.build(),
                     )
                 )
@@ -467,7 +485,7 @@
                 listOf(
                     activeNotificationModel(
                         key = "notif",
-                        statusBarChipIcon = mock<StatusBarIconView>(),
+                        statusBarChipIcon = createStatusBarIconViewOrNull(),
                         promotedContent = promotedContentBuilder.build(),
                     )
                 )
@@ -483,7 +501,99 @@
 
     @Test
     @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
-    fun chips_hasHeadsUpByUser_onlyShowsIcon() =
+    fun chips_hasHeadsUpBySystem_showsTime() =
+        kosmos.runTest {
+            val latest by collectLastValue(underTest.chips)
+
+            val promotedContentBuilder =
+                PromotedNotificationContentModel.Builder("notif").apply {
+                    this.time =
+                        PromotedNotificationContentModel.When(
+                            time = 6543L,
+                            mode = PromotedNotificationContentModel.When.Mode.BasicTime,
+                        )
+                }
+            setNotifs(
+                listOf(
+                    activeNotificationModel(
+                        key = "notif",
+                        statusBarChipIcon = createStatusBarIconViewOrNull(),
+                        promotedContent = promotedContentBuilder.build(),
+                    )
+                )
+            )
+
+            // WHEN there's a HUN pinned by the system
+            kosmos.headsUpNotificationRepository.setNotifications(
+                UnconfinedFakeHeadsUpRowRepository(
+                    key = "notif",
+                    pinnedStatus = MutableStateFlow(PinnedStatus.PinnedBySystem),
+                )
+            )
+
+            // THEN the chip keeps showing time
+            // (In real life the chip won't show at all, but that's handled in a different part of
+            // the system. What we know here is that the chip shouldn't shrink to icon only.)
+            assertThat(latest!![0])
+                .isInstanceOf(OngoingActivityChipModel.Shown.ShortTimeDelta::class.java)
+        }
+
+    @Test
+    @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
+    fun chips_hasHeadsUpByUser_forOtherNotif_showsTime() =
+        kosmos.runTest {
+            val latest by collectLastValue(underTest.chips)
+
+            val promotedContentBuilder =
+                PromotedNotificationContentModel.Builder("notif").apply {
+                    this.time =
+                        PromotedNotificationContentModel.When(
+                            time = 6543L,
+                            mode = PromotedNotificationContentModel.When.Mode.BasicTime,
+                        )
+                }
+            val otherPromotedContentBuilder =
+                PromotedNotificationContentModel.Builder("other notif").apply {
+                    this.time =
+                        PromotedNotificationContentModel.When(
+                            time = 654321L,
+                            mode = PromotedNotificationContentModel.When.Mode.BasicTime,
+                        )
+                }
+            val icon = createStatusBarIconViewOrNull()
+            val otherIcon = createStatusBarIconViewOrNull()
+            setNotifs(
+                listOf(
+                    activeNotificationModel(
+                        key = "notif",
+                        statusBarChipIcon = icon,
+                        promotedContent = promotedContentBuilder.build(),
+                    ),
+                    activeNotificationModel(
+                        key = "other notif",
+                        statusBarChipIcon = otherIcon,
+                        promotedContent = otherPromotedContentBuilder.build(),
+                    ),
+                )
+            )
+
+            // WHEN there's a HUN pinned for the "other notif" chip
+            kosmos.headsUpNotificationRepository.setNotifications(
+                UnconfinedFakeHeadsUpRowRepository(
+                    key = "other notif",
+                    pinnedStatus = MutableStateFlow(PinnedStatus.PinnedByUser),
+                )
+            )
+
+            // THEN the "notif" chip keeps showing time
+            val chip = latest!![0]
+            assertThat(chip).isInstanceOf(OngoingActivityChipModel.Shown.ShortTimeDelta::class.java)
+            assertIsNotifChip(chip, icon, "notif")
+        }
+
+    @Test
+    @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
+    fun chips_hasHeadsUpByUser_forThisNotif_onlyShowsIcon() =
         kosmos.runTest {
             val latest by collectLastValue(underTest.chips)
 
@@ -505,7 +615,7 @@
                 )
             )
 
-            // WHEN there's a HUN pinned by a user
+            // WHEN this notification is pinned by the user
             kosmos.headsUpNotificationRepository.setNotifications(
                 UnconfinedFakeHeadsUpRowRepository(
                     key = "notif",
@@ -513,6 +623,7 @@
                 )
             )
 
+            // THEN the chip shrinks to icon only
             assertThat(latest!![0])
                 .isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java)
         }
@@ -531,7 +642,7 @@
                 listOf(
                     activeNotificationModel(
                         key = "clickTest",
-                        statusBarChipIcon = mock<StatusBarIconView>(),
+                        statusBarChipIcon = createStatusBarIconViewOrNull(),
                         promotedContent =
                             PromotedNotificationContentModel.Builder("clickTest").build(),
                     )
@@ -552,9 +663,21 @@
     }
 
     companion object {
-        fun assertIsNotifChip(latest: OngoingActivityChipModel?, expectedIcon: StatusBarIconView) {
-            assertThat((latest as OngoingActivityChipModel.Shown).icon)
-                .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarView(expectedIcon))
+        fun assertIsNotifChip(
+            latest: OngoingActivityChipModel?,
+            expectedIcon: StatusBarIconView?,
+            notificationKey: String,
+        ) {
+            val shown = latest as OngoingActivityChipModel.Shown
+            if (StatusBarConnectedDisplays.isEnabled) {
+                assertThat(shown.icon)
+                    .isEqualTo(
+                        OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon(notificationKey)
+                    )
+            } else {
+                assertThat(latest.icon)
+                    .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarView(expectedIcon!!))
+            }
         }
 
         fun assertIsNotifKey(latest: OngoingActivityChipModel?, expectedKey: String) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
index 4fb42e9..42358cc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
@@ -41,6 +41,7 @@
 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
 import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
@@ -169,29 +170,35 @@
     @Test
     fun primaryChip_screenRecordAndShareToAppAndCastToOtherHideAndCallShown_callShown() =
         testScope.runTest {
+            val notificationKey = "call"
             screenRecordState.value = ScreenRecordModel.DoingNothing
             // MediaProjection covers both share-to-app and cast-to-other-device
             mediaProjectionState.value = MediaProjectionState.NotProjecting
 
-            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+            callRepo.setOngoingCallState(
+                inCallModel(startTimeMs = 34, notificationKey = notificationKey)
+            )
 
             val latest by collectLastValue(underTest.primaryChip)
 
-            assertIsCallChip(latest)
+            assertIsCallChip(latest, notificationKey)
         }
 
     @Test
     fun primaryChip_higherPriorityChipAdded_lowerPriorityChipReplaced() =
         testScope.runTest {
             // Start with just the lowest priority chip shown
-            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+            val callNotificationKey = "call"
+            callRepo.setOngoingCallState(
+                inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+            )
             // And everything else hidden
             mediaProjectionState.value = MediaProjectionState.NotProjecting
             screenRecordState.value = ScreenRecordModel.DoingNothing
 
             val latest by collectLastValue(underTest.primaryChip)
 
-            assertIsCallChip(latest)
+            assertIsCallChip(latest, callNotificationKey)
 
             // WHEN the higher priority media projection chip is added
             mediaProjectionState.value =
@@ -218,7 +225,10 @@
             screenRecordState.value = ScreenRecordModel.Recording
             mediaProjectionState.value =
                 MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
-            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+            val callNotificationKey = "call"
+            callRepo.setOngoingCallState(
+                inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+            )
 
             val latest by collectLastValue(underTest.primaryChip)
 
@@ -235,7 +245,7 @@
             mediaProjectionState.value = MediaProjectionState.NotProjecting
 
             // THEN the lower priority call is used
-            assertIsCallChip(latest)
+            assertIsCallChip(latest, callNotificationKey)
         }
 
     /** Regression test for b/347726238. */
@@ -364,13 +374,27 @@
             assertThat(icon.res).isEqualTo(R.drawable.ic_present_to_all)
         }
 
-        fun assertIsCallChip(latest: OngoingActivityChipModel?) {
-            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+        fun assertIsCallChip(latest: OngoingActivityChipModel?, notificationKey: String) {
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
+            if (StatusBarConnectedDisplays.isEnabled) {
+                assertNotificationIcon(latest, notificationKey)
+                return
+            }
             val icon =
                 (((latest as OngoingActivityChipModel.Shown).icon)
                         as OngoingActivityChipModel.ChipIcon.SingleColorIcon)
                     .impl as Icon.Resource
             assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone)
         }
+
+        private fun assertNotificationIcon(
+            latest: OngoingActivityChipModel?,
+            notificationKey: String,
+        ) {
+            val shown = latest as OngoingActivityChipModel.Shown
+            val notificationIcon =
+                shown.icon as OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon
+            assertThat(notificationIcon.notificationKey).isEqualTo(notificationKey)
+        }
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
index 0050ebe..0f42f29 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
@@ -34,7 +34,7 @@
 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.StatusBarIconView
+import com.android.systemui.statusbar.chips.call.ui.viewmodel.CallChipViewModelTest.Companion.createStatusBarIconViewOrNull
 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.notification.domain.interactor.statusBarNotificationChipsInteractor
@@ -186,13 +186,16 @@
     @Test
     fun chips_screenRecordShowAndCallShow_primaryIsScreenRecordSecondaryIsCall() =
         testScope.runTest {
+            val callNotificationKey = "call"
             screenRecordState.value = ScreenRecordModel.Recording
-            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+            callRepo.setOngoingCallState(
+                inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+            )
 
             val latest by collectLastValue(underTest.chips)
 
             assertIsScreenRecordChip(latest!!.primary)
-            assertIsCallChip(latest!!.secondary)
+            assertIsCallChip(latest!!.secondary, callNotificationKey)
         }
 
     @Test
@@ -240,15 +243,18 @@
     @Test
     fun chips_shareToAppShowAndCallShow_primaryIsShareToAppSecondaryIsCall() =
         testScope.runTest {
+            val callNotificationKey = "call"
             screenRecordState.value = ScreenRecordModel.DoingNothing
             mediaProjectionState.value =
                 MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
-            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+            callRepo.setOngoingCallState(
+                inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+            )
 
             val latest by collectLastValue(underTest.chips)
 
             assertIsShareToAppChip(latest!!.primary)
-            assertIsCallChip(latest!!.secondary)
+            assertIsCallChip(latest!!.secondary, callNotificationKey)
         }
 
     @Test
@@ -258,25 +264,31 @@
             // MediaProjection covers both share-to-app and cast-to-other-device
             mediaProjectionState.value = MediaProjectionState.NotProjecting
 
-            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+            val callNotificationKey = "call"
+            callRepo.setOngoingCallState(
+                inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+            )
 
             val latest by collectLastValue(underTest.primaryChip)
 
-            assertIsCallChip(latest)
+            assertIsCallChip(latest, callNotificationKey)
         }
 
     @Test
     fun chips_onlyCallShown_primaryIsCallSecondaryIsHidden() =
         testScope.runTest {
+            val callNotificationKey = "call"
             screenRecordState.value = ScreenRecordModel.DoingNothing
             // MediaProjection covers both share-to-app and cast-to-other-device
             mediaProjectionState.value = MediaProjectionState.NotProjecting
 
-            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+            callRepo.setOngoingCallState(
+                inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+            )
 
             val latest by collectLastValue(underTest.chips)
 
-            assertIsCallChip(latest!!.primary)
+            assertIsCallChip(latest!!.primary, callNotificationKey)
             assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
         }
 
@@ -285,7 +297,7 @@
         testScope.runTest {
             val latest by collectLastValue(underTest.chips)
 
-            val icon = mock<StatusBarIconView>()
+            val icon = createStatusBarIconViewOrNull()
             setNotifs(
                 listOf(
                     activeNotificationModel(
@@ -296,7 +308,7 @@
                 )
             )
 
-            assertIsNotifChip(latest!!.primary, icon)
+            assertIsNotifChip(latest!!.primary, icon, "notif")
             assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
         }
 
@@ -305,8 +317,8 @@
         testScope.runTest {
             val latest by collectLastValue(underTest.chips)
 
-            val firstIcon = mock<StatusBarIconView>()
-            val secondIcon = mock<StatusBarIconView>()
+            val firstIcon = createStatusBarIconViewOrNull()
+            val secondIcon = createStatusBarIconViewOrNull()
             setNotifs(
                 listOf(
                     activeNotificationModel(
@@ -324,8 +336,8 @@
                 )
             )
 
-            assertIsNotifChip(latest!!.primary, firstIcon)
-            assertIsNotifChip(latest!!.secondary, secondIcon)
+            assertIsNotifChip(latest!!.primary, firstIcon, "firstNotif")
+            assertIsNotifChip(latest!!.secondary, secondIcon, "secondNotif")
         }
 
     @Test
@@ -333,9 +345,9 @@
         testScope.runTest {
             val latest by collectLastValue(underTest.chips)
 
-            val firstIcon = mock<StatusBarIconView>()
-            val secondIcon = mock<StatusBarIconView>()
-            val thirdIcon = mock<StatusBarIconView>()
+            val firstIcon = createStatusBarIconViewOrNull()
+            val secondIcon = createStatusBarIconViewOrNull()
+            val thirdIcon = createStatusBarIconViewOrNull()
             setNotifs(
                 listOf(
                     activeNotificationModel(
@@ -359,8 +371,8 @@
                 )
             )
 
-            assertIsNotifChip(latest!!.primary, firstIcon)
-            assertIsNotifChip(latest!!.secondary, secondIcon)
+            assertIsNotifChip(latest!!.primary, firstIcon, "firstNotif")
+            assertIsNotifChip(latest!!.secondary, secondIcon, "secondNotif")
         }
 
     @Test
@@ -368,8 +380,12 @@
         testScope.runTest {
             val latest by collectLastValue(underTest.chips)
 
-            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
-            val firstIcon = mock<StatusBarIconView>()
+            val callNotificationKey = "call"
+            callRepo.setOngoingCallState(
+                inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+            )
+
+            val firstIcon = createStatusBarIconViewOrNull()
             setNotifs(
                 listOf(
                     activeNotificationModel(
@@ -380,43 +396,47 @@
                     ),
                     activeNotificationModel(
                         key = "secondNotif",
-                        statusBarChipIcon = mock<StatusBarIconView>(),
+                        statusBarChipIcon = createStatusBarIconViewOrNull(),
                         promotedContent =
                             PromotedNotificationContentModel.Builder("secondNotif").build(),
                     ),
                 )
             )
 
-            assertIsCallChip(latest!!.primary)
-            assertIsNotifChip(latest!!.secondary, firstIcon)
+            assertIsCallChip(latest!!.primary, callNotificationKey)
+            assertIsNotifChip(latest!!.secondary, firstIcon, "firstNotif")
         }
 
     @Test
     fun chips_screenRecordAndCallAndPromotedNotifs_notifsNotShown() =
         testScope.runTest {
+            val callNotificationKey = "call"
             val latest by collectLastValue(underTest.chips)
 
-            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+            callRepo.setOngoingCallState(
+                inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+            )
             screenRecordState.value = ScreenRecordModel.Recording
             setNotifs(
                 listOf(
                     activeNotificationModel(
                         key = "notif",
-                        statusBarChipIcon = mock<StatusBarIconView>(),
+                        statusBarChipIcon = createStatusBarIconViewOrNull(),
                         promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
                     )
                 )
             )
 
             assertIsScreenRecordChip(latest!!.primary)
-            assertIsCallChip(latest!!.secondary)
+            assertIsCallChip(latest!!.secondary, callNotificationKey)
         }
 
     @Test
     fun primaryChip_higherPriorityChipAdded_lowerPriorityChipReplaced() =
         testScope.runTest {
+            val callNotificationKey = "call"
             // Start with just the lowest priority chip shown
-            val notifIcon = mock<StatusBarIconView>()
+            val notifIcon = createStatusBarIconViewOrNull()
             setNotifs(
                 listOf(
                     activeNotificationModel(
@@ -433,13 +453,15 @@
 
             val latest by collectLastValue(underTest.primaryChip)
 
-            assertIsNotifChip(latest, notifIcon)
+            assertIsNotifChip(latest, notifIcon, "notif")
 
             // WHEN the higher priority call chip is added
-            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+            callRepo.setOngoingCallState(
+                inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+            )
 
             // THEN the higher priority call chip is used
-            assertIsCallChip(latest)
+            assertIsCallChip(latest, callNotificationKey)
 
             // WHEN the higher priority media projection chip is added
             mediaProjectionState.value =
@@ -462,12 +484,15 @@
     @Test
     fun primaryChip_highestPriorityChipRemoved_showsNextPriorityChip() =
         testScope.runTest {
+            val callNotificationKey = "call"
             // WHEN all chips are active
             screenRecordState.value = ScreenRecordModel.Recording
             mediaProjectionState.value =
                 MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
-            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
-            val notifIcon = mock<StatusBarIconView>()
+            callRepo.setOngoingCallState(
+                inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+            )
+            val notifIcon = createStatusBarIconViewOrNull()
             setNotifs(
                 listOf(
                     activeNotificationModel(
@@ -493,20 +518,21 @@
             mediaProjectionState.value = MediaProjectionState.NotProjecting
 
             // THEN the lower priority call is used
-            assertIsCallChip(latest)
+            assertIsCallChip(latest, callNotificationKey)
 
             // WHEN the higher priority call is removed
             callRepo.setOngoingCallState(OngoingCallModel.NoCall)
 
             // THEN the lower priority notif is used
-            assertIsNotifChip(latest, notifIcon)
+            assertIsNotifChip(latest, notifIcon, "notif")
         }
 
     @Test
     fun chips_movesChipsAroundAccordingToPriority() =
         testScope.runTest {
+            val callNotificationKey = "call"
             // Start with just the lowest priority chip shown
-            val notifIcon = mock<StatusBarIconView>()
+            val notifIcon = createStatusBarIconViewOrNull()
             setNotifs(
                 listOf(
                     activeNotificationModel(
@@ -523,16 +549,18 @@
 
             val latest by collectLastValue(underTest.chips)
 
-            assertIsNotifChip(latest!!.primary, notifIcon)
+            assertIsNotifChip(latest!!.primary, notifIcon, "notif")
             assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
 
             // WHEN the higher priority call chip is added
-            callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+            callRepo.setOngoingCallState(
+                inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+            )
 
             // THEN the higher priority call chip is used as primary and notif is demoted to
             // secondary
-            assertIsCallChip(latest!!.primary)
-            assertIsNotifChip(latest!!.secondary, notifIcon)
+            assertIsCallChip(latest!!.primary, callNotificationKey)
+            assertIsNotifChip(latest!!.secondary, notifIcon, "notif")
 
             // WHEN the higher priority media projection chip is added
             mediaProjectionState.value =
@@ -545,7 +573,7 @@
             // THEN the higher priority media projection chip is used as primary and call is demoted
             // to secondary (and notif is dropped altogether)
             assertIsShareToAppChip(latest!!.primary)
-            assertIsCallChip(latest!!.secondary)
+            assertIsCallChip(latest!!.secondary, callNotificationKey)
 
             // WHEN the higher priority screen record chip is added
             screenRecordState.value = ScreenRecordModel.Recording
@@ -559,13 +587,13 @@
 
             // THEN media projection and notif remain
             assertIsShareToAppChip(latest!!.primary)
-            assertIsNotifChip(latest!!.secondary, notifIcon)
+            assertIsNotifChip(latest!!.secondary, notifIcon, "notif")
 
             // WHEN media projection is dropped
             mediaProjectionState.value = MediaProjectionState.NotProjecting
 
             // THEN notif is promoted to primary
-            assertIsNotifChip(latest!!.primary, notifIcon)
+            assertIsNotifChip(latest!!.primary, notifIcon, "notif")
             assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt
index feda0c6..ab475c5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt
@@ -32,10 +32,10 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.data.model.StatusBarMode
-import com.android.systemui.statusbar.phone.BoundsPair
-import com.android.systemui.statusbar.phone.LetterboxAppearance
-import com.android.systemui.statusbar.phone.LetterboxAppearanceCalculator
-import com.android.systemui.statusbar.phone.StatusBarBoundsProvider
+import com.android.systemui.statusbar.layout.BoundsPair
+import com.android.systemui.statusbar.layout.LetterboxAppearance
+import com.android.systemui.statusbar.layout.LetterboxAppearanceCalculator
+import com.android.systemui.statusbar.layout.StatusBarBoundsProvider
 import com.android.systemui.statusbar.phone.fragment.dagger.HomeStatusBarComponent
 import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt
index 4795a12..db51a58 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt
@@ -33,7 +33,7 @@
 import com.android.systemui.statusbar.events.PrivacyDotCorner.BottomRight
 import com.android.systemui.statusbar.events.PrivacyDotCorner.TopLeft
 import com.android.systemui.statusbar.events.PrivacyDotCorner.TopRight
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
 import com.android.systemui.statusbar.policy.FakeConfigurationController
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
index 5f3668a..0a96013 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
@@ -27,8 +27,8 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.AnimatorTestRule
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsChangedListener
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
 import com.android.systemui.statusbar.window.StatusBarWindowController
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt
index dd81b75..1a5f57d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.featurepods.media.domain.interactor
 
+import android.graphics.drawable.Drawable
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -23,12 +24,15 @@
 import com.android.systemui.kosmos.runTest
 import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.media.controls.data.repository.mediaFilterRepository
+import com.android.systemui.media.controls.shared.model.MediaAction
+import com.android.systemui.media.controls.shared.model.MediaButton
 import com.android.systemui.media.controls.shared.model.MediaData
 import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -102,4 +106,70 @@
 
             assertThat(model?.songName).isEqualTo(newSongName)
         }
+
+    @Test
+    fun mediaControlModel_playPauseActionChanges_emitsUpdatedModel() =
+        kosmos.runTest {
+            val model by collectLastValue(underTest.mediaControlModel)
+
+            val mockDrawable = mock<Drawable>()
+
+            val initialAction =
+                MediaAction(
+                    icon = mockDrawable,
+                    action = {},
+                    contentDescription = "Initial Action",
+                    background = mockDrawable,
+                )
+            val mediaButton = MediaButton(playOrPause = initialAction)
+            val userMedia = MediaData(active = true, semanticActions = mediaButton)
+            val instanceId = userMedia.instanceId
+            mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
+            mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId))
+
+            assertThat(model).isNotNull()
+            assertThat(model?.playOrPause).isEqualTo(initialAction)
+
+            val newAction =
+                MediaAction(
+                    icon = mockDrawable,
+                    action = {},
+                    contentDescription = "New Action",
+                    background = mockDrawable,
+                )
+            val updatedMediaButton = MediaButton(playOrPause = newAction)
+            val updatedUserMedia = userMedia.copy(semanticActions = updatedMediaButton)
+            mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia)
+
+            assertThat(model?.playOrPause).isEqualTo(newAction)
+        }
+
+    @Test
+    fun mediaControlModel_playPauseActionRemoved_playPauseNull() =
+        kosmos.runTest {
+            val model by collectLastValue(underTest.mediaControlModel)
+
+            val mockDrawable = mock<Drawable>()
+
+            val initialAction =
+                MediaAction(
+                    icon = mockDrawable,
+                    action = {},
+                    contentDescription = "Initial Action",
+                    background = mockDrawable,
+                )
+            val mediaButton = MediaButton(playOrPause = initialAction)
+            val userMedia = MediaData(active = true, semanticActions = mediaButton)
+            val instanceId = userMedia.instanceId
+            mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
+            mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId))
+
+            assertThat(model).isNotNull()
+            assertThat(model?.playOrPause).isEqualTo(initialAction)
+
+            val updatedUserMedia = userMedia.copy(semanticActions = MediaButton())
+            mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia)
+
+            assertThat(model?.playOrPause).isNull()
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/LetterboxAppearanceCalculatorTest.kt
similarity index 82%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculatorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/LetterboxAppearanceCalculatorTest.kt
index 518b327..f1affbc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/LetterboxAppearanceCalculatorTest.kt
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
 
 import android.graphics.Color
 import android.graphics.Rect
-import android.view.WindowInsetsController
+import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
 import android.view.WindowInsetsController.APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -42,7 +42,7 @@
 
     companion object {
         private const val DEFAULT_APPEARANCE = 0
-        private const val TEST_APPEARANCE = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
+        private const val TEST_APPEARANCE = APPEARANCE_LIGHT_STATUS_BARS
         private val TEST_APPEARANCE_REGION_BOUNDS = Rect(0, 0, 20, 100)
         private val TEST_APPEARANCE_REGION =
             AppearanceRegion(TEST_APPEARANCE, TEST_APPEARANCE_REGION_BOUNDS)
@@ -74,7 +74,11 @@
 
         val letterboxAppearance =
             calculator.getLetterboxAppearance(
-                TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, listOf(letterbox), BoundsPair(start, end))
+                TEST_APPEARANCE,
+                TEST_APPEARANCE_REGIONS,
+                listOf(letterbox),
+                BoundsPair(start, end),
+            )
 
         expect
             .that(letterboxAppearance.appearance)
@@ -90,7 +94,11 @@
 
         val letterboxAppearance =
             calculator.getLetterboxAppearance(
-                TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, listOf(letterbox), BoundsPair(start, end))
+                TEST_APPEARANCE,
+                TEST_APPEARANCE_REGIONS,
+                listOf(letterbox),
+                BoundsPair(start, end),
+            )
 
         expect
             .that(letterboxAppearance.appearance)
@@ -109,10 +117,10 @@
         val letterBoxInnerBoundsCopy = Rect(letterBoxInnerBounds)
 
         calculator.getLetterboxAppearance(
-                TEST_APPEARANCE,
-                TEST_APPEARANCE_REGIONS,
+            TEST_APPEARANCE,
+            TEST_APPEARANCE_REGIONS,
             listOf(letterboxWithInnerBounds(letterBoxInnerBounds)),
-            BoundsPair(statusBarStartSideBounds, statusBarEndSideBounds)
+            BoundsPair(statusBarStartSideBounds, statusBarEndSideBounds),
         )
 
         expect.that(statusBarStartSideBounds).isEqualTo(statusBarStartSideBoundsCopy)
@@ -129,11 +137,15 @@
 
         val letterboxAppearance =
             calculator.getLetterboxAppearance(
-                TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, listOf(letterbox), BoundsPair(start, end))
+                TEST_APPEARANCE,
+                TEST_APPEARANCE_REGIONS,
+                listOf(letterbox),
+                BoundsPair(start, end),
+            )
 
         expect
-                .that(letterboxAppearance.appearance)
-                .isEqualTo(TEST_APPEARANCE or APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS)
+            .that(letterboxAppearance.appearance)
+            .isEqualTo(TEST_APPEARANCE or APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS)
         expect.that(letterboxAppearance.appearanceRegions).isEqualTo(TEST_APPEARANCE_REGIONS)
     }
 
@@ -145,7 +157,11 @@
 
         val letterboxAppearance =
             calculator.getLetterboxAppearance(
-                TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, listOf(letterbox), BoundsPair(start, end))
+                TEST_APPEARANCE,
+                TEST_APPEARANCE_REGIONS,
+                listOf(letterbox),
+                BoundsPair(start, end),
+            )
 
         assertThat(letterboxAppearance.appearance).isEqualTo(TEST_APPEARANCE)
     }
@@ -158,7 +174,11 @@
 
         val letterboxAppearance =
             calculator.getLetterboxAppearance(
-                TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, listOf(letterbox), BoundsPair(start, end))
+                TEST_APPEARANCE,
+                TEST_APPEARANCE_REGIONS,
+                listOf(letterbox),
+                BoundsPair(start, end),
+            )
 
         assertThat(letterboxAppearance.appearance).isEqualTo(TEST_APPEARANCE)
     }
@@ -171,7 +191,11 @@
 
         val letterboxAppearance =
             calculator.getLetterboxAppearance(
-                TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, listOf(letterbox), BoundsPair(start, end))
+                TEST_APPEARANCE,
+                TEST_APPEARANCE_REGIONS,
+                listOf(letterbox),
+                BoundsPair(start, end),
+            )
 
         assertThat(letterboxAppearance.appearance).isEqualTo(TEST_APPEARANCE)
     }
@@ -184,7 +208,11 @@
 
         val letterboxAppearance =
             calculator.getLetterboxAppearance(
-                TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, listOf(letterbox), BoundsPair(start, end))
+                TEST_APPEARANCE,
+                TEST_APPEARANCE_REGIONS,
+                listOf(letterbox),
+                BoundsPair(start, end),
+            )
 
         assertThat(letterboxAppearance.appearance).isEqualTo(TEST_APPEARANCE)
     }
@@ -198,7 +226,11 @@
 
         val letterboxAppearance =
             calculator.getLetterboxAppearance(
-                TEST_APPEARANCE, listOf(letterboxRegion), listOf(letterbox), BoundsPair(start, end))
+                TEST_APPEARANCE,
+                listOf(letterboxRegion),
+                listOf(letterbox),
+                BoundsPair(start, end),
+            )
 
         val letterboxAdaptedRegion = letterboxRegion.copy(bounds = letterbox.letterboxInnerBounds)
         assertThat(letterboxAppearance.appearanceRegions.toList()).contains(letterboxAdaptedRegion)
@@ -212,12 +244,17 @@
         val letterbox =
             letterboxWithBounds(
                 innerBounds = Rect(left = 25, top = 0, right = 75, bottom = 100),
-                fullBounds = Rect(left = 0, top = 0, right = 100, bottom = 100))
+                fullBounds = Rect(left = 0, top = 0, right = 100, bottom = 100),
+            )
         val letterboxRegion = TEST_APPEARANCE_REGION.copy(bounds = letterbox.letterboxFullBounds)
 
         val letterboxAppearance =
             calculator.getLetterboxAppearance(
-                TEST_APPEARANCE, listOf(letterboxRegion), listOf(letterbox), BoundsPair(start, end))
+                TEST_APPEARANCE,
+                listOf(letterboxRegion),
+                listOf(letterbox),
+                BoundsPair(start, end),
+            )
 
         val outerRegions =
             listOf(
@@ -230,8 +267,7 @@
                     Rect(left = 75, top = 0, right = 100, bottom = 100),
                 ),
             )
-        assertThat(letterboxAppearance.appearanceRegions)
-            .containsAtLeastElementsIn(outerRegions)
+        assertThat(letterboxAppearance.appearanceRegions).containsAtLeastElementsIn(outerRegions)
     }
 
     private fun letterboxWithBounds(innerBounds: Rect, fullBounds: Rect) =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarBoundsProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt
similarity index 95%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarBoundsProviderTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt
index b9cfe21..04319f05 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarBoundsProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
 
 import android.graphics.Rect
 import android.testing.TestableLooper.RunWithLooper
@@ -23,7 +23,6 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.phone.StatusBarBoundsProvider.BoundsChangeListener
 import com.android.systemui.util.mockito.any
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -47,7 +46,7 @@
         private val END_SIDE_BOUNDS = Rect(250, 300, 350, 400)
     }
 
-    @Mock private lateinit var boundsChangeListener: BoundsChangeListener
+    @Mock private lateinit var boundsChangeListener: StatusBarBoundsProvider.BoundsChangeListener
 
     private lateinit var boundsProvider: StatusBarBoundsProvider
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt
similarity index 99%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt
index 7a51b2d..49b95d9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
 
 import android.content.Context
 import android.content.res.Configuration
@@ -32,6 +32,7 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.statusbar.commandline.CommandRegistry
 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.android.systemui.statusbar.phone.ConfigurationControllerImpl
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.leak.RotationUtils
 import com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE
@@ -44,7 +45,7 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.any
+import org.mockito.kotlin.any
 import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelTest.kt
new file mode 100644
index 0000000..7200175
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelTest.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2025 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.layout.ui.viewmodel
+
+import android.content.res.Configuration
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectValues
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.android.systemui.statusbar.layout.statusBarContentInsetsProvider
+import com.android.systemui.statusbar.policy.configurationController
+import com.android.systemui.statusbar.policy.fake
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+class StatusBarContentInsetsViewModelTest : SysuiTestCase() {
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+    private val configuration = Configuration()
+
+    private val Kosmos.underTest by Kosmos.Fixture { statusBarContentInsetsViewModel }
+
+    @Test
+    fun contentArea_onMaxBoundsChanged_emitsNewValue() =
+        kosmos.runTest {
+            statusBarContentInsetsProvider.start()
+
+            val values by collectValues(underTest.contentArea)
+
+            // WHEN the content area changes
+            configurationController.fake.notifyLayoutDirectionChanged(isRtl = true)
+            configurationController.fake.notifyDensityOrFontScaleChanged()
+
+            // THEN the flow emits the new bounds
+            assertThat(values[0]).isNotEqualTo(values[1])
+        }
+
+    @Test
+    fun contentArea_onDensityOrFontScaleChanged_emitsLastBounds() =
+        kosmos.runTest {
+            configuration.densityDpi = 12
+            statusBarContentInsetsProvider.start()
+
+            val values by collectValues(underTest.contentArea)
+
+            // WHEN a change happens but it doesn't affect content area
+            configuration.densityDpi = 20
+            configurationController.onConfigurationChanged(configuration)
+            configurationController.fake.notifyDensityOrFontScaleChanged()
+
+            // THEN it still has the last bounds
+            assertThat(values).hasSize(1)
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
index a3ffd91..609885d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
@@ -458,7 +458,7 @@
 
     @Test
     @EnableFlags(StatusBarNotifChips.FLAG_NAME)
-    fun showPromotedNotification_hasNotifEntry_shownAsHUN() =
+    fun onPromotedNotificationChipTapped_hasNotifEntry_shownAsHUN() =
         testScope.runTest {
             whenever(notifCollection.getEntry(entry.key)).thenReturn(entry)
 
@@ -473,7 +473,7 @@
 
     @Test
     @EnableFlags(StatusBarNotifChips.FLAG_NAME)
-    fun showPromotedNotification_noNotifEntry_noHUN() =
+    fun onPromotedNotificationChipTapped_noNotifEntry_noHUN() =
         testScope.runTest {
             whenever(notifCollection.getEntry(entry.key)).thenReturn(null)
 
@@ -488,7 +488,7 @@
 
     @Test
     @EnableFlags(StatusBarNotifChips.FLAG_NAME)
-    fun showPromotedNotification_shownAsHUNEvenIfEntryShouldNot() =
+    fun onPromotedNotificationChipTapped_shownAsHUNEvenIfEntryShouldNot() =
         testScope.runTest {
             whenever(notifCollection.getEntry(entry.key)).thenReturn(entry)
 
@@ -511,7 +511,7 @@
 
     @Test
     @EnableFlags(StatusBarNotifChips.FLAG_NAME)
-    fun showPromotedNotification_atSameTimeAsOnAdded_promotedShownAsHUN() =
+    fun onPromotedNotificationChipTapped_atSameTimeAsOnAdded_promotedShownAsHUN() =
         testScope.runTest {
             // First, the promoted notification appears as not heads up
             val promotedEntry = NotificationEntryBuilder().setPkg("promotedPackage").build()
@@ -548,6 +548,33 @@
         }
 
     @Test
+    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
+    fun onPromotedNotificationChipTapped_chipTappedTwice_hunHiddenOnSecondTap() =
+        testScope.runTest {
+            whenever(notifCollection.getEntry(entry.key)).thenReturn(entry)
+
+            // WHEN chip tapped first
+            statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(entry.key)
+            executor.advanceClockToLast()
+            executor.runAllReady()
+            beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry))
+
+            // THEN HUN is shown
+            finishBind(entry)
+            verify(headsUpManager).showNotification(entry, isPinnedByUser = true)
+            addHUN(entry)
+
+            // WHEN chip is tapped again
+            statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(entry.key)
+            executor.advanceClockToLast()
+            executor.runAllReady()
+            beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry))
+
+            // THEN HUN is hidden
+            verify(headsUpManager).removeNotification(eq(entry.key), eq(false), any())
+        }
+
+    @Test
     fun testTransferIsolatedChildAlert_withGroupAlertSummary() {
         setShouldHeadsUp(groupSummary)
         whenever(notifPipeline.allNotifs).thenReturn(listOf(groupSummary, groupSibling1))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt
index a70d24e..e6fbc72 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt
@@ -28,11 +28,11 @@
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderEntryListener
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderGroupListener
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderListListener
+import com.android.systemui.util.mockito.withArgCaptor
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.any
-import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.inOrder
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.never
@@ -59,10 +59,9 @@
     fun setUp() {
         renderStageManager = RenderStageManager()
         renderStageManager.attach(shadeListBuilder)
-
-        val captor = argumentCaptor<ShadeListBuilder.OnRenderListListener>()
-        verify(shadeListBuilder).setOnRenderListListener(captor.capture())
-        onRenderListListener = captor.lastValue
+        onRenderListListener = withArgCaptor {
+            verify(shadeListBuilder).setOnRenderListListener(capture())
+        }
     }
 
     private fun setUpRenderer() {
@@ -102,7 +101,6 @@
         // VERIFY that the renderer is not queried for group or row controllers
         inOrder(spyViewRenderer).apply {
             verify(spyViewRenderer, times(1)).onRenderList(any())
-            verify(spyViewRenderer, times(1)).getStackController()
             verify(spyViewRenderer, never()).getGroupController(any())
             verify(spyViewRenderer, never()).getRowController(any())
             verify(spyViewRenderer, times(1)).onDispatchComplete()
@@ -122,7 +120,6 @@
         // VERIFY that the renderer is queried once per group/entry
         inOrder(spyViewRenderer).apply {
             verify(spyViewRenderer, times(1)).onRenderList(any())
-            verify(spyViewRenderer, times(1)).getStackController()
             verify(spyViewRenderer, times(2)).getGroupController(any())
             verify(spyViewRenderer, times(8)).getRowController(any())
             verify(spyViewRenderer, times(1)).onDispatchComplete()
@@ -145,7 +142,6 @@
         // VERIFY that the renderer is queried once per group/entry
         inOrder(spyViewRenderer).apply {
             verify(spyViewRenderer, times(1)).onRenderList(any())
-            verify(spyViewRenderer, times(1)).getStackController()
             verify(spyViewRenderer, times(2)).getGroupController(any())
             verify(spyViewRenderer, times(8)).getRowController(any())
             verify(spyViewRenderer, times(1)).onDispatchComplete()
@@ -163,7 +159,7 @@
         onRenderListListener.onRenderList(listWith2Groups8Entries())
 
         // VERIFY that the listeners are invoked once per group and once per entry
-        verify(onAfterRenderListListener, times(1)).onAfterRenderList(any(), any())
+        verify(onAfterRenderListListener, times(1)).onAfterRenderList(any())
         verify(onAfterRenderGroupListener, times(2)).onAfterRenderGroup(any(), any())
         verify(onAfterRenderEntryListener, times(8)).onAfterRenderEntry(any(), any())
         verifyNoMoreInteractions(
@@ -183,7 +179,7 @@
         onRenderListListener.onRenderList(listOf())
 
         // VERIFY that the stack listener is invoked once but other listeners are not
-        verify(onAfterRenderListListener, times(1)).onAfterRenderList(any(), any())
+        verify(onAfterRenderListListener, times(1)).onAfterRenderList(any())
         verify(onAfterRenderGroupListener, never()).onAfterRenderGroup(any(), any())
         verify(onAfterRenderEntryListener, never()).onAfterRenderEntry(any(), any())
         verifyNoMoreInteractions(
@@ -204,8 +200,6 @@
     private class FakeNotifViewRenderer : NotifViewRenderer {
         override fun onRenderList(notifList: List<ListEntry>) {}
 
-        override fun getStackController(): NotifStackController = mock()
-
         override fun getGroupController(group: GroupEntry): NotifGroupController = mock()
 
         override fun getRowController(entry: NotificationEntry): NotifRowController = mock()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
index 54ce88b..83c6150 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
@@ -26,7 +26,7 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
-import com.android.systemui.statusbar.notification.collection.render.NotifStats
+import com.android.systemui.statusbar.notification.data.model.NotifStats
 import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
@@ -275,7 +275,6 @@
 
             activeNotificationListRepository.notifStats.value =
                 NotifStats(
-                    numActiveNotifs = 2,
                     hasNonClearableAlertingNotifs = false,
                     hasClearableAlertingNotifs = true,
                     hasNonClearableSilentNotifs = false,
@@ -293,7 +292,6 @@
 
             activeNotificationListRepository.notifStats.value =
                 NotifStats(
-                    numActiveNotifs = 2,
                     hasNonClearableAlertingNotifs = false,
                     hasClearableAlertingNotifs = false,
                     hasNonClearableSilentNotifs = false,
@@ -311,7 +309,6 @@
 
             activeNotificationListRepository.notifStats.value =
                 NotifStats(
-                    numActiveNotifs = 0,
                     hasNonClearableAlertingNotifs = false,
                     hasClearableAlertingNotifs = false,
                     hasNonClearableSilentNotifs = false,
@@ -329,7 +326,6 @@
 
             activeNotificationListRepository.notifStats.value =
                 NotifStats(
-                    numActiveNotifs = 2,
                     hasNonClearableAlertingNotifs = false,
                     hasClearableAlertingNotifs = false,
                     hasNonClearableSilentNotifs = false,
@@ -347,7 +343,6 @@
 
             activeNotificationListRepository.notifStats.value =
                 NotifStats(
-                    numActiveNotifs = 2,
                     hasNonClearableAlertingNotifs = true,
                     hasClearableAlertingNotifs = false,
                     hasNonClearableSilentNotifs = true,
@@ -365,7 +360,6 @@
 
             activeNotificationListRepository.notifStats.value =
                 NotifStats(
-                    numActiveNotifs = 2,
                     hasNonClearableAlertingNotifs = false,
                     hasClearableAlertingNotifs = true,
                     hasNonClearableSilentNotifs = false,
@@ -383,7 +377,6 @@
 
             activeNotificationListRepository.notifStats.value =
                 NotifStats(
-                    numActiveNotifs = 2,
                     hasNonClearableAlertingNotifs = false,
                     hasClearableAlertingNotifs = false,
                     hasNonClearableSilentNotifs = true,
@@ -401,7 +394,6 @@
 
             activeNotificationListRepository.notifStats.value =
                 NotifStats(
-                    numActiveNotifs = 2,
                     hasNonClearableAlertingNotifs = false,
                     hasClearableAlertingNotifs = false,
                     hasNonClearableSilentNotifs = false,
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 22ef408..fae7d51 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
@@ -32,6 +32,7 @@
 import com.android.systemui.shade.shadeTestUtil
 import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
 import com.android.systemui.statusbar.notification.data.repository.notificationsKeyguardViewStateRepository
+import com.android.systemui.statusbar.notification.domain.model.TopPinnedState
 import com.android.systemui.statusbar.notification.headsup.PinnedStatus
 import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
 import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
@@ -412,46 +413,53 @@
     @Test
     fun statusBarHeadsUpState_pinnedBySystem() =
         testScope.runTest {
-            val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState)
+            val state by collectLastValue(underTest.statusBarHeadsUpState)
+            val status by collectLastValue(underTest.statusBarHeadsUpStatus)
 
             headsUpRepository.setNotifications(
                 FakeHeadsUpRowRepository(key = "key 0", pinnedStatus = PinnedStatus.PinnedBySystem)
             )
             runCurrent()
 
-            assertThat(statusBarHeadsUpState).isEqualTo(PinnedStatus.PinnedBySystem)
+            assertThat(state).isEqualTo(TopPinnedState.Pinned("key 0", PinnedStatus.PinnedBySystem))
+            assertThat(status).isEqualTo(PinnedStatus.PinnedBySystem)
         }
 
     @Test
     fun statusBarHeadsUpState_pinnedByUser() =
         testScope.runTest {
-            val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState)
+            val state by collectLastValue(underTest.statusBarHeadsUpState)
+            val status by collectLastValue(underTest.statusBarHeadsUpStatus)
 
             headsUpRepository.setNotifications(
                 FakeHeadsUpRowRepository(key = "key 0", pinnedStatus = PinnedStatus.PinnedByUser)
             )
             runCurrent()
 
-            assertThat(statusBarHeadsUpState).isEqualTo(PinnedStatus.PinnedByUser)
+            assertThat(state).isEqualTo(TopPinnedState.Pinned("key 0", PinnedStatus.PinnedByUser))
+            assertThat(status).isEqualTo(PinnedStatus.PinnedByUser)
         }
 
     @Test
     fun statusBarHeadsUpState_withoutPinnedNotifications_notPinned() =
         testScope.runTest {
-            val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState)
+            val state by collectLastValue(underTest.statusBarHeadsUpState)
+            val status by collectLastValue(underTest.statusBarHeadsUpStatus)
 
             headsUpRepository.setNotifications(
                 FakeHeadsUpRowRepository(key = "key 0", PinnedStatus.NotPinned)
             )
             runCurrent()
 
-            assertThat(statusBarHeadsUpState).isEqualTo(PinnedStatus.NotPinned)
+            assertThat(state).isEqualTo(TopPinnedState.NothingPinned)
+            assertThat(status).isEqualTo(PinnedStatus.NotPinned)
         }
 
     @Test
     fun statusBarHeadsUpState_whenShadeExpanded_false() =
         testScope.runTest {
-            val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState)
+            val state by collectLastValue(underTest.statusBarHeadsUpState)
+            val status by collectLastValue(underTest.statusBarHeadsUpStatus)
 
             // WHEN a row is pinned
             headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true))
@@ -463,13 +471,15 @@
             // should emit `false`.
             kosmos.fakeShadeRepository.setLegacyShadeExpansion(1.0f)
 
-            assertThat(statusBarHeadsUpState!!.isPinned).isFalse()
+            assertThat(state).isEqualTo(TopPinnedState.NothingPinned)
+            assertThat(status!!.isPinned).isFalse()
         }
 
     @Test
     fun statusBarHeadsUpState_notificationsAreHidden_false() =
         testScope.runTest {
-            val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState)
+            val state by collectLastValue(underTest.statusBarHeadsUpState)
+            val status by collectLastValue(underTest.statusBarHeadsUpStatus)
 
             // WHEN a row is pinned
             headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true))
@@ -477,13 +487,15 @@
             // AND the notifications are hidden
             keyguardViewStateRepository.areNotificationsFullyHidden.value = true
 
-            assertThat(statusBarHeadsUpState!!.isPinned).isFalse()
+            assertThat(state).isEqualTo(TopPinnedState.NothingPinned)
+            assertThat(status!!.isPinned).isFalse()
         }
 
     @Test
     fun statusBarHeadsUpState_onLockScreen_false() =
         testScope.runTest {
-            val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState)
+            val state by collectLastValue(underTest.statusBarHeadsUpState)
+            val status by collectLastValue(underTest.statusBarHeadsUpStatus)
 
             // WHEN a row is pinned
             headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true))
@@ -494,13 +506,15 @@
                 testSetup = true,
             )
 
-            assertThat(statusBarHeadsUpState!!.isPinned).isFalse()
+            assertThat(state).isEqualTo(TopPinnedState.NothingPinned)
+            assertThat(status!!.isPinned).isFalse()
         }
 
     @Test
     fun statusBarHeadsUpState_onByPassLockScreen_true() =
         testScope.runTest {
-            val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState)
+            val state by collectLastValue(underTest.statusBarHeadsUpState)
+            val status by collectLastValue(underTest.statusBarHeadsUpStatus)
 
             // WHEN a row is pinned
             headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true))
@@ -513,13 +527,15 @@
             // AND bypass is enabled
             faceAuthRepository.isBypassEnabled.value = true
 
-            assertThat(statusBarHeadsUpState!!.isPinned).isTrue()
+            assertThat(state).isInstanceOf(TopPinnedState.Pinned::class.java)
+            assertThat(status!!.isPinned).isTrue()
         }
 
     @Test
     fun statusBarHeadsUpState_onByPassLockScreen_withoutNotifications_false() =
         testScope.runTest {
-            val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState)
+            val state by collectLastValue(underTest.statusBarHeadsUpState)
+            val status by collectLastValue(underTest.statusBarHeadsUpStatus)
 
             // WHEN no pinned rows
             // AND the lock screen is shown
@@ -530,7 +546,8 @@
             // AND bypass is enabled
             faceAuthRepository.isBypassEnabled.value = true
 
-            assertThat(statusBarHeadsUpState!!.isPinned).isFalse()
+            assertThat(state).isEqualTo(TopPinnedState.NothingPinned)
+            assertThat(status!!.isPinned).isFalse()
         }
 
     private fun fakeHeadsUpRowRepository(key: String, isPinned: Boolean = false) =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
index 34f4608..3d5d1ed 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
@@ -33,7 +33,6 @@
 import com.android.systemui.shared.settings.data.repository.fakeSecureSettingsRepository
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
 import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
-import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
@@ -48,7 +47,6 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(ParameterizedAndroidJunit4::class)
 @SmallTest
-@EnableFlags(FooterViewRefactor.FLAG_NAME)
 class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java
index 615f4b01..daa1db2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java
@@ -16,8 +16,6 @@
 
 package com.android.systemui.statusbar.notification.footer.ui.view;
 
-import static com.android.systemui.log.LogAssertKt.assertLogsWtf;
-
 import static com.google.common.truth.Truth.assertThat;
 
 import static junit.framework.Assert.assertFalse;
@@ -34,7 +32,6 @@
 
 import android.content.Context;
 import android.platform.test.annotations.DisableFlags;
-import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.FlagsParameterization;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -44,7 +41,6 @@
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.res.R;
-import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
 import com.android.systemui.statusbar.notification.footer.shared.NotifRedesignFooter;
 
 import org.junit.Before;
@@ -62,8 +58,7 @@
 
     @Parameters(name = "{0}")
     public static List<FlagsParameterization> getFlags() {
-        return FlagsParameterization.progressionOf(FooterViewRefactor.FLAG_NAME,
-                NotifRedesignFooter.FLAG_NAME);
+        return FlagsParameterization.allCombinationsOf(NotifRedesignFooter.FLAG_NAME);
     }
 
     public FooterViewTest(FlagsParameterization flags) {
@@ -106,24 +101,6 @@
     }
 
     @Test
-    @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME})
-    public void setHistoryShown() {
-        mView.showHistory(true);
-        assertTrue(mView.isHistoryShown());
-        assertTrue(((TextView) mView.findViewById(R.id.manage_text))
-                .getText().toString().contains("History"));
-    }
-
-    @Test
-    @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME})
-    public void setHistoryNotShown() {
-        mView.showHistory(false);
-        assertFalse(mView.isHistoryShown());
-        assertTrue(((TextView) mView.findViewById(R.id.manage_text))
-                .getText().toString().contains("Manage"));
-    }
-
-    @Test
     public void testPerformVisibilityAnimation() {
         mView.setVisible(false /* visible */, false /* animate */);
         assertFalse(mView.isVisible());
@@ -140,7 +117,6 @@
     }
 
     @Test
-    @EnableFlags(FooterViewRefactor.FLAG_NAME)
     @DisableFlags(NotifRedesignFooter.FLAG_NAME)
     public void testSetManageOrHistoryButtonText_resourceOnlyFetchedOnce() {
         int resId = R.string.manage_notifications_history_text;
@@ -160,16 +136,6 @@
     }
 
     @Test
-    @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME})
-    public void testSetManageOrHistoryButtonText_expectsFlagEnabled() {
-        clearInvocations(mSpyContext);
-        int resId = R.string.manage_notifications_history_text;
-        assertLogsWtf(() -> mView.setManageOrHistoryButtonText(resId));
-        verify(mSpyContext, never()).getString(anyInt());
-    }
-
-    @Test
-    @EnableFlags(FooterViewRefactor.FLAG_NAME)
     @DisableFlags(NotifRedesignFooter.FLAG_NAME)
     public void testSetManageOrHistoryButtonDescription_resourceOnlyFetchedOnce() {
         int resId = R.string.manage_notifications_history_text;
@@ -189,16 +155,6 @@
     }
 
     @Test
-    @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME})
-    public void testSetManageOrHistoryButtonDescription_expectsFlagEnabled() {
-        clearInvocations(mSpyContext);
-        int resId = R.string.accessibility_clear_all;
-        assertLogsWtf(() -> mView.setManageOrHistoryButtonDescription(resId));
-        verify(mSpyContext, never()).getString(anyInt());
-    }
-
-    @Test
-    @EnableFlags(FooterViewRefactor.FLAG_NAME)
     public void testSetClearAllButtonText_resourceOnlyFetchedOnce() {
         int resId = R.string.clear_all_notifications_text;
         mView.setClearAllButtonText(resId);
@@ -217,16 +173,6 @@
     }
 
     @Test
-    @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME})
-    public void testSetClearAllButtonText_expectsFlagEnabled() {
-        clearInvocations(mSpyContext);
-        int resId = R.string.clear_all_notifications_text;
-        assertLogsWtf(() -> mView.setClearAllButtonText(resId));
-        verify(mSpyContext, never()).getString(anyInt());
-    }
-
-    @Test
-    @EnableFlags(FooterViewRefactor.FLAG_NAME)
     public void testSetClearAllButtonDescription_resourceOnlyFetchedOnce() {
         int resId = R.string.accessibility_clear_all;
         mView.setClearAllButtonDescription(resId);
@@ -245,16 +191,6 @@
     }
 
     @Test
-    @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME})
-    public void testSetClearAllButtonDescription_expectsFlagEnabled() {
-        clearInvocations(mSpyContext);
-        int resId = R.string.accessibility_clear_all;
-        assertLogsWtf(() -> mView.setClearAllButtonDescription(resId));
-        verify(mSpyContext, never()).getString(anyInt());
-    }
-
-    @Test
-    @EnableFlags(FooterViewRefactor.FLAG_NAME)
     public void testSetMessageString_resourceOnlyFetchedOnce() {
         int resId = R.string.unlock_to_see_notif_text;
         mView.setMessageString(resId);
@@ -273,16 +209,6 @@
     }
 
     @Test
-    @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME})
-    public void testSetMessageString_expectsFlagEnabled() {
-        clearInvocations(mSpyContext);
-        int resId = R.string.unlock_to_see_notif_text;
-        assertLogsWtf(() -> mView.setMessageString(resId));
-        verify(mSpyContext, never()).getString(anyInt());
-    }
-
-    @Test
-    @EnableFlags(FooterViewRefactor.FLAG_NAME)
     public void testSetMessageIcon_resourceOnlyFetchedOnce() {
         int resId = R.drawable.ic_friction_lock_closed;
         mView.setMessageIcon(resId);
@@ -298,15 +224,6 @@
     }
 
     @Test
-    @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME})
-    public void testSetMessageIcon_expectsFlagEnabled() {
-        clearInvocations(mSpyContext);
-        int resId = R.drawable.ic_friction_lock_closed;
-        assertLogsWtf(() -> mView.setMessageIcon(resId));
-        verify(mSpyContext, never()).getDrawable(anyInt());
-    }
-
-    @Test
     public void testSetFooterLabelVisible() {
         mView.setFooterLabelVisible(true);
         assertThat(mView.findViewById(R.id.unlock_prompt_footer).getVisibility())
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
index 1adfc2b..b3a60b0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
@@ -37,10 +37,9 @@
 import com.android.systemui.res.R
 import com.android.systemui.shade.shadeTestUtil
 import com.android.systemui.shared.settings.data.repository.fakeSecureSettingsRepository
-import com.android.systemui.statusbar.notification.collection.render.NotifStats
+import com.android.systemui.statusbar.notification.data.model.NotifStats
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
 import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
-import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.footer.shared.NotifRedesignFooter
 import com.android.systemui.testKosmos
 import com.android.systemui.util.ui.isAnimating
@@ -57,7 +56,6 @@
 
 @RunWith(ParameterizedAndroidJunit4::class)
 @SmallTest
-@EnableFlags(FooterViewRefactor.FLAG_NAME)
 class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
     private val kosmos =
         testKosmos().apply {
@@ -117,7 +115,6 @@
 
             activeNotificationListRepository.notifStats.value =
                 NotifStats(
-                    numActiveNotifs = 2,
                     hasNonClearableAlertingNotifs = false,
                     hasClearableAlertingNotifs = true,
                     hasNonClearableSilentNotifs = false,
@@ -135,7 +132,6 @@
 
             activeNotificationListRepository.notifStats.value =
                 NotifStats(
-                    numActiveNotifs = 2,
                     hasNonClearableAlertingNotifs = false,
                     hasClearableAlertingNotifs = false,
                     hasNonClearableSilentNotifs = false,
@@ -153,7 +149,6 @@
 
             activeNotificationListRepository.notifStats.value =
                 NotifStats(
-                    numActiveNotifs = 2,
                     hasNonClearableAlertingNotifs = false,
                     hasClearableAlertingNotifs = true,
                     hasNonClearableSilentNotifs = false,
@@ -185,7 +180,6 @@
             // AND there are clearable notifications
             activeNotificationListRepository.notifStats.value =
                 NotifStats(
-                    numActiveNotifs = 2,
                     hasNonClearableAlertingNotifs = false,
                     hasClearableAlertingNotifs = true,
                     hasNonClearableSilentNotifs = false,
@@ -219,7 +213,6 @@
             // AND there are clearable notifications
             activeNotificationListRepository.notifStats.value =
                 NotifStats(
-                    numActiveNotifs = 2,
                     hasNonClearableAlertingNotifs = false,
                     hasClearableAlertingNotifs = true,
                     hasNonClearableSilentNotifs = false,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
index 72a91bc..14bbd38 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
@@ -279,7 +279,7 @@
                 notification);
         RemoteViews headerRemoteViews;
         if (lowPriority) {
-            headerRemoteViews = builder.makeLowPriorityContentView(true, false);
+            headerRemoteViews = builder.makeLowPriorityContentView(true);
         } else {
             headerRemoteViews = builder.makeNotificationGroupHeader();
         }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index c6cffa9..20cd6c7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -25,14 +25,10 @@
 
 import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
 
-import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.argThat;
-import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
-import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -45,7 +41,6 @@
 import android.platform.test.annotations.EnableFlags;
 import android.testing.TestableLooper;
 import android.view.MotionEvent;
-import android.view.View;
 import android.view.ViewTreeObserver;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -57,15 +52,12 @@
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.ExpandHelper;
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.DisableSceneContainer;
 import com.android.systemui.flags.EnableSceneContainer;
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository;
-import com.android.systemui.keyguard.shared.model.KeyguardState;
-import com.android.systemui.keyguard.shared.model.TransitionStep;
 import com.android.systemui.kosmos.KosmosJavaAdapter;
 import com.android.systemui.media.controls.ui.controller.KeyguardMediaController;
 import com.android.systemui.plugins.ActivityStarter;
@@ -78,23 +70,18 @@
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager.UserChangedListener;
-import com.android.systemui.statusbar.NotificationRemoteInputManager;
-import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.ColorUpdateLogger;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
-import com.android.systemui.statusbar.notification.headsup.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
 import com.android.systemui.statusbar.notification.collection.NotifCollection;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider;
 import com.android.systemui.statusbar.notification.collection.provider.VisibilityLocationProviderDelegator;
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
-import com.android.systemui.statusbar.notification.collection.render.NotifStats;
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
-import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
-import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor;
-import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
+import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
+import com.android.systemui.statusbar.notification.headsup.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.notification.init.NotificationsController;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
@@ -106,11 +93,8 @@
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.DeviceProvisionedController;
-import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController;
 import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController;
-import com.android.systemui.statusbar.policy.ZenModeController;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.util.settings.SecureSettings;
 import com.android.systemui.wallpapers.domain.interactor.WallpaperInteractor;
@@ -145,16 +129,13 @@
     @Mock private Provider<IStatusBarService> mStatusBarService;
     @Mock private NotificationRoundnessManager mNotificationRoundnessManager;
     @Mock private TunerService mTunerService;
-    @Mock private DeviceProvisionedController mDeviceProvisionedController;
     @Mock private DynamicPrivacyController mDynamicPrivacyController;
     @Mock private ConfigurationController mConfigurationController;
     @Mock private NotificationStackScrollLayout mNotificationStackScrollLayout;
-    @Mock private ZenModeController mZenModeController;
     @Mock private KeyguardMediaController mKeyguardMediaController;
     @Mock private SysuiStatusBarStateController mSysuiStatusBarStateController;
     @Mock private KeyguardBypassController mKeyguardBypassController;
     @Mock private PowerInteractor mPowerInteractor;
-    @Mock private PrimaryBouncerInteractor mPrimaryBouncerInteractor;
     @Mock private WallpaperInteractor mWallpaperInteractor;
     @Mock private NotificationLockscreenUserManager mNotificationLockscreenUserManager;
     @Mock private MetricsLogger mMetricsLogger;
@@ -164,12 +145,10 @@
     private NotificationSwipeHelper.Builder mNotificationSwipeHelperBuilder;
     @Mock private NotificationSwipeHelper mNotificationSwipeHelper;
     @Mock private GroupExpansionManager mGroupExpansionManager;
-    @Mock private SectionHeaderController mSilentHeaderController;
     @Mock private NotifPipeline mNotifPipeline;
     @Mock private NotifCollection mNotifCollection;
     @Mock private UiEventLogger mUiEventLogger;
     @Mock private LockscreenShadeTransitionController mLockscreenShadeTransitionController;
-    @Mock private NotificationRemoteInputManager mRemoteInputManager;
     @Mock private VisibilityLocationProviderDelegator mVisibilityLocationProviderDelegator;
     @Mock private ShadeController mShadeController;
     @Mock private Provider<WindowRootView> mWindowRootView;
@@ -193,9 +172,6 @@
     @Captor
     private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerArgumentCaptor;
 
-    private final SeenNotificationsInteractor mSeenNotificationsInteractor =
-            mKosmos.getSeenNotificationsInteractor();
-
     private NotificationStackScrollLayoutController mController;
 
     private NotificationTestHelper mNotificationTestHelper;
@@ -279,114 +255,6 @@
     }
 
     @Test
-    @DisableFlags(FooterViewRefactor.FLAG_NAME)
-    public void testUpdateEmptyShadeView_notificationsVisible_zenHiding() {
-        when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(true);
-        initController(/* viewIsAttached= */ true);
-
-        setupShowEmptyShadeViewState(true);
-        reset(mNotificationStackScrollLayout);
-        mController.updateShowEmptyShadeView();
-        verify(mNotificationStackScrollLayout).updateEmptyShadeView(
-                /* visible= */ true,
-                /* notifVisibleInShade= */ true);
-
-        setupShowEmptyShadeViewState(false);
-        reset(mNotificationStackScrollLayout);
-        mController.updateShowEmptyShadeView();
-        verify(mNotificationStackScrollLayout).updateEmptyShadeView(
-                /* visible= */ false,
-                /* notifVisibleInShade= */ true);
-    }
-
-    @Test
-    @DisableFlags(FooterViewRefactor.FLAG_NAME)
-    public void testUpdateEmptyShadeView_notificationsHidden_zenNotHiding() {
-        when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false);
-        initController(/* viewIsAttached= */ true);
-
-        setupShowEmptyShadeViewState(true);
-        reset(mNotificationStackScrollLayout);
-        mController.updateShowEmptyShadeView();
-        verify(mNotificationStackScrollLayout).updateEmptyShadeView(
-                /* visible= */ true,
-                /* notifVisibleInShade= */ false);
-
-        setupShowEmptyShadeViewState(false);
-        reset(mNotificationStackScrollLayout);
-        mController.updateShowEmptyShadeView();
-        verify(mNotificationStackScrollLayout).updateEmptyShadeView(
-                /* visible= */ false,
-                /* notifVisibleInShade= */ false);
-    }
-
-    @Test
-    @DisableFlags(FooterViewRefactor.FLAG_NAME)
-    public void testUpdateEmptyShadeView_splitShadeMode_alwaysShowEmptyView() {
-        when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false);
-        initController(/* viewIsAttached= */ true);
-
-        verify(mSysuiStatusBarStateController).addCallback(
-                mStateListenerArgumentCaptor.capture(), anyInt());
-        StatusBarStateController.StateListener stateListener =
-                mStateListenerArgumentCaptor.getValue();
-        stateListener.onStateChanged(SHADE);
-        mController.getView().removeAllViews();
-
-        mController.setQsFullScreen(false);
-        reset(mNotificationStackScrollLayout);
-        mController.updateShowEmptyShadeView();
-        verify(mNotificationStackScrollLayout).updateEmptyShadeView(
-                /* visible= */ true,
-                /* notifVisibleInShade= */ false);
-
-        mController.setQsFullScreen(true);
-        reset(mNotificationStackScrollLayout);
-        mController.updateShowEmptyShadeView();
-        verify(mNotificationStackScrollLayout).updateEmptyShadeView(
-                /* visible= */ true,
-                /* notifVisibleInShade= */ false);
-    }
-
-    @Test
-    @DisableFlags(FooterViewRefactor.FLAG_NAME)
-    public void testUpdateEmptyShadeView_bouncerShowing_hideEmptyView() {
-        when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false);
-        initController(/* viewIsAttached= */ true);
-
-        when(mPrimaryBouncerInteractor.isBouncerShowing()).thenReturn(true);
-
-        setupShowEmptyShadeViewState(true);
-        reset(mNotificationStackScrollLayout);
-        mController.updateShowEmptyShadeView();
-
-        // THEN the PrimaryBouncerInteractor value is used. Since the bouncer is showing, we
-        // hide the empty view.
-        verify(mNotificationStackScrollLayout).updateEmptyShadeView(
-                /* visible= */ false,
-                /* areNotificationsHiddenInShade= */ false);
-    }
-
-    @Test
-    @DisableFlags(FooterViewRefactor.FLAG_NAME)
-    public void testUpdateEmptyShadeView_bouncerNotShowing_showEmptyView() {
-        when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false);
-        initController(/* viewIsAttached= */ true);
-
-        when(mPrimaryBouncerInteractor.isBouncerShowing()).thenReturn(false);
-
-        setupShowEmptyShadeViewState(true);
-        reset(mNotificationStackScrollLayout);
-        mController.updateShowEmptyShadeView();
-
-        // THEN the PrimaryBouncerInteractor value is used. Since the bouncer isn't showing, we
-        // can show the empty view.
-        verify(mNotificationStackScrollLayout).updateEmptyShadeView(
-                /* visible= */ true,
-                /* areNotificationsHiddenInShade= */ false);
-    }
-
-    @Test
     public void testOnUserChange_verifyNotSensitive() {
         when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false);
         initController(/* viewIsAttached= */ true);
@@ -788,31 +656,6 @@
     }
 
     @Test
-    @DisableFlags(FooterViewRefactor.FLAG_NAME)
-    public void testUpdateFooter_remoteInput() {
-        ArgumentCaptor<RemoteInputController.Callback> callbackCaptor =
-                ArgumentCaptor.forClass(RemoteInputController.Callback.class);
-        doNothing().when(mRemoteInputManager).addControllerCallback(callbackCaptor.capture());
-        when(mRemoteInputManager.isRemoteInputActive()).thenReturn(false);
-        initController(/* viewIsAttached= */ true);
-        verify(mNotificationStackScrollLayout).setIsRemoteInputActive(false);
-        RemoteInputController.Callback callback = callbackCaptor.getValue();
-        callback.onRemoteInputActive(true);
-        verify(mNotificationStackScrollLayout).setIsRemoteInputActive(true);
-    }
-
-    @Test
-    @DisableFlags(FooterViewRefactor.FLAG_NAME)
-    public void testSetNotifStats_updatesHasFilteredOutSeenNotifications() {
-        initController(/* viewIsAttached= */ true);
-        mSeenNotificationsInteractor.setHasFilteredOutSeenNotifications(true);
-        mController.getNotifStackController().setNotifStats(NotifStats.getEmpty());
-        verify(mNotificationStackScrollLayout).setHasFilteredOutSeenNotifications(true);
-        verify(mNotificationStackScrollLayout).updateFooter();
-        verify(mNotificationStackScrollLayout).updateEmptyShadeView(anyBoolean(), anyBoolean());
-    }
-
-    @Test
     public void testAttach_updatesViewStatusBarState() {
         // GIVEN: Controller is attached
         initController(/* viewIsAttached= */ true);
@@ -844,98 +687,6 @@
     }
 
     @Test
-    @DisableFlags(FooterViewRefactor.FLAG_NAME)
-    public void updateImportantForAccessibility_noChild_onKeyGuard_notImportantForA11y() {
-        // GIVEN: Controller is attached, active notifications is empty,
-        // and mNotificationStackScrollLayout.onKeyguard() is true
-        initController(/* viewIsAttached= */ true);
-        when(mNotificationStackScrollLayout.onKeyguard()).thenReturn(true);
-        mController.getNotifStackController().setNotifStats(NotifStats.getEmpty());
-
-        // THEN: mNotificationStackScrollLayout should not be important for A11y
-        verify(mNotificationStackScrollLayout)
-                .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
-    }
-
-    @Test
-    @DisableFlags(FooterViewRefactor.FLAG_NAME)
-    public void updateImportantForAccessibility_hasChild_onKeyGuard_importantForA11y() {
-        // GIVEN: Controller is attached, active notifications is not empty,
-        // and mNotificationStackScrollLayout.onKeyguard() is true
-        initController(/* viewIsAttached= */ true);
-        when(mNotificationStackScrollLayout.onKeyguard()).thenReturn(true);
-        mController.getNotifStackController().setNotifStats(
-                new NotifStats(
-                        /* numActiveNotifs = */ 1,
-                        /* hasNonClearableAlertingNotifs = */ false,
-                        /* hasClearableAlertingNotifs = */ false,
-                        /* hasNonClearableSilentNotifs = */ false,
-                        /* hasClearableSilentNotifs = */ false)
-        );
-
-        // THEN: mNotificationStackScrollLayout should be important for A11y
-        verify(mNotificationStackScrollLayout)
-                .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
-    }
-
-    @Test
-    @DisableFlags(FooterViewRefactor.FLAG_NAME)
-    public void updateImportantForAccessibility_hasChild_notOnKeyGuard_importantForA11y() {
-        // GIVEN: Controller is attached, active notifications is not empty,
-        // and mNotificationStackScrollLayout.onKeyguard() is false
-        initController(/* viewIsAttached= */ true);
-        when(mNotificationStackScrollLayout.onKeyguard()).thenReturn(false);
-        mController.getNotifStackController().setNotifStats(
-                new NotifStats(
-                        /* numActiveNotifs = */ 1,
-                        /* hasNonClearableAlertingNotifs = */ false,
-                        /* hasClearableAlertingNotifs = */ false,
-                        /* hasNonClearableSilentNotifs = */ false,
-                        /* hasClearableSilentNotifs = */ false)
-        );
-
-        // THEN: mNotificationStackScrollLayout should be important for A11y
-        verify(mNotificationStackScrollLayout)
-                .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
-    }
-
-    @Test
-    @DisableFlags(FooterViewRefactor.FLAG_NAME)
-    public void updateImportantForAccessibility_noChild_notOnKeyGuard_importantForA11y() {
-        // GIVEN: Controller is attached, active notifications is empty,
-        // and mNotificationStackScrollLayout.onKeyguard() is false
-        initController(/* viewIsAttached= */ true);
-        when(mNotificationStackScrollLayout.onKeyguard()).thenReturn(false);
-        mController.getNotifStackController().setNotifStats(NotifStats.getEmpty());
-
-        // THEN: mNotificationStackScrollLayout should be important for A11y
-        verify(mNotificationStackScrollLayout)
-                .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
-    }
-
-    @Test
-    @DisableFlags(FooterViewRefactor.FLAG_NAME)
-    public void updateEmptyShadeView_onKeyguardTransitionToAod_hidesView() {
-        initController(/* viewIsAttached= */ true);
-        mController.onKeyguardTransitionChanged(
-                new TransitionStep(
-                        /* from= */ KeyguardState.GONE,
-                        /* to= */ KeyguardState.AOD));
-        verify(mNotificationStackScrollLayout).updateEmptyShadeView(eq(false), anyBoolean());
-    }
-
-    @Test
-    @DisableFlags(FooterViewRefactor.FLAG_NAME)
-    public void updateEmptyShadeView_onKeyguardOccludedTransitionToAod_hidesView() {
-        initController(/* viewIsAttached= */ true);
-        mController.onKeyguardTransitionChanged(
-                new TransitionStep(
-                        /* from= */ KeyguardState.OCCLUDED,
-                        /* to= */ KeyguardState.AOD));
-        verify(mNotificationStackScrollLayout).updateEmptyShadeView(eq(false), anyBoolean());
-    }
-
-    @Test
     @DisableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
     public void sensitiveNotificationProtectionControllerListenerNotRegistered() {
         initController(/* viewIsAttached= */ true);
@@ -996,24 +747,6 @@
         return argThat(new LogMatcher(category, type));
     }
 
-    private void setupShowEmptyShadeViewState(boolean toShow) {
-        if (toShow) {
-            mController.onKeyguardTransitionChanged(
-                    new TransitionStep(
-                            /* from= */ KeyguardState.LOCKSCREEN,
-                            /* to= */ KeyguardState.GONE));
-            mController.setQsFullScreen(false);
-            mController.getView().removeAllViews();
-        } else {
-            mController.onKeyguardTransitionChanged(
-                    new TransitionStep(
-                            /* from= */ KeyguardState.GONE,
-                            /* to= */ KeyguardState.AOD));
-            mController.setQsFullScreen(true);
-            mController.getView().addContainerView(mock(ExpandableNotificationRow.class));
-        }
-    }
-
     private void initController(boolean viewIsAttached) {
         when(mNotificationStackScrollLayout.isAttachedToWindow()).thenReturn(viewIsAttached);
         ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class);
@@ -1033,16 +766,12 @@
                 mStatusBarService,
                 mNotificationRoundnessManager,
                 mTunerService,
-                mDeviceProvisionedController,
                 mDynamicPrivacyController,
                 mConfigurationController,
                 mSysuiStatusBarStateController,
                 mKeyguardMediaController,
                 mKeyguardBypassController,
                 mPowerInteractor,
-                mPrimaryBouncerInteractor,
-                mKeyguardTransitionRepo,
-                mZenModeController,
                 mNotificationLockscreenUserManager,
                 mMetricsLogger,
                 mColorUpdateLogger,
@@ -1051,14 +780,11 @@
                 new FalsingManagerFake(),
                 mNotificationSwipeHelperBuilder,
                 mGroupExpansionManager,
-                mSilentHeaderController,
                 mNotifPipeline,
                 mNotifCollection,
                 mLockscreenShadeTransitionController,
                 mUiEventLogger,
-                mRemoteInputManager,
                 mVisibilityLocationProviderDelegator,
-                mSeenNotificationsInteractor,
                 mViewBinder,
                 mShadeController,
                 mWindowRootView,
@@ -1076,7 +802,7 @@
     }
 
     static class LogMatcher implements ArgumentMatcher<LogMaker> {
-        private int mCategory, mType;
+        private final int mCategory, mType;
 
         LogMatcher(int category, int type) {
             mCategory = category;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index dcac294..39cff63 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -2,12 +2,10 @@
 
 import android.annotation.DimenRes
 import android.content.pm.PackageManager
-import android.platform.test.annotations.DisableFlags
 import android.platform.test.flag.junit.FlagsParameterization
 import android.widget.FrameLayout
 import androidx.test.filters.SmallTest
 import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress
-import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.ShadeInterpolation.getContentAlpha
 import com.android.systemui.dump.DumpManager
@@ -740,20 +738,6 @@
         assertThat((footerView.viewState as FooterViewState).hideContent).isTrue()
     }
 
-    @DisableFlags(Flags.FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR)
-    @Test
-    fun resetViewStates_clearAllInProgress_allRowsRemoved_emptyShade_footerHidden() {
-        ambientState.isClearAllInProgress = true
-        ambientState.isShadeExpanded = true
-        ambientState.stackEndHeight = maxPanelHeight // plenty space for the footer in the stack
-        hostView.removeAllViews() // remove all rows
-        hostView.addView(footerView)
-
-        stackScrollAlgorithm.resetViewStates(ambientState, 0)
-
-        assertThat((footerView.viewState as FooterViewState).hideContent).isTrue()
-    }
-
     @Test
     fun getGapForLocation_onLockscreen_returnsSmallGap() {
         val gap =
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 e592e4b..1b4f9a7 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
@@ -18,7 +18,6 @@
 
 package com.android.systemui.statusbar.notification.stack.ui.viewmodel
 
-import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.FlagsParameterization
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -41,7 +40,6 @@
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
 import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
 import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
-import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.headsup.PinnedStatus
 import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
 import com.android.systemui.statusbar.policy.data.repository.fakeUserSetupRepository
@@ -63,7 +61,6 @@
 
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
-@EnableFlags(FooterViewRefactor.FLAG_NAME)
 class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
     private val kosmos =
         testKosmos().apply {
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 45977886..a045b37 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
@@ -70,6 +70,7 @@
 import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel.HorizontalPosition
 import com.android.systemui.testKosmos
+import com.android.systemui.window.ui.viewmodel.fakeBouncerTransitions
 import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import kotlin.test.assertIs
@@ -1395,6 +1396,19 @@
             assertThat(stackAbsoluteBottom).isEqualTo(100F)
         }
 
+    @Test
+    fun blurRadius_emitsValues_fromPrimaryBouncerTransitions() =
+        testScope.runTest {
+            val blurRadius by collectLastValue(underTest.blurRadius)
+            assertThat(blurRadius).isEqualTo(0.0f)
+
+            kosmos.fakeBouncerTransitions.first().notificationBlurRadius.value = 30.0f
+            assertThat(blurRadius).isEqualTo(30.0f)
+
+            kosmos.fakeBouncerTransitions.last().notificationBlurRadius.value = 40.0f
+            assertThat(blurRadius).isEqualTo(40.0f)
+        }
+
     private suspend fun TestScope.showLockscreen() {
         shadeTestUtil.setQsExpansion(0f)
         shadeTestUtil.setLockscreenShadeExpansion(0f)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
index 9eafcdb..e2330f4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
@@ -34,6 +34,7 @@
 import static org.mockito.Mockito.when;
 
 import android.hardware.biometrics.BiometricSourceType;
+import android.hardware.fingerprint.FingerprintManager;
 import android.os.Handler;
 import android.os.PowerManager;
 import android.os.UserHandle;
@@ -431,9 +432,9 @@
     }
 
     @Test
-    public void onUdfpsConsecutivelyFailedThreeTimes_showPrimaryBouncer() {
-        // GIVEN UDFPS is supported
-        when(mUpdateMonitor.isUdfpsSupported()).thenReturn(true);
+    public void onOpticalUdfpsConsecutivelyFailedThreeTimes_showPrimaryBouncer() {
+        // GIVEN optical UDFPS is supported
+        when(mUpdateMonitor.isOpticalUdfpsSupported()).thenReturn(true);
 
         // WHEN udfps fails once - then don't show the bouncer yet
         mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
@@ -451,6 +452,25 @@
     }
 
     @Test
+    public void onUltrasonicUdfpsLockout_showPrimaryBouncer() {
+        // GIVEN ultrasonic UDFPS is supported
+        when(mUpdateMonitor.isOpticalUdfpsSupported()).thenReturn(false);
+
+        // WHEN udfps fails three times, don't show bouncer
+        mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
+        mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
+        mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
+        verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean());
+
+        // WHEN lockout is received
+        mBiometricUnlockController.onBiometricError(FingerprintManager.FINGERPRINT_ERROR_LOCKOUT,
+                "Lockout", BiometricSourceType.FINGERPRINT);
+
+        // THEN show bouncer
+        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(true);
+    }
+
+    @Test
     public void onFinishedGoingToSleep_authenticatesWhenPending() {
         when(mUpdateMonitor.isGoingToSleep()).thenReturn(true);
         mBiometricUnlockController.mWakefulnessObserver.onFinishedGoingToSleep();
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
index 92318b9..43ad042 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
@@ -53,6 +53,7 @@
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler
+import com.android.systemui.statusbar.layout.mockStatusBarContentInsetsProvider
 import com.android.systemui.statusbar.phone.ui.StatusBarIconController
 import com.android.systemui.statusbar.phone.ui.TintedIconManager
 import com.android.systemui.statusbar.policy.BatteryController
@@ -152,7 +153,8 @@
         shadeViewStateProvider = TestShadeViewStateProvider()
 
         Mockito.`when`(
-                kosmos.statusBarContentInsetsProvider.getStatusBarContentInsetsForCurrentRotation()
+                kosmos.mockStatusBarContentInsetsProvider
+                    .getStatusBarContentInsetsForCurrentRotation()
             )
             .thenReturn(Insets.of(0, 0, 0, 0))
 
@@ -161,7 +163,7 @@
         Mockito.`when`(iconManagerFactory.create(ArgumentMatchers.any(), ArgumentMatchers.any()))
             .thenReturn(iconManager)
         Mockito.`when`(statusBarContentInsetsProviderStore.defaultDisplay)
-            .thenReturn(kosmos.statusBarContentInsetsProvider)
+            .thenReturn(kosmos.mockStatusBarContentInsetsProvider)
         allowTestableLooperAsMainThread()
         looper.runWithLooper {
             keyguardStatusBarView =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProviderTest.kt
index 788c2cb2..7786689 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProviderTest.kt
@@ -25,6 +25,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.layout.LetterboxBackgroundProvider
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.time.FakeSystemClock
@@ -134,6 +135,7 @@
     fun isLetterboxBackgroundMultiColored_defaultValue_returnsFalse() {
         assertThat(provider.isLetterboxBackgroundMultiColored).isEqualTo(false)
     }
+
     @Test
     fun isLetterboxBackgroundMultiColored_afterOnStart_executorNotDone_returnsDefaultValue() {
         whenever(windowManager.isLetterboxBackgroundMultiColored).thenReturn(true)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
index 9099334..a65ccad 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
@@ -51,6 +51,7 @@
 import com.android.systemui.statusbar.data.model.StatusBarAppearance;
 import com.android.systemui.statusbar.data.model.StatusBarMode;
 import com.android.systemui.statusbar.data.repository.FakeStatusBarModePerDisplayRepository;
+import com.android.systemui.statusbar.layout.BoundsPair;
 import com.android.systemui.statusbar.policy.BatteryController;
 
 import kotlinx.coroutines.test.TestScope;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index d174484..2e12336 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -40,7 +40,6 @@
 
 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;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.TestableLooper;
@@ -610,7 +609,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER)
     public void testPredictiveBackCallback_registration() {
         /* verify that a predictive back callback is registered when the bouncer becomes visible */
         mBouncerExpansionCallback.onVisibilityChanged(true);
@@ -625,7 +623,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER)
     public void testPredictiveBackCallback_invocationHidesBouncer() {
         mBouncerExpansionCallback.onVisibilityChanged(true);
         /* capture the predictive back callback during registration */
@@ -643,7 +640,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER)
     public void testPredictiveBackCallback_noBackAnimationForFullScreenBouncer() {
         when(mKeyguardSecurityModel.getSecurityMode(anyInt()))
                 .thenReturn(KeyguardSecurityModel.SecurityMode.SimPin);
@@ -663,7 +659,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER)
     public void testPredictiveBackCallback_forwardsBackDispatches() {
         mBouncerExpansionCallback.onVisibilityChanged(true);
         /* capture the predictive back callback during registration */
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
index 0652a83..650fa7c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
@@ -31,7 +31,6 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.res.Configuration;
-import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.platform.test.ravenwood.RavenwoodRule;
@@ -41,7 +40,6 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.Dependency;
-import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.DialogTransitionAnimator;
 import com.android.systemui.animation.back.BackAnimationSpec;
@@ -137,7 +135,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_DIALOGS)
     public void usePredictiveBackAnimFlag() {
         final SystemUIDialog dialog = new SystemUIDialog(mContext);
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaRepoTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
similarity index 93%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaRepoTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
index cf512cd..b984099 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaRepoTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
@@ -28,9 +28,7 @@
 import android.widget.LinearLayout
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON
 import com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS
-import com.android.systemui.Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.concurrency.fakeExecutor
 import com.android.systemui.dump.DumpManager
@@ -42,7 +40,6 @@
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
 import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler
-import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
 import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
@@ -76,9 +73,8 @@
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
 @OptIn(ExperimentalCoroutinesApi::class)
-@EnableFlags(FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP)
 @DisableFlags(StatusBarChipsModernization.FLAG_NAME)
-class OngoingCallControllerViaRepoTest : SysuiTestCase() {
+class OngoingCallControllerTest : SysuiTestCase() {
     private val kosmos = Kosmos()
 
     private val clock = kosmos.fakeSystemClock
@@ -114,7 +110,6 @@
                 testScope.backgroundScope,
                 context,
                 ongoingCallRepository,
-                mock<CommonNotifCollection>(),
                 kosmos.activeNotificationsInteractor,
                 clock,
                 mockActivityStarter,
@@ -162,28 +157,7 @@
         }
 
     @Test
-    @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
-    fun interactorHasOngoingCallNotif_notifIconFlagOff_repoHasNoNotifIcon() =
-        testScope.runTest {
-            val icon = mock<StatusBarIconView>()
-            setNotifOnRepo(
-                activeNotificationModel(
-                    key = "ongoingNotif",
-                    callType = CallType.Ongoing,
-                    uid = CALL_UID,
-                    statusBarChipIcon = icon,
-                    whenTime = 567,
-                )
-            )
-
-            val repoState = ongoingCallRepository.ongoingCallState.value
-            assertThat(repoState).isInstanceOf(OngoingCallModel.InCall::class.java)
-            assertThat((repoState as OngoingCallModel.InCall).notificationIconView).isNull()
-        }
-
-    @Test
-    @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
-    fun interactorHasOngoingCallNotif_notifIconFlagOn_repoHasNotifIcon() =
+    fun interactorHasOngoingCallNotif_repoHasNotifIcon() =
         testScope.runTest {
             val icon = mock<StatusBarIconView>()
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt
deleted file mode 100644
index cd3539d..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt
+++ /dev/null
@@ -1,694 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.phone.ongoingcall
-
-import android.app.ActivityManager
-import android.app.IActivityManager
-import android.app.IUidObserver
-import android.app.Notification
-import android.app.PendingIntent
-import android.app.Person
-import android.platform.test.annotations.DisableFlags
-import android.platform.test.annotations.EnableFlags
-import android.service.notification.NotificationListenerService.REASON_USER_STOPPED
-import android.testing.TestableLooper
-import android.view.LayoutInflater
-import android.view.View
-import android.widget.LinearLayout
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS
-import com.android.systemui.Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.log.logcatLogBuffer
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.res.R
-import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository
-import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler
-import com.android.systemui.statusbar.notification.collection.NotificationEntry
-import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
-import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
-import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
-import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
-import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
-import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
-import com.android.systemui.statusbar.window.StatusBarWindowController
-import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
-import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.anyBoolean
-import org.mockito.ArgumentMatchers.anyString
-import org.mockito.ArgumentMatchers.nullable
-import org.mockito.Mock
-import org.mockito.Mockito.eq
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.reset
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
-import org.mockito.MockitoAnnotations
-import org.mockito.kotlin.whenever
-
-private const val CALL_UID = 900
-
-// A process state that represents the process being visible to the user.
-private const val PROC_STATE_VISIBLE = ActivityManager.PROCESS_STATE_TOP
-
-// A process state that represents the process being invisible to the user.
-private const val PROC_STATE_INVISIBLE = ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-@TestableLooper.RunWithLooper
-@OptIn(ExperimentalCoroutinesApi::class)
-@DisableFlags(FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP, StatusBarChipsModernization.FLAG_NAME)
-class OngoingCallControllerViaListenerTest : SysuiTestCase() {
-    private val kosmos = Kosmos()
-
-    private val clock = FakeSystemClock()
-    private val mainExecutor = FakeExecutor(clock)
-    private val testScope = TestScope()
-    private val statusBarModeRepository = FakeStatusBarModeRepository()
-    private val ongoingCallRepository = kosmos.ongoingCallRepository
-
-    private lateinit var controller: OngoingCallController
-    private lateinit var notifCollectionListener: NotifCollectionListener
-
-    @Mock
-    private lateinit var mockSwipeStatusBarAwayGestureHandler: SwipeStatusBarAwayGestureHandler
-    @Mock private lateinit var mockOngoingCallListener: OngoingCallListener
-    @Mock private lateinit var mockActivityStarter: ActivityStarter
-    @Mock private lateinit var mockIActivityManager: IActivityManager
-    @Mock private lateinit var mockStatusBarWindowController: StatusBarWindowController
-    @Mock private lateinit var mockStatusBarWindowControllerStore: StatusBarWindowControllerStore
-
-    private lateinit var chipView: View
-
-    @Before
-    fun setUp() {
-        allowTestableLooperAsMainThread()
-        TestableLooper.get(this).runWithLooper {
-            chipView =
-                LayoutInflater.from(mContext).inflate(R.layout.ongoing_activity_chip_primary, null)
-        }
-
-        MockitoAnnotations.initMocks(this)
-        val notificationCollection = mock(CommonNotifCollection::class.java)
-        whenever(mockStatusBarWindowControllerStore.defaultDisplay)
-            .thenReturn(mockStatusBarWindowController)
-
-        controller =
-            OngoingCallController(
-                testScope.backgroundScope,
-                context,
-                ongoingCallRepository,
-                notificationCollection,
-                kosmos.activeNotificationsInteractor,
-                clock,
-                mockActivityStarter,
-                mainExecutor,
-                mockIActivityManager,
-                DumpManager(),
-                mockStatusBarWindowControllerStore,
-                mockSwipeStatusBarAwayGestureHandler,
-                statusBarModeRepository,
-                logcatLogBuffer("OngoingCallControllerViaListenerTest"),
-            )
-        controller.start()
-        controller.addCallback(mockOngoingCallListener)
-        controller.setChipView(chipView)
-        onTeardown { controller.tearDownChipView() }
-
-        val collectionListenerCaptor = ArgumentCaptor.forClass(NotifCollectionListener::class.java)
-        verify(notificationCollection).addCollectionListener(collectionListenerCaptor.capture())
-        notifCollectionListener = collectionListenerCaptor.value!!
-
-        `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
-            .thenReturn(PROC_STATE_INVISIBLE)
-    }
-
-    @Test
-    fun onEntryUpdated_isOngoingCallNotif_listenerAndRepoNotified() {
-        val notification = NotificationEntryBuilder(createOngoingCallNotifEntry())
-        notification.modifyNotification(context).setWhen(567)
-        notifCollectionListener.onEntryUpdated(notification.build())
-
-        verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
-        val repoState = ongoingCallRepository.ongoingCallState.value
-        assertThat(repoState).isInstanceOf(OngoingCallModel.InCall::class.java)
-        assertThat((repoState as OngoingCallModel.InCall).startTimeMs).isEqualTo(567)
-    }
-
-    @Test
-    fun onEntryUpdated_isOngoingCallNotif_windowControllerUpdated() {
-        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-
-        verify(mockStatusBarWindowController).setOngoingProcessRequiresStatusBarVisible(true)
-    }
-
-    @Test
-    fun onEntryUpdated_notOngoingCallNotif_listenerAndRepoNotNotified() {
-        notifCollectionListener.onEntryUpdated(createNotCallNotifEntry())
-
-        verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean())
-        assertThat(ongoingCallRepository.ongoingCallState.value)
-            .isInstanceOf(OngoingCallModel.NoCall::class.java)
-    }
-
-    @Test
-    fun onEntryUpdated_ongoingCallNotifThenScreeningCallNotif_listenerNotifiedTwice() {
-        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-        notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry())
-
-        verify(mockOngoingCallListener, times(2)).onOngoingCallStateChanged(anyBoolean())
-    }
-
-    @Test
-    fun onEntryUpdated_ongoingCallNotifThenScreeningCallNotif_repoUpdated() {
-        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-        assertThat(ongoingCallRepository.ongoingCallState.value)
-            .isInstanceOf(OngoingCallModel.InCall::class.java)
-
-        notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry())
-
-        assertThat(ongoingCallRepository.ongoingCallState.value)
-            .isInstanceOf(OngoingCallModel.NoCall::class.java)
-    }
-
-    /** Regression test for b/191472854. */
-    @Test
-    fun onEntryUpdated_notifHasNullContentIntent_noCrash() {
-        notifCollectionListener.onEntryUpdated(
-            createCallNotifEntry(ongoingCallStyle, nullContentIntent = true)
-        )
-    }
-
-    /** Regression test for b/192379214. */
-    @Test
-    @DisableFlags(android.app.Flags.FLAG_SORT_SECTION_BY_TIME, FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    fun onEntryUpdated_notificationWhenIsZero_timeHidden() {
-        val notification = NotificationEntryBuilder(createOngoingCallNotifEntry())
-        notification.modifyNotification(context).setWhen(0)
-
-        notifCollectionListener.onEntryUpdated(notification.build())
-        chipView.measure(
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
-        )
-
-        assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth)
-            .isEqualTo(0)
-    }
-
-    @Test
-    @EnableFlags(android.app.Flags.FLAG_SORT_SECTION_BY_TIME)
-    @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    fun onEntryUpdated_notificationWhenIsZero_timeShown() {
-        val notification = NotificationEntryBuilder(createOngoingCallNotifEntry())
-        notification.modifyNotification(context).setWhen(0)
-
-        notifCollectionListener.onEntryUpdated(notification.build())
-        chipView.measure(
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
-        )
-
-        assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth)
-            .isGreaterThan(0)
-    }
-
-    @Test
-    @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    fun onEntryUpdated_notificationWhenIsValid_timeShown() {
-        val notification = NotificationEntryBuilder(createOngoingCallNotifEntry())
-        notification.modifyNotification(context).setWhen(clock.currentTimeMillis())
-
-        notifCollectionListener.onEntryUpdated(notification.build())
-        chipView.measure(
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
-        )
-
-        assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth)
-            .isGreaterThan(0)
-    }
-
-    /** Regression test for b/194731244. */
-    @Test
-    fun onEntryUpdated_calledManyTimes_uidObserverOnlyRegisteredOnce() {
-        for (i in 0 until 4) {
-            // Re-create the notification each time so that it's considered a different object and
-            // will re-trigger the whole flow.
-            notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-        }
-
-        verify(mockIActivityManager, times(1)).registerUidObserver(any(), any(), any(), any())
-    }
-
-    /** Regression test for b/216248574. */
-    @Test
-    fun entryUpdated_getUidProcessStateThrowsException_noCrash() {
-        `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
-            .thenThrow(SecurityException())
-
-        // No assert required, just check no crash
-        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-    }
-
-    /** Regression test for b/216248574. */
-    @Test
-    fun entryUpdated_registerUidObserverThrowsException_noCrash() {
-        `when`(
-                mockIActivityManager.registerUidObserver(
-                    any(),
-                    any(),
-                    any(),
-                    nullable(String::class.java),
-                )
-            )
-            .thenThrow(SecurityException())
-
-        // No assert required, just check no crash
-        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-    }
-
-    /** Regression test for b/216248574. */
-    @Test
-    fun entryUpdated_packageNameProvidedToActivityManager() {
-        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-
-        val packageNameCaptor = ArgumentCaptor.forClass(String::class.java)
-        verify(mockIActivityManager)
-            .registerUidObserver(any(), any(), any(), packageNameCaptor.capture())
-        assertThat(packageNameCaptor.value).isNotNull()
-    }
-
-    /**
-     * If a call notification is never added before #onEntryRemoved is called, then the listener
-     * should never be notified.
-     */
-    @Test
-    fun onEntryRemoved_callNotifNeverAddedBeforehand_listenerNotNotified() {
-        notifCollectionListener.onEntryRemoved(createOngoingCallNotifEntry(), REASON_USER_STOPPED)
-
-        verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean())
-    }
-
-    @Test
-    fun onEntryRemoved_callNotifAddedThenRemoved_listenerNotified() {
-        val ongoingCallNotifEntry = createOngoingCallNotifEntry()
-        notifCollectionListener.onEntryAdded(ongoingCallNotifEntry)
-        reset(mockOngoingCallListener)
-
-        notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED)
-
-        verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
-    }
-
-    @Test
-    fun onEntryRemoved_callNotifAddedThenRemoved_repoUpdated() {
-        val ongoingCallNotifEntry = createOngoingCallNotifEntry()
-        notifCollectionListener.onEntryAdded(ongoingCallNotifEntry)
-        assertThat(ongoingCallRepository.ongoingCallState.value)
-            .isInstanceOf(OngoingCallModel.InCall::class.java)
-
-        notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED)
-
-        assertThat(ongoingCallRepository.ongoingCallState.value)
-            .isInstanceOf(OngoingCallModel.NoCall::class.java)
-    }
-
-    @Test
-    fun onEntryUpdated_callNotifAddedThenRemoved_windowControllerUpdated() {
-        val ongoingCallNotifEntry = createOngoingCallNotifEntry()
-        notifCollectionListener.onEntryAdded(ongoingCallNotifEntry)
-
-        notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED)
-
-        verify(mockStatusBarWindowController).setOngoingProcessRequiresStatusBarVisible(false)
-    }
-
-    /** Regression test for b/188491504. */
-    @Test
-    fun onEntryRemoved_removedNotifHasSameKeyAsAddedNotif_listenerNotified() {
-        val ongoingCallNotifEntry = createOngoingCallNotifEntry()
-        notifCollectionListener.onEntryAdded(ongoingCallNotifEntry)
-        reset(mockOngoingCallListener)
-
-        // Create another notification based on the ongoing call one, but remove the features that
-        // made it a call notification.
-        val removedEntryBuilder = NotificationEntryBuilder(ongoingCallNotifEntry)
-        removedEntryBuilder.modifyNotification(context).style = null
-
-        notifCollectionListener.onEntryRemoved(removedEntryBuilder.build(), REASON_USER_STOPPED)
-
-        verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
-    }
-
-    /** Regression test for b/188491504. */
-    @Test
-    fun onEntryRemoved_removedNotifHasSameKeyAsAddedNotif_repoUpdated() {
-        val ongoingCallNotifEntry = createOngoingCallNotifEntry()
-        notifCollectionListener.onEntryAdded(ongoingCallNotifEntry)
-
-        // Create another notification based on the ongoing call one, but remove the features that
-        // made it a call notification.
-        val removedEntryBuilder = NotificationEntryBuilder(ongoingCallNotifEntry)
-        removedEntryBuilder.modifyNotification(context).style = null
-
-        notifCollectionListener.onEntryRemoved(removedEntryBuilder.build(), REASON_USER_STOPPED)
-
-        assertThat(ongoingCallRepository.ongoingCallState.value)
-            .isInstanceOf(OngoingCallModel.NoCall::class.java)
-    }
-
-    @Test
-    fun onEntryRemoved_notifKeyDoesNotMatchOngoingCallNotif_listenerNotNotified() {
-        notifCollectionListener.onEntryAdded(createOngoingCallNotifEntry())
-        reset(mockOngoingCallListener)
-
-        notifCollectionListener.onEntryRemoved(createNotCallNotifEntry(), REASON_USER_STOPPED)
-
-        verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean())
-    }
-
-    @Test
-    fun onEntryRemoved_notifKeyDoesNotMatchOngoingCallNotif_repoNotUpdated() {
-        notifCollectionListener.onEntryAdded(createOngoingCallNotifEntry())
-
-        notifCollectionListener.onEntryRemoved(createNotCallNotifEntry(), REASON_USER_STOPPED)
-
-        assertThat(ongoingCallRepository.ongoingCallState.value)
-            .isInstanceOf(OngoingCallModel.InCall::class.java)
-    }
-
-    @Test
-    fun hasOngoingCall_noOngoingCallNotifSent_returnsFalse() {
-        assertThat(controller.hasOngoingCall()).isFalse()
-    }
-
-    @Test
-    fun hasOngoingCall_unrelatedNotifSent_returnsFalse() {
-        notifCollectionListener.onEntryUpdated(createNotCallNotifEntry())
-
-        assertThat(controller.hasOngoingCall()).isFalse()
-    }
-
-    @Test
-    fun hasOngoingCall_screeningCallNotifSent_returnsFalse() {
-        notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry())
-
-        assertThat(controller.hasOngoingCall()).isFalse()
-    }
-
-    @Test
-    fun hasOngoingCall_ongoingCallNotifSentAndCallAppNotVisible_returnsTrue() {
-        `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
-            .thenReturn(PROC_STATE_INVISIBLE)
-
-        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-
-        assertThat(controller.hasOngoingCall()).isTrue()
-    }
-
-    @Test
-    fun hasOngoingCall_ongoingCallNotifSentButCallAppVisible_returnsFalse() {
-        `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
-            .thenReturn(PROC_STATE_VISIBLE)
-
-        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-
-        assertThat(controller.hasOngoingCall()).isFalse()
-    }
-
-    @Test
-    fun hasOngoingCall_ongoingCallNotifSentButInvalidChipView_returnsFalse() {
-        val invalidChipView = LinearLayout(context)
-        controller.setChipView(invalidChipView)
-
-        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-
-        assertThat(controller.hasOngoingCall()).isFalse()
-    }
-
-    @Test
-    fun hasOngoingCall_ongoingCallNotifSentThenRemoved_returnsFalse() {
-        val ongoingCallNotifEntry = createOngoingCallNotifEntry()
-
-        notifCollectionListener.onEntryUpdated(ongoingCallNotifEntry)
-        notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED)
-
-        assertThat(controller.hasOngoingCall()).isFalse()
-    }
-
-    @Test
-    fun hasOngoingCall_ongoingCallNotifSentThenScreeningCallNotifSent_returnsFalse() {
-        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-        notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry())
-
-        assertThat(controller.hasOngoingCall()).isFalse()
-    }
-
-    @Test
-    fun hasOngoingCall_ongoingCallNotifSentThenUnrelatedNotifSent_returnsTrue() {
-        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-        notifCollectionListener.onEntryUpdated(createNotCallNotifEntry())
-
-        assertThat(controller.hasOngoingCall()).isTrue()
-    }
-
-    /**
-     * This test fakes a theme change during an ongoing call.
-     *
-     * When a theme change happens, [CollapsedStatusBarFragment] and its views get re-created, so
-     * [OngoingCallController.setChipView] gets called with a new view. If there's an active ongoing
-     * call when the theme changes, the new view needs to be updated with the call information.
-     */
-    @Test
-    fun setChipView_whenHasOngoingCallIsTrue_listenerNotifiedWithNewView() {
-        // Start an ongoing call.
-        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-        reset(mockOngoingCallListener)
-
-        lateinit var newChipView: View
-        TestableLooper.get(this).runWithLooper {
-            newChipView =
-                LayoutInflater.from(mContext).inflate(R.layout.ongoing_activity_chip_primary, null)
-        }
-
-        // Change the chip view associated with the controller.
-        controller.setChipView(newChipView)
-
-        verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
-    }
-
-    @Test
-    fun callProcessChangesToVisible_listenerNotified() {
-        // Start the call while the process is invisible.
-        `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
-            .thenReturn(PROC_STATE_INVISIBLE)
-        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-        reset(mockOngoingCallListener)
-
-        val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java)
-        verify(mockIActivityManager)
-            .registerUidObserver(captor.capture(), any(), any(), nullable(String::class.java))
-        val uidObserver = captor.value
-
-        // Update the process to visible.
-        uidObserver.onUidStateChanged(CALL_UID, PROC_STATE_VISIBLE, 0, 0)
-        mainExecutor.advanceClockToLast()
-        mainExecutor.runAllReady()
-
-        verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
-    }
-
-    @Test
-    fun callProcessChangesToInvisible_listenerNotified() {
-        // Start the call while the process is visible.
-        `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
-            .thenReturn(PROC_STATE_VISIBLE)
-        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-        reset(mockOngoingCallListener)
-
-        val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java)
-        verify(mockIActivityManager)
-            .registerUidObserver(captor.capture(), any(), any(), nullable(String::class.java))
-        val uidObserver = captor.value
-
-        // Update the process to invisible.
-        uidObserver.onUidStateChanged(CALL_UID, PROC_STATE_INVISIBLE, 0, 0)
-        mainExecutor.advanceClockToLast()
-        mainExecutor.runAllReady()
-
-        verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
-    }
-
-    /** Regression test for b/212467440. */
-    @Test
-    @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    fun chipClicked_activityStarterTriggeredWithUnmodifiedIntent() {
-        val notifEntry = createOngoingCallNotifEntry()
-        val pendingIntent = notifEntry.sbn.notification.contentIntent
-        notifCollectionListener.onEntryUpdated(notifEntry)
-
-        chipView.performClick()
-
-        // Ensure that the sysui didn't modify the notification's intent -- see b/212467440.
-        verify(mockActivityStarter).postStartActivityDismissingKeyguard(eq(pendingIntent), any())
-    }
-
-    @Test
-    @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    fun callNotificationAdded_chipIsClickable() {
-        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-
-        assertThat(chipView.hasOnClickListeners()).isTrue()
-    }
-
-    @Test
-    @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    fun callNotificationAdded_newChipsEnabled_chipNotClickable() {
-        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-
-        assertThat(chipView.hasOnClickListeners()).isFalse()
-    }
-
-    @Test
-    @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    fun fullscreenIsTrue_chipStillClickable() {
-        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-        statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
-        testScope.runCurrent()
-
-        assertThat(chipView.hasOnClickListeners()).isTrue()
-    }
-
-    // Swipe gesture tests
-
-    @Test
-    fun callStartedInImmersiveMode_swipeGestureCallbackAdded() {
-        statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
-        testScope.runCurrent()
-
-        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-
-        verify(mockSwipeStatusBarAwayGestureHandler)
-            .addOnGestureDetectedCallback(anyString(), any())
-    }
-
-    @Test
-    fun callStartedNotInImmersiveMode_swipeGestureCallbackNotAdded() {
-        statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false
-        testScope.runCurrent()
-
-        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-
-        verify(mockSwipeStatusBarAwayGestureHandler, never())
-            .addOnGestureDetectedCallback(anyString(), any())
-    }
-
-    @Test
-    fun transitionToImmersiveMode_swipeGestureCallbackAdded() {
-        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-
-        statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
-        testScope.runCurrent()
-
-        verify(mockSwipeStatusBarAwayGestureHandler)
-            .addOnGestureDetectedCallback(anyString(), any())
-    }
-
-    @Test
-    fun transitionOutOfImmersiveMode_swipeGestureCallbackRemoved() {
-        statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
-        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-        reset(mockSwipeStatusBarAwayGestureHandler)
-
-        statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false
-        testScope.runCurrent()
-
-        verify(mockSwipeStatusBarAwayGestureHandler).removeOnGestureDetectedCallback(anyString())
-    }
-
-    @Test
-    fun callEndedWhileInImmersiveMode_swipeGestureCallbackRemoved() {
-        statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
-        testScope.runCurrent()
-        val ongoingCallNotifEntry = createOngoingCallNotifEntry()
-        notifCollectionListener.onEntryAdded(ongoingCallNotifEntry)
-        reset(mockSwipeStatusBarAwayGestureHandler)
-
-        notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED)
-
-        verify(mockSwipeStatusBarAwayGestureHandler).removeOnGestureDetectedCallback(anyString())
-    }
-
-    // TODO(b/195839150): Add test
-    //  swipeGesturedTriggeredPreviously_entersImmersiveModeAgain_callbackNotAdded(). That's
-    //  difficult to add now because we have no way to trigger [SwipeStatusBarAwayGestureHandler]'s
-    //  callbacks in test.
-
-    // END swipe gesture tests
-
-    private fun createOngoingCallNotifEntry() = createCallNotifEntry(ongoingCallStyle)
-
-    private fun createScreeningCallNotifEntry() = createCallNotifEntry(screeningCallStyle)
-
-    private fun createCallNotifEntry(
-        callStyle: Notification.CallStyle,
-        nullContentIntent: Boolean = false,
-    ): NotificationEntry {
-        val notificationEntryBuilder = NotificationEntryBuilder()
-        notificationEntryBuilder.modifyNotification(context).style = callStyle
-        notificationEntryBuilder.setUid(CALL_UID)
-
-        if (nullContentIntent) {
-            notificationEntryBuilder.modifyNotification(context).setContentIntent(null)
-        } else {
-            val contentIntent = mock(PendingIntent::class.java)
-            notificationEntryBuilder.modifyNotification(context).setContentIntent(contentIntent)
-        }
-
-        return notificationEntryBuilder.build()
-    }
-
-    private fun createNotCallNotifEntry() = NotificationEntryBuilder().build()
-}
-
-private val person = Person.Builder().setName("name").build()
-private val hangUpIntent = mock(PendingIntent::class.java)
-
-private val ongoingCallStyle = Notification.CallStyle.forOngoingCall(person, hangUpIntent)
-private val screeningCallStyle =
-    Notification.CallStyle.forScreeningCall(
-        person,
-        hangUpIntent,
-        /* answerIntent= */ mock(PendingIntent::class.java),
-    )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt
index a9db0b7..6feada1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt
@@ -31,7 +31,7 @@
 class FakeHomeStatusBarViewModel(
     override val operatorNameViewModel: StatusBarOperatorNameViewModel
 ) : HomeStatusBarViewModel {
-    private val areNotificationLightsOut = MutableStateFlow(false)
+    override val areNotificationsLightsOut = MutableStateFlow(false)
 
     override val isTransitioningFromLockscreenToOccluded = MutableStateFlow(false)
 
@@ -77,14 +77,14 @@
 
     override val iconBlockList: MutableStateFlow<List<String>> = MutableStateFlow(listOf())
 
-    override fun areNotificationsLightsOut(displayId: Int): Flow<Boolean> = areNotificationLightsOut
+    override val contentArea = MutableStateFlow(Rect(0, 0, 1, 1))
 
     val darkRegions = mutableListOf<Rect>()
 
     var darkIconTint = Color.BLACK
     var lightIconTint = Color.WHITE
 
-    override fun areaTint(displayId: Int): Flow<StatusBarTintColor> =
+    override val areaTint: Flow<StatusBarTintColor> =
         MutableStateFlow(
             StatusBarTintColor { viewBounds ->
                 if (DarkIconDispatcher.isInAreas(darkRegions, viewBounds)) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt
index e91875c..e95bc33 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt
@@ -22,13 +22,17 @@
 import android.app.StatusBarManager.DISABLE_NONE
 import android.app.StatusBarManager.DISABLE_NOTIFICATION_ICONS
 import android.app.StatusBarManager.DISABLE_SYSTEM_INFO
+import android.content.testableContext
 import android.graphics.Rect
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
+import android.view.Display.DEFAULT_DISPLAY
 import android.view.View
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.display.data.repository.displayRepository
+import com.android.systemui.display.data.repository.fake
 import com.android.systemui.flags.DisableSceneContainer
 import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
@@ -59,7 +63,6 @@
 import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsScreenRecordChip
 import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsShareToAppChip
 import com.android.systemui.statusbar.data.model.StatusBarMode
-import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository.Companion.DISPLAY_ID
 import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
 import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
 import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
@@ -85,6 +88,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import org.junit.Before
 import org.junit.Test
@@ -104,6 +108,9 @@
         setUpPackageManagerForMediaProjection(kosmos)
     }
 
+    @Before
+    fun addDisplays() = runBlocking { kosmos.displayRepository.fake.addDisplay(DEFAULT_DISPLAY) }
+
     @Test
     fun isTransitioningFromLockscreenToOccluded_started_isTrue() =
         kosmos.runTest {
@@ -363,7 +370,7 @@
             activeNotificationListRepository.activeNotifications.value =
                 activeNotificationsStore(testNotifications)
 
-            val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID))
+            val actual by collectLastValue(underTest.areNotificationsLightsOut)
 
             assertThat(actual).isTrue()
         }
@@ -377,7 +384,7 @@
             activeNotificationListRepository.activeNotifications.value =
                 activeNotificationsStore(emptyList())
 
-            val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID))
+            val actual by collectLastValue(underTest.areNotificationsLightsOut)
 
             assertThat(actual).isFalse()
         }
@@ -391,7 +398,7 @@
             activeNotificationListRepository.activeNotifications.value =
                 activeNotificationsStore(emptyList())
 
-            val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID))
+            val actual by collectLastValue(underTest.areNotificationsLightsOut)
 
             assertThat(actual).isFalse()
         }
@@ -405,7 +412,7 @@
             activeNotificationListRepository.activeNotifications.value =
                 activeNotificationsStore(testNotifications)
 
-            val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID))
+            val actual by collectLastValue(underTest.areNotificationsLightsOut)
 
             assertThat(actual).isFalse()
         }
@@ -415,7 +422,7 @@
     fun areNotificationsLightsOut_requiresFlagEnabled() =
         kosmos.runTest {
             assertLogsWtf {
-                val flow = underTest.areNotificationsLightsOut(DISPLAY_ID)
+                val flow = underTest.areNotificationsLightsOut
                 assertThat(flow).isEqualTo(emptyFlow<Boolean>())
             }
         }
@@ -1005,11 +1012,11 @@
     @Test
     fun areaTint_viewIsInDarkBounds_getsDarkTint() =
         kosmos.runTest {
-            val displayId = 321
+            val displayId = testableContext.displayId
             fakeDarkIconRepository.darkState(displayId).value =
                 SysuiDarkIconDispatcher.DarkChange(listOf(Rect(0, 0, 5, 5)), 0f, 0xAABBCC)
 
-            val areaTint by collectLastValue(underTest.areaTint(displayId))
+            val areaTint by collectLastValue(underTest.areaTint)
 
             val tint = areaTint?.tint(Rect(1, 1, 3, 3))
 
@@ -1019,11 +1026,11 @@
     @Test
     fun areaTint_viewIsNotInDarkBounds_getsDefaultTint() =
         kosmos.runTest {
-            val displayId = 321
+            val displayId = testableContext.displayId
             fakeDarkIconRepository.darkState(displayId).value =
                 SysuiDarkIconDispatcher.DarkChange(listOf(Rect(0, 0, 5, 5)), 0f, 0xAABBCC)
 
-            val areaTint by collectLastValue(underTest.areaTint(displayId))
+            val areaTint by collectLastValue(underTest.areaTint)
 
             val tint = areaTint?.tint(Rect(6, 6, 7, 7))
 
@@ -1033,11 +1040,11 @@
     @Test
     fun areaTint_viewIsInDarkBounds_darkBoundsChange_viewUpdates() =
         kosmos.runTest {
-            val displayId = 321
+            val displayId = testableContext.displayId
             fakeDarkIconRepository.darkState(displayId).value =
                 SysuiDarkIconDispatcher.DarkChange(listOf(Rect(0, 0, 5, 5)), 0f, 0xAABBCC)
 
-            val areaTint by collectLastValue(underTest.areaTint(displayId))
+            val areaTint by collectLastValue(underTest.areaTint)
 
             var tint = areaTint?.tint(Rect(1, 1, 3, 3))
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
index 3d6882c..7802b92 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
@@ -18,9 +18,8 @@
 
 import android.app.AutomaticZenRule
 import android.app.Flags
-import android.app.NotificationManager.INTERRUPTION_FILTER_NONE
-import android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY
 import android.app.NotificationManager.Policy
+import android.media.AudioManager
 import android.platform.test.annotations.EnableFlags
 import android.provider.Settings
 import android.provider.Settings.Secure.ZEN_DURATION
@@ -34,6 +33,8 @@
 import com.android.internal.R
 import com.android.settingslib.notification.data.repository.updateNotificationPolicy
 import com.android.settingslib.notification.modes.TestModeBuilder
+import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND
+import com.android.settingslib.volume.shared.model.AudioStream
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
@@ -204,7 +205,7 @@
     @Test
     fun shouldAskForZenDuration_changesWithSetting() =
         testScope.runTest {
-            val manualDnd = TestModeBuilder.MANUAL_DND_ACTIVE
+            val manualDnd = TestModeBuilder().makeManualDnd().setActive(true).build()
 
             settingsRepository.setInt(ZEN_DURATION, ZEN_DURATION_FOREVER)
             runCurrent()
@@ -233,29 +234,27 @@
     @Test
     fun activateMode_usesCorrectDuration() =
         testScope.runTest {
-            val manualDnd = TestModeBuilder.MANUAL_DND_ACTIVE
-            zenModeRepository.addModes(listOf(manualDnd))
             settingsRepository.setInt(ZEN_DURATION, ZEN_DURATION_FOREVER)
             runCurrent()
 
-            underTest.activateMode(manualDnd)
-            assertThat(zenModeRepository.getModeActiveDuration(manualDnd.id)).isNull()
+            underTest.activateMode(MANUAL_DND)
+            assertThat(zenModeRepository.getModeActiveDuration(MANUAL_DND.id)).isNull()
 
-            zenModeRepository.deactivateMode(manualDnd.id)
+            zenModeRepository.deactivateMode(MANUAL_DND)
             settingsRepository.setInt(ZEN_DURATION, 60)
             runCurrent()
 
-            underTest.activateMode(manualDnd)
-            assertThat(zenModeRepository.getModeActiveDuration(manualDnd.id))
+            underTest.activateMode(MANUAL_DND)
+            assertThat(zenModeRepository.getModeActiveDuration(MANUAL_DND.id))
                 .isEqualTo(Duration.ofMinutes(60))
         }
 
     @Test
     fun deactivateAllModes_updatesCorrectModes() =
         testScope.runTest {
+            zenModeRepository.activateMode(MANUAL_DND)
             zenModeRepository.addModes(
                 listOf(
-                    TestModeBuilder.MANUAL_DND_ACTIVE,
                     TestModeBuilder().setName("Inactive").setActive(false).build(),
                     TestModeBuilder().setName("Active").setActive(true).build(),
                 )
@@ -389,12 +388,9 @@
         testScope.runTest {
             val dndMode by collectLastValue(underTest.dndMode)
 
-            zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE)
-            runCurrent()
-
             assertThat(dndMode!!.isActive).isFalse()
 
-            zenModeRepository.activateMode(TestModeBuilder.MANUAL_DND_INACTIVE.id)
+            zenModeRepository.activateMode(MANUAL_DND)
             runCurrent()
 
             assertThat(dndMode!!.isActive).isTrue()
@@ -402,47 +398,12 @@
 
     @Test
     @EnableFlags(Flags.FLAG_MODES_UI)
-    fun activeModesBlockingEverything_hasModesWithFilterNone() =
-        testScope.runTest {
-            val blockingEverything by collectLastValue(underTest.activeModesBlockingEverything)
-
-            zenModeRepository.addModes(
-                listOf(
-                    TestModeBuilder()
-                        .setName("Filter=None, Not active")
-                        .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
-                        .setActive(false)
-                        .build(),
-                    TestModeBuilder()
-                        .setName("Filter=Priority, Active")
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setActive(true)
-                        .build(),
-                    TestModeBuilder()
-                        .setName("Filter=None, Active")
-                        .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
-                        .setActive(true)
-                        .build(),
-                    TestModeBuilder()
-                        .setName("Filter=None, Active Too")
-                        .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
-                        .setActive(true)
-                        .build(),
-                )
-            )
-            runCurrent()
-
-            assertThat(blockingEverything!!.mainMode!!.name).isEqualTo("Filter=None, Active")
-            assertThat(blockingEverything!!.modeNames)
-                .containsExactly("Filter=None, Active", "Filter=None, Active Too")
-                .inOrder()
-        }
-
-    @Test
-    @EnableFlags(Flags.FLAG_MODES_UI)
     fun activeModesBlockingMedia_hasModesWithPolicyBlockingMedia() =
         testScope.runTest {
-            val blockingMedia by collectLastValue(underTest.activeModesBlockingMedia)
+            val blockingMedia by
+                collectLastValue(
+                    underTest.activeModesBlockingStream(AudioStream(AudioManager.STREAM_MUSIC))
+                )
 
             zenModeRepository.addModes(
                 listOf(
@@ -480,7 +441,10 @@
     @EnableFlags(Flags.FLAG_MODES_UI)
     fun activeModesBlockingAlarms_hasModesWithPolicyBlockingAlarms() =
         testScope.runTest {
-            val blockingAlarms by collectLastValue(underTest.activeModesBlockingAlarms)
+            val blockingAlarms by
+                collectLastValue(
+                    underTest.activeModesBlockingStream(AudioStream(AudioManager.STREAM_ALARM))
+                )
 
             zenModeRepository.addModes(
                 listOf(
@@ -515,6 +479,47 @@
         }
 
     @Test
+    @EnableFlags(Flags.FLAG_MODES_UI)
+    fun activeModesBlockingAlarms_hasModesWithPolicyBlockingSystem() =
+        testScope.runTest {
+            val blockingSystem by
+                collectLastValue(
+                    underTest.activeModesBlockingStream(AudioStream(AudioManager.STREAM_SYSTEM))
+                )
+
+            zenModeRepository.addModes(
+                listOf(
+                    TestModeBuilder()
+                        .setName("Blocks system, Not active")
+                        .setZenPolicy(ZenPolicy.Builder().allowSystem(false).build())
+                        .setActive(false)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("Allows system, Active")
+                        .setZenPolicy(ZenPolicy.Builder().allowSystem(true).build())
+                        .setActive(true)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("Blocks system, Active")
+                        .setZenPolicy(ZenPolicy.Builder().allowSystem(false).build())
+                        .setActive(true)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("Blocks system, Active Too")
+                        .setZenPolicy(ZenPolicy.Builder().allowSystem(false).build())
+                        .setActive(true)
+                        .build(),
+                )
+            )
+            runCurrent()
+
+            assertThat(blockingSystem!!.mainMode!!.name).isEqualTo("Blocks system, Active")
+            assertThat(blockingSystem!!.modeNames)
+                .containsExactly("Blocks system, Active", "Blocks system, Active Too")
+                .inOrder()
+        }
+
+    @Test
     @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
     fun modesHidingNotifications_onlyIncludesModesWithNotifListSuppression() =
         testScope.runTest {
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
index 07d088b..856de8e 100644
--- 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
@@ -28,6 +28,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.settingslib.notification.modes.TestModeBuilder
+import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND
 import com.android.settingslib.notification.modes.ZenMode
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
@@ -111,7 +112,6 @@
                         .setName("Disabled by other")
                         .setEnabled(false, /* byUser= */ false)
                         .build(),
-                    TestModeBuilder.MANUAL_DND_ACTIVE,
                     TestModeBuilder()
                         .setName("Enabled")
                         .setEnabled(true)
@@ -128,14 +128,14 @@
 
             assertThat(tiles).hasSize(3)
             with(tiles?.elementAt(0)!!) {
-                assertThat(this.text).isEqualTo("Disabled by other")
-                assertThat(this.subtext).isEqualTo("Not set")
+                assertThat(this.text).isEqualTo("Do Not Disturb")
+                assertThat(this.subtext).isEqualTo("Off")
                 assertThat(this.enabled).isEqualTo(false)
             }
             with(tiles?.elementAt(1)!!) {
-                assertThat(this.text).isEqualTo("Do Not Disturb")
-                assertThat(this.subtext).isEqualTo("On")
-                assertThat(this.enabled).isEqualTo(true)
+                assertThat(this.text).isEqualTo("Disabled by other")
+                assertThat(this.subtext).isEqualTo("Not set")
+                assertThat(this.enabled).isEqualTo(false)
             }
             with(tiles?.elementAt(2)!!) {
                 assertThat(this.text).isEqualTo("Enabled")
@@ -176,18 +176,24 @@
             )
             runCurrent()
 
-            assertThat(tiles).hasSize(3)
+            // Manual DND is included by default
+            assertThat(tiles).hasSize(4)
             with(tiles?.elementAt(0)!!) {
+                assertThat(this.text).isEqualTo("Do Not Disturb")
+                assertThat(this.subtext).isEqualTo("Off")
+                assertThat(this.enabled).isEqualTo(false)
+            }
+            with(tiles?.elementAt(1)!!) {
                 assertThat(this.text).isEqualTo("Active without manual")
                 assertThat(this.subtext).isEqualTo("On")
                 assertThat(this.enabled).isEqualTo(true)
             }
-            with(tiles?.elementAt(1)!!) {
+            with(tiles?.elementAt(2)!!) {
                 assertThat(this.text).isEqualTo("Active with manual")
                 assertThat(this.subtext).isEqualTo("On • trigger description")
                 assertThat(this.enabled).isEqualTo(true)
             }
-            with(tiles?.elementAt(2)!!) {
+            with(tiles?.elementAt(3)!!) {
                 assertThat(this.text).isEqualTo("Inactive with manual")
                 assertThat(this.subtext).isEqualTo("Off")
                 assertThat(this.enabled).isEqualTo(false)
@@ -226,10 +232,11 @@
             )
             runCurrent()
 
-            assertThat(tiles).hasSize(3)
+            // Manual DND is included by default
+            assertThat(tiles).hasSize(4)
 
             // Check that tile is initially present
-            with(tiles?.elementAt(0)!!) {
+            with(tiles?.elementAt(1)!!) {
                 assertThat(this.text).isEqualTo("Active without manual")
                 assertThat(this.subtext).isEqualTo("On")
                 assertThat(this.enabled).isEqualTo(true)
@@ -239,8 +246,8 @@
                 runCurrent()
             }
             // Check that tile is still present at the same location, but turned off
-            assertThat(tiles).hasSize(3)
-            with(tiles?.elementAt(0)!!) {
+            assertThat(tiles).hasSize(4)
+            with(tiles?.elementAt(1)!!) {
                 assertThat(this.text).isEqualTo("Active without manual")
                 assertThat(this.subtext).isEqualTo("Manage in settings")
                 assertThat(this.enabled).isEqualTo(false)
@@ -252,9 +259,9 @@
             runCurrent()
 
             // Check that tile is now gone
-            assertThat(tiles2).hasSize(2)
-            assertThat(tiles2?.elementAt(0)!!.text).isEqualTo("Active with manual")
-            assertThat(tiles2?.elementAt(1)!!.text).isEqualTo("Inactive with manual")
+            assertThat(tiles2).hasSize(3)
+            assertThat(tiles2?.elementAt(1)!!.text).isEqualTo("Active with manual")
+            assertThat(tiles2?.elementAt(2)!!.text).isEqualTo("Inactive with manual")
         }
 
     @Test
@@ -287,22 +294,23 @@
             )
             runCurrent()
 
-            assertThat(tiles).hasSize(3)
+            // Manual DND is included by default
+            assertThat(tiles).hasSize(4)
 
             repository.removeMode("A")
             runCurrent()
 
-            assertThat(tiles).hasSize(2)
+            assertThat(tiles).hasSize(3)
 
             repository.removeMode("B")
             runCurrent()
 
-            assertThat(tiles).hasSize(1)
+            assertThat(tiles).hasSize(2)
 
             repository.removeMode("C")
             runCurrent()
 
-            assertThat(tiles).hasSize(0)
+            assertThat(tiles).hasSize(1)
         }
 
     @Test
@@ -353,14 +361,15 @@
             )
             runCurrent()
 
-            assertThat(tiles!!).hasSize(7)
-            assertThat(tiles!![0].subtext).isEqualTo("When the going gets tough")
-            assertThat(tiles!![1].subtext).isEqualTo("On • When in Rome")
-            assertThat(tiles!![2].subtext).isEqualTo("Not set")
-            assertThat(tiles!![3].subtext).isEqualTo("Off")
-            assertThat(tiles!![4].subtext).isEqualTo("On")
-            assertThat(tiles!![5].subtext).isEqualTo("Not set")
-            assertThat(tiles!![6].subtext).isEqualTo(timeScheduleMode.triggerDescription)
+            // Manual DND is included by default
+            assertThat(tiles!!).hasSize(8)
+            assertThat(tiles!![1].subtext).isEqualTo("When the going gets tough")
+            assertThat(tiles!![2].subtext).isEqualTo("On • When in Rome")
+            assertThat(tiles!![3].subtext).isEqualTo("Not set")
+            assertThat(tiles!![4].subtext).isEqualTo("Off")
+            assertThat(tiles!![5].subtext).isEqualTo("On")
+            assertThat(tiles!![6].subtext).isEqualTo("Not set")
+            assertThat(tiles!![7].subtext).isEqualTo(timeScheduleMode.triggerDescription)
         }
 
     @Test
@@ -411,32 +420,33 @@
             )
             runCurrent()
 
-            assertThat(tiles!!).hasSize(7)
-            with(tiles?.elementAt(0)!!) {
+            // Manual DND is included by default
+            assertThat(tiles!!).hasSize(8)
+            with(tiles?.elementAt(1)!!) {
                 assertThat(this.stateDescription).isEqualTo("Off")
                 assertThat(this.subtextDescription).isEqualTo("When the going gets tough")
             }
-            with(tiles?.elementAt(1)!!) {
+            with(tiles?.elementAt(2)!!) {
                 assertThat(this.stateDescription).isEqualTo("On")
                 assertThat(this.subtextDescription).isEqualTo("When in Rome")
             }
-            with(tiles?.elementAt(2)!!) {
+            with(tiles?.elementAt(3)!!) {
                 assertThat(this.stateDescription).isEqualTo("Off")
                 assertThat(this.subtextDescription).isEqualTo("Not set")
             }
-            with(tiles?.elementAt(3)!!) {
-                assertThat(this.stateDescription).isEqualTo("Off")
-                assertThat(this.subtextDescription).isEmpty()
-            }
             with(tiles?.elementAt(4)!!) {
-                assertThat(this.stateDescription).isEqualTo("On")
+                assertThat(this.stateDescription).isEqualTo("Off")
                 assertThat(this.subtextDescription).isEmpty()
             }
             with(tiles?.elementAt(5)!!) {
+                assertThat(this.stateDescription).isEqualTo("On")
+                assertThat(this.subtextDescription).isEmpty()
+            }
+            with(tiles?.elementAt(6)!!) {
                 assertThat(this.stateDescription).isEqualTo("Off")
                 assertThat(this.subtextDescription).isEqualTo("Not set")
             }
-            with(tiles?.elementAt(6)!!) {
+            with(tiles?.elementAt(7)!!) {
                 assertThat(this.stateDescription).isEqualTo("Off")
                 assertThat(this.subtextDescription)
                     .isEqualTo(
@@ -456,31 +466,30 @@
             val tiles by collectLastValue(underTest.tiles)
 
             val modeId = "id"
-            repository.addModes(
-                listOf(
-                    TestModeBuilder()
-                        .setId(modeId)
-                        .setName("Test")
-                        .setManualInvocationAllowed(true)
-                        .build()
-                )
+            repository.addMode(
+                TestModeBuilder()
+                    .setId(modeId)
+                    .setName("Test")
+                    .setManualInvocationAllowed(true)
+                    .build()
             )
             runCurrent()
 
-            assertThat(tiles).hasSize(1)
-            assertThat(tiles?.elementAt(0)?.enabled).isFalse()
+            // Manual DND is included by default
+            assertThat(tiles).hasSize(2)
+            assertThat(tiles?.elementAt(1)?.enabled).isFalse()
 
             // Trigger onClick
-            tiles?.first()?.onClick?.let { it() }
+            tiles?.elementAt(1)?.onClick?.let { it() }
             runCurrent()
 
-            assertThat(tiles?.first()?.enabled).isTrue()
+            assertThat(tiles?.elementAt(1)?.enabled).isTrue()
 
             // Trigger onClick
-            tiles?.first()?.onClick?.let { it() }
+            tiles?.elementAt(1)?.onClick?.let { it() }
             runCurrent()
 
-            assertThat(tiles?.first()?.enabled).isFalse()
+            assertThat(tiles?.elementAt(1)?.enabled).isFalse()
         }
 
     @Test
@@ -489,25 +498,24 @@
             val job = Job()
             val tiles by collectLastValue(underTest.tiles, context = job)
 
-            repository.addModes(
-                listOf(
-                    TestModeBuilder()
-                        .setName("Active without manual")
-                        .setActive(true)
-                        .setManualInvocationAllowed(false)
-                        .build()
-                )
+            repository.addMode(
+                TestModeBuilder()
+                    .setName("Active without manual")
+                    .setActive(true)
+                    .setManualInvocationAllowed(false)
+                    .build()
             )
             runCurrent()
 
-            assertThat(tiles).hasSize(1)
+            // Manual DND is included by default
+            assertThat(tiles).hasSize(2)
 
             // Click tile to toggle it off
-            tiles?.elementAt(0)!!.onClick()
+            tiles?.elementAt(1)!!.onClick()
             runCurrent()
 
-            assertThat(tiles).hasSize(1)
-            with(tiles?.elementAt(0)!!) {
+            assertThat(tiles).hasSize(2)
+            with(tiles?.elementAt(1)!!) {
                 assertThat(this.text).isEqualTo("Active without manual")
                 assertThat(this.subtext).isEqualTo("Manage in settings")
                 assertThat(this.enabled).isEqualTo(false)
@@ -518,7 +526,7 @@
             }
 
             // Check that nothing happened
-            with(tiles?.elementAt(0)!!) {
+            with(tiles?.elementAt(1)!!) {
                 assertThat(this.text).isEqualTo("Active without manual")
                 assertThat(this.subtext).isEqualTo("Manage in settings")
                 assertThat(this.enabled).isEqualTo(false)
@@ -530,19 +538,18 @@
         testScope.runTest {
             val tiles by collectLastValue(underTest.tiles)
 
-            repository.addModes(
-                listOf(
-                    TestModeBuilder()
-                        .setId("ID")
-                        .setName("Disabled by other")
-                        .setEnabled(false, /* byUser= */ false)
-                        .build()
-                )
+            repository.addMode(
+                TestModeBuilder()
+                    .setId("ID")
+                    .setName("Disabled by other")
+                    .setEnabled(false, /* byUser= */ false)
+                    .build()
             )
             runCurrent()
 
-            assertThat(tiles).hasSize(1)
-            with(tiles?.elementAt(0)!!) {
+            // Manual DND is included by default
+            assertThat(tiles).hasSize(2)
+            with(tiles?.elementAt(1)!!) {
                 assertThat(this.text).isEqualTo("Disabled by other")
                 assertThat(this.subtext).isEqualTo("Not set")
                 assertThat(this.enabled).isEqualTo(false)
@@ -561,7 +568,7 @@
                 .isEqualTo("ID")
 
             // Check that nothing happened to the tile
-            with(tiles?.elementAt(0)!!) {
+            with(tiles?.elementAt(1)!!) {
                 assertThat(this.text).isEqualTo("Disabled by other")
                 assertThat(this.subtext).isEqualTo("Not set")
                 assertThat(this.enabled).isEqualTo(false)
@@ -593,10 +600,11 @@
             )
             runCurrent()
 
-            assertThat(tiles).hasSize(2)
+            // Manual DND is included by default
+            assertThat(tiles).hasSize(3)
 
             // Trigger onLongClick for A
-            tiles?.first()?.onLongClick?.let { it() }
+            tiles?.elementAt(1)?.onLongClick?.let { it() }
             runCurrent()
 
             // Check that it launched the correct intent
@@ -625,9 +633,9 @@
         testScope.runTest {
             val tiles by collectLastValue(underTest.tiles)
 
+            repository.activateMode(MANUAL_DND)
             repository.addModes(
                 listOf(
-                    TestModeBuilder.MANUAL_DND_ACTIVE,
                     TestModeBuilder()
                         .setId("id1")
                         .setName("Inactive Mode One")
@@ -644,6 +652,7 @@
             )
             runCurrent()
 
+            // Manual DND is included by default
             assertThat(tiles).hasSize(3)
 
             // Trigger onClick for each tile in sequence
@@ -672,19 +681,17 @@
         testScope.runTest {
             val tiles by collectLastValue(underTest.tiles)
 
-            repository.addModes(
-                listOf(
-                    TestModeBuilder.MANUAL_DND_ACTIVE,
-                    TestModeBuilder()
-                        .setId("id1")
-                        .setName("Inactive Mode One")
-                        .setActive(false)
-                        .setManualInvocationAllowed(true)
-                        .build(),
-                )
+            repository.addMode(
+                TestModeBuilder()
+                    .setId("id1")
+                    .setName("Inactive Mode One")
+                    .setActive(false)
+                    .setManualInvocationAllowed(true)
+                    .build()
             )
             runCurrent()
 
+            // Manual DND is included by default
             assertThat(tiles).hasSize(2)
             val modeCaptor = argumentCaptor<ZenMode>()
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImplTest.kt
index 61c7193..824955d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImplTest.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.statusbar.policy.statusBarConfigurationController
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.any
@@ -41,13 +42,18 @@
     private val kosmos =
         testKosmos().also { it.statusBarWindowViewInflater = it.fakeStatusBarWindowViewInflater }
 
-    private val underTest = kosmos.statusBarWindowControllerImpl
+    private lateinit var underTest: StatusBarWindowControllerImpl
     private val fakeExecutor = kosmos.fakeExecutor
     private val fakeWindowManager = kosmos.fakeWindowManager
     private val mockFragmentService = kosmos.fragmentService
     private val fakeStatusBarWindowViewInflater = kosmos.fakeStatusBarWindowViewInflater
     private val statusBarConfigurationController = kosmos.statusBarConfigurationController
 
+    @Before
+    fun setUp() {
+        underTest = kosmos.statusBarWindowControllerImpl
+    }
+
     @Test
     @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
     fun attach_connectedDisplaysFlagEnabled_setsConfigControllerOnWindowView() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/StateTransitionsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/StateTransitionsTest.kt
deleted file mode 100644
index 2ad1124..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/StateTransitionsTest.kt
+++ /dev/null
@@ -1,111 +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.touchpad.tutorial.ui
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState
-import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Error
-import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Finished
-import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgress
-import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgressAfterError
-import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.NotStarted
-import com.android.systemui.touchpad.tutorial.ui.composable.toGestureUiState
-import com.android.systemui.touchpad.tutorial.ui.composable.toTutorialActionState
-import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class StateTransitionsTest : SysuiTestCase() {
-
-    companion object {
-        private const val START_MARKER = "startMarker"
-        private const val END_MARKER = "endMarker"
-        private const val SUCCESS_ANIMATION = 0
-    }
-
-    // needed to simulate caching last state as it's used to create new state
-    private var lastState: TutorialActionState = NotStarted
-
-    private fun GestureState.toTutorialActionState(): TutorialActionState {
-        val newState =
-            this.toGestureUiState(
-                    progressStartMarker = START_MARKER,
-                    progressEndMarker = END_MARKER,
-                    successAnimation = SUCCESS_ANIMATION,
-                )
-                .toTutorialActionState(lastState)
-        lastState = newState
-        return lastState
-    }
-
-    @Test
-    fun gestureStateProducesEquivalentTutorialActionStateInHappyPath() {
-        val happyPath =
-            listOf(
-                GestureState.NotStarted,
-                GestureState.InProgress(0f),
-                GestureState.InProgress(0.5f),
-                GestureState.InProgress(1f),
-                GestureState.Finished,
-            )
-
-        val resultingStates = mutableListOf<TutorialActionState>()
-        happyPath.forEach { resultingStates.add(it.toTutorialActionState()) }
-
-        assertThat(resultingStates)
-            .containsExactly(
-                NotStarted,
-                InProgress(0f, START_MARKER, END_MARKER),
-                InProgress(0.5f, START_MARKER, END_MARKER),
-                InProgress(1f, START_MARKER, END_MARKER),
-                Finished(SUCCESS_ANIMATION),
-            )
-            .inOrder()
-    }
-
-    @Test
-    fun gestureStateProducesEquivalentTutorialActionStateInErrorPath() {
-        val errorPath =
-            listOf(
-                GestureState.NotStarted,
-                GestureState.InProgress(0f),
-                GestureState.Error,
-                GestureState.InProgress(0.5f),
-                GestureState.InProgress(1f),
-                GestureState.Finished,
-            )
-
-        val resultingStates = mutableListOf<TutorialActionState>()
-        errorPath.forEach { resultingStates.add(it.toTutorialActionState()) }
-
-        assertThat(resultingStates)
-            .containsExactly(
-                NotStarted,
-                InProgress(0f, START_MARKER, END_MARKER),
-                Error,
-                InProgressAfterError(InProgress(0.5f, START_MARKER, END_MARKER)),
-                InProgressAfterError(InProgress(1f, START_MARKER, END_MARKER)),
-                Finished(SUCCESS_ANIMATION),
-            )
-            .inOrder()
-    }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModelTest.kt
index 4aec88e..d752046 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModelTest.kt
@@ -23,16 +23,16 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
 import com.android.systemui.inputdevice.tutorial.inputDeviceTutorialLogger
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Error
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Finished
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgress
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.collectLastValue
 import com.android.systemui.kosmos.runTest
 import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.res.R
 import com.android.systemui.testKosmos
-import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState
-import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.Error
-import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.Finished
-import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.InProgress
 import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
 import com.android.systemui.touchpad.tutorial.ui.gesture.ThreeFingerGesture
 import com.android.systemui.touchpad.ui.gesture.touchpadGestureResources
@@ -71,8 +71,8 @@
                 expected =
                     InProgress(
                         progress = 1f,
-                        progressStartMarker = "gesture to L",
-                        progressEndMarker = "end progress L",
+                        startMarker = "gesture to L",
+                        endMarker = "end progress L",
                     ),
             )
         }
@@ -85,8 +85,8 @@
                 expected =
                     InProgress(
                         progress = 1f,
-                        progressStartMarker = "gesture to R",
-                        progressEndMarker = "end progress R",
+                        startMarker = "gesture to R",
+                        endMarker = "end progress R",
                     ),
             )
         }
@@ -114,7 +114,7 @@
         kosmos.runTest {
             fun performBackGesture() =
                 ThreeFingerGesture.swipeLeft().forEach { viewModel.handleEvent(it) }
-            val state by collectLastValue(viewModel.gestureUiState)
+            val state by collectLastValue(viewModel.tutorialState)
             performBackGesture()
             assertThat(state).isInstanceOf(Finished::class.java)
 
@@ -134,15 +134,21 @@
         fakeConfigRepository.onAnyConfigurationChange()
     }
 
-    private fun Kosmos.assertProgressWhileMovingFingers(deltaX: Float, expected: GestureUiState) {
+    private fun Kosmos.assertProgressWhileMovingFingers(
+        deltaX: Float,
+        expected: TutorialActionState,
+    ) {
         assertStateAfterEvents(
             events = ThreeFingerGesture.eventsForGestureInProgress { move(deltaX = deltaX) },
             expected = expected,
         )
     }
 
-    private fun Kosmos.assertStateAfterEvents(events: List<MotionEvent>, expected: GestureUiState) {
-        val state by collectLastValue(viewModel.gestureUiState)
+    private fun Kosmos.assertStateAfterEvents(
+        events: List<MotionEvent>,
+        expected: TutorialActionState,
+    ) {
+        val state by collectLastValue(viewModel.tutorialState)
         events.forEach { viewModel.handleEvent(it) }
         assertThat(state).isEqualTo(expected)
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModelTest.kt
index 65a995d..7862fd3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModelTest.kt
@@ -23,16 +23,16 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
 import com.android.systemui.inputdevice.tutorial.inputDeviceTutorialLogger
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Error
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Finished
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgress
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.collectLastValue
 import com.android.systemui.kosmos.runTest
 import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.res.R
 import com.android.systemui.testKosmos
-import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState
-import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.Error
-import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.Finished
-import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.InProgress
 import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
 import com.android.systemui.touchpad.tutorial.ui.gesture.ThreeFingerGesture
 import com.android.systemui.touchpad.tutorial.ui.gesture.Velocity
@@ -86,8 +86,8 @@
                 expected =
                     InProgress(
                         progress = 1f,
-                        progressStartMarker = "drag with gesture",
-                        progressEndMarker = "release playback realtime",
+                        startMarker = "drag with gesture",
+                        endMarker = "release playback realtime",
                     ),
             )
         }
@@ -108,7 +108,7 @@
     @Test
     fun gestureRecognitionTakesLatestDistanceThresholdIntoAccount() =
         kosmos.runTest {
-            val state by collectLastValue(viewModel.gestureUiState)
+            val state by collectLastValue(viewModel.tutorialState)
             performHomeGesture()
             assertThat(state).isInstanceOf(Finished::class.java)
 
@@ -121,7 +121,7 @@
     @Test
     fun gestureRecognitionTakesLatestVelocityThresholdIntoAccount() =
         kosmos.runTest {
-            val state by collectLastValue(viewModel.gestureUiState)
+            val state by collectLastValue(viewModel.tutorialState)
             performHomeGesture()
             assertThat(state).isInstanceOf(Finished::class.java)
 
@@ -147,8 +147,11 @@
         fakeConfigRepository.onAnyConfigurationChange()
     }
 
-    private fun Kosmos.assertStateAfterEvents(events: List<MotionEvent>, expected: GestureUiState) {
-        val state by collectLastValue(viewModel.gestureUiState)
+    private fun Kosmos.assertStateAfterEvents(
+        events: List<MotionEvent>,
+        expected: TutorialActionState,
+    ) {
+        val state by collectLastValue(viewModel.tutorialState)
         events.forEach { viewModel.handleEvent(it) }
         assertThat(state).isEqualTo(expected)
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModelTest.kt
index 1bc60b6..6180fa9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModelTest.kt
@@ -23,16 +23,16 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
 import com.android.systemui.inputdevice.tutorial.inputDeviceTutorialLogger
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Error
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Finished
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgress
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.collectLastValue
 import com.android.systemui.kosmos.runTest
 import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.res.R
 import com.android.systemui.testKosmos
-import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState
-import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.Error
-import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.Finished
-import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.InProgress
 import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
 import com.android.systemui.touchpad.tutorial.ui.gesture.ThreeFingerGesture
 import com.android.systemui.touchpad.tutorial.ui.gesture.Velocity
@@ -89,8 +89,8 @@
                 expected =
                     InProgress(
                         progress = 1f,
-                        progressStartMarker = "drag with gesture",
-                        progressEndMarker = "onPause",
+                        startMarker = "drag with gesture",
+                        endMarker = "onPause",
                     ),
             )
         }
@@ -111,7 +111,7 @@
     @Test
     fun gestureRecognitionTakesLatestDistanceThresholdIntoAccount() =
         kosmos.runTest {
-            val state by collectLastValue(viewModel.gestureUiState)
+            val state by collectLastValue(viewModel.tutorialState)
             performRecentAppsGesture()
             assertThat(state).isInstanceOf(Finished::class.java)
 
@@ -124,7 +124,7 @@
     @Test
     fun gestureRecognitionTakesLatestVelocityThresholdIntoAccount() =
         kosmos.runTest {
-            val state by collectLastValue(viewModel.gestureUiState)
+            val state by collectLastValue(viewModel.tutorialState)
             performRecentAppsGesture()
             assertThat(state).isInstanceOf(Finished::class.java)
 
@@ -150,8 +150,11 @@
         fakeConfigRepository.onAnyConfigurationChange()
     }
 
-    private fun Kosmos.assertStateAfterEvents(events: List<MotionEvent>, expected: GestureUiState) {
-        val state by collectLastValue(viewModel.gestureUiState)
+    private fun Kosmos.assertStateAfterEvents(
+        events: List<MotionEvent>,
+        expected: TutorialActionState,
+    ) {
+        val state by collectLastValue(viewModel.tutorialState)
         events.forEach { viewModel.handleEvent(it) }
         assertThat(state).isEqualTo(expected)
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialScreenViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialScreenViewModelTest.kt
new file mode 100644
index 0000000..c113dd9
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialScreenViewModelTest.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.touchpad.tutorial.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Error
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Finished
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgress
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgressAfterError
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.NotStarted
+import com.android.systemui.kosmos.collectValues
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.testKosmos
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asFlow
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TouchpadTutorialScreenViewModelTest : SysuiTestCase() {
+
+    companion object {
+        private const val START_MARKER = "startMarker"
+        private const val END_MARKER = "endMarker"
+        private const val SUCCESS_ANIMATION = 0
+    }
+
+    private val kosmos = testKosmos()
+    private val animationProperties =
+        TutorialAnimationProperties(
+            progressStartMarker = START_MARKER,
+            progressEndMarker = END_MARKER,
+            successAnimation = SUCCESS_ANIMATION,
+        )
+
+    @Before
+    fun before() {
+        kosmos.useUnconfinedTestDispatcher()
+    }
+
+    @Test
+    fun gestureStateProducesEquivalentTutorialActionStateInHappyPath() =
+        kosmos.runTest {
+            val happyPath: Flow<Pair<GestureState, TutorialAnimationProperties>> =
+                listOf(
+                        GestureState.NotStarted,
+                        GestureState.InProgress(0f),
+                        GestureState.InProgress(0.5f),
+                        GestureState.InProgress(1f),
+                        GestureState.Finished,
+                    )
+                    .map { it to animationProperties }
+                    .asFlow()
+
+            val resultingStates by collectValues(happyPath.mapToTutorialState())
+
+            assertThat(resultingStates)
+                .containsExactly(
+                    NotStarted,
+                    InProgress(0f, START_MARKER, END_MARKER),
+                    InProgress(0.5f, START_MARKER, END_MARKER),
+                    InProgress(1f, START_MARKER, END_MARKER),
+                    Finished(SUCCESS_ANIMATION),
+                )
+                .inOrder()
+        }
+
+    @Test
+    fun gestureStateProducesEquivalentTutorialActionStateInErrorPath() =
+        kosmos.runTest {
+            val errorPath: Flow<Pair<GestureState, TutorialAnimationProperties>> =
+                listOf(
+                        GestureState.NotStarted,
+                        GestureState.InProgress(0f),
+                        GestureState.Error,
+                        GestureState.InProgress(0.5f),
+                        GestureState.InProgress(1f),
+                        GestureState.Finished,
+                    )
+                    .map { it to animationProperties }
+                    .asFlow()
+
+            val resultingStates by collectValues(errorPath.mapToTutorialState())
+
+            assertThat(resultingStates)
+                .containsExactly(
+                    NotStarted,
+                    InProgress(0f, START_MARKER, END_MARKER),
+                    Error,
+                    InProgressAfterError(InProgress(0.5f, START_MARKER, END_MARKER)),
+                    InProgressAfterError(InProgress(1f, START_MARKER, END_MARKER)),
+                    Finished(SUCCESS_ANIMATION),
+                )
+                .inOrder()
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt
index d3071f8..51cac69 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt
@@ -23,66 +23,40 @@
 import android.service.notification.ZenPolicy
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.internal.logging.uiEventLogger
 import com.android.settingslib.notification.modes.TestModeBuilder
 import com.android.settingslib.volume.shared.model.AudioStream
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory
-import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runCurrent
+import com.android.systemui.kosmos.runTest
 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.android.systemui.volume.domain.interactor.audioVolumeInteractor
-import com.android.systemui.volume.shared.volumePanelLogger
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class AudioStreamSliderViewModelTest : SysuiTestCase() {
 
     private val kosmos = testKosmos()
-    private val testScope = kosmos.testScope
     private val zenModeRepository = kosmos.fakeZenModeRepository
 
-    private lateinit var mediaStream: AudioStreamSliderViewModel
-    private lateinit var alarmsStream: AudioStreamSliderViewModel
-    private lateinit var notificationStream: AudioStreamSliderViewModel
-    private lateinit var otherStream: AudioStreamSliderViewModel
-
-    @Before
-    fun setUp() {
-        mediaStream = audioStreamSliderViewModel(AudioManager.STREAM_MUSIC)
-        alarmsStream = audioStreamSliderViewModel(AudioManager.STREAM_ALARM)
-        notificationStream = audioStreamSliderViewModel(AudioManager.STREAM_NOTIFICATION)
-        otherStream = audioStreamSliderViewModel(AudioManager.STREAM_VOICE_CALL)
-    }
-
-    private fun audioStreamSliderViewModel(stream: Int): AudioStreamSliderViewModel {
-        return AudioStreamSliderViewModel(
+    private fun Kosmos.audioStreamSliderViewModel(stream: Int): AudioStreamSliderViewModel {
+        return audioStreamSliderViewModelFactory.create(
             AudioStreamSliderViewModel.FactoryAudioStreamWrapper(AudioStream(stream)),
-            testScope.backgroundScope,
-            context,
-            kosmos.audioVolumeInteractor,
-            kosmos.zenModeInteractor,
-            kosmos.uiEventLogger,
-            kosmos.volumePanelLogger,
-            kosmos.sliderHapticsViewModelFactory,
+            applicationCoroutineScope,
         )
     }
 
     @Test
     @EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS)
     fun slider_media_hasDisabledByModesText() =
-        testScope.runTest {
-            val mediaSlider by collectLastValue(mediaStream.slider)
+        kosmos.runTest {
+            val mediaSlider by
+                collectLastValue(audioStreamSliderViewModel(AudioManager.STREAM_MUSIC).slider)
 
             zenModeRepository.addMode(
                 TestModeBuilder()
@@ -112,8 +86,9 @@
     @Test
     @EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS)
     fun slider_alarms_hasDisabledByModesText() =
-        testScope.runTest {
-            val alarmsSlider by collectLastValue(alarmsStream.slider)
+        kosmos.runTest {
+            val alarmsSlider by
+                collectLastValue(audioStreamSliderViewModel(AudioManager.STREAM_ALARM).slider)
 
             zenModeRepository.addMode(
                 TestModeBuilder()
@@ -141,9 +116,10 @@
 
     @Test
     @EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS)
-    fun slider_other_hasDisabledByModesText() =
-        testScope.runTest {
-            val otherSlider by collectLastValue(otherStream.slider)
+    fun slider_other_hasDisabledText() =
+        kosmos.runTest {
+            val otherSlider by
+                collectLastValue(audioStreamSliderViewModel(AudioManager.STREAM_VOICE_CALL).slider)
 
             zenModeRepository.addMode(
                 TestModeBuilder()
@@ -154,20 +130,17 @@
             )
             runCurrent()
 
-            assertThat(otherSlider!!.disabledMessage)
-                .isEqualTo("Unavailable because Everything blocked is on")
-
-            zenModeRepository.clearModes()
-            runCurrent()
-
             assertThat(otherSlider!!.disabledMessage).isEqualTo("Unavailable")
         }
 
     @Test
     @EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS)
     fun slider_notification_hasSpecialDisabledText() =
-        testScope.runTest {
-            val notificationSlider by collectLastValue(notificationStream.slider)
+        kosmos.runTest {
+            val notificationSlider by
+                collectLastValue(
+                    audioStreamSliderViewModel(AudioManager.STREAM_NOTIFICATION).slider
+                )
             runCurrent()
 
             assertThat(notificationSlider!!.disabledMessage)
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt
index d84d890..812a964 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt
@@ -30,10 +30,6 @@
     /** Transition to AOD should move smartspace like large clock instead of small clock */
     val useAlternateSmartspaceAODTransition: Boolean = false,
 
-    /** Deprecated version of isReactiveToTone; moved to ClockPickerConfig */
-    @Deprecated("TODO(b/352049256): Remove in favor of ClockPickerConfig.isReactiveToTone")
-    val isReactiveToTone: Boolean = true,
-
     /** True if the clock is large frame clock, which will use weather in compose. */
     val useCustomClockScene: Boolean = false,
 )
diff --git a/packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt b/packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt
index d93f7d3..81156d9 100644
--- a/packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt
+++ b/packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt
@@ -24,6 +24,7 @@
 import javax.lang.model.element.Element
 import javax.lang.model.element.ElementKind
 import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.Modifier
 import javax.lang.model.element.PackageElement
 import javax.lang.model.element.TypeElement
 import javax.lang.model.type.TypeKind
@@ -183,11 +184,17 @@
                     // Method implementations
                     for (method in methods) {
                         val methodName = method.simpleName
+                        if (methods.any { methodName.startsWith("${it.simpleName}\$") }) {
+                            continue
+                        }
                         val returnTypeName = method.returnType.toString()
                         val callArgs = StringBuilder()
                         var isFirst = true
+                        val isStatic = method.modifiers.contains(Modifier.STATIC)
 
-                        line("@Override")
+                        if (!isStatic) {
+                            line("@Override")
+                        }
                         parenBlock("public $returnTypeName $methodName") {
                             // While copying the method signature for the proxy type, we also
                             // accumulate arguments for the nested callsite.
@@ -202,7 +209,8 @@
                         }
 
                         val isVoid = method.returnType.kind == TypeKind.VOID
-                        val nestedCall = "mInstance.$methodName($callArgs)"
+                        val methodContainer = if (isStatic) sourceName else "mInstance"
+                        val nestedCall = "$methodContainer.$methodName($callArgs)"
                         val callStatement =
                             when {
                                 isVoid -> "$nestedCall;"
diff --git a/packages/SystemUI/proguard_common.flags b/packages/SystemUI/proguard_common.flags
index 162d8ae..02b2bcf 100644
--- a/packages/SystemUI/proguard_common.flags
+++ b/packages/SystemUI/proguard_common.flags
@@ -1,5 +1,11 @@
 -include proguard_kotlin.flags
--keep class com.android.systemui.VendorServices
+
+# VendorServices implements CoreStartable and may be instantiated reflectively in
+# SystemUIApplication#startAdditionalStartable.
+# TODO(b/373579455): Rewrite this to a @UsesReflection keep annotation.
+-keep class com.android.systemui.VendorServices {
+  public void <init>();
+}
 
 # Needed to ensure callback field references are kept in their respective
 # owning classes when the downstream callback registrars only store weak refs.
diff --git a/packages/SystemUI/res/color/active_track_color.xml b/packages/SystemUI/res/color/active_track_color.xml
new file mode 100644
index 0000000..2325555
--- /dev/null
+++ b/packages/SystemUI/res/color/active_track_color.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2025 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.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+    <item android:color="@androidprv:color/materialColorPrimary" android:state_enabled="true" />
+    <item android:alpha="0.38" android:color="@androidprv:color/materialColorOnSurface" />
+</selector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/color/inactive_track_color.xml b/packages/SystemUI/res/color/inactive_track_color.xml
new file mode 100644
index 0000000..2ba5ebd
--- /dev/null
+++ b/packages/SystemUI/res/color/inactive_track_color.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2025 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.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+    <item android:color="@androidprv:color/materialColorSurfaceContainerHighest" android:state_enabled="true" />
+    <item android:alpha="0.12" android:color="@androidprv:color/materialColorOnSurface" />
+</selector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/color/on_active_track_color.xml b/packages/SystemUI/res/color/on_active_track_color.xml
new file mode 100644
index 0000000..7ca79a9
--- /dev/null
+++ b/packages/SystemUI/res/color/on_active_track_color.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2025 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.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+    <item android:color="@androidprv:color/materialColorOnPrimary" android:state_enabled="true" />
+    <item android:color="@androidprv:color/materialColorOnSurfaceVariant" />
+</selector>
diff --git a/packages/SystemUI/res/color/on_inactive_track_color.xml b/packages/SystemUI/res/color/on_inactive_track_color.xml
new file mode 100644
index 0000000..0eb4bfa
--- /dev/null
+++ b/packages/SystemUI/res/color/on_inactive_track_color.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2025 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.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+    <item android:color="@androidprv:color/materialColorPrimary" android:state_enabled="true" />
+    <item android:color="@androidprv:color/materialColorOnSurfaceVariant" />
+</selector>
diff --git a/packages/SystemUI/res/color/thumb_color.xml b/packages/SystemUI/res/color/thumb_color.xml
new file mode 100644
index 0000000..2b0e3a9
--- /dev/null
+++ b/packages/SystemUI/res/color/thumb_color.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2025 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.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+    <item android:color="@androidprv:color/materialColorPrimary" android:state_enabled="true" />
+    <item android:alpha="0.38" android:color="@androidprv:color/materialColorOnSurface" />
+</selector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_media_connecting_status_container.xml b/packages/SystemUI/res/drawable/ic_media_connecting_status_container.xml
new file mode 100644
index 0000000..f8c0fa0
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_media_connecting_status_container.xml
@@ -0,0 +1,199 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2025 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.
+  -->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <target android:name="_R_G_L_1_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="83"
+                    android:propertyName="scaleX"
+                    android:startOffset="1000"
+                    android:valueFrom="0.45561"
+                    android:valueTo="0.69699"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="83"
+                    android:propertyName="scaleY"
+                    android:startOffset="1000"
+                    android:valueFrom="0.6288400000000001"
+                    android:valueTo="0.81618"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="417"
+                    android:propertyName="scaleX"
+                    android:startOffset="1083"
+                    android:valueFrom="0.69699"
+                    android:valueTo="1.05905"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="417"
+                    android:propertyName="scaleY"
+                    android:startOffset="1083"
+                    android:valueFrom="0.81618"
+                    android:valueTo="1.0972"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="500"
+                    android:propertyName="rotation"
+                    android:startOffset="0"
+                    android:valueFrom="90"
+                    android:valueTo="135"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="500"
+                    android:propertyName="rotation"
+                    android:startOffset="500"
+                    android:valueFrom="135"
+                    android:valueTo="180"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="83"
+                    android:propertyName="scaleX"
+                    android:startOffset="1000"
+                    android:valueFrom="0.0434"
+                    android:valueTo="0.05063"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="83"
+                    android:propertyName="scaleY"
+                    android:startOffset="1000"
+                    android:valueFrom="0.0434"
+                    android:valueTo="0.042350000000000006"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="417"
+                    android:propertyName="scaleX"
+                    android:startOffset="1083"
+                    android:valueFrom="0.05063"
+                    android:valueTo="0.06146"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="417"
+                    android:propertyName="scaleY"
+                    android:startOffset="1083"
+                    android:valueFrom="0.042350000000000006"
+                    android:valueTo="0.040780000000000004"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="1017"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="88dp"
+            android:height="56dp"
+            android:viewportHeight="56"
+            android:viewportWidth="88">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_1_G"
+                    android:pivotX="0.493"
+                    android:pivotY="0.124"
+                    android:scaleX="1.05905"
+                    android:scaleY="1.0972"
+                    android:translateX="43.528999999999996"
+                    android:translateY="27.898">
+                    <path
+                        android:name="_R_G_L_1_G_D_0_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#3d90ff"
+                        android:fillType="nonZero"
+                        android:pathData=" M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c " />
+                </group>
+                <group
+                    android:name="_R_G_L_0_G"
+                    android:rotation="0"
+                    android:scaleX="0.06146"
+                    android:scaleY="0.040780000000000004"
+                    android:translateX="44"
+                    android:translateY="28">
+                    <path
+                        android:name="_R_G_L_0_G_D_0_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#3d90ff"
+                        android:fillType="nonZero"
+                        android:pathData=" M-0.65 -437.37 C-0.65,-437.37 8.33,-437.66 8.33,-437.66 C8.33,-437.66 17.31,-437.95 17.31,-437.95 C17.31,-437.95 26.25,-438.78 26.25,-438.78 C26.25,-438.78 35.16,-439.95 35.16,-439.95 C35.16,-439.95 44.07,-441.11 44.07,-441.11 C44.07,-441.11 52.85,-443 52.85,-443 C52.85,-443 61.6,-445.03 61.6,-445.03 C61.6,-445.03 70.35,-447.09 70.35,-447.09 C70.35,-447.09 78.91,-449.83 78.91,-449.83 C78.91,-449.83 87.43,-452.67 87.43,-452.67 C87.43,-452.67 95.79,-455.97 95.79,-455.97 C95.79,-455.97 104.11,-459.35 104.11,-459.35 C104.11,-459.35 112.36,-462.93 112.36,-462.93 C112.36,-462.93 120.6,-466.51 120.6,-466.51 C120.6,-466.51 128.84,-470.09 128.84,-470.09 C128.84,-470.09 137.09,-473.67 137.09,-473.67 C137.09,-473.67 145.49,-476.84 145.49,-476.84 C145.49,-476.84 153.9,-480.01 153.9,-480.01 C153.9,-480.01 162.31,-483.18 162.31,-483.18 C162.31,-483.18 170.98,-485.54 170.98,-485.54 C170.98,-485.54 179.66,-487.85 179.66,-487.85 C179.66,-487.85 188.35,-490.15 188.35,-490.15 C188.35,-490.15 197.22,-491.58 197.22,-491.58 C197.22,-491.58 206.09,-493.01 206.09,-493.01 C206.09,-493.01 214.98,-494.28 214.98,-494.28 C214.98,-494.28 223.95,-494.81 223.95,-494.81 C223.95,-494.81 232.93,-495.33 232.93,-495.33 C232.93,-495.33 241.9,-495.5 241.9,-495.5 C241.9,-495.5 250.88,-495.13 250.88,-495.13 C250.88,-495.13 259.86,-494.75 259.86,-494.75 C259.86,-494.75 268.78,-493.78 268.78,-493.78 C268.78,-493.78 277.68,-492.52 277.68,-492.52 C277.68,-492.52 286.57,-491.26 286.57,-491.26 C286.57,-491.26 295.31,-489.16 295.31,-489.16 C295.31,-489.16 304.04,-487.04 304.04,-487.04 C304.04,-487.04 312.7,-484.65 312.7,-484.65 C312.7,-484.65 321.19,-481.72 321.19,-481.72 C321.19,-481.72 329.68,-478.78 329.68,-478.78 C329.68,-478.78 337.96,-475.31 337.96,-475.31 C337.96,-475.31 346.14,-471.59 346.14,-471.59 C346.14,-471.59 354.3,-467.82 354.3,-467.82 C354.3,-467.82 362.11,-463.38 362.11,-463.38 C362.11,-463.38 369.92,-458.93 369.92,-458.93 C369.92,-458.93 377.53,-454.17 377.53,-454.17 C377.53,-454.17 384.91,-449.04 384.91,-449.04 C384.91,-449.04 392.29,-443.91 392.29,-443.91 C392.29,-443.91 399.26,-438.24 399.26,-438.24 C399.26,-438.24 406.15,-432.48 406.15,-432.48 C406.15,-432.48 412.92,-426.57 412.92,-426.57 C412.92,-426.57 419.27,-420.22 419.27,-420.22 C419.27,-420.22 425.62,-413.87 425.62,-413.87 C425.62,-413.87 431.61,-407.18 431.61,-407.18 C431.61,-407.18 437.38,-400.29 437.38,-400.29 C437.38,-400.29 443.14,-393.39 443.14,-393.39 C443.14,-393.39 448.27,-386.01 448.27,-386.01 C448.27,-386.01 453.4,-378.64 453.4,-378.64 C453.4,-378.64 458.26,-371.09 458.26,-371.09 C458.26,-371.09 462.71,-363.28 462.71,-363.28 C462.71,-363.28 467.16,-355.47 467.16,-355.47 C467.16,-355.47 471.03,-347.37 471.03,-347.37 C471.03,-347.37 474.75,-339.19 474.75,-339.19 C474.75,-339.19 478.34,-330.95 478.34,-330.95 C478.34,-330.95 481.28,-322.46 481.28,-322.46 C481.28,-322.46 484.21,-313.97 484.21,-313.97 C484.21,-313.97 486.72,-305.35 486.72,-305.35 C486.72,-305.35 488.84,-296.62 488.84,-296.62 C488.84,-296.62 490.96,-287.88 490.96,-287.88 C490.96,-287.88 492.33,-279.01 492.33,-279.01 C492.33,-279.01 493.59,-270.11 493.59,-270.11 C493.59,-270.11 494.69,-261.2 494.69,-261.2 C494.69,-261.2 495.07,-252.22 495.07,-252.22 C495.07,-252.22 495.44,-243.24 495.44,-243.24 C495.44,-243.24 495.41,-234.27 495.41,-234.27 C495.41,-234.27 494.88,-225.29 494.88,-225.29 C494.88,-225.29 494.35,-216.32 494.35,-216.32 C494.35,-216.32 493.22,-207.42 493.22,-207.42 C493.22,-207.42 491.79,-198.55 491.79,-198.55 C491.79,-198.55 490.36,-189.68 490.36,-189.68 C490.36,-189.68 488.19,-180.96 488.19,-180.96 C488.19,-180.96 485.88,-172.28 485.88,-172.28 C485.88,-172.28 483.56,-163.6 483.56,-163.6 C483.56,-163.6 480.48,-155.16 480.48,-155.16 C480.48,-155.16 477.31,-146.75 477.31,-146.75 C477.31,-146.75 474.14,-138.34 474.14,-138.34 C474.14,-138.34 470.62,-130.07 470.62,-130.07 C470.62,-130.07 467.04,-121.83 467.04,-121.83 C467.04,-121.83 463.46,-113.59 463.46,-113.59 C463.46,-113.59 459.88,-105.35 459.88,-105.35 C459.88,-105.35 456.54,-97.01 456.54,-97.01 C456.54,-97.01 453.37,-88.6 453.37,-88.6 C453.37,-88.6 450.21,-80.19 450.21,-80.19 C450.21,-80.19 447.68,-71.57 447.68,-71.57 C447.68,-71.57 445.36,-62.89 445.36,-62.89 C445.36,-62.89 443.04,-54.21 443.04,-54.21 C443.04,-54.21 441.54,-45.35 441.54,-45.35 C441.54,-45.35 440.09,-36.48 440.09,-36.48 C440.09,-36.48 438.78,-27.6 438.78,-27.6 C438.78,-27.6 438.19,-18.63 438.19,-18.63 C438.19,-18.63 437.61,-9.66 437.61,-9.66 C437.61,-9.66 437.36,-0.69 437.36,-0.69 C437.36,-0.69 437.65,8.29 437.65,8.29 C437.65,8.29 437.95,17.27 437.95,17.27 C437.95,17.27 438.77,26.21 438.77,26.21 C438.77,26.21 439.94,35.12 439.94,35.12 C439.94,35.12 441.11,44.03 441.11,44.03 C441.11,44.03 442.99,52.81 442.99,52.81 C442.99,52.81 445.02,61.57 445.02,61.57 C445.02,61.57 447.07,70.31 447.07,70.31 C447.07,70.31 449.82,78.87 449.82,78.87 C449.82,78.87 452.65,87.4 452.65,87.4 C452.65,87.4 455.96,95.75 455.96,95.75 C455.96,95.75 459.33,104.08 459.33,104.08 C459.33,104.08 462.91,112.32 462.91,112.32 C462.91,112.32 466.49,120.57 466.49,120.57 C466.49,120.57 470.07,128.81 470.07,128.81 C470.07,128.81 473.65,137.05 473.65,137.05 C473.65,137.05 476.82,145.46 476.82,145.46 C476.82,145.46 479.99,153.87 479.99,153.87 C479.99,153.87 483.17,162.28 483.17,162.28 C483.17,162.28 485.52,170.94 485.52,170.94 C485.52,170.94 487.84,179.63 487.84,179.63 C487.84,179.63 490.14,188.31 490.14,188.31 C490.14,188.31 491.57,197.18 491.57,197.18 C491.57,197.18 493,206.06 493,206.06 C493,206.06 494.27,214.95 494.27,214.95 C494.27,214.95 494.8,223.92 494.8,223.92 C494.8,223.92 495.33,232.89 495.33,232.89 C495.33,232.89 495.5,241.86 495.5,241.86 C495.5,241.86 495.12,250.84 495.12,250.84 C495.12,250.84 494.75,259.82 494.75,259.82 C494.75,259.82 493.78,268.74 493.78,268.74 C493.78,268.74 492.52,277.64 492.52,277.64 C492.52,277.64 491.27,286.54 491.27,286.54 C491.27,286.54 489.16,295.27 489.16,295.27 C489.16,295.27 487.05,304.01 487.05,304.01 C487.05,304.01 484.66,312.66 484.66,312.66 C484.66,312.66 481.73,321.16 481.73,321.16 C481.73,321.16 478.79,329.65 478.79,329.65 C478.79,329.65 475.32,337.93 475.32,337.93 C475.32,337.93 471.6,346.11 471.6,346.11 C471.6,346.11 467.84,354.27 467.84,354.27 C467.84,354.27 463.39,362.08 463.39,362.08 C463.39,362.08 458.94,369.89 458.94,369.89 C458.94,369.89 454.19,377.5 454.19,377.5 C454.19,377.5 449.06,384.88 449.06,384.88 C449.06,384.88 443.93,392.26 443.93,392.26 C443.93,392.26 438.26,399.23 438.26,399.23 C438.26,399.23 432.5,406.12 432.5,406.12 C432.5,406.12 426.6,412.89 426.6,412.89 C426.6,412.89 420.24,419.24 420.24,419.24 C420.24,419.24 413.89,425.6 413.89,425.6 C413.89,425.6 407.2,431.59 407.2,431.59 C407.2,431.59 400.31,437.36 400.31,437.36 C400.31,437.36 393.42,443.12 393.42,443.12 C393.42,443.12 386.04,448.25 386.04,448.25 C386.04,448.25 378.66,453.38 378.66,453.38 C378.66,453.38 371.11,458.24 371.11,458.24 C371.11,458.24 363.31,462.69 363.31,462.69 C363.31,462.69 355.5,467.14 355.5,467.14 C355.5,467.14 347.4,471.02 347.4,471.02 C347.4,471.02 339.22,474.73 339.22,474.73 C339.22,474.73 330.99,478.33 330.99,478.33 C330.99,478.33 322.49,481.27 322.49,481.27 C322.49,481.27 314,484.2 314,484.2 C314,484.2 305.38,486.71 305.38,486.71 C305.38,486.71 296.65,488.83 296.65,488.83 C296.65,488.83 287.91,490.95 287.91,490.95 C287.91,490.95 279.04,492.33 279.04,492.33 C279.04,492.33 270.14,493.59 270.14,493.59 C270.14,493.59 261.23,494.69 261.23,494.69 C261.23,494.69 252.25,495.07 252.25,495.07 C252.25,495.07 243.28,495.44 243.28,495.44 C243.28,495.44 234.3,495.41 234.3,495.41 C234.3,495.41 225.33,494.88 225.33,494.88 C225.33,494.88 216.36,494.35 216.36,494.35 C216.36,494.35 207.45,493.23 207.45,493.23 C207.45,493.23 198.58,491.8 198.58,491.8 C198.58,491.8 189.71,490.37 189.71,490.37 C189.71,490.37 180.99,488.21 180.99,488.21 C180.99,488.21 172.31,485.89 172.31,485.89 C172.31,485.89 163.63,483.57 163.63,483.57 C163.63,483.57 155.19,480.5 155.19,480.5 C155.19,480.5 146.78,477.32 146.78,477.32 C146.78,477.32 138.37,474.15 138.37,474.15 C138.37,474.15 130.11,470.63 130.11,470.63 C130.11,470.63 121.86,467.06 121.86,467.06 C121.86,467.06 113.62,463.48 113.62,463.48 C113.62,463.48 105.38,459.9 105.38,459.9 C105.38,459.9 97.04,456.56 97.04,456.56 C97.04,456.56 88.63,453.39 88.63,453.39 C88.63,453.39 80.22,450.22 80.22,450.22 C80.22,450.22 71.6,447.7 71.6,447.7 C71.6,447.7 62.92,445.37 62.92,445.37 C62.92,445.37 54.24,443.05 54.24,443.05 C54.24,443.05 45.38,441.55 45.38,441.55 C45.38,441.55 36.52,440.1 36.52,440.1 C36.52,440.1 27.63,438.78 27.63,438.78 C27.63,438.78 18.66,438.2 18.66,438.2 C18.66,438.2 9.7,437.61 9.7,437.61 C9.7,437.61 0.72,437.36 0.72,437.36 C0.72,437.36 -8.26,437.65 -8.26,437.65 C-8.26,437.65 -17.24,437.95 -17.24,437.95 C-17.24,437.95 -26.18,438.77 -26.18,438.77 C-26.18,438.77 -35.09,439.94 -35.09,439.94 C-35.09,439.94 -44,441.1 -44,441.1 C-44,441.1 -52.78,442.98 -52.78,442.98 C-52.78,442.98 -61.53,445.02 -61.53,445.02 C-61.53,445.02 -70.28,447.07 -70.28,447.07 C-70.28,447.07 -78.84,449.81 -78.84,449.81 C-78.84,449.81 -87.37,452.64 -87.37,452.64 C-87.37,452.64 -95.72,455.95 -95.72,455.95 C-95.72,455.95 -104.05,459.32 -104.05,459.32 C-104.05,459.32 -112.29,462.9 -112.29,462.9 C-112.29,462.9 -120.53,466.48 -120.53,466.48 C-120.53,466.48 -128.78,470.06 -128.78,470.06 C-128.78,470.06 -137.02,473.63 -137.02,473.63 C-137.02,473.63 -145.43,476.81 -145.43,476.81 C-145.43,476.81 -153.84,479.98 -153.84,479.98 C-153.84,479.98 -162.24,483.15 -162.24,483.15 C-162.24,483.15 -170.91,485.52 -170.91,485.52 C-170.91,485.52 -179.59,487.83 -179.59,487.83 C-179.59,487.83 -188.28,490.13 -188.28,490.13 C-188.28,490.13 -197.15,491.56 -197.15,491.56 C-197.15,491.56 -206.02,492.99 -206.02,492.99 C-206.02,492.99 -214.91,494.27 -214.91,494.27 C-214.91,494.27 -223.88,494.8 -223.88,494.8 C-223.88,494.8 -232.85,495.33 -232.85,495.33 C-232.85,495.33 -241.83,495.5 -241.83,495.5 C-241.83,495.5 -250.81,495.13 -250.81,495.13 C-250.81,495.13 -259.79,494.75 -259.79,494.75 C-259.79,494.75 -268.71,493.79 -268.71,493.79 C-268.71,493.79 -277.61,492.53 -277.61,492.53 C-277.61,492.53 -286.51,491.27 -286.51,491.27 C-286.51,491.27 -295.24,489.17 -295.24,489.17 C-295.24,489.17 -303.98,487.06 -303.98,487.06 C-303.98,487.06 -312.63,484.67 -312.63,484.67 C-312.63,484.67 -321.12,481.74 -321.12,481.74 C-321.12,481.74 -329.62,478.8 -329.62,478.8 C-329.62,478.8 -337.9,475.33 -337.9,475.33 C-337.9,475.33 -346.08,471.62 -346.08,471.62 C-346.08,471.62 -354.24,467.85 -354.24,467.85 C-354.24,467.85 -362.05,463.41 -362.05,463.41 C-362.05,463.41 -369.86,458.96 -369.86,458.96 C-369.86,458.96 -377.47,454.21 -377.47,454.21 C-377.47,454.21 -384.85,449.08 -384.85,449.08 C-384.85,449.08 -392.23,443.95 -392.23,443.95 C-392.23,443.95 -399.2,438.29 -399.2,438.29 C-399.2,438.29 -406.09,432.52 -406.09,432.52 C-406.09,432.52 -412.86,426.62 -412.86,426.62 C-412.86,426.62 -419.22,420.27 -419.22,420.27 C-419.22,420.27 -425.57,413.91 -425.57,413.91 C-425.57,413.91 -431.57,407.23 -431.57,407.23 C-431.57,407.23 -437.33,400.34 -437.33,400.34 C-437.33,400.34 -443.1,393.44 -443.1,393.44 C-443.1,393.44 -448.23,386.07 -448.23,386.07 C-448.23,386.07 -453.36,378.69 -453.36,378.69 C-453.36,378.69 -458.23,371.15 -458.23,371.15 C-458.23,371.15 -462.67,363.33 -462.67,363.33 C-462.67,363.33 -467.12,355.53 -467.12,355.53 C-467.12,355.53 -471,347.43 -471,347.43 C-471,347.43 -474.72,339.25 -474.72,339.25 C-474.72,339.25 -478.32,331.02 -478.32,331.02 C-478.32,331.02 -481.25,322.52 -481.25,322.52 C-481.25,322.52 -484.19,314.03 -484.19,314.03 C-484.19,314.03 -486.71,305.42 -486.71,305.42 C-486.71,305.42 -488.82,296.68 -488.82,296.68 C-488.82,296.68 -490.94,287.95 -490.94,287.95 C-490.94,287.95 -492.32,279.07 -492.32,279.07 C-492.32,279.07 -493.58,270.18 -493.58,270.18 C-493.58,270.18 -494.69,261.27 -494.69,261.27 C-494.69,261.27 -495.07,252.29 -495.07,252.29 C-495.07,252.29 -495.44,243.31 -495.44,243.31 C-495.44,243.31 -495.42,234.33 -495.42,234.33 C-495.42,234.33 -494.89,225.36 -494.89,225.36 C-494.89,225.36 -494.36,216.39 -494.36,216.39 C-494.36,216.39 -493.23,207.49 -493.23,207.49 C-493.23,207.49 -491.8,198.61 -491.8,198.61 C-491.8,198.61 -490.37,189.74 -490.37,189.74 C-490.37,189.74 -488.22,181.02 -488.22,181.02 C-488.22,181.02 -485.9,172.34 -485.9,172.34 C-485.9,172.34 -483.58,163.66 -483.58,163.66 C-483.58,163.66 -480.51,155.22 -480.51,155.22 C-480.51,155.22 -477.34,146.81 -477.34,146.81 C-477.34,146.81 -474.17,138.41 -474.17,138.41 C-474.17,138.41 -470.65,130.14 -470.65,130.14 C-470.65,130.14 -467.07,121.9 -467.07,121.9 C-467.07,121.9 -463.49,113.65 -463.49,113.65 C-463.49,113.65 -459.91,105.41 -459.91,105.41 C-459.91,105.41 -456.57,97.07 -456.57,97.07 C-456.57,97.07 -453.4,88.66 -453.4,88.66 C-453.4,88.66 -450.23,80.25 -450.23,80.25 C-450.23,80.25 -447.7,71.64 -447.7,71.64 C-447.7,71.64 -445.38,62.96 -445.38,62.96 C-445.38,62.96 -443.06,54.28 -443.06,54.28 C-443.06,54.28 -441.56,45.42 -441.56,45.42 C-441.56,45.42 -440.1,36.55 -440.1,36.55 C-440.1,36.55 -438.78,27.67 -438.78,27.67 C-438.78,27.67 -438.2,18.7 -438.2,18.7 C-438.2,18.7 -437.62,9.73 -437.62,9.73 C-437.62,9.73 -437.36,0.76 -437.36,0.76 C-437.36,0.76 -437.66,-8.22 -437.66,-8.22 C-437.66,-8.22 -437.95,-17.2 -437.95,-17.2 C-437.95,-17.2 -438.77,-26.14 -438.77,-26.14 C-438.77,-26.14 -439.93,-35.05 -439.93,-35.05 C-439.93,-35.05 -441.1,-43.96 -441.1,-43.96 C-441.1,-43.96 -442.98,-52.75 -442.98,-52.75 C-442.98,-52.75 -445.01,-61.5 -445.01,-61.5 C-445.01,-61.5 -447.06,-70.25 -447.06,-70.25 C-447.06,-70.25 -449.8,-78.81 -449.8,-78.81 C-449.8,-78.81 -452.63,-87.33 -452.63,-87.33 C-452.63,-87.33 -455.94,-95.69 -455.94,-95.69 C-455.94,-95.69 -459.31,-104.02 -459.31,-104.02 C-459.31,-104.02 -462.89,-112.26 -462.89,-112.26 C-462.89,-112.26 -466.47,-120.5 -466.47,-120.5 C-466.47,-120.5 -470.05,-128.74 -470.05,-128.74 C-470.05,-128.74 -473.68,-137.12 -473.68,-137.12 C-473.68,-137.12 -476.85,-145.53 -476.85,-145.53 C-476.85,-145.53 -480.03,-153.94 -480.03,-153.94 C-480.03,-153.94 -483.2,-162.34 -483.2,-162.34 C-483.2,-162.34 -485.55,-171.02 -485.55,-171.02 C-485.55,-171.02 -487.86,-179.7 -487.86,-179.7 C-487.86,-179.7 -490.15,-188.39 -490.15,-188.39 C-490.15,-188.39 -491.58,-197.26 -491.58,-197.26 C-491.58,-197.26 -493.01,-206.13 -493.01,-206.13 C-493.01,-206.13 -494.28,-215.02 -494.28,-215.02 C-494.28,-215.02 -494.81,-223.99 -494.81,-223.99 C-494.81,-223.99 -495.33,-232.96 -495.33,-232.96 C-495.33,-232.96 -495.5,-241.94 -495.5,-241.94 C-495.5,-241.94 -495.12,-250.92 -495.12,-250.92 C-495.12,-250.92 -494.75,-259.9 -494.75,-259.9 C-494.75,-259.9 -493.78,-268.82 -493.78,-268.82 C-493.78,-268.82 -492.52,-277.72 -492.52,-277.72 C-492.52,-277.72 -491.26,-286.61 -491.26,-286.61 C-491.26,-286.61 -489.15,-295.35 -489.15,-295.35 C-489.15,-295.35 -487.03,-304.08 -487.03,-304.08 C-487.03,-304.08 -484.64,-312.73 -484.64,-312.73 C-484.64,-312.73 -481.7,-321.23 -481.7,-321.23 C-481.7,-321.23 -478.77,-329.72 -478.77,-329.72 C-478.77,-329.72 -475.29,-338 -475.29,-338 C-475.29,-338 -471.57,-346.18 -471.57,-346.18 C-471.57,-346.18 -467.8,-354.33 -467.8,-354.33 C-467.8,-354.33 -463.36,-362.14 -463.36,-362.14 C-463.36,-362.14 -458.91,-369.95 -458.91,-369.95 C-458.91,-369.95 -454.15,-377.56 -454.15,-377.56 C-454.15,-377.56 -449.02,-384.94 -449.02,-384.94 C-449.02,-384.94 -443.88,-392.32 -443.88,-392.32 C-443.88,-392.32 -438.22,-399.28 -438.22,-399.28 C-438.22,-399.28 -432.45,-406.18 -432.45,-406.18 C-432.45,-406.18 -426.55,-412.94 -426.55,-412.94 C-426.55,-412.94 -420.19,-419.3 -420.19,-419.3 C-420.19,-419.3 -413.84,-425.65 -413.84,-425.65 C-413.84,-425.65 -407.15,-431.64 -407.15,-431.64 C-407.15,-431.64 -400.26,-437.41 -400.26,-437.41 C-400.26,-437.41 -393.36,-443.16 -393.36,-443.16 C-393.36,-443.16 -385.98,-448.29 -385.98,-448.29 C-385.98,-448.29 -378.6,-453.43 -378.6,-453.43 C-378.6,-453.43 -371.05,-458.28 -371.05,-458.28 C-371.05,-458.28 -363.24,-462.73 -363.24,-462.73 C-363.24,-462.73 -355.43,-467.18 -355.43,-467.18 C-355.43,-467.18 -347.33,-471.05 -347.33,-471.05 C-347.33,-471.05 -339.15,-474.76 -339.15,-474.76 C-339.15,-474.76 -330.92,-478.35 -330.92,-478.35 C-330.92,-478.35 -322.42,-481.29 -322.42,-481.29 C-322.42,-481.29 -313.93,-484.23 -313.93,-484.23 C-313.93,-484.23 -305.31,-486.73 -305.31,-486.73 C-305.31,-486.73 -296.58,-488.85 -296.58,-488.85 C-296.58,-488.85 -287.85,-490.97 -287.85,-490.97 C-287.85,-490.97 -278.97,-492.34 -278.97,-492.34 C-278.97,-492.34 -270.07,-493.6 -270.07,-493.6 C-270.07,-493.6 -261.16,-494.7 -261.16,-494.7 C-261.16,-494.7 -252.18,-495.07 -252.18,-495.07 C-252.18,-495.07 -243.2,-495.44 -243.2,-495.44 C-243.2,-495.44 -234.23,-495.41 -234.23,-495.41 C-234.23,-495.41 -225.26,-494.88 -225.26,-494.88 C-225.26,-494.88 -216.29,-494.35 -216.29,-494.35 C-216.29,-494.35 -207.38,-493.22 -207.38,-493.22 C-207.38,-493.22 -198.51,-491.79 -198.51,-491.79 C-198.51,-491.79 -189.64,-490.36 -189.64,-490.36 C-189.64,-490.36 -180.92,-488.19 -180.92,-488.19 C-180.92,-488.19 -172.24,-485.87 -172.24,-485.87 C-172.24,-485.87 -163.56,-483.56 -163.56,-483.56 C-163.56,-483.56 -155.12,-480.47 -155.12,-480.47 C-155.12,-480.47 -146.72,-477.3 -146.72,-477.3 C-146.72,-477.3 -138.31,-474.13 -138.31,-474.13 C-138.31,-474.13 -130.04,-470.61 -130.04,-470.61 C-130.04,-470.61 -121.8,-467.03 -121.8,-467.03 C-121.8,-467.03 -113.55,-463.45 -113.55,-463.45 C-113.55,-463.45 -105.31,-459.87 -105.31,-459.87 C-105.31,-459.87 -96.97,-456.53 -96.97,-456.53 C-96.97,-456.53 -88.56,-453.37 -88.56,-453.37 C-88.56,-453.37 -80.15,-450.2 -80.15,-450.2 C-80.15,-450.2 -71.53,-447.68 -71.53,-447.68 C-71.53,-447.68 -62.85,-445.36 -62.85,-445.36 C-62.85,-445.36 -54.17,-443.04 -54.17,-443.04 C-54.17,-443.04 -45.31,-441.54 -45.31,-441.54 C-45.31,-441.54 -36.44,-440.09 -36.44,-440.09 C-36.44,-440.09 -27.56,-438.78 -27.56,-438.78 C-27.56,-438.78 -18.59,-438.19 -18.59,-438.19 C-18.59,-438.19 -9.62,-437.61 -9.62,-437.61 C-9.62,-437.61 -0.65,-437.37 -0.65,-437.37c " />
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+</animated-vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_media_pause_button.xml b/packages/SystemUI/res/drawable/ic_media_pause_button.xml
new file mode 100644
index 0000000..6ae89f9
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_media_pause_button.xml
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2025 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.
+  -->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <target android:name="_R_G_L_1_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M-5.06 -18 C-5.06,-18 -5.06,-1.24 -5.06,-1.24 C-5.06,-1.24 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c "
+                    android:valueTo="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.449,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M-5.06 -18 C-5.06,-18 -5.06,-0.75 -5.06,-0.75 C-5.06,-0.75 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c "
+                    android:valueTo="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.449,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="56"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="15.485"
+                    android:valueTo="12.321"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="278"
+                    android:propertyName="translateX"
+                    android:startOffset="56"
+                    android:valueFrom="12.321"
+                    android:valueTo="7.576"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="517"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="24dp"
+            android:height="24dp"
+            android:viewportHeight="24"
+            android:viewportWidth="24">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_1_G"
+                    android:pivotX="-12.031"
+                    android:scaleX="0.33299999999999996"
+                    android:scaleY="0.33299999999999996"
+                    android:translateX="19.524"
+                    android:translateY="12.084">
+                    <path
+                        android:name="_R_G_L_1_G_D_0_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#ffffff"
+                        android:fillType="nonZero"
+                        android:pathData=" M-5.06 -18 C-5.06,-18 -5.06,-1.24 -5.06,-1.24 C-5.06,-1.24 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " />
+                </group>
+                <group
+                    android:name="_R_G_L_0_G_T_1"
+                    android:scaleX="0.33299999999999996"
+                    android:scaleY="0.33299999999999996"
+                    android:translateX="15.485"
+                    android:translateY="12.084">
+                    <group
+                        android:name="_R_G_L_0_G"
+                        android:translateX="12.031">
+                        <path
+                            android:name="_R_G_L_0_G_D_0_P_0"
+                            android:fillAlpha="1"
+                            android:fillColor="#ffffff"
+                            android:fillType="nonZero"
+                            android:pathData=" M-5.06 -18 C-5.06,-18 -5.06,-0.75 -5.06,-0.75 C-5.06,-0.75 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " />
+                    </group>
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+</animated-vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_media_pause_button_container.xml b/packages/SystemUI/res/drawable/ic_media_pause_button_container.xml
new file mode 100644
index 0000000..571f69d
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_media_pause_button_container.xml
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2025 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.
+  -->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="88dp"
+            android:height="56dp"
+            android:viewportHeight="56"
+            android:viewportWidth="88">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_0_G"
+                    android:pivotX="0.493"
+                    android:pivotY="0.124"
+                    android:scaleX="1.05905"
+                    android:scaleY="1.0972"
+                    android:translateX="43.528999999999996"
+                    android:translateY="27.898">
+                    <path
+                        android:name="_R_G_L_0_G_D_0_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#3d90ff"
+                        android:fillType="nonZero"
+                        android:pathData=" M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c " />
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="133"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c "
+                    android:valueTo="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.473,0 0.065,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="367"
+                    android:propertyName="pathData"
+                    android:startOffset="133"
+                    android:valueFrom="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c "
+                    android:valueTo="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.473,0 0.065,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="167"
+                    android:propertyName="scaleX"
+                    android:startOffset="0"
+                    android:valueFrom="1.05905"
+                    android:valueTo="1.17758"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="167"
+                    android:propertyName="scaleY"
+                    android:startOffset="0"
+                    android:valueFrom="1.0972"
+                    android:valueTo="1.22"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="scaleX"
+                    android:startOffset="167"
+                    android:valueFrom="1.17758"
+                    android:valueTo="1.05905"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="scaleY"
+                    android:startOffset="167"
+                    android:valueFrom="1.22"
+                    android:valueTo="1.0972"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="517"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_media_play_button.xml b/packages/SystemUI/res/drawable/ic_media_play_button.xml
new file mode 100644
index 0000000..f646902
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_media_play_button.xml
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2025 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.
+  -->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <target android:name="_R_G_L_1_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c "
+                    android:valueTo="M-5.06 -18 C-5.06,-18 -5.06,-1.24 -5.06,-1.24 C-5.06,-1.24 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.433,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="pathData"
+                    android:startOffset="0"
+                    android:valueFrom="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c "
+                    android:valueTo="M-5.06 -18 C-5.06,-18 -5.06,-0.75 -5.06,-0.75 C-5.06,-0.75 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.433,0 0,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="7.576"
+                    android:valueTo="15.485"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.583,0 0.089,0.874 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="517"
+                    android:propertyName="translateX"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="24dp"
+            android:height="24dp"
+            android:viewportHeight="24"
+            android:viewportWidth="24">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_1_G"
+                    android:pivotX="-12.031"
+                    android:scaleX="0.33299999999999996"
+                    android:scaleY="0.33299999999999996"
+                    android:translateX="19.524"
+                    android:translateY="12.084">
+                    <path
+                        android:name="_R_G_L_1_G_D_0_P_0"
+                        android:fillAlpha="1"
+                        android:fillColor="#ffffff"
+                        android:fillType="nonZero"
+                        android:pathData=" M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " />
+                </group>
+                <group
+                    android:name="_R_G_L_0_G_T_1"
+                    android:scaleX="0.33299999999999996"
+                    android:scaleY="0.33299999999999996"
+                    android:translateX="7.576"
+                    android:translateY="12.084">
+                    <group
+                        android:name="_R_G_L_0_G"
+                        android:translateX="12.031">
+                        <path
+                            android:name="_R_G_L_0_G_D_0_P_0"
+                            android:fillAlpha="1"
+                            android:fillColor="#ffffff"
+                            android:fillType="nonZero"
+                            android:pathData=" M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " />
+                    </group>
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+</animated-vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_media_play_button_container.xml b/packages/SystemUI/res/drawable/ic_media_play_button_container.xml
new file mode 100644
index 0000000..aa4e09fa
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_media_play_button_container.xml
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2025 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.
+  -->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <aapt:attr name="android:drawable">
+        <vector
+            android:height="56dp"
+            android:width="88dp"
+            android:viewportHeight="56"
+            android:viewportWidth="88">
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_0_G"
+                    android:translateX="43.528999999999996"
+                    android:translateY="27.898"
+                    android:pivotX="0.493"
+                    android:pivotY="0.124"
+                    android:scaleX="1.05905"
+                    android:scaleY="1.0972">
+                    <path
+                        android:name="_R_G_L_0_G_D_0_P_0"
+                        android:fillColor="#3d90ff"
+                        android:fillAlpha="1"
+                        android:fillType="nonZero"
+                        android:pathData=" M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c "/>
+                </group>
+            </group>
+            <group android:name="time_group"/>
+        </vector>
+    </aapt:attr>
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="pathData"
+                    android:duration="167"
+                    android:startOffset="0"
+                    android:valueFrom="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c "
+                    android:valueTo="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.493,0 0,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:propertyName="pathData"
+                    android:duration="333"
+                    android:startOffset="167"
+                    android:valueFrom="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c "
+                    android:valueTo="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c "
+                    android:valueType="pathType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.493,0 0,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="scaleX"
+                    android:duration="167"
+                    android:startOffset="0"
+                    android:valueFrom="1.05905"
+                    android:valueTo="1.17758"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:propertyName="scaleY"
+                    android:duration="167"
+                    android:startOffset="0"
+                    android:valueFrom="1.0972"
+                    android:valueTo="1.22"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:propertyName="scaleX"
+                    android:duration="333"
+                    android:startOffset="167"
+                    android:valueFrom="1.17758"
+                    android:valueTo="1.05905"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:propertyName="scaleY"
+                    android:duration="333"
+                    android:startOffset="167"
+                    android:valueFrom="1.22"
+                    android:valueTo="1.0972"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:propertyName="translateX"
+                    android:duration="517"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml
index 0b624e1..67f620f 100644
--- a/packages/SystemUI/res/layout/volume_dialog.xml
+++ b/packages/SystemUI/res/layout/volume_dialog.xml
@@ -19,6 +19,7 @@
     android:id="@+id/volume_dialog_root"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
+    android:alpha="0"
     android:clipChildren="false"
     app:layoutDescription="@xml/volume_dialog_scene">
 
@@ -44,7 +45,7 @@
         app:layout_constraintBottom_toTopOf="@id/volume_dialog_main_slider_container"
         app:layout_constraintEnd_toEndOf="@id/volume_dialog_main_slider_container"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent"/>
+        app:layout_constraintTop_toTopOf="parent" />
 
     <include
         android:id="@+id/volume_dialog_main_slider_container"
diff --git a/packages/SystemUI/res/layout/volume_dialog_slider.xml b/packages/SystemUI/res/layout/volume_dialog_slider.xml
index 967cb3f..6eb7b73 100644
--- a/packages/SystemUI/res/layout/volume_dialog_slider.xml
+++ b/packages/SystemUI/res/layout/volume_dialog_slider.xml
@@ -14,8 +14,9 @@
      limitations under the License.
 -->
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="@dimen/volume_dialog_slider_width"
-    android:layout_height="@dimen/volume_dialog_slider_height">
+    android:layout_width="0dp"
+    android:layout_height="0dp"
+    android:maxHeight="@dimen/volume_dialog_slider_height">
 
     <com.google.android.material.slider.Slider
         android:id="@+id/volume_dialog_slider"
diff --git a/packages/SystemUI/res/layout/volume_ringer_button.xml b/packages/SystemUI/res/layout/volume_ringer_button.xml
index e65d0b9..6748cfa 100644
--- a/packages/SystemUI/res/layout/volume_ringer_button.xml
+++ b/packages/SystemUI/res/layout/volume_ringer_button.xml
@@ -20,10 +20,9 @@
 
     <ImageButton
         android:id="@+id/volume_drawer_button"
-        android:layout_width="@dimen/volume_dialog_ringer_drawer_button_size"
-        android:layout_height="@dimen/volume_dialog_ringer_drawer_button_size"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
         android:padding="@dimen/volume_dialog_ringer_drawer_button_icon_radius"
-        android:layout_marginBottom="@dimen/volume_dialog_components_spacing"
         android:contentDescription="@string/volume_ringer_mode"
         android:gravity="center"
         android:tint="@androidprv:color/materialColorOnSurface"
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 8bf4e37..2ffa3d1 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1278,6 +1278,7 @@
     <dimen name="qs_center_guideline_padding">10dp</dimen>
     <dimen name="qs_media_action_spacing">4dp</dimen>
     <dimen name="qs_media_action_margin">12dp</dimen>
+    <dimen name="qs_media_action_play_pause_width">72dp</dimen>
     <dimen name="qs_seamless_height">24dp</dimen>
     <dimen name="qs_seamless_icon_size">12dp</dimen>
     <dimen name="qs_media_disabled_seekbar_height">1dp</dimen>
@@ -2116,6 +2117,11 @@
     <dimen name="volume_dialog_button_size">40dp</dimen>
     <dimen name="volume_dialog_slider_width">52dp</dimen>
     <dimen name="volume_dialog_slider_height">254dp</dimen>
+    <!--
+        A primary goal of this margin is to vertically constraint slider height in the landscape
+        orientation when the vertical space is limited
+    -->
+    <dimen name="volume_dialog_slider_vertical_margin">124dp</dimen>
 
     <fraction name="volume_dialog_half_opened_bias">0.2</fraction>
 
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index cd37c22..80fb8b9 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2309,6 +2309,9 @@
     <string name="group_system_lock_screen">Lock screen</string>
     <!-- User visible title for the keyboard shortcut that pulls up Notes app for quick memo. [CHAR LIMIT=70] -->
     <string name="group_system_quick_memo">Take a note</string>
+    <!-- TODO(b/383734125): make it translatable once string is finalized by UXW.-->
+    <!-- User visible title for the keyboard shortcut that toggles Voice Access. [CHAR LIMIT=70] -->
+    <string name="group_system_toggle_voice_access" translatable="false">Toggle Voice Access</string>
 
     <!-- User visible title for the multitasking keyboard shortcuts list. [CHAR LIMIT=70] -->
     <string name="keyboard_shortcut_group_system_multitasking">Multitasking</string>
@@ -3798,7 +3801,7 @@
     <!-- Title at the top of the keyboard shortcut helper UI when in customize mode. The helper
          is a component that shows the user which keyboard shortcuts they can use.
          [CHAR LIMIT=NONE] -->
-    <string name="shortcut_helper_customize_mode_title">Customize keyboard shortcuts</string>
+    <string name="shortcut_helper_customize_mode_title">Customize shortcuts</string>
     <!-- Title at the top of the keyboard shortcut helper remove shortcut dialog.
          The helper is a component that shows the user which keyboard shortcuts they can use. Also
          allows the user to add/remove custom shortcuts.[CHAR LIMIT=NONE] -->
@@ -3919,6 +3922,16 @@
          The helper is a component that shows the user which keyboard shortcuts they can use.
          [CHAR LIMIT=NONE] -->
     <string name="shortcut_helper_plus_symbol">+</string>
+    <!-- Accessibility label for the plus icon on a shortcut in shortcut helper that allows the user
+         to add a new custom shortcut.
+         The helper is a component that shows the user which keyboard shortcuts they can use.
+         [CHAR LIMIT=NONE] -->
+    <string name="shortcut_helper_add_shortcut_button_label">Add shortcut</string>
+    <!-- Accessibility label for the bin(trash) icon on a shortcut in shortcut helper that allows the
+         user to delete an existing custom shortcut.
+         The helper is a component that shows the user which keyboard shortcuts they can use.
+         [CHAR LIMIT=NONE] -->
+    <string name="shortcut_helper_delete_shortcut_button_label">Delete shortcut</string>
 
     <!-- Keyboard touchpad tutorial scheduler-->
     <!-- Notification title for launching keyboard tutorial [CHAR_LIMIT=100] -->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 691fb50..08891aa 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -576,12 +576,12 @@
 
     <style name="SystemUI.Material3.Slider" parent="@style/Widget.Material3.Slider">
         <item name="labelStyle">@style/Widget.Material3.Slider.Label</item>
-        <item name="thumbColor">@androidprv:color/materialColorPrimary</item>
-        <item name="tickColorActive">@androidprv:color/materialColorSurfaceContainerHighest</item>
-        <item name="tickColorInactive">@androidprv:color/materialColorPrimary</item>
-        <item name="trackColorActive">@androidprv:color/materialColorPrimary</item>
-        <item name="trackColorInactive">@androidprv:color/materialColorSurfaceContainerHighest</item>
-        <item name="trackIconActiveColor">@androidprv:color/materialColorSurfaceContainerHighest</item>
+        <item name="thumbColor">@color/thumb_color</item>
+        <item name="tickColorActive">@color/on_active_track_color</item>
+        <item name="tickColorInactive">@color/on_inactive_track_color</item>
+        <item name="trackColorActive">@color/active_track_color</item>
+        <item name="trackColorInactive">@color/inactive_track_color</item>
+        <item name="trackIconActiveColor">@color/on_active_track_color</item>
     </style>
 
     <style name="Theme.SystemUI.DayNightDialog" parent="@android:style/Theme.DeviceDefault.Light.Dialog"/>
diff --git a/packages/SystemUI/res/xml/volume_dialog_constraint_set.xml b/packages/SystemUI/res/xml/volume_dialog_constraint_set.xml
index 9018e5b..a8f616c 100644
--- a/packages/SystemUI/res/xml/volume_dialog_constraint_set.xml
+++ b/packages/SystemUI/res/xml/volume_dialog_constraint_set.xml
@@ -6,10 +6,13 @@
     <Constraint
         android:id="@id/volume_dialog_main_slider_container"
         android:layout_width="@dimen/volume_dialog_slider_width"
-        android:layout_height="@dimen/volume_dialog_slider_height"
+        android:layout_height="0dp"
+        android:layout_marginTop="@dimen/volume_dialog_slider_vertical_margin"
         android:layout_marginEnd="@dimen/volume_dialog_components_spacing"
+        android:layout_marginBottom="@dimen/volume_dialog_slider_vertical_margin"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHeight_max="@dimen/volume_dialog_slider_height"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintVertical_bias="0.5" />
 </ConstraintSet>
\ No newline at end of file
diff --git a/packages/SystemUI/res/xml/volume_dialog_half_folded_constraint_set.xml b/packages/SystemUI/res/xml/volume_dialog_half_folded_constraint_set.xml
index 297c388..b4d8ae7 100644
--- a/packages/SystemUI/res/xml/volume_dialog_half_folded_constraint_set.xml
+++ b/packages/SystemUI/res/xml/volume_dialog_half_folded_constraint_set.xml
@@ -6,10 +6,13 @@
     <Constraint
         android:id="@id/volume_dialog_main_slider_container"
         android:layout_width="@dimen/volume_dialog_slider_width"
-        android:layout_height="@dimen/volume_dialog_slider_height"
+        android:layout_height="0dp"
+        android:layout_marginTop="@dimen/volume_dialog_slider_vertical_margin"
         android:layout_marginEnd="@dimen/volume_dialog_components_spacing"
+        android:layout_marginBottom="@dimen/volume_dialog_slider_vertical_margin"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHeight_max="@dimen/volume_dialog_slider_height"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintVertical_bias="@fraction/volume_dialog_half_opened_bias" />
 </ConstraintSet>
\ No newline at end of file
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java
index 7d220b5..6e23a07 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java
@@ -21,11 +21,9 @@
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.window.PictureInPictureSurfaceTransaction;
-import android.window.TaskSnapshot;
 import android.window.WindowAnimationState;
 
 import com.android.internal.os.IResultReceiver;
-import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.wm.shell.recents.IRecentsAnimationController;
 
 public class RecentsAnimationControllerCompat {
@@ -40,18 +38,6 @@
         mAnimationController = animationController;
     }
 
-    public ThumbnailData screenshotTask(int taskId) {
-        try {
-            final TaskSnapshot snapshot = mAnimationController.screenshotTask(taskId);
-            if (snapshot != null) {
-                return ThumbnailData.fromSnapshot(snapshot);
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "Failed to screenshot task", e);
-        }
-        return new ThumbnailData();
-    }
-
     public void setInputConsumerEnabled(boolean enabled) {
         try {
             mAnimationController.setInputConsumerEnabled(enabled);
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java
index 51892aa..ff6bcdb 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java
@@ -19,6 +19,7 @@
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.view.RemoteAnimationTarget;
+import android.window.TransitionInfo;
 
 import com.android.systemui.shared.recents.model.ThumbnailData;
 
@@ -30,7 +31,7 @@
      */
     void onAnimationStart(RecentsAnimationControllerCompat controller,
             RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers,
-            Rect homeContentInsets, Rect minimizedHomeBounds, Bundle extras);
+            Rect homeContentInsets, Rect minimizedHomeBounds, Bundle extras, TransitionInfo info);
 
     /**
      * Called when the animation into Recents was canceled. This call is made on the binder thread.
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
index acfa086..c7ae02b 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
@@ -142,33 +142,28 @@
 
     private boolean isKeyguardShowable(Display display) {
         if (display == null) {
-            if (DEBUG) Log.i(TAG, "Cannot show Keyguard on null display");
+            Log.i(TAG, "Cannot show Keyguard on null display");
             return false;
         }
         if (ShadeWindowGoesAround.isEnabled()) {
             int shadeDisplayId = mShadePositionRepositoryProvider.get().getDisplayId().getValue();
             if (display.getDisplayId() == shadeDisplayId) {
-                if (DEBUG) {
-                    Log.i(TAG,
-                            "Do not show KeyguardPresentation on the shade window display");
-                }
+                Log.i(TAG, "Do not show KeyguardPresentation on the shade window display");
                 return false;
             }
         } else {
             if (display.getDisplayId() == mDisplayTracker.getDefaultDisplayId()) {
-                if (DEBUG) Log.i(TAG, "Do not show KeyguardPresentation on the default display");
+                Log.i(TAG, "Do not show KeyguardPresentation on the default display");
                 return false;
             }
         }
         display.getDisplayInfo(mTmpDisplayInfo);
         if ((mTmpDisplayInfo.flags & Display.FLAG_PRIVATE) != 0) {
-            if (DEBUG) Log.i(TAG, "Do not show KeyguardPresentation on a private display");
+            Log.i(TAG, "Do not show KeyguardPresentation on a private display");
             return false;
         }
         if ((mTmpDisplayInfo.flags & Display.FLAG_ALWAYS_UNLOCKED) != 0) {
-            if (DEBUG) {
-                Log.i(TAG, "Do not show KeyguardPresentation on an unlocked display");
-            }
+            Log.i(TAG, "Do not show KeyguardPresentation on an unlocked display");
             return false;
         }
 
@@ -176,14 +171,11 @@
                 mDeviceStateHelper.isConcurrentDisplayActive(display)
                         || mDeviceStateHelper.isRearDisplayOuterDefaultActive(display);
         if (mKeyguardStateController.isOccluded() && deviceStateOccludesKeyguard) {
-            if (DEBUG) {
-                // When activities with FLAG_SHOW_WHEN_LOCKED are shown on top of Keyguard, the
-                // Keyguard state becomes "occluded". In this case, we should not show the
-                // KeyguardPresentation, since the activity is presenting content onto the
-                // non-default display.
-                Log.i(TAG, "Do not show KeyguardPresentation when occluded and concurrent or rear"
-                        + " display is active");
-            }
+            // When activities with FLAG_SHOW_WHEN_LOCKED are shown on top of Keyguard, the Keyguard
+            // state becomes "occluded". In this case, we should not show the KeyguardPresentation,
+            // since the activity is presenting content onto the non-default display.
+            Log.i(TAG, "Do not show KeyguardPresentation when occluded and concurrent or rear"
+                    + " display is active");
             return false;
         }
 
@@ -197,7 +189,7 @@
      */
     private boolean showPresentation(Display display) {
         if (!isKeyguardShowable(display)) return false;
-        if (DEBUG) Log.i(TAG, "Keyguard enabled on display: " + display);
+        Log.i(TAG, "Keyguard enabled on display: " + display);
         final int displayId = display.getDisplayId();
         Presentation presentation = mPresentations.get(displayId);
         if (presentation == null) {
@@ -239,7 +231,7 @@
 
     public void show() {
         if (!mShowing) {
-            if (DEBUG) Log.v(TAG, "show");
+            Log.v(TAG, "show");
             if (mMediaRouter != null) {
                 mMediaRouter.addCallback(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY,
                         mMediaRouterCallback, MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY);
@@ -253,7 +245,7 @@
 
     public void hide() {
         if (mShowing) {
-            if (DEBUG) Log.v(TAG, "hide");
+            Log.v(TAG, "hide");
             if (mMediaRouter != null) {
                 mMediaRouter.removeCallback(mMediaRouterCallback);
             }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
index d3c02e6..b159a70 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
@@ -29,7 +29,6 @@
 import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
@@ -93,10 +92,8 @@
         mPasswordEntry.setUserActivityListener(this::onUserInput);
         mView.onDevicePostureChanged(mPostureController.getDevicePosture());
         mPostureController.addCallback(mPostureCallback);
-        if (mFeatureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)) {
-            mPasswordEntry.setUsePinShapes(true);
-            updateAutoConfirmationState();
-        }
+        mPasswordEntry.setUsePinShapes(true);
+        updateAutoConfirmationState();
     }
 
     protected void onUserInput() {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index a703b02..7d291c3 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -2591,6 +2591,15 @@
     }
 
     /**
+     * @return true if optical udfps HW is supported on this device. Can return true even if the
+     * user has not enrolled udfps. This may be false if called before
+     * onAllAuthenticatorsRegistered.
+     */
+    public boolean isOpticalUdfpsSupported() {
+        return mAuthController.isOpticalUdfpsSupported();
+    }
+
+    /**
      * @return true if there's at least one sfps enrollment for the current user.
      */
     public boolean isSfpsEnrolled() {
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
index f530522..5f79c8c 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
@@ -100,7 +100,8 @@
                     .setDisplayAreaHelper(mWMComponent.getDisplayAreaHelper())
                     .setRecentTasks(mWMComponent.getRecentTasks())
                     .setBackAnimation(mWMComponent.getBackAnimation())
-                    .setDesktopMode(mWMComponent.getDesktopMode());
+                    .setDesktopMode(mWMComponent.getDesktopMode())
+                    .setAppZoomOut(mWMComponent.getAppZoomOut());
 
             // Only initialize when not starting from tests since this currently initializes some
             // components that shouldn't be run in the test environment
@@ -121,7 +122,8 @@
                     .setStartingSurface(Optional.ofNullable(null))
                     .setRecentTasks(Optional.ofNullable(null))
                     .setBackAnimation(Optional.ofNullable(null))
-                    .setDesktopMode(Optional.ofNullable(null));
+                    .setDesktopMode(Optional.ofNullable(null))
+                    .setAppZoomOut(Optional.ofNullable(null));
         }
         mSysUIComponent = builder.build();
 
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java
index 04afd86..caf043a 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java
@@ -22,6 +22,8 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
+import android.annotation.IntDef;
+import android.annotation.Nullable;
 import android.annotation.UiContext;
 import android.content.ComponentCallbacks;
 import android.content.Context;
@@ -44,7 +46,8 @@
 import android.view.View;
 import android.view.WindowManager;
 import android.view.accessibility.AccessibilityManager;
-import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
 import android.view.animation.Interpolator;
 
 import androidx.annotation.NonNull;
@@ -57,12 +60,16 @@
 import com.android.systemui.res.R;
 import com.android.systemui.util.leak.RotationUtils;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.concurrent.Executor;
 import java.util.function.Supplier;
 
 public class FullscreenMagnificationController implements ComponentCallbacks {
 
-    private static final String TAG = "FullscreenMagnificationController";
+    private static final String TAG = "FullscreenMagController";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
     private final Context mContext;
     private final AccessibilityManager mAccessibilityManager;
     private final WindowManager mWindowManager;
@@ -77,12 +84,14 @@
     private int mBorderStoke;
     private final int mDisplayId;
     private static final Region sEmptyRegion = new Region();
-    private ValueAnimator mShowHideBorderAnimator;
+    @VisibleForTesting
+    @Nullable
+    ValueAnimator mShowHideBorderAnimator;
     private Handler mHandler;
     private Executor mExecutor;
-    private boolean mFullscreenMagnificationActivated = false;
     private final Configuration mConfiguration;
-    private final Runnable mShowBorderRunnable = this::showBorderWithNullCheck;
+    private final Runnable mHideBorderImmediatelyRunnable = this::hideBorderImmediately;
+    private final Runnable mShowBorderRunnable = this::showBorder;
     private int mRotation;
     private final IRotationWatcher mRotationWatcher = new IRotationWatcher.Stub() {
         @Override
@@ -95,6 +104,21 @@
     private final DisplayManager.DisplayListener mDisplayListener;
     private String mCurrentDisplayUniqueId;
 
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            DISABLED,
+            DISABLING,
+            ENABLING,
+            ENABLED
+    })
+    @interface FullscreenMagnificationActivationState {}
+    private static final int DISABLED = 0;
+    private static final int DISABLING  = 1;
+    private static final int ENABLING = 2;
+    private static final int ENABLED = 3;
+    @FullscreenMagnificationActivationState
+    private int mActivationState = DISABLED;
+
     public FullscreenMagnificationController(
             @UiContext Context context,
             @Main Handler handler,
@@ -106,7 +130,7 @@
             Supplier<SurfaceControlViewHost> scvhSupplier) {
         this(context, handler, executor, displayManager, accessibilityManager,
                 windowManager, iWindowManager, scvhSupplier,
-                new SurfaceControl.Transaction(), null);
+                new SurfaceControl.Transaction());
     }
 
     @VisibleForTesting
@@ -119,8 +143,7 @@
             WindowManager windowManager,
             IWindowManager iWindowManager,
             Supplier<SurfaceControlViewHost> scvhSupplier,
-            SurfaceControl.Transaction transaction,
-            ValueAnimator valueAnimator) {
+            SurfaceControl.Transaction transaction) {
         mContext = context;
         mHandler = handler;
         mExecutor = executor;
@@ -135,18 +158,6 @@
         mConfiguration = new Configuration(context.getResources().getConfiguration());
         mLongAnimationTimeMs = mContext.getResources().getInteger(
                 com.android.internal.R.integer.config_longAnimTime);
-        mShowHideBorderAnimator = (valueAnimator == null)
-                ? createNullTargetObjectAnimator() : valueAnimator;
-        mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) {
-                if (isReverse) {
-                    // The animation was played in reverse, which means we are hiding the border.
-                    // We would like to perform clean up after the border is fully hidden.
-                    cleanUpBorder();
-                }
-            }
-        });
         mCurrentDisplayUniqueId = mContext.getDisplayNoVerify().getUniqueId();
         mDisplayManager = displayManager;
         mDisplayListener = new DisplayManager.DisplayListener() {
@@ -167,20 +178,51 @@
                     // Same unique ID means the physical display doesn't change. Early return.
                     return;
                 }
-
                 mCurrentDisplayUniqueId = uniqueId;
-                applyCornerRadiusToBorder();
+                mHandler.post(FullscreenMagnificationController.this::applyCornerRadiusToBorder);
             }
         };
     }
 
-    private ValueAnimator createNullTargetObjectAnimator() {
+    @VisibleForTesting
+    @UiThread
+    ValueAnimator createShowTargetAnimator(@NonNull View target) {
+        if (mShowHideBorderAnimator != null) {
+            mShowHideBorderAnimator.cancel();
+        }
+
         final ValueAnimator valueAnimator =
-                ObjectAnimator.ofFloat(/* target= */ null, View.ALPHA, 0f, 1f);
-        Interpolator interpolator = new AccelerateDecelerateInterpolator();
+                ObjectAnimator.ofFloat(target, View.ALPHA, 0f, 1f);
+        Interpolator interpolator = new AccelerateInterpolator();
 
         valueAnimator.setInterpolator(interpolator);
         valueAnimator.setDuration(mLongAnimationTimeMs);
+        valueAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(@NonNull Animator animation) {
+                mHandler.post(() -> setState(ENABLED));
+            }});
+        return valueAnimator;
+    }
+
+    @VisibleForTesting
+    @UiThread
+    ValueAnimator createHideTargetAnimator(@NonNull View target) {
+        if (mShowHideBorderAnimator != null) {
+            mShowHideBorderAnimator.cancel();
+        }
+
+        final ValueAnimator valueAnimator =
+                ObjectAnimator.ofFloat(target, View.ALPHA, 1f, 0f);
+        Interpolator interpolator = new DecelerateInterpolator();
+
+        valueAnimator.setInterpolator(interpolator);
+        valueAnimator.setDuration(mLongAnimationTimeMs);
+        valueAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(@NonNull Animator animation) {
+                mHandler.post(() -> cleanUpBorder());
+            }});
         return valueAnimator;
     }
 
@@ -190,14 +232,10 @@
      */
     @UiThread
     public void onFullscreenMagnificationActivationChanged(boolean activated) {
-        final boolean changed = (mFullscreenMagnificationActivated != activated);
-        if (changed) {
-            mFullscreenMagnificationActivated = activated;
-            if (activated) {
-                createFullscreenMagnificationBorder();
-            } else {
-                removeFullscreenMagnificationBorder();
-            }
+        if (activated) {
+            createFullscreenMagnificationBorder();
+        } else {
+            removeFullscreenMagnificationBorder();
         }
     }
 
@@ -207,16 +245,21 @@
      */
     @UiThread
     private void removeFullscreenMagnificationBorder() {
-        if (mHandler.hasCallbacks(mShowBorderRunnable)) {
-            mHandler.removeCallbacks(mShowBorderRunnable);
+        int state = getState();
+        if (state == DISABLING || state == DISABLED) {
+            // If there is an ongoing disable process or it is already disabled, return
+            return;
         }
-        mContext.unregisterComponentCallbacks(this);
-
-
-        mShowHideBorderAnimator.reverse();
+        setState(DISABLING);
+        mShowHideBorderAnimator = createHideTargetAnimator(mFullscreenBorder);
+        mShowHideBorderAnimator.start();
     }
 
-    private void cleanUpBorder() {
+    @VisibleForTesting
+    @UiThread
+    void cleanUpBorder() {
+        mContext.unregisterComponentCallbacks(this);
+
         if (Flags.updateCornerRadiusOnDisplayChanged()) {
             mDisplayManager.unregisterDisplayListener(mDisplayListener);
         }
@@ -227,6 +270,12 @@
         }
 
         if (mFullscreenBorder != null) {
+            if (mHandler.hasCallbacks(mHideBorderImmediatelyRunnable)) {
+                mHandler.removeCallbacks(mHideBorderImmediatelyRunnable);
+            }
+            if (mHandler.hasCallbacks(mShowBorderRunnable)) {
+                mHandler.removeCallbacks(mShowBorderRunnable);
+            }
             mFullscreenBorder = null;
             try {
                 mIWindowManager.removeRotationWatcher(mRotationWatcher);
@@ -234,6 +283,7 @@
                 Log.w(TAG, "Failed to remove rotation watcher", e);
             }
         }
+        setState(DISABLED);
     }
 
     /**
@@ -242,44 +292,47 @@
      */
     @UiThread
     private void createFullscreenMagnificationBorder() {
+        int state = getState();
+        if (state == ENABLING || state == ENABLED) {
+            // If there is an ongoing enable process or it is already enabled, return
+            return;
+        }
+        if (mShowHideBorderAnimator != null) {
+            mShowHideBorderAnimator.cancel();
+        }
+        setState(ENABLING);
+
         onConfigurationChanged(mContext.getResources().getConfiguration());
         mContext.registerComponentCallbacks(this);
 
         if (mSurfaceControlViewHost == null) {
-            // Create the view only if it does not exist yet. If we are trying to enable fullscreen
-            // magnification before it was fully disabled, we use the previous view instead of
-            // creating a new one.
+            // Create the view only if it does not exist yet. If we are trying to enable
+            // fullscreen magnification before it was fully disabled, we use the previous view
+            // instead of creating a new one.
             mFullscreenBorder = LayoutInflater.from(mContext)
                     .inflate(R.layout.fullscreen_magnification_border, null);
-            // Set the initial border view alpha manually so we won't show the border accidentally
-            // after we apply show() to the SurfaceControl and before the animation starts to run.
+            // Set the initial border view alpha manually so we won't show the border
+            // accidentally after we apply show() to the SurfaceControl and before the
+            // animation starts to run.
             mFullscreenBorder.setAlpha(0f);
-            mShowHideBorderAnimator.setTarget(mFullscreenBorder);
             mSurfaceControlViewHost = mScvhSupplier.get();
             mSurfaceControlViewHost.setView(mFullscreenBorder, getBorderLayoutParams());
-            mBorderSurfaceControl = mSurfaceControlViewHost.getSurfacePackage().getSurfaceControl();
+            mBorderSurfaceControl =
+                    mSurfaceControlViewHost.getSurfacePackage().getSurfaceControl();
             try {
                 mIWindowManager.watchRotation(mRotationWatcher, Display.DEFAULT_DISPLAY);
             } catch (Exception e) {
                 Log.w(TAG, "Failed to register rotation watcher", e);
             }
             if (Flags.updateCornerRadiusOnDisplayChanged()) {
-                mHandler.post(this::applyCornerRadiusToBorder);
+                applyCornerRadiusToBorder();
             }
         }
 
         mTransaction
                 .addTransactionCommittedListener(
                         mExecutor,
-                        () -> {
-                            if (mShowHideBorderAnimator.isRunning()) {
-                                // Since the method is only called when there is an activation
-                                // status change, the running animator is hiding the border.
-                                mShowHideBorderAnimator.reverse();
-                            } else {
-                                mShowHideBorderAnimator.start();
-                            }
-                        })
+                        this::showBorder)
                 .setPosition(mBorderSurfaceControl, -mBorderOffset, -mBorderOffset)
                 .setLayer(mBorderSurfaceControl, Integer.MAX_VALUE)
                 .show(mBorderSurfaceControl)
@@ -380,19 +433,25 @@
             mHandler.removeCallbacks(mShowBorderRunnable);
         }
 
-        // We hide the border immediately as early as possible to beat the redrawing of window
-        // in response to the orientation change so users won't see a weird shape border.
-        mHandler.postAtFrontOfQueue(() -> {
-            mFullscreenBorder.setAlpha(0f);
-        });
-
+        // We hide the border immediately as early as possible to beat the redrawing of
+        // window in response to the orientation change so users won't see a weird shape
+        // border.
+        mHandler.postAtFrontOfQueue(mHideBorderImmediatelyRunnable);
         mHandler.postDelayed(mShowBorderRunnable, mLongAnimationTimeMs);
     }
 
-    private void showBorderWithNullCheck() {
+    @UiThread
+    private void hideBorderImmediately() {
         if (mShowHideBorderAnimator != null) {
-            mShowHideBorderAnimator.start();
+            mShowHideBorderAnimator.cancel();
         }
+        mFullscreenBorder.setAlpha(0f);
+    }
+
+    @UiThread
+    private void showBorder() {
+        mShowHideBorderAnimator = createShowTargetAnimator(mFullscreenBorder);
+        mShowHideBorderAnimator.start();
     }
 
     private void updateDimensions() {
@@ -404,7 +463,9 @@
                 R.dimen.magnifier_border_width_fullscreen_with_offset);
     }
 
-    private void applyCornerRadiusToBorder() {
+    @UiThread
+    @VisibleForTesting
+    void applyCornerRadiusToBorder() {
         if (!isActivated()) {
             return;
         }
@@ -422,6 +483,20 @@
         backgroundDrawable.setCornerRadius(cornerRadius);
     }
 
+    @UiThread
+    private void setState(@FullscreenMagnificationActivationState int state) {
+        if (DEBUG) {
+            Log.d(TAG, "setState from " + mActivationState + " to " + state);
+        }
+        mActivationState = state;
+    }
+
+    @VisibleForTesting
+    @UiThread
+    int getState() {
+        return mActivationState;
+    }
+
     @Override
     public void onLowMemory() {
 
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
index 5f0acfa..67aa4ff 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
@@ -23,7 +23,6 @@
 import android.content.Context;
 import android.hardware.display.DisplayManager;
 import android.os.Handler;
-import android.os.UserHandle;
 import android.text.TextUtils;
 import android.view.Display;
 import android.view.WindowManager;
@@ -58,7 +57,7 @@
     private final AccessibilityButtonTargetsObserver mAccessibilityButtonTargetsObserver;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
 
-    private Context mContext;
+    private final Context mContext;
     private final WindowManager mWindowManager;
     private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager;
     private final DisplayManager mDisplayManager;
@@ -226,7 +225,6 @@
         @Override
         public void onUserInitializationComplete(int userId) {
             mIsUserInInitialization = false;
-            mContext = mContext.createContextAsUser(UserHandle.of(userId), /* flags= */ 0);
             mBtnMode = mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode();
             mBtnTargets =
                     mAccessibilityButtonTargetsObserver.getCurrentAccessibilityButtonTargets();
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java
index 121b51f..a1cb036 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java
@@ -79,6 +79,8 @@
     private static final int DEFAULT_MIGRATION_TOOLTIP_VALUE_PROMPT = MigrationPrompt.DISABLED;
 
     private final Context mContext;
+    // Pref always get the userId from the context to store SharedPreferences for the correct user
+    private final Context mCurrentUserContext;
     private final Configuration mConfiguration;
     private final AccessibilityManager mAccessibilityManager;
     private final AccessibilityManager.AccessibilityServicesStateChangeListener
@@ -157,6 +159,9 @@
             OnContentsChanged settingsContentsChanged, SecureSettings secureSettings,
             @Nullable HearingAidDeviceManager hearingAidDeviceManager) {
         mContext = context;
+        final int currentUserId = secureSettings.getRealUserHandle(UserHandle.USER_CURRENT);
+        mCurrentUserContext = context.createContextAsUser(
+                UserHandle.of(currentUserId), /* flags= */ 0);
         mAccessibilityManager = accessibilityManager;
         mConfiguration = new Configuration(context.getResources().getConfiguration());
         mSettingsContentsCallback = settingsContentsChanged;
@@ -168,12 +173,13 @@
 
     void loadMenuMoveToTucked(OnInfoReady<Boolean> callback) {
         callback.onReady(
-                Prefs.getBoolean(mContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED,
+                Prefs.getBoolean(
+                        mCurrentUserContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED,
                         DEFAULT_MOVE_TO_TUCKED_VALUE));
     }
 
     void loadDockTooltipVisibility(OnInfoReady<Boolean> callback) {
-        callback.onReady(Prefs.getBoolean(mContext,
+        callback.onReady(Prefs.getBoolean(mCurrentUserContext,
                 Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP,
                 DEFAULT_HAS_SEEN_DOCK_TOOLTIP_VALUE));
     }
@@ -215,19 +221,19 @@
     }
 
     void updateMoveToTucked(boolean isMoveToTucked) {
-        Prefs.putBoolean(mContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED,
+        Prefs.putBoolean(mCurrentUserContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED,
                 isMoveToTucked);
     }
 
     void updateMenuSavingPosition(Position percentagePosition) {
         mPercentagePosition = percentagePosition;
-        Prefs.putString(mContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION,
+        Prefs.putString(mCurrentUserContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION,
                 percentagePosition.toString());
     }
 
     void updateDockTooltipVisibility(boolean hasSeen) {
-        Prefs.putBoolean(mContext, Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP,
-                hasSeen);
+        Prefs.putBoolean(mCurrentUserContext,
+                Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP, hasSeen);
     }
 
     void updateMigrationTooltipVisibility(boolean visible) {
@@ -243,7 +249,7 @@
     }
 
     private Position getStartPosition() {
-        final String absolutePositionString = Prefs.getString(mContext,
+        final String absolutePositionString = Prefs.getString(mCurrentUserContext,
                 Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null);
 
         final float defaultPositionXPercent =
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java
index 184518a..e7470a3 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java
@@ -17,6 +17,7 @@
 package com.android.systemui.accessibility.floatingmenu;
 
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION;
+import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
 
 import android.content.Context;
 import android.graphics.PixelFormat;
@@ -90,7 +91,8 @@
                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                 PixelFormat.TRANSLUCENT);
         params.receiveInsetsIgnoringZOrder = true;
-        params.privateFlags |= PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION;
+        params.privateFlags |=
+                PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION | SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
         params.windowAnimations = android.R.style.Animation_Translucent;
         // Insets are configured to allow the menu to display over navigation and system bars.
         params.setFitInsetsTypes(0);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index d0cb507..eee5f9e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -1011,6 +1011,16 @@
     }
 
     /**
+     * @return true if optical udfps HW is supported on this device. Can return true even if
+     * the user has not enrolled udfps. This may be false if called before
+     * onAllAuthenticatorsRegistered.
+     */
+    public boolean isOpticalUdfpsSupported() {
+        return getUdfpsProps() != null && !getUdfpsProps().isEmpty() && getUdfpsProps()
+                .get(0).isOpticalUdfps();
+    }
+
+    /**
      * @return true if ultrasonic udfps HW is supported on this device. Can return true even if
      * the user has not enrolled udfps. This may be false if called before
      * onAllAuthenticatorsRegistered.
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt
index 4dc2a13..0303048 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt
@@ -104,6 +104,31 @@
         }
     }
 
+    override suspend fun onActionIconClick(deviceItem: DeviceItem, onIntent: (Intent) -> Unit) {
+        withContext(backgroundDispatcher) {
+            if (!audioSharingInteractor.audioSharingAvailable()) {
+                return@withContext deviceItemActionInteractorImpl.onActionIconClick(
+                    deviceItem,
+                    onIntent,
+                )
+            }
+
+            when (deviceItem.type) {
+                DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
+                    uiEventLogger.log(BluetoothTileDialogUiEvent.CHECK_MARK_ACTION_BUTTON_CLICKED)
+                    audioSharingInteractor.stopAudioSharing()
+                }
+                DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
+                    uiEventLogger.log(BluetoothTileDialogUiEvent.PLUS_ACTION_BUTTON_CLICKED)
+                    audioSharingInteractor.startAudioSharing()
+                }
+                else -> {
+                    deviceItemActionInteractorImpl.onActionIconClick(deviceItem, onIntent)
+                }
+            }
+        }
+    }
+
     private fun inSharingAndDeviceNoSource(
         inAudioSharing: Boolean,
         deviceItem: DeviceItem,
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt
index c4f26cd..116e76c 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt
@@ -29,6 +29,7 @@
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.drop
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.firstOrNull
 import kotlinx.coroutines.flow.flatMapLatest
@@ -54,6 +55,8 @@
 
     suspend fun startAudioSharing()
 
+    suspend fun stopAudioSharing()
+
     suspend fun audioSharingAvailable(): Boolean
 
     suspend fun qsDialogImprovementAvailable(): Boolean
@@ -61,7 +64,7 @@
 
 @SysUISingleton
 @OptIn(ExperimentalCoroutinesApi::class)
-class AudioSharingInteractorImpl
+open class AudioSharingInteractorImpl
 @Inject
 constructor(
     private val context: Context,
@@ -99,6 +102,9 @@
             if (audioSharingAvailable()) {
                 audioSharingRepository.leAudioBroadcastProfile?.let { profile ->
                     isAudioSharingOn
+                        // Skip the default value, we only care about adding source for newly
+                        // started audio sharing session
+                        .drop(1)
                         .mapNotNull { audioSharingOn ->
                             if (audioSharingOn) {
                                 // onBroadcastMetadataChanged could emit multiple times during one
@@ -145,6 +151,13 @@
         audioSharingRepository.startAudioSharing()
     }
 
+    override suspend fun stopAudioSharing() {
+        if (!audioSharingAvailable()) {
+            return
+        }
+        audioSharingRepository.stopAudioSharing()
+    }
+
     // TODO(b/367965193): Move this after flags rollout
     override suspend fun audioSharingAvailable(): Boolean {
         return audioSharingRepository.audioSharingAvailable()
@@ -181,6 +194,8 @@
 
     override suspend fun startAudioSharing() {}
 
+    override suspend fun stopAudioSharing() {}
+
     override suspend fun audioSharingAvailable(): Boolean = false
 
     override suspend fun qsDialogImprovementAvailable(): Boolean = false
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt
index b9b8d36..44f9769 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt
@@ -45,6 +45,8 @@
     suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice)
 
     suspend fun startAudioSharing()
+
+    suspend fun stopAudioSharing()
 }
 
 @SysUISingleton
@@ -100,6 +102,15 @@
             leAudioBroadcastProfile?.startPrivateBroadcast()
         }
     }
+
+    override suspend fun stopAudioSharing() {
+        withContext(backgroundDispatcher) {
+            if (!settingsLibAudioSharingRepository.audioSharingAvailable()) {
+                return@withContext
+            }
+            leAudioBroadcastProfile?.stopLatestBroadcast()
+        }
+    }
 }
 
 @SysUISingleton
@@ -117,4 +128,6 @@
     override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) {}
 
     override suspend fun startAudioSharing() {}
+
+    override suspend fun stopAudioSharing() {}
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
index b294dd1..56caddf 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
@@ -56,6 +56,13 @@
 import kotlinx.coroutines.isActive
 import kotlinx.coroutines.withContext
 
+data class DeviceItemClick(val deviceItem: DeviceItem, val clickedView: View, val target: Target) {
+    enum class Target {
+        ENTIRE_ROW,
+        ACTION_ICON,
+    }
+}
+
 /** Dialog for showing active, connected and saved bluetooth devices. */
 class BluetoothTileDialogDelegate
 @AssistedInject
@@ -80,7 +87,7 @@
     internal val bluetoothAutoOnToggle
         get() = mutableBluetoothAutoOnToggle.asStateFlow()
 
-    private val mutableDeviceItemClick: MutableSharedFlow<DeviceItem> =
+    private val mutableDeviceItemClick: MutableSharedFlow<DeviceItemClick> =
         MutableSharedFlow(extraBufferCapacity = 1)
     internal val deviceItemClick
         get() = mutableDeviceItemClick.asSharedFlow()
@@ -90,7 +97,7 @@
     internal val contentHeight
         get() = mutableContentHeight.asSharedFlow()
 
-    private val deviceItemAdapter: Adapter = Adapter(bluetoothTileDialogCallback)
+    private val deviceItemAdapter: Adapter = Adapter()
 
     private var lastUiUpdateMs: Long = -1
 
@@ -334,8 +341,7 @@
         }
     }
 
-    internal inner class Adapter(private val onClickCallback: BluetoothTileDialogCallback) :
-        RecyclerView.Adapter<Adapter.DeviceItemViewHolder>() {
+    internal inner class Adapter : RecyclerView.Adapter<Adapter.DeviceItemViewHolder>() {
 
         private val diffUtilCallback =
             object : DiffUtil.ItemCallback<DeviceItem>() {
@@ -376,7 +382,7 @@
 
         override fun onBindViewHolder(holder: DeviceItemViewHolder, position: Int) {
             val item = getItem(position)
-            holder.bind(item, onClickCallback)
+            holder.bind(item)
         }
 
         internal fun getItem(position: Int) = asyncListDiffer.currentList[position]
@@ -390,19 +396,18 @@
             private val nameView = view.requireViewById<TextView>(R.id.bluetooth_device_name)
             private val summaryView = view.requireViewById<TextView>(R.id.bluetooth_device_summary)
             private val iconView = view.requireViewById<ImageView>(R.id.bluetooth_device_icon)
-            private val iconGear = view.requireViewById<ImageView>(R.id.gear_icon_image)
-            private val gearView = view.requireViewById<View>(R.id.gear_icon)
+            private val actionIcon = view.requireViewById<ImageView>(R.id.gear_icon_image)
+            private val actionIconView = view.requireViewById<View>(R.id.gear_icon)
             private val divider = view.requireViewById<View>(R.id.divider)
 
-            internal fun bind(
-                item: DeviceItem,
-                deviceItemOnClickCallback: BluetoothTileDialogCallback,
-            ) {
+            internal fun bind(item: DeviceItem) {
                 container.apply {
                     isEnabled = item.isEnabled
                     background = item.background?.let { context.getDrawable(it) }
                     setOnClickListener {
-                        mutableDeviceItemClick.tryEmit(item)
+                        mutableDeviceItemClick.tryEmit(
+                            DeviceItemClick(item, it, DeviceItemClick.Target.ENTIRE_ROW)
+                        )
                         uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_CLICKED)
                     }
 
@@ -421,7 +426,8 @@
                         }
                     }
 
-                    iconGear.apply { drawable?.let { it.mutate()?.setTint(tintColor) } }
+                    actionIcon.setImageResource(item.actionIconRes)
+                    actionIcon.drawable?.setTint(tintColor)
 
                     divider.setBackgroundColor(tintColor)
 
@@ -454,8 +460,10 @@
                 nameView.text = item.deviceName
                 summaryView.text = item.connectionSummary
 
-                gearView.setOnClickListener {
-                    deviceItemOnClickCallback.onDeviceItemGearClicked(item, it)
+                actionIconView.setOnClickListener {
+                    mutableDeviceItemClick.tryEmit(
+                        DeviceItemClick(item, it, DeviceItemClick.Target.ACTION_ICON)
+                    )
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt
index aad233f..7c66ec0 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt
@@ -49,7 +49,7 @@
     LAUNCH_SETTINGS_NOT_SHARING_SAVED_LE_DEVICE_CLICKED(1719),
     @Deprecated(
         "Use case no longer needed",
-        ReplaceWith("LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED")
+        ReplaceWith("LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED"),
     )
     @UiEvent(doc = "Not broadcasting, one of the two connected LE audio devices is clicked")
     LAUNCH_SETTINGS_NOT_SHARING_CONNECTED_LE_DEVICE_CLICKED(1720),
@@ -59,7 +59,11 @@
     @UiEvent(doc = "Clicked on switch active button on audio sharing dialog")
     AUDIO_SHARING_DIALOG_SWITCH_ACTIVE_CLICKED(1890),
     @UiEvent(doc = "Clicked on share audio button on audio sharing dialog")
-    AUDIO_SHARING_DIALOG_SHARE_AUDIO_CLICKED(1891);
+    AUDIO_SHARING_DIALOG_SHARE_AUDIO_CLICKED(1891),
+    @UiEvent(doc = "Clicked on plus action button")
+    PLUS_ACTION_BUTTON_CLICKED(2061),
+    @UiEvent(doc = "Clicked on checkmark action button")
+    CHECK_MARK_ACTION_BUTTON_CLICKED(2062);
 
     override fun getId() = metricId
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
index 497d8cf..9460e7c 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
@@ -35,7 +35,6 @@
 import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.animation.Expandable
 import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_AUDIO_SHARING
-import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_BLUETOOTH_DEVICE_DETAILS
 import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PAIR_NEW_DEVICE
 import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PREVIOUSLY_CONNECTED_DEVICE
 import com.android.systemui.dagger.SysUISingleton
@@ -227,8 +226,22 @@
                 // deviceItemClick is emitted when user clicked on a device item.
                 dialogDelegate.deviceItemClick
                     .onEach {
-                        deviceItemActionInteractor.onClick(it, dialog)
-                        logger.logDeviceClick(it.cachedBluetoothDevice.address, it.type)
+                        when (it.target) {
+                            DeviceItemClick.Target.ENTIRE_ROW -> {
+                                deviceItemActionInteractor.onClick(it.deviceItem, dialog)
+                                logger.logDeviceClick(
+                                    it.deviceItem.cachedBluetoothDevice.address,
+                                    it.deviceItem.type,
+                                )
+                            }
+
+                            DeviceItemClick.Target.ACTION_ICON -> {
+                                deviceItemActionInteractor.onActionIconClick(it.deviceItem) { intent
+                                    ->
+                                    startSettingsActivity(intent, it.clickedView)
+                                }
+                            }
+                        }
                     }
                     .launchIn(this)
 
@@ -287,20 +300,6 @@
         )
     }
 
-    override fun onDeviceItemGearClicked(deviceItem: DeviceItem, view: View) {
-        uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_GEAR_CLICKED)
-        val intent =
-            Intent(ACTION_BLUETOOTH_DEVICE_DETAILS).apply {
-                putExtra(
-                    EXTRA_SHOW_FRAGMENT_ARGUMENTS,
-                    Bundle().apply {
-                        putString("device_address", deviceItem.cachedBluetoothDevice.address)
-                    },
-                )
-            }
-        startSettingsActivity(intent, view)
-    }
-
     override fun onSeeAllClicked(view: View) {
         uiEventLogger.log(BluetoothTileDialogUiEvent.SEE_ALL_CLICKED)
         startSettingsActivity(Intent(ACTION_PREVIOUSLY_CONNECTED_DEVICE), view)
@@ -382,8 +381,6 @@
 }
 
 interface BluetoothTileDialogCallback {
-    fun onDeviceItemGearClicked(deviceItem: DeviceItem, view: View)
-
     fun onSeeAllClicked(view: View)
 
     fun onPairNewDeviceClicked(view: View)
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt
index 2ba4c73..f7af16d 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt
@@ -53,5 +53,6 @@
     val background: Int? = null,
     var isEnabled: Boolean = true,
     var actionAccessibilityLabel: String = "",
-    var isActive: Boolean = false
+    var isActive: Boolean = false,
+    val actionIconRes: Int = -1,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
index 2b55e1c..cb4ec37 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.bluetooth.qsdialog
 
+import android.content.Intent
+import android.os.Bundle
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
@@ -25,7 +27,9 @@
 import kotlinx.coroutines.withContext
 
 interface DeviceItemActionInteractor {
-    suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {}
+    suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog)
+
+    suspend fun onActionIconClick(deviceItem: DeviceItem, onIntent: (Intent) -> Unit)
 }
 
 @SysUISingleton
@@ -67,4 +71,44 @@
             }
         }
     }
+
+    override suspend fun onActionIconClick(deviceItem: DeviceItem, onIntent: (Intent) -> Unit) {
+        withContext(backgroundDispatcher) {
+            deviceItem.cachedBluetoothDevice.apply {
+                when (deviceItem.type) {
+                    DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE,
+                    DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
+                    DeviceItemType.CONNECTED_BLUETOOTH_DEVICE,
+                    DeviceItemType.SAVED_BLUETOOTH_DEVICE -> {
+                        uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_GEAR_CLICKED)
+                        val intent =
+                            Intent(ACTION_BLUETOOTH_DEVICE_DETAILS).apply {
+                                putExtra(
+                                    EXTRA_SHOW_FRAGMENT_ARGUMENTS,
+                                    Bundle().apply {
+                                        putString(
+                                            "device_address",
+                                            deviceItem.cachedBluetoothDevice.address,
+                                        )
+                                    },
+                                )
+                            }
+                        onIntent(intent)
+                    }
+                    DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE,
+                    DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
+                        throw IllegalArgumentException("Invalid device type: ${deviceItem.type}")
+                        // Throw exception. Should already be handled in
+                        // AudioSharingDeviceItemActionInteractor.
+                    }
+                }
+            }
+        }
+    }
+
+    private companion object {
+        const val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args"
+        const val ACTION_BLUETOOTH_DEVICE_DETAILS =
+            "com.android.settings.BLUETOOTH_DEVICE_DETAIL_SETTINGS"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
index 92f0580..095e6e7 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
@@ -30,6 +30,8 @@
 private val backgroundOffBusy = R.drawable.bluetooth_tile_dialog_bg_off_busy
 private val connected = R.string.quick_settings_bluetooth_device_connected
 private val audioSharing = R.string.quick_settings_bluetooth_device_audio_sharing
+private val audioSharingAddIcon = R.drawable.ic_add
+private val audioSharingOnGoingIcon = R.drawable.ic_check
 private val saved = R.string.quick_settings_bluetooth_device_saved
 private val actionAccessibilityLabelActivate =
     R.string.accessibility_quick_settings_bluetooth_device_tap_to_activate
@@ -63,6 +65,7 @@
             background: Int,
             actionAccessibilityLabel: String,
             isActive: Boolean,
+            actionIconRes: Int = R.drawable.ic_settings_24dp,
         ): DeviceItem {
             return DeviceItem(
                 type = type,
@@ -75,6 +78,7 @@
                 isEnabled = !cachedDevice.isBusy,
                 actionAccessibilityLabel = actionAccessibilityLabel,
                 isActive = isActive,
+                actionIconRes = actionIconRes,
             )
         }
     }
@@ -125,6 +129,7 @@
             if (cachedDevice.isBusy) backgroundOffBusy else backgroundOn,
             "",
             isActive = !cachedDevice.isBusy,
+            actionIconRes = audioSharingOnGoingIcon,
         )
     }
 }
@@ -156,6 +161,7 @@
             if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff,
             "",
             isActive = false,
+            actionIconRes = audioSharingAddIcon,
         )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayout.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayout.kt
deleted file mode 100644
index 554dd69..0000000
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayout.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.bouncer.ui.helper
-
-import androidx.annotation.VisibleForTesting
-
-/** Enumerates all known adaptive layout configurations. */
-enum class BouncerSceneLayout {
-    /** The default UI with the bouncer laid out normally. */
-    STANDARD_BOUNCER,
-    /** The bouncer is displayed vertically stacked with the user switcher. */
-    BELOW_USER_SWITCHER,
-    /** The bouncer is displayed side-by-side with the user switcher or an empty space. */
-    BESIDE_USER_SWITCHER,
-    /** The bouncer is split in two with both sides shown side-by-side. */
-    SPLIT_BOUNCER,
-}
-
-/** Enumerates the supported window size classes. */
-enum class SizeClass {
-    COMPACT,
-    MEDIUM,
-    EXPANDED,
-}
-
-/**
- * Internal version of `calculateLayout` in the System UI Compose library, extracted here to allow
- * for testing that's not dependent on Compose.
- */
-@VisibleForTesting
-fun calculateLayoutInternal(
-    width: SizeClass,
-    height: SizeClass,
-    isOneHandedModeSupported: Boolean,
-): BouncerSceneLayout {
-    return when (height) {
-        SizeClass.COMPACT -> BouncerSceneLayout.SPLIT_BOUNCER
-        SizeClass.MEDIUM ->
-            when (width) {
-                SizeClass.COMPACT -> BouncerSceneLayout.STANDARD_BOUNCER
-                SizeClass.MEDIUM -> BouncerSceneLayout.STANDARD_BOUNCER
-                SizeClass.EXPANDED -> BouncerSceneLayout.BESIDE_USER_SWITCHER
-            }
-        SizeClass.EXPANDED ->
-            when (width) {
-                SizeClass.COMPACT -> BouncerSceneLayout.STANDARD_BOUNCER
-                SizeClass.MEDIUM -> BouncerSceneLayout.BELOW_USER_SWITCHER
-                SizeClass.EXPANDED -> BouncerSceneLayout.BESIDE_USER_SWITCHER
-            }
-    }.takeIf { it != BouncerSceneLayout.BESIDE_USER_SWITCHER || isOneHandedModeSupported }
-        ?: BouncerSceneLayout.STANDARD_BOUNCER
-}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt
index 78156db..ca49de3 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt
@@ -25,6 +25,7 @@
  */
 object CommunalTransitionKeys {
     /** Fades the glanceable hub without any translation */
+    @Deprecated("No longer supported as all hub transitions will be fades.")
     val SimpleFade = TransitionKey("SimpleFade")
     /** Transition from the glanceable hub before entering edit mode */
     val ToEditMode = TransitionKey("ToEditMode")
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
index 00eead6..555fe6e 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -31,6 +31,7 @@
 import com.android.systemui.statusbar.QsFrameTranslateModule;
 import com.android.systemui.statusbar.phone.ConfigurationForwarder;
 import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.wm.shell.appzoomout.AppZoomOut;
 import com.android.wm.shell.back.BackAnimation;
 import com.android.wm.shell.bubbles.Bubbles;
 import com.android.wm.shell.desktopmode.DesktopMode;
@@ -115,6 +116,9 @@
         @BindsInstance
         Builder setDesktopMode(Optional<DesktopMode> d);
 
+        @BindsInstance
+        Builder setAppZoomOut(Optional<AppZoomOut> a);
+
         SysUIComponent build();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt
index 41a59a9..ae62387 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor
 import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardBypassInteractor
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.power.shared.model.WakeSleepReason
 import com.android.systemui.util.kotlin.FlowDumperImpl
@@ -50,6 +51,8 @@
 constructor(
     biometricSettingsRepository: BiometricSettingsRepository,
     deviceEntryBiometricAuthInteractor: DeviceEntryBiometricAuthInteractor,
+    deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor,
+    keyguardBypassInteractor: KeyguardBypassInteractor,
     deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
     deviceEntrySourceInteractor: DeviceEntrySourceInteractor,
     fingerprintPropertyRepository: FingerprintPropertyRepository,
@@ -82,7 +85,7 @@
                 emit(recentPowerButtonPressThresholdMs * -1L - 1L)
             }
 
-    val playSuccessHaptic: Flow<Unit> =
+    private val playHapticsOnDeviceEntry: Flow<Boolean> =
         deviceEntrySourceInteractor.deviceEntryFromBiometricSource
             .sample(
                 combine(
@@ -92,17 +95,29 @@
                     ::Triple,
                 )
             )
-            .filter { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) ->
+            .map { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) ->
                 val sideFpsAllowsHaptic =
                     !powerButtonDown &&
                         systemClock.uptimeMillis() - lastPowerButtonWakeup >
                             recentPowerButtonPressThresholdMs
                 val allowHaptic = !sideFpsEnrolled || sideFpsAllowsHaptic
                 if (!allowHaptic) {
-                    logger.d("Skip success haptic. Recent power button press or button is down.")
+                    logger.d(
+                        "Skip success entry haptic from power button. Recent power button press or button is down."
+                    )
                 }
                 allowHaptic
             }
+
+    private val playHapticsOnFaceAuthSuccessAndBypassDisabled: Flow<Boolean> =
+        deviceEntryFaceAuthInteractor.isAuthenticated
+            .filter { it }
+            .sample(keyguardBypassInteractor.isBypassAvailable)
+            .map { !it }
+
+    val playSuccessHaptic: Flow<Unit> =
+        merge(playHapticsOnDeviceEntry, playHapticsOnFaceAuthSuccessAndBypassDisabled)
+            .filter { it }
             // map to Unit
             .map {}
             .dumpWhileCollecting("playSuccessHaptic")
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
index e02e3fb..10f060c 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
@@ -22,10 +22,10 @@
 import android.annotation.MainThread;
 import android.content.res.Configuration;
 import android.hardware.display.AmbientDisplayConfiguration;
-import android.os.Trace;
 import android.util.Log;
 import android.view.Display;
 
+import com.android.app.tracing.coroutines.TrackTracer;
 import com.android.internal.util.Preconditions;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.doze.dagger.DozeScope;
@@ -314,7 +314,7 @@
         mState = newState;
 
         mDozeLog.traceState(newState);
-        Trace.traceCounter(Trace.TRACE_TAG_APP, "doze_machine_state", newState.ordinal());
+        TrackTracer.instantForGroup("keyguard", "doze_machine_state", newState.ordinal());
 
         updatePulseReason(newState, oldState, pulseReason);
         performTransitionOnComponents(oldState, newState);
diff --git a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
index 2978595..9596a54 100644
--- a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
@@ -20,10 +20,12 @@
 import android.hardware.input.InputManager
 import android.hardware.input.KeyGestureEvent
 import androidx.datastore.core.DataStore
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
 import androidx.datastore.preferences.core.MutablePreferences
 import androidx.datastore.preferences.core.PreferenceDataStoreFactory
 import androidx.datastore.preferences.core.Preferences
 import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.emptyPreferences
 import androidx.datastore.preferences.core.intPreferencesKey
 import androidx.datastore.preferences.core.longPreferencesKey
 import androidx.datastore.preferences.preferencesDataStoreFile
@@ -68,7 +70,7 @@
 
     suspend fun updateGestureEduModel(
         gestureType: GestureType,
-        transform: (GestureEduModel) -> GestureEduModel
+        transform: (GestureEduModel) -> GestureEduModel,
     )
 
     suspend fun updateEduDeviceConnectionTime(
@@ -149,6 +151,8 @@
                         String.format(DATASTORE_DIR, userId)
                     )
                 },
+                corruptionHandler =
+                    ReplaceFileCorruptionHandler(produceNewData = { emptyPreferences() }),
                 scope = newDsScope,
             )
         dataStoreScope = newDsScope
@@ -159,7 +163,7 @@
 
     private fun getGestureEduModel(
         gestureType: GestureType,
-        preferences: Preferences
+        preferences: Preferences,
     ): GestureEduModel {
         return GestureEduModel(
             signalCount = preferences[getSignalCountKey(gestureType)] ?: 0,
@@ -183,7 +187,7 @@
 
     override suspend fun updateGestureEduModel(
         gestureType: GestureType,
-        transform: (GestureEduModel) -> GestureEduModel
+        transform: (GestureEduModel) -> GestureEduModel,
     ) {
         datastore.filterNotNull().first().edit { preferences ->
             val currentModel = getGestureEduModel(gestureType, preferences)
@@ -193,17 +197,17 @@
             setInstant(
                 preferences,
                 updatedModel.lastShortcutTriggeredTime,
-                getLastShortcutTriggeredTimeKey(gestureType)
+                getLastShortcutTriggeredTimeKey(gestureType),
             )
             setInstant(
                 preferences,
                 updatedModel.usageSessionStartTime,
-                getUsageSessionStartTimeKey(gestureType)
+                getUsageSessionStartTimeKey(gestureType),
             )
             setInstant(
                 preferences,
                 updatedModel.lastEducationTime,
-                getLastEducationTimeKey(gestureType)
+                getLastEducationTimeKey(gestureType),
             )
         }
     }
@@ -220,12 +224,12 @@
             setInstant(
                 preferences,
                 updatedModel.keyboardFirstConnectionTime,
-                getKeyboardFirstConnectionTimeKey()
+                getKeyboardFirstConnectionTimeKey(),
             )
             setInstant(
                 preferences,
                 updatedModel.touchpadFirstConnectionTime,
-                getTouchpadFirstConnectionTimeKey()
+                getTouchpadFirstConnectionTimeKey(),
             )
         }
     }
@@ -235,7 +239,7 @@
             keyboardFirstConnectionTime =
                 preferences[getKeyboardFirstConnectionTimeKey()]?.let { Instant.ofEpochSecond(it) },
             touchpadFirstConnectionTime =
-                preferences[getTouchpadFirstConnectionTimeKey()]?.let { Instant.ofEpochSecond(it) }
+                preferences[getTouchpadFirstConnectionTimeKey()]?.let { Instant.ofEpochSecond(it) },
         )
     }
 
@@ -263,7 +267,7 @@
     private fun setInstant(
         preferences: MutablePreferences,
         instant: Instant?,
-        key: Preferences.Key<Long>
+        key: Preferences.Key<Long>,
     ) {
         if (instant != null) {
             // Use epochSecond because an instant is defined as a signed long (64bit number) of
diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
index 21002c6..d7a4dba 100644
--- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
@@ -278,11 +278,11 @@
         }
 
     private suspend fun hasInitialDelayElapsed(deviceType: DeviceType): Boolean {
-        val oobeLaunchTime =
-            tutorialRepository.getScheduledTutorialLaunchTime(deviceType) ?: return false
-        return clock
-            .instant()
-            .isAfter(oobeLaunchTime.plusSeconds(initialDelayDuration.inWholeSeconds))
+        val oobeTime =
+            tutorialRepository.getScheduledTutorialLaunchTime(deviceType)
+                ?: tutorialRepository.getNotifiedTime(deviceType)
+                ?: return false
+        return clock.instant().isAfter(oobeTime.plusSeconds(initialDelayDuration.inWholeSeconds))
     }
 
     private data class StatsUpdateRequest(
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 63ac783..2ed0671 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -23,19 +23,12 @@
 import com.android.server.notification.Flags.politeNotifications
 import com.android.server.notification.Flags.vibrateWhileUnlocked
 import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
-import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON
-import com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS
-import com.android.systemui.Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP
 import com.android.systemui.Flags.communalHub
-import com.android.systemui.Flags.statusBarCallChipNotificationIcon
-import com.android.systemui.Flags.statusBarScreenSharingChips
-import com.android.systemui.Flags.statusBarUseReposForCallChip
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.shade.shared.flag.DualShade
 import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag
 import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
-import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression
 import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
@@ -57,7 +50,6 @@
         NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token
         PriorityPeopleSection.token dependsOn SortBySectionTimeFlag.token
         NotificationMinimalism.token dependsOn NotificationThrottleHun.token
-        ModesEmptyShadeFix.token dependsOn FooterViewRefactor.token
         ModesEmptyShadeFix.token dependsOn modesUi
 
         // SceneContainer dependencies
@@ -65,10 +57,6 @@
 
         // DualShade dependencies
         DualShade.token dependsOn SceneContainerFlag.getMainAconfigFlag()
-
-        // Status bar chip dependencies
-        statusBarCallChipNotificationIconToken dependsOn statusBarUseReposForCallChipToken
-        statusBarCallChipNotificationIconToken dependsOn statusBarScreenSharingChipsToken
     }
 
     private inline val politeNotifications
@@ -88,17 +76,4 @@
 
     private inline val communalHub
         get() = FlagToken(FLAG_COMMUNAL_HUB, communalHub())
-
-    private inline val statusBarCallChipNotificationIconToken
-        get() =
-            FlagToken(
-                FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON,
-                statusBarCallChipNotificationIcon(),
-            )
-
-    private inline val statusBarScreenSharingChipsToken
-        get() = FlagToken(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, statusBarScreenSharingChips())
-
-    private inline val statusBarUseReposForCallChipToken
-        get() = FlagToken(FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP, statusBarUseReposForCallChip())
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index c039e01..2c33c0b 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -76,21 +76,10 @@
     val LOCKSCREEN_CUSTOM_CLOCKS =
         resourceBooleanFlag(R.bool.config_enableLockScreenCustomClocks, "lockscreen_custom_clocks")
 
-    /**
-     * Migration from the legacy isDozing/dozeAmount paths to the new KeyguardTransitionRepository
-     * will occur in stages. This is one stage of many to come.
-     */
-    // TODO(b/255607168): Tracking Bug
-    @JvmField val DOZING_MIGRATION_1 = unreleasedFlag("dozing_migration_1")
-
     /** Flag to control the revamp of keyguard biometrics progress animation */
     // TODO(b/244313043): Tracking bug
     @JvmField val BIOMETRICS_ANIMATION_REVAMP = unreleasedFlag("biometrics_animation_revamp")
 
-    // flag for controlling auto pin confirmation and material u shapes in bouncer
-    @JvmField
-    val AUTO_PIN_CONFIRMATION = releasedFlag("auto_pin_confirmation", "auto_pin_confirmation")
-
     /** Enables code to show contextual loyalty cards in wallet entrypoints */
     // TODO(b/294110497): Tracking Bug
     @JvmField
@@ -100,10 +89,6 @@
     // TODO(b/242908637): Tracking Bug
     @JvmField val WALLPAPER_FULLSCREEN_PREVIEW = releasedFlag("wallpaper_fullscreen_preview")
 
-    /** Inflate and bind views upon emitting a blueprint value . */
-    // TODO(b/297365780): Tracking Bug
-    @JvmField val LAZY_INFLATE_KEYGUARD = releasedFlag("lazy_inflate_keyguard")
-
     /** Enables UI updates for AI wallpapers in the wallpaper picker. */
     // TODO(b/267722622): Tracking Bug
     @JvmField val WALLPAPER_PICKER_UI_FOR_AIWP = releasedFlag("wallpaper_picker_ui_for_aiwp")
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
index e1ebf7c..cf5c340 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
@@ -463,6 +463,9 @@
         mTelephonyListenerManager.removeServiceStateListener(mPhoneStateListener);
         mGlobalSettings.unregisterContentObserverSync(mAirplaneModeObserver);
         mConfigurationController.removeCallback(this);
+        if (mShowSilentToggle) {
+            mRingerModeTracker.getRingerMode().removeObservers(this);
+        }
     }
 
     protected Context getContext() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperCoreStartable.kt
index 19a19d5..c702ba9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperCoreStartable.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyboard.shortcut.data.repository.CustomInputGesturesRepository
 import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperStateRepository
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.statusbar.CommandQueue
@@ -41,6 +42,7 @@
     private val stateRepository: ShortcutHelperStateRepository,
     private val activityStarter: ActivityStarter,
     @Background private val backgroundScope: CoroutineScope,
+    private val customInputGesturesRepository: CustomInputGesturesRepository
 ) : CoreStartable {
     override fun start() {
         registerBroadcastReceiver(
@@ -55,6 +57,10 @@
             action = Intent.ACTION_CLOSE_SYSTEM_DIALOGS,
             onReceive = { stateRepository.hide() },
         )
+        registerBroadcastReceiver(
+            action = Intent.ACTION_USER_SWITCHED,
+            onReceive = { customInputGesturesRepository.refreshCustomInputGestures() },
+        )
         commandQueue.addCallback(
             object : CommandQueue.Callbacks {
                 override fun dismissKeyboardShortcutsMenu() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt
index 36cd400..e5c638c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt
@@ -25,6 +25,7 @@
 import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS
 import android.hardware.input.InputSettings
 import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestResult
 import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestResult.ERROR_OTHER
@@ -37,6 +38,7 @@
 import javax.inject.Inject
 import kotlin.coroutines.CoroutineContext
 
+@SysUISingleton
 class CustomInputGesturesRepository
 @Inject
 constructor(private val userTracker: UserTracker,
@@ -56,7 +58,7 @@
     val customInputGestures =
         _customInputGesture.onStart { refreshCustomInputGestures() }
 
-    private fun refreshCustomInputGestures() {
+    fun refreshCustomInputGestures() {
         setCustomInputGestures(inputGestures = retrieveCustomInputGestures())
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt
index d7be5e6..6a42cdc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt
@@ -27,15 +27,22 @@
 import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS
 import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT
 import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY
 import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION
 import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES
 import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER
 import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS
 import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW
 import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT
 import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT
 import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW
 import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS
+import com.android.hardware.input.Flags.enableVoiceAccessKeyGestures
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.AppCategories
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System
@@ -58,6 +65,7 @@
             KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to System,
             KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to System,
             KEY_GESTURE_TYPE_ALL_APPS to System,
+            KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to System,
 
             // Multitasking Category
             KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to MultiTasking,
@@ -66,6 +74,11 @@
             KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION to MultiTasking,
             KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT to MultiTasking,
             KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT to MultiTasking,
+            KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW to MultiTasking,
+            KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW to MultiTasking,
+            KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW to MultiTasking,
+            KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW to MultiTasking,
+            KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY to MultiTasking,
 
             // App Category
             KEY_GESTURE_TYPE_LAUNCH_APPLICATION to AppCategories,
@@ -90,6 +103,7 @@
             KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to R.string.shortcut_helper_category_system_apps,
             KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to
                 R.string.shortcut_helper_category_system_apps,
+            KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to R.string.shortcut_helper_category_system_apps,
 
             // Multitasking Category
             KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT to
@@ -102,15 +116,23 @@
                 R.string.shortcutHelper_category_split_screen,
             KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT to
                 R.string.shortcutHelper_category_split_screen,
+            KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW to
+                R.string.shortcutHelper_category_split_screen,
+            KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW to
+                R.string.shortcutHelper_category_split_screen,
+            KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW to
+                R.string.shortcutHelper_category_split_screen,
+            KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW to
+                R.string.shortcutHelper_category_split_screen,
+            KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY to R.string.shortcutHelper_category_split_screen,
 
             // App Category
-            KEY_GESTURE_TYPE_LAUNCH_APPLICATION to
-                R.string.keyboard_shortcut_group_applications,
+            KEY_GESTURE_TYPE_LAUNCH_APPLICATION to R.string.keyboard_shortcut_group_applications,
         )
 
     /**
-     * App Category shortcut labels are mapped dynamically based on intent
-     * see [InputGestureDataAdapter.fetchShortcutLabelByAppLaunchData]
+     * App Category shortcut labels are mapped dynamically based on intent see
+     * [InputGestureDataAdapter.fetchShortcutLabelByAppLaunchData]
      */
     val gestureToInternalKeyboardShortcutInfoLabelResIdMap =
         mapOf(
@@ -130,12 +152,23 @@
             KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to R.string.group_system_access_google_assistant,
             KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to
                 R.string.group_system_access_google_assistant,
+            KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to R.string.group_system_toggle_voice_access,
 
             // Multitasking Category
             KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to R.string.group_system_cycle_forward,
             KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT to R.string.system_multitasking_lhs,
             KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT to R.string.system_multitasking_rhs,
             KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION to R.string.system_multitasking_full_screen,
+            KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW to
+                R.string.system_desktop_mode_snap_left_window,
+            KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW to
+                R.string.system_desktop_mode_snap_right_window,
+            KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW to
+                R.string.system_desktop_mode_minimize_window,
+            KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW to
+                R.string.system_desktop_mode_toggle_maximize_window,
+            KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY to
+                R.string.system_multitasking_move_to_next_display,
         )
 
     val shortcutLabelToKeyGestureTypeMap: Map<String, Int>
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt
index 5060abd..c3c9df9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt
@@ -32,12 +32,14 @@
 import android.view.KeyEvent.KEYCODE_S
 import android.view.KeyEvent.KEYCODE_SLASH
 import android.view.KeyEvent.KEYCODE_TAB
+import android.view.KeyEvent.KEYCODE_V
 import android.view.KeyEvent.META_ALT_ON
 import android.view.KeyEvent.META_CTRL_ON
 import android.view.KeyEvent.META_META_ON
 import android.view.KeyEvent.META_SHIFT_ON
 import android.view.KeyboardShortcutGroup
 import android.view.KeyboardShortcutInfo
+import com.android.hardware.input.Flags.enableVoiceAccessKeyGestures
 import com.android.systemui.Flags.shortcutHelperKeyGlyph
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyboard.shortcut.data.model.shortcutInfo
@@ -118,8 +120,8 @@
         return shortcuts
     }
 
-    private fun systemControlsShortcuts() =
-        listOf(
+    private fun systemControlsShortcuts(): List<KeyboardShortcutInfo>  {
+        val shortcuts = mutableListOf(
             // Access list of all apps and search (i.e. Search/Launcher):
             //  - Meta
             shortcutInfo(resources.getString(R.string.group_system_access_all_apps_search)) {
@@ -176,6 +178,19 @@
             },
         )
 
+        if (enableVoiceAccessKeyGestures()) {
+            shortcuts.add(
+                // Toggle voice access:
+                //  - Meta + Alt + V
+                shortcutInfo(resources.getString(R.string.group_system_toggle_voice_access)) {
+                    command(META_META_ON or META_ALT_ON, KEYCODE_V)
+                }
+            )
+        }
+
+        return shortcuts
+    }
+
     private fun systemAppsShortcuts() =
         listOf(
             // Pull up Notes app for quick memo:
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt
index 274fa59..a16b4a6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt
@@ -53,11 +53,11 @@
 
     override suspend fun onActivated(): Nothing {
         viewModel.shortcutCustomizationUiState.collect { uiState ->
-            when(uiState){
+            when (uiState) {
                 is AddShortcutDialog,
                 is DeleteShortcutDialog,
                 is ResetShortcutDialog -> {
-                    if (dialog == null){
+                    if (dialog == null) {
                         dialog = createDialog().also { it.show() }
                     }
                 }
@@ -85,7 +85,9 @@
             ShortcutCustomizationDialog(
                 uiState = uiState,
                 modifier = Modifier.width(364.dp).wrapContentHeight().padding(vertical = 24.dp),
-                onKeyPress = { viewModel.onKeyPressed(it) },
+                onShortcutKeyCombinationSelected = {
+                    viewModel.onShortcutKeyCombinationSelected(it)
+                },
                 onCancel = { dialog.dismiss() },
                 onConfirmSetShortcut = { coroutineScope.launch { viewModel.onSetShortcut() } },
                 onConfirmDeleteShortcut = {
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt
index 3819f6d..d9e55f8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt
@@ -49,8 +49,12 @@
 import androidx.compose.ui.focus.focusProperties
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.key.Key
 import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.input.key.KeyEventType
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.onPreviewKeyEvent
+import androidx.compose.ui.input.key.type
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.font.FontWeight
@@ -65,7 +69,7 @@
 fun ShortcutCustomizationDialog(
     uiState: ShortcutCustomizationUiState,
     modifier: Modifier = Modifier,
-    onKeyPress: (KeyEvent) -> Boolean,
+    onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean,
     onCancel: () -> Unit,
     onConfirmSetShortcut: () -> Unit,
     onConfirmDeleteShortcut: () -> Unit,
@@ -73,7 +77,13 @@
 ) {
     when (uiState) {
         is ShortcutCustomizationUiState.AddShortcutDialog -> {
-            AddShortcutDialog(modifier, uiState, onKeyPress, onCancel, onConfirmSetShortcut)
+            AddShortcutDialog(
+                modifier,
+                uiState,
+                onShortcutKeyCombinationSelected,
+                onCancel,
+                onConfirmSetShortcut,
+            )
         }
         is ShortcutCustomizationUiState.DeleteShortcutDialog -> {
             DeleteShortcutDialog(modifier, onCancel, onConfirmDeleteShortcut)
@@ -91,29 +101,27 @@
 private fun AddShortcutDialog(
     modifier: Modifier,
     uiState: ShortcutCustomizationUiState.AddShortcutDialog,
-    onKeyPress: (KeyEvent) -> Boolean,
+    onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean,
     onCancel: () -> Unit,
-    onConfirmSetShortcut: () -> Unit
-){
+    onConfirmSetShortcut: () -> Unit,
+) {
     Column(modifier = modifier) {
         Title(uiState.shortcutLabel)
         Description(
-            text =
-            stringResource(
-                id = R.string.shortcut_customize_mode_add_shortcut_description
-            )
+            text = stringResource(id = R.string.shortcut_customize_mode_add_shortcut_description)
         )
         PromptShortcutModifier(
             modifier =
-            Modifier.padding(top = 24.dp, start = 116.5.dp, end = 116.5.dp)
-                .width(131.dp)
-                .height(48.dp),
+                Modifier.padding(top = 24.dp, start = 116.5.dp, end = 116.5.dp)
+                    .width(131.dp)
+                    .height(48.dp),
             defaultModifierKey = uiState.defaultCustomShortcutModifierKey,
         )
         SelectedKeyCombinationContainer(
             shouldShowError = uiState.errorMessage.isNotEmpty(),
-            onKeyPress = onKeyPress,
+            onShortcutKeyCombinationSelected = onShortcutKeyCombinationSelected,
             pressedKeys = uiState.pressedKeys,
+            onConfirmSetShortcut = onConfirmSetShortcut,
         )
         ErrorMessageContainer(uiState.errorMessage)
         DialogButtons(
@@ -121,9 +129,7 @@
             isConfirmButtonEnabled = uiState.pressedKeys.isNotEmpty(),
             onConfirm = onConfirmSetShortcut,
             confirmButtonText =
-            stringResource(
-                R.string.shortcut_helper_customize_dialog_set_shortcut_button_label
-            ),
+                stringResource(R.string.shortcut_helper_customize_dialog_set_shortcut_button_label),
         )
     }
 }
@@ -132,20 +138,15 @@
 private fun DeleteShortcutDialog(
     modifier: Modifier,
     onCancel: () -> Unit,
-    onConfirmDeleteShortcut: () -> Unit
-){
+    onConfirmDeleteShortcut: () -> Unit,
+) {
     ConfirmationDialog(
         modifier = modifier,
-        title =
-        stringResource(
-            id = R.string.shortcut_customize_mode_remove_shortcut_dialog_title
-        ),
+        title = stringResource(id = R.string.shortcut_customize_mode_remove_shortcut_dialog_title),
         description =
-        stringResource(
-            id = R.string.shortcut_customize_mode_remove_shortcut_description
-        ),
+            stringResource(id = R.string.shortcut_customize_mode_remove_shortcut_description),
         confirmButtonText =
-        stringResource(R.string.shortcut_helper_customize_dialog_remove_button_label),
+            stringResource(R.string.shortcut_helper_customize_dialog_remove_button_label),
         onCancel = onCancel,
         onConfirm = onConfirmDeleteShortcut,
     )
@@ -155,20 +156,15 @@
 private fun ResetShortcutDialog(
     modifier: Modifier,
     onCancel: () -> Unit,
-    onConfirmResetShortcut: () -> Unit
-){
+    onConfirmResetShortcut: () -> Unit,
+) {
     ConfirmationDialog(
         modifier = modifier,
-        title =
-        stringResource(
-            id = R.string.shortcut_customize_mode_reset_shortcut_dialog_title
-        ),
+        title = stringResource(id = R.string.shortcut_customize_mode_reset_shortcut_dialog_title),
         description =
-        stringResource(
-            id = R.string.shortcut_customize_mode_reset_shortcut_description
-        ),
+            stringResource(id = R.string.shortcut_customize_mode_reset_shortcut_description),
         confirmButtonText =
-        stringResource(R.string.shortcut_helper_customize_dialog_reset_button_label),
+            stringResource(R.string.shortcut_helper_customize_dialog_reset_button_label),
         onCancel = onCancel,
         onConfirm = onConfirmResetShortcut,
     )
@@ -201,6 +197,9 @@
     onConfirm: () -> Unit,
     confirmButtonText: String,
 ) {
+    val focusRequester = remember { FocusRequester() }
+    LaunchedEffect(Unit) { focusRequester.requestFocus() }
+
     Row(
         modifier =
             Modifier.padding(top = 24.dp, start = 24.dp, end = 24.dp)
@@ -218,6 +217,10 @@
         )
         Spacer(modifier = Modifier.width(8.dp))
         ShortcutHelperButton(
+            modifier =
+                Modifier.focusRequester(focusRequester).focusProperties {
+                    canFocus = true
+                }, // enable focus on touch/click mode
             onClick = onConfirm,
             color = MaterialTheme.colorScheme.primary,
             width = 116.dp,
@@ -248,8 +251,9 @@
 @Composable
 private fun SelectedKeyCombinationContainer(
     shouldShowError: Boolean,
-    onKeyPress: (KeyEvent) -> Boolean,
+    onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean,
     pressedKeys: List<ShortcutKey>,
+    onConfirmSetShortcut: () -> Unit,
 ) {
     val interactionSource = remember { MutableInteractionSource() }
     val isFocused by interactionSource.collectIsFocusedAsState()
@@ -269,7 +273,17 @@
             Modifier.padding(all = 16.dp)
                 .sizeIn(minWidth = 332.dp, minHeight = 56.dp)
                 .border(width = 2.dp, color = outlineColor, shape = RoundedCornerShape(50.dp))
-                .onKeyEvent { onKeyPress(it) }
+                .onPreviewKeyEvent { keyEvent ->
+                    val keyEventProcessed = onShortcutKeyCombinationSelected(keyEvent)
+                    if (
+                        !keyEventProcessed &&
+                            keyEvent.key == Key.Enter &&
+                            keyEvent.type == KeyEventType.KeyUp
+                    ) {
+                        onConfirmSetShortcut()
+                        true
+                    } else keyEventProcessed
+                }
                 .focusProperties { canFocus = true } // enables keyboard focus when in touch mode
                 .focusRequester(focusRequester),
         interactionSource = interactionSource,
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
index aea583d..ba31d08 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
@@ -729,6 +729,7 @@
         contentColor = MaterialTheme.colorScheme.primary,
         contentPaddingVertical = 0.dp,
         contentPaddingHorizontal = 0.dp,
+        contentDescription = stringResource(R.string.shortcut_helper_add_shortcut_button_label),
     )
 }
 
@@ -749,6 +750,7 @@
         contentColor = MaterialTheme.colorScheme.primary,
         contentPaddingVertical = 0.dp,
         contentPaddingHorizontal = 0.dp,
+        contentDescription = stringResource(R.string.shortcut_helper_delete_shortcut_button_label),
     )
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt
index 55c0fe2..9a380f4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt
@@ -230,6 +230,7 @@
     contentPaddingVertical: Dp = 10.dp,
     enabled: Boolean = true,
     border: BorderStroke? = null,
+    contentDescription: String? = null,
 ) {
     ShortcutHelperButtonSurface(
         onClick = onClick,
@@ -254,8 +255,7 @@
                 Icon(
                     tint = contentColor,
                     imageVector = iconSource.imageVector,
-                    contentDescription =
-                        null, // TODO this probably should not be null for accessibility.
+                    contentDescription = contentDescription,
                     modifier = Modifier.size(20.dp).wrapContentSize(Alignment.Center),
                 )
             }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
index 373eb25..915a66c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
@@ -46,6 +46,7 @@
     private val context: Context,
     private val shortcutCustomizationInteractor: ShortcutCustomizationInteractor,
 ) {
+    private var keyDownEventCache: KeyEvent? = null
     private val _shortcutCustomizationUiState =
         MutableStateFlow<ShortcutCustomizationUiState>(ShortcutCustomizationUiState.Inactive)
 
@@ -94,9 +95,16 @@
         shortcutCustomizationInteractor.updateUserSelectedKeyCombination(null)
     }
 
-    fun onKeyPressed(keyEvent: KeyEvent): Boolean {
-        if ((keyEvent.isMetaPressed && keyEvent.type == KeyEventType.KeyDown)) {
-            updatePressedKeys(keyEvent)
+    fun onShortcutKeyCombinationSelected(keyEvent: KeyEvent): Boolean {
+        if (isModifier(keyEvent)) {
+            return false
+        }
+        if (keyEvent.isMetaPressed && keyEvent.type == KeyEventType.KeyDown) {
+            keyDownEventCache = keyEvent
+            return true
+        } else if (keyEvent.type == KeyEventType.KeyUp && keyEvent.key == keyDownEventCache?.key) {
+            updatePressedKeys(keyDownEventCache!!)
+            clearKeyDownEventCache()
             return true
         }
         return false
@@ -157,16 +165,21 @@
         return (uiState as? AddShortcutDialog)?.copy(errorMessage = errorMessage) ?: uiState
     }
 
+    private fun isModifier(keyEvent: KeyEvent) = SUPPORTED_MODIFIERS.contains(keyEvent.key)
+
     private fun updatePressedKeys(keyEvent: KeyEvent) {
-        val isModifier = SUPPORTED_MODIFIERS.contains(keyEvent.key)
         val keyCombination =
             KeyCombination(
                 modifiers = keyEvent.nativeKeyEvent.modifiers,
-                keyCode = if (!isModifier) keyEvent.key.nativeKeyCode else null,
+                keyCode = if (!isModifier(keyEvent)) keyEvent.key.nativeKeyCode else null,
             )
         shortcutCustomizationInteractor.updateUserSelectedKeyCombination(keyCombination)
     }
 
+    private fun clearKeyDownEventCache() {
+        keyDownEventCache = null
+    }
+
     @AssistedFactory
     interface Factory {
         fun create(): ShortcutCustomizationViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index d40fe46..5913839 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -538,27 +538,30 @@
 
         @Override // Binder interface
         public void onFinishedGoingToSleep(
-                @PowerManager.GoToSleepReason int pmSleepReason, boolean cameraGestureTriggered) {
+                @PowerManager.GoToSleepReason int pmSleepReason, boolean
+                powerButtonLaunchGestureTriggered) {
             trace("onFinishedGoingToSleep pmSleepReason=" + pmSleepReason
-                    + " cameraGestureTriggered=" + cameraGestureTriggered);
+                    + " powerButtonLaunchTriggered=" + powerButtonLaunchGestureTriggered);
             checkPermission();
             mKeyguardViewMediator.onFinishedGoingToSleep(
                     WindowManagerPolicyConstants.translateSleepReasonToOffReason(pmSleepReason),
-                    cameraGestureTriggered);
-            mPowerInteractor.onFinishedGoingToSleep(cameraGestureTriggered);
+                    powerButtonLaunchGestureTriggered);
+            mPowerInteractor.onFinishedGoingToSleep(powerButtonLaunchGestureTriggered);
             mKeyguardLifecyclesDispatcher.dispatch(
                     KeyguardLifecyclesDispatcher.FINISHED_GOING_TO_SLEEP);
         }
 
         @Override // Binder interface
         public void onStartedWakingUp(
-                @PowerManager.WakeReason int pmWakeReason, boolean cameraGestureTriggered) {
+                @PowerManager.WakeReason int pmWakeReason,
+                boolean powerButtonLaunchGestureTriggered) {
             trace("onStartedWakingUp pmWakeReason=" + pmWakeReason
-                    + " cameraGestureTriggered=" + cameraGestureTriggered);
+                    + " powerButtonLaunchGestureTriggered=" + powerButtonLaunchGestureTriggered);
             Trace.beginSection("KeyguardService.mBinder#onStartedWakingUp");
             checkPermission();
-            mKeyguardViewMediator.onStartedWakingUp(pmWakeReason, cameraGestureTriggered);
-            mPowerInteractor.onStartedWakingUp(pmWakeReason, cameraGestureTriggered);
+            mKeyguardViewMediator.onStartedWakingUp(pmWakeReason,
+                    powerButtonLaunchGestureTriggered);
+            mPowerInteractor.onStartedWakingUp(pmWakeReason, powerButtonLaunchGestureTriggered);
             mKeyguardLifecyclesDispatcher.dispatch(
                     KeyguardLifecyclesDispatcher.STARTED_WAKING_UP, pmWakeReason);
             Trace.endSection();
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 63ac509..6473628 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -109,6 +109,7 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.app.animation.Interpolators;
+import com.android.app.tracing.coroutines.TrackTracer;
 import com.android.internal.foldables.FoldGracePeriodProvider;
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.jank.InteractionJankMonitor.Configuration;
@@ -813,7 +814,7 @@
             if (targetUserId != mSelectedUserInteractor.getSelectedUserId()) {
                 return;
             }
-            if (DEBUG) Log.d(TAG, "keyguardDone");
+            Log.d(TAG, "keyguardDone");
             tryKeyguardDone();
         }
 
@@ -832,7 +833,7 @@
         @Override
         public void keyguardDonePending(int targetUserId) {
             Trace.beginSection("KeyguardViewMediator.mViewMediatorCallback#keyguardDonePending");
-            if (DEBUG) Log.d(TAG, "keyguardDonePending");
+            Log.d(TAG, "keyguardDonePending");
             if (targetUserId != mSelectedUserInteractor.getSelectedUserId()) {
                 Trace.endSection();
                 return;
@@ -2735,10 +2736,8 @@
     }
 
     private void tryKeyguardDone() {
-        if (DEBUG) {
-            Log.d(TAG, "tryKeyguardDone: pending - " + mKeyguardDonePending + ", animRan - "
-                    + mHideAnimationRun + " animRunning - " + mHideAnimationRunning);
-        }
+        Log.d(TAG, "tryKeyguardDone: pending - " + mKeyguardDonePending + ", animRan - "
+                + mHideAnimationRun + " animRunning - " + mHideAnimationRunning);
         if (!mKeyguardDonePending && mHideAnimationRun && !mHideAnimationRunning) {
             handleKeyguardDone();
         } else if (mSurfaceBehindRemoteAnimationRunning) {
@@ -3040,7 +3039,7 @@
     }
 
     private final Runnable mHideAnimationFinishedRunnable = () -> {
-        Log.e(TAG, "mHideAnimationFinishedRunnable#run");
+        Log.d(TAG, "mHideAnimationFinishedRunnable#run");
         mHideAnimationRunning = false;
         tryKeyguardDone();
     };
@@ -3983,7 +3982,7 @@
 
     public void setPendingLock(boolean hasPendingLock) {
         mPendingLock = hasPendingLock;
-        Trace.traceCounter(Trace.TRACE_TAG_APP, "pendingLock", mPendingLock ? 1 : 0);
+        TrackTracer.instantForGroup("keyguard", "pendingLock", mPendingLock ? 1 : 0);
     }
 
     private boolean isViewRootReady() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ScreenLifecycle.java b/packages/SystemUI/src/com/android/systemui/keyguard/ScreenLifecycle.java
index 633628f..c318200 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ScreenLifecycle.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ScreenLifecycle.java
@@ -16,8 +16,7 @@
 
 package com.android.systemui.keyguard;
 
-import android.os.Trace;
-
+import com.android.app.tracing.coroutines.TrackTracer;
 import com.android.systemui.Dumpable;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.power.domain.interactor.PowerInteractor;
@@ -80,7 +79,7 @@
 
     private void setScreenState(int screenState) {
         mScreenState = screenState;
-        Trace.traceCounter(Trace.TRACE_TAG_APP, "screenState", screenState);
+        TrackTracer.instantForGroup("screen", "screenState", screenState);
     }
 
     public interface Observer {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java b/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java
index c0ffda6..c261cfe 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java
@@ -24,11 +24,11 @@
 import android.os.Bundle;
 import android.os.PowerManager;
 import android.os.RemoteException;
-import android.os.Trace;
 import android.util.DisplayMetrics;
 
 import androidx.annotation.Nullable;
 
+import com.android.app.tracing.coroutines.TrackTracer;
 import com.android.systemui.Dumpable;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpManager;
@@ -197,7 +197,7 @@
 
     private void setWakefulness(@Wakefulness int wakefulness) {
         mWakefulness = wakefulness;
-        Trace.traceCounter(Trace.TRACE_TAG_APP, "wakefulness", wakefulness);
+        TrackTracer.instantForGroup("screen", "wakefulness", wakefulness);
     }
 
     private void updateLastWakeOriginLocation() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt
index 5b28a3f..a74384f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt
@@ -156,6 +156,13 @@
         setWmLockscreenState(lockscreenShowing = lockscreenShown)
     }
 
+    /**
+     * Called when the keyguard going away remote animation is started, and we have a
+     * RemoteAnimationTarget to animate.
+     *
+     * This is triggered either by this class calling ATMS#keyguardGoingAway, or by WM directly,
+     * such as when an activity with FLAG_DISMISS_KEYGUARD is launched over a dismissible keyguard.
+     */
     fun onKeyguardGoingAwayRemoteAnimationStart(
         @WindowManager.TransitionOldType transit: Int,
         apps: Array<RemoteAnimationTarget>,
@@ -163,19 +170,32 @@
         nonApps: Array<RemoteAnimationTarget>,
         finishedCallback: IRemoteAnimationFinishedCallback,
     ) {
-        // Make sure this is true - we set it true when requesting keyguardGoingAway, but there are
-        // cases where WM starts this transition on its own.
-        isKeyguardGoingAway = true
+        // If we weren't expecting the keyguard to be going away, WM triggered this transition.
+        if (!isKeyguardGoingAway) {
+            // Since WM triggered this, we're likely not transitioning to GONE yet. See if we can
+            // start that transition.
+            val startedDismiss =
+                keyguardDismissTransitionInteractor.startDismissKeyguardTransition(
+                    reason = "Going away remote animation started"
+                )
 
-        // Ensure that we've started a dismiss keyguard transition. WindowManager can start the
-        // going away animation on its own, if an activity launches and then requests dismissing the
-        // keyguard. In this case, this is the first and only signal we'll receive to start
-        // a transition to GONE. This transition needs to start even if we're not provided an app
-        // animation target - it's possible the app is destroyed on creation, etc. but we'll still
-        // be unlocking.
-        keyguardDismissTransitionInteractor.startDismissKeyguardTransition(
-            reason = "Going away remote animation started"
-        )
+            if (!startedDismiss) {
+                // If the transition wasn't started, we're already GONE. This can happen with timing
+                // issues, where the remote animation took a long time to start, and something else
+                // caused us to unlock in the meantime. Since we're already GONE, simply end the
+                // remote animatiom immediately.
+                Log.d(
+                    TAG,
+                    "onKeyguardGoingAwayRemoteAnimationStart: " +
+                        "Dismiss transition was not started; we're already GONE. " +
+                        "Ending remote animation.",
+                )
+                finishedCallback.onAnimationFinished()
+                return
+            }
+
+            isKeyguardGoingAway = true
+        }
 
         if (apps.isNotEmpty()) {
             goingAwayRemoteAnimationFinishedCallback = finishedCallback
@@ -278,6 +298,6 @@
     }
 
     companion object {
-        private val TAG = WindowManagerLockscreenVisibilityManager::class.java.simpleName
+        private val TAG = "WindowManagerLsVis"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index ac04dd5..a39982d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.keyguard.data.repository
 
 import android.graphics.Point
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.internal.widget.LockPatternUtils
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
@@ -64,7 +65,6 @@
 import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.stateIn
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 /** Defines interface for classes that encapsulate application state for the keyguard. */
 interface KeyguardRepository {
@@ -248,13 +248,6 @@
     val keyguardDoneAnimationsFinished: Flow<Unit>
 
     /**
-     * Receive whether clock should be centered on lockscreen.
-     *
-     * @deprecated When scene container flag is on use clockShouldBeCentered from domain level.
-     */
-    val clockShouldBeCentered: Flow<Boolean>
-
-    /**
      * Whether the primary authentication is required for the given user due to lockdown or
      * encryption after reboot.
      */
@@ -306,8 +299,6 @@
 
     suspend fun setKeyguardDone(keyguardDoneType: KeyguardDone)
 
-    fun setClockShouldBeCentered(shouldBeCentered: Boolean)
-
     /**
      * Updates signal that the keyguard done animations are finished
      *
@@ -390,9 +381,6 @@
 
     override val panelAlpha: MutableStateFlow<Float> = MutableStateFlow(1f)
 
-    private val _clockShouldBeCentered = MutableStateFlow(true)
-    override val clockShouldBeCentered: Flow<Boolean> = _clockShouldBeCentered.asStateFlow()
-
     override val topClippingBounds = MutableStateFlow<Int?>(null)
 
     override val isKeyguardShowing: MutableStateFlow<Boolean> =
@@ -681,10 +669,6 @@
         _isQuickSettingsVisible.value = isVisible
     }
 
-    override fun setClockShouldBeCentered(shouldBeCentered: Boolean) {
-        _clockShouldBeCentered.value = shouldBeCentered
-    }
-
     override fun setKeyguardEnabled(enabled: Boolean) {
         _isKeyguardEnabled.value = enabled
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index 354fc3d..24f2493 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -212,8 +212,11 @@
                 Log.i(TAG, "Duplicate call to start the transition, rejecting: $info")
                 return@withContext null
             }
+            val isAnimatorRunning = lastAnimator?.isRunning() ?: false
+            val isManualTransitionRunning =
+                updateTransitionId != null && lastStep.transitionState != TransitionState.FINISHED
             val startingValue =
-                if (lastStep.transitionState != TransitionState.FINISHED) {
+                if (isAnimatorRunning || isManualTransitionRunning) {
                     Log.i(TAG, "Transition still active: $lastStep, canceling")
                     when (info.modeOnCanceled) {
                         TransitionModeOnCanceled.LAST_VALUE -> lastStep.value
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 9896365..b42da52 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
@@ -132,6 +132,8 @@
                             if (SceneContainerFlag.isEnabled) return@collect
                             startTransitionTo(
                                 toState = KeyguardState.GONE,
+                                modeOnCanceled = TransitionModeOnCanceled.REVERSE,
+                                ownerReason = "canWakeDirectlyToGone = true",
                             )
                         } else if (shouldTransitionToLockscreen) {
                             val modeOnCanceled =
@@ -146,7 +148,7 @@
                             startTransitionTo(
                                 toState = KeyguardState.LOCKSCREEN,
                                 modeOnCanceled = modeOnCanceled,
-                                ownerReason = "listen for aod to awake"
+                                ownerReason = "listen for aod to awake",
                             )
                         } else if (shouldTransitionToOccluded) {
                             startTransitionTo(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
index f792935..ab5fdd6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
@@ -25,6 +25,10 @@
 import com.android.systemui.keyguard.shared.model.ClockSize
 import com.android.systemui.keyguard.shared.model.ClockSizeSetting
 import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
+import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
 import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
 import com.android.systemui.plugins.clocks.ClockController
 import com.android.systemui.plugins.clocks.ClockId
@@ -39,6 +43,7 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 
@@ -117,7 +122,43 @@
                 }
             }
         } else {
-            keyguardInteractor.clockShouldBeCentered
+            combine(
+                    shadeInteractor.isShadeLayoutWide,
+                    activeNotificationsInteractor.areAnyNotificationsPresent,
+                    keyguardInteractor.dozeTransitionModel,
+                    keyguardTransitionInteractor.startedKeyguardTransitionStep.map { it.to == AOD },
+                    keyguardTransitionInteractor.startedKeyguardTransitionStep.map {
+                        it.to == LOCKSCREEN
+                    },
+                    keyguardTransitionInteractor.startedKeyguardTransitionStep.map {
+                        it.to == DOZING
+                    },
+                    keyguardInteractor.isPulsing,
+                    keyguardTransitionInteractor.startedKeyguardTransitionStep.map { it.to == GONE },
+                ) {
+                    isShadeLayoutWide,
+                    areAnyNotificationsPresent,
+                    dozeTransitionModel,
+                    startedToAod,
+                    startedToLockScreen,
+                    startedToDoze,
+                    isPulsing,
+                    startedToGone ->
+                    when {
+                        !isShadeLayoutWide -> true
+                        // [areAnyNotificationsPresent] also reacts to notification stack in
+                        // homescreen
+                        // it may cause unnecessary `false` emission when there's notification in
+                        // homescreen
+                        // but none in lockscreen when going from GONE to AOD / DOZING
+                        // use null to skip emitting wrong value
+                        startedToGone || startedToDoze -> null
+                        startedToLockScreen -> !areAnyNotificationsPresent
+                        startedToAod -> !isPulsing
+                        else -> true
+                    }
+                }
+                .filterNotNull()
         }
 
     fun setClockSize(size: ClockSize) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissTransitionInteractor.kt
index 4793d95..089e5dc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissTransitionInteractor.kt
@@ -48,9 +48,12 @@
      *
      * This is called exclusively by sources that can authoritatively say we should be unlocked,
      * including KeyguardSecurityContainerController and WindowManager.
+     *
+     * Returns [false] if the transition was not started, because we're already GONE or we don't
+     * know how to dismiss keyguard from the current state.
      */
-    fun startDismissKeyguardTransition(reason: String = "") {
-        if (SceneContainerFlag.isEnabled) return
+    fun startDismissKeyguardTransition(reason: String = ""): Boolean {
+        if (SceneContainerFlag.isEnabled) return false
         Log.d(TAG, "#startDismissKeyguardTransition(reason=$reason)")
         val startedState =
             if (transitionRaceCondition()) {
@@ -65,13 +68,20 @@
             AOD -> fromAodTransitionInteractor.dismissAod()
             DOZING -> fromDozingTransitionInteractor.dismissFromDozing()
             KeyguardState.OCCLUDED -> fromOccludedTransitionInteractor.dismissFromOccluded()
-            KeyguardState.GONE ->
+            KeyguardState.GONE -> {
                 Log.i(
                     TAG,
                     "Already transitioning to GONE; ignoring startDismissKeyguardTransition.",
                 )
-            else -> Log.e(TAG, "We don't know how to dismiss keyguard from state $startedState.")
+                return false
+            }
+            else -> {
+                Log.e(TAG, "We don't know how to dismiss keyguard from state $startedState.")
+                return false
+            }
         }
+
+        return true
     }
 
     companion object {
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 0193d7cb..8f7f2a0 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
@@ -44,7 +44,6 @@
 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.StatusBarState
-import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.res.R
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
@@ -84,7 +83,6 @@
 @Inject
 constructor(
     private val repository: KeyguardRepository,
-    powerInteractor: PowerInteractor,
     bouncerRepository: KeyguardBouncerRepository,
     @ShadeDisplayAware configurationInteractor: ConfigurationInteractor,
     shadeRepository: ShadeRepository,
@@ -216,11 +214,7 @@
                     // should actually be quite strange to leave AOD and then go straight to
                     // DREAMING so this should be fine.
                     delay(IS_ABLE_TO_DREAM_DELAY_MS)
-                    isDreaming
-                        .sample(powerInteractor.isAwake) { isDreaming, isAwake ->
-                            isDreaming && isAwake
-                        }
-                        .debounce(50L)
+                    isDreaming.debounce(50L)
                 } else {
                     flowOf(false)
                 }
@@ -418,8 +412,6 @@
                 initialValue = 0f,
             )
 
-    val clockShouldBeCentered: Flow<Boolean> = repository.clockShouldBeCentered
-
     /** Whether to animate the next doze mode transition. */
     val animateDozingTransitions: Flow<Boolean> by lazy {
         if (SceneContainerFlag.isEnabled) {
@@ -485,10 +477,6 @@
         repository.setAnimateDozingTransitions(animate)
     }
 
-    fun setClockShouldBeCentered(shouldBeCentered: Boolean) {
-        repository.setClockShouldBeCentered(shouldBeCentered)
-    }
-
     fun setLastRootViewTapPosition(point: Point?) {
         repository.lastRootViewTapPosition.value = point
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt
index a133f06..3bdc32d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt
@@ -116,9 +116,10 @@
      * - We're wake and unlocking (fingerprint auth occurred while asleep).
      * - We're allowed to ignore auth and return to GONE, due to timeouts not elapsing.
      * - We're DREAMING and dismissible.
-     * - We're already GONE. Technically you're already awake when GONE, but this makes it easier to
-     *   reason about this state (for example, if canWakeDirectlyToGone, don't tell WM to pause the
-     *   top activity - something you should never do while GONE as well).
+     * - We're already GONE and not transitioning out of GONE. Technically you're already awake when
+     *   GONE, but this makes it easier to reason about this state (for example, if
+     *   canWakeDirectlyToGone, don't tell WM to pause the top activity - something you should never
+     *   do while GONE as well).
      */
     val canWakeDirectlyToGone =
         combine(
@@ -138,7 +139,8 @@
                     canIgnoreAuthAndReturnToGone ||
                     (currentState == KeyguardState.DREAMING &&
                         keyguardInteractor.isKeyguardDismissible.value) ||
-                    currentState == KeyguardState.GONE
+                    (currentState == KeyguardState.GONE &&
+                        transitionInteractor.getStartedState() == KeyguardState.GONE)
             }
             .distinctUntilChanged()
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt
index 542fb9b..3eb8522 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt
@@ -23,4 +23,10 @@
     // No-op config that will be used by dagger of other SysUI variants which don't blur the
     // background surface.
     @Inject constructor() : this(0.0f, 0.0f)
+
+    companion object {
+        // Blur the shade much lesser than the background surface so that the surface is
+        // distinguishable from the background.
+        @JvmStatic fun Float.maxBlurRadiusToNotificationPanelBlurRadius(): Float = this / 3.0f
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt
index e77e9dd..eb1afb4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt
@@ -30,6 +30,9 @@
     /** Radius of blur applied to the window's root view. */
     val windowBlurRadius: Flow<Float>
 
+    /** Radius of blur applied to the notifications on expanded shade */
+    val notificationBlurRadius: Flow<Float>
+
     fun transitionProgressToBlurRadius(
         starBlurRadius: Float,
         endBlurRadius: Float,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt
index f174557..92bb5e6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import com.android.systemui.Flags
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromAlternateBouncerTransitionInteractor
 import com.android.systemui.keyguard.shared.model.Edge
@@ -23,6 +24,7 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.BlurConfig
+import com.android.systemui.keyguard.ui.transitions.BlurConfig.Companion.maxBlurRadiusToNotificationPanelBlurRadius
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
 import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
@@ -73,7 +75,28 @@
 
     val lockscreenAlpha: Flow<Float> = if (WindowBlurFlag.isEnabled) alphaFlow else emptyFlow()
 
-    val notificationAlpha: Flow<Float> = alphaFlow
+    val notificationAlpha: Flow<Float> =
+        if (Flags.bouncerUiRevamp()) {
+            shadeDependentFlows.transitionFlow(
+                flowWhenShadeIsNotExpanded = lockscreenAlpha,
+                flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(1f),
+            )
+        } else {
+            alphaFlow
+        }
+
+    override val notificationBlurRadius: Flow<Float> =
+        if (Flags.bouncerUiRevamp()) {
+            shadeDependentFlows.transitionFlow(
+                flowWhenShadeIsNotExpanded = emptyFlow(),
+                flowWhenShadeIsExpanded =
+                    transitionAnimation.immediatelyTransitionTo(
+                        blurConfig.maxBlurRadiusPx.maxBlurRadiusToNotificationPanelBlurRadius()
+                    ),
+            )
+        } else {
+            emptyFlow<Float>()
+        }
 
     override val deviceEntryParentViewAlpha: Flow<Float> =
         transitionAnimation.immediatelyTransitionTo(0f)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt
index 2497def..d981eeb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import android.util.MathUtils
+import com.android.systemui.Flags.lightRevealMigration
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor
 import com.android.systemui.keyguard.shared.model.Edge
@@ -32,9 +33,7 @@
 @SysUISingleton
 class AodToOccludedTransitionViewModel
 @Inject
-constructor(
-    animationFlow: KeyguardTransitionAnimationFlow,
-) : DeviceEntryIconTransition {
+constructor(animationFlow: KeyguardTransitionAnimationFlow) : DeviceEntryIconTransition {
     private val transitionAnimation =
         animationFlow.setup(
             duration = FromAodTransitionInteractor.TO_OCCLUDED_DURATION,
@@ -52,10 +51,20 @@
         var currentAlpha = 0f
         return transitionAnimation.sharedFlow(
             duration = 250.milliseconds,
-            startTime = 100.milliseconds, // Wait for the light reveal to "hit" the LS elements.
-            onStart = { currentAlpha = viewState.alpha() },
+            startTime =
+                if (lightRevealMigration()) {
+                    100.milliseconds // Wait for the light reveal to "hit" the LS elements.
+                } else {
+                    0.milliseconds
+                },
+            onStart = {
+                if (lightRevealMigration()) {
+                    currentAlpha = viewState.alpha()
+                } else {
+                    currentAlpha = 0f
+                }
+            },
             onStep = { MathUtils.lerp(currentAlpha, 0f, it) },
-            onCancel = { 0f },
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt
index dbb6a49..e3b5587 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt
@@ -53,4 +53,7 @@
 
     override val windowBlurRadius: Flow<Float> =
         transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx)
+
+    override val notificationBlurRadius: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(0.0f)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt
index d8b617a..c937d5c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt
@@ -64,4 +64,6 @@
             },
             onFinish = { blurConfig.maxBlurRadiusPx },
         )
+    override val notificationBlurRadius: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(0f)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt
index 597df15..5ab4583 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt
@@ -42,4 +42,7 @@
 
     override val windowBlurRadius: Flow<Float> =
         transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx)
+
+    override val notificationBlurRadius: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(0.0f)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
index c373fd0..44c4c87 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import com.android.systemui.Flags
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
 import com.android.systemui.keyguard.shared.model.Edge
@@ -23,6 +24,7 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.BlurConfig
+import com.android.systemui.keyguard.ui.transitions.BlurConfig.Companion.maxBlurRadiusToNotificationPanelBlurRadius
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
 import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
@@ -32,6 +34,7 @@
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
 
 /**
  * Breaks down LOCKSCREEN->PRIMARY BOUNCER transition into discrete steps for corresponding views to
@@ -70,6 +73,29 @@
 
     val lockscreenAlpha: Flow<Float> = shortcutsAlpha
 
+    val notificationAlpha: Flow<Float> =
+        if (Flags.bouncerUiRevamp()) {
+            shadeDependentFlows.transitionFlow(
+                flowWhenShadeIsNotExpanded = lockscreenAlpha,
+                flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(1f),
+            )
+        } else {
+            lockscreenAlpha
+        }
+
+    override val notificationBlurRadius: Flow<Float> =
+        if (Flags.bouncerUiRevamp()) {
+            shadeDependentFlows.transitionFlow(
+                flowWhenShadeIsNotExpanded = emptyFlow(),
+                flowWhenShadeIsExpanded =
+                    transitionAnimation.immediatelyTransitionTo(
+                        blurConfig.maxBlurRadiusPx.maxBlurRadiusToNotificationPanelBlurRadius()
+                    ),
+            )
+        } else {
+            emptyFlow()
+        }
+
     override val deviceEntryParentViewAlpha: Flow<Float> =
         shadeDependentFlows.transitionFlow(
             flowWhenShadeIsNotExpanded =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt
index 4459810..4d3e272 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt
@@ -42,4 +42,7 @@
 
     override val windowBlurRadius: Flow<Float> =
         transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx)
+
+    override val notificationBlurRadius: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(0.0f)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt
index fab8008..224191b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt
@@ -91,4 +91,7 @@
             },
             onFinish = { blurConfig.minBlurRadiusPx },
         )
+
+    override val notificationBlurRadius: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(0.0f)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt
index eebdf2e..0f8495f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt
@@ -80,4 +80,6 @@
             },
             onFinish = { blurConfig.minBlurRadiusPx },
         )
+    override val notificationBlurRadius: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(0.0f)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt
index 3636b74..a13eef2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt
@@ -43,4 +43,7 @@
 
     override val windowBlurRadius: Flow<Float> =
         transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx)
+
+    override val notificationBlurRadius: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(0.0f)
 }
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 4ed3e6c..d1233f2 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
@@ -166,6 +166,9 @@
             createBouncerWindowBlurFlow(primaryBouncerInteractor::willRunDismissFromKeyguard)
         }
 
+    override val notificationBlurRadius: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(0.0f)
+
     val scrimAlpha: Flow<ScrimAlpha> =
         bouncerToGoneFlows.scrimAlpha(TO_GONE_DURATION, PRIMARY_BOUNCER)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
index 2edc93cb..c53a408 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
@@ -91,4 +91,7 @@
                     },
                 ),
         )
+
+    override val notificationBlurRadius: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(0.0f)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt
index 3a54a26..fe1708e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt
@@ -42,4 +42,7 @@
 
     override val windowBlurRadius: Flow<Float> =
         transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx)
+
+    override val notificationBlurRadius: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(0.0f)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt
index 0954482..a6b9442 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt
@@ -31,6 +31,7 @@
 import androidx.media3.session.MediaController as Media3Controller
 import androidx.media3.session.SessionCommand
 import androidx.media3.session.SessionToken
+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
@@ -128,7 +129,11 @@
                     drawable,
                     null, // no action to perform when clicked
                     context.getString(R.string.controls_media_button_connecting),
-                    context.getDrawable(R.drawable.ic_media_connecting_container),
+                    if (Flags.mediaControlsUiUpdate()) {
+                        context.getDrawable(R.drawable.ic_media_connecting_status_container)
+                    } else {
+                        context.getDrawable(R.drawable.ic_media_connecting_container)
+                    },
                     // Specify a rebind id to prevent the spinner from restarting on later binds.
                     com.android.internal.R.drawable.progress_small_material,
                 )
@@ -230,17 +235,33 @@
                 Player.COMMAND_PLAY_PAUSE -> {
                     if (!controller.isPlaying) {
                         MediaAction(
-                            context.getDrawable(R.drawable.ic_media_play),
+                            if (Flags.mediaControlsUiUpdate()) {
+                                context.getDrawable(R.drawable.ic_media_play_button)
+                            } else {
+                                context.getDrawable(R.drawable.ic_media_play)
+                            },
                             { executeAction(packageName, token, Player.COMMAND_PLAY_PAUSE) },
                             context.getString(R.string.controls_media_button_play),
-                            context.getDrawable(R.drawable.ic_media_play_container),
+                            if (Flags.mediaControlsUiUpdate()) {
+                                context.getDrawable(R.drawable.ic_media_play_button_container)
+                            } else {
+                                context.getDrawable(R.drawable.ic_media_play_container)
+                            },
                         )
                     } else {
                         MediaAction(
-                            context.getDrawable(R.drawable.ic_media_pause),
+                            if (Flags.mediaControlsUiUpdate()) {
+                                context.getDrawable(R.drawable.ic_media_pause_button)
+                            } else {
+                                context.getDrawable(R.drawable.ic_media_pause)
+                            },
                             { executeAction(packageName, token, Player.COMMAND_PLAY_PAUSE) },
                             context.getString(R.string.controls_media_button_pause),
-                            context.getDrawable(R.drawable.ic_media_pause_container),
+                            if (Flags.mediaControlsUiUpdate()) {
+                                context.getDrawable(R.drawable.ic_media_pause_button_container)
+                            } else {
+                                context.getDrawable(R.drawable.ic_media_pause_container)
+                            },
                         )
                     }
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
index 4f97913..9bf556c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
@@ -29,6 +29,7 @@
 import android.service.notification.StatusBarNotification
 import android.util.Log
 import androidx.media.utils.MediaConstants
+import com.android.systemui.Flags
 import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_COMPACT_ACTIONS
 import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_NOTIFICATION_ACTIONS
 import com.android.systemui.media.controls.shared.MediaControlDrawables
@@ -69,7 +70,11 @@
                 drawable,
                 null, // no action to perform when clicked
                 context.getString(R.string.controls_media_button_connecting),
-                context.getDrawable(R.drawable.ic_media_connecting_container),
+                if (Flags.mediaControlsUiUpdate()) {
+                    context.getDrawable(R.drawable.ic_media_connecting_status_container)
+                } else {
+                    context.getDrawable(R.drawable.ic_media_connecting_container)
+                },
                 // Specify a rebind id to prevent the spinner from restarting on later binds.
                 com.android.internal.R.drawable.progress_small_material,
             )
@@ -157,18 +162,34 @@
     return when (action) {
         PlaybackState.ACTION_PLAY -> {
             MediaAction(
-                context.getDrawable(R.drawable.ic_media_play),
+                if (Flags.mediaControlsUiUpdate()) {
+                    context.getDrawable(R.drawable.ic_media_play_button)
+                } else {
+                    context.getDrawable(R.drawable.ic_media_play)
+                },
                 { controller.transportControls.play() },
                 context.getString(R.string.controls_media_button_play),
-                context.getDrawable(R.drawable.ic_media_play_container),
+                if (Flags.mediaControlsUiUpdate()) {
+                    context.getDrawable(R.drawable.ic_media_play_button_container)
+                } else {
+                    context.getDrawable(R.drawable.ic_media_play_container)
+                },
             )
         }
         PlaybackState.ACTION_PAUSE -> {
             MediaAction(
-                context.getDrawable(R.drawable.ic_media_pause),
+                if (Flags.mediaControlsUiUpdate()) {
+                    context.getDrawable(R.drawable.ic_media_pause_button)
+                } else {
+                    context.getDrawable(R.drawable.ic_media_pause)
+                },
                 { controller.transportControls.pause() },
                 context.getString(R.string.controls_media_button_pause),
-                context.getDrawable(R.drawable.ic_media_pause_container),
+                if (Flags.mediaControlsUiUpdate()) {
+                    context.getDrawable(R.drawable.ic_media_pause_button_container)
+                } else {
+                    context.getDrawable(R.drawable.ic_media_pause_container)
+                },
             )
         }
         PlaybackState.ACTION_SKIP_TO_PREVIOUS -> {
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt
index 3928a71..a2ddc20 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt
@@ -1016,9 +1016,24 @@
                 expandedLayout.load(context, R.xml.media_recommendations_expanded)
             }
         }
+        readjustPlayPauseWidth()
         refreshState()
     }
 
+    private fun readjustPlayPauseWidth() {
+        // TODO: move to xml file when flag is removed.
+        if (Flags.mediaControlsUiUpdate()) {
+            collapsedLayout.constrainWidth(
+                R.id.actionPlayPause,
+                context.resources.getDimensionPixelSize(R.dimen.qs_media_action_play_pause_width),
+            )
+            expandedLayout.constrainWidth(
+                R.id.actionPlayPause,
+                context.resources.getDimensionPixelSize(R.dimen.qs_media_action_play_pause_width),
+            )
+        }
+    }
+
     /** Get a view state based on the width and height set by the scene */
     private fun obtainSceneContainerViewState(state: MediaHostState?): TransitionViewState? {
         logger.logMediaSize("scene container", widthInSceneContainerPx, heightInSceneContainerPx)
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index af37eea..1a2238c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -234,8 +234,6 @@
                     (ViewGroup.MarginLayoutParams) mItemLayout.getLayoutParams();
             params.rightMargin = showEndTouchArea ? mController.getItemMarginEndSelectable()
                     : mController.getItemMarginEndDefault();
-            mTitleIcon.setBackgroundTintList(
-                    ColorStateList.valueOf(mController.getColorItemContent()));
         }
 
         void setTwoLineLayout(MediaDevice device, boolean bFocused, boolean showSeekBar,
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
index 1dbb317..15afd22 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
@@ -37,9 +37,6 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.graphics.Bitmap;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffColorFilter;
-import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
 import android.media.AudioManager;
@@ -535,17 +532,9 @@
             // Use default Bluetooth device icon to handle getIcon() is null case.
             drawable = mContext.getDrawable(com.android.internal.R.drawable.ic_bt_headphones_a2dp);
         }
-        if (!(drawable instanceof BitmapDrawable)) {
-            setColorFilter(drawable, isActiveItem(device));
-        }
         return BluetoothUtils.createIconWithDrawable(drawable);
     }
 
-    void setColorFilter(Drawable drawable, boolean isActive) {
-        drawable.setColorFilter(new PorterDuffColorFilter(mColorItemContent,
-                PorterDuff.Mode.SRC_IN));
-    }
-
     boolean isActiveItem(MediaDevice device) {
         boolean isConnected = mLocalMediaManager.getCurrentConnectedDevice().getId().equals(
                 device.getId());
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt
index b4dca5d..b6395aa 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt
@@ -58,7 +58,6 @@
             hostUid,
             mediaProjectionMetricsLogger,
             defaultSelectedMode,
-            dialog,
         )
     }
 
@@ -79,7 +78,7 @@
         if (!::viewBinder.isInitialized) {
             viewBinder = createViewBinder()
         }
-        viewBinder.bind()
+        viewBinder.bind(dialog.requireViewById(R.id.screen_share_permission_dialog))
     }
 
     private fun updateIcon() {
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionViewBinder.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionViewBinder.kt
index d23db7c..c6e4db7 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionViewBinder.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.mediaprojection.permission
 
-import android.app.AlertDialog
 import android.content.Context
 import android.view.LayoutInflater
 import android.view.View
@@ -37,8 +36,8 @@
     private val hostUid: Int,
     private val mediaProjectionMetricsLogger: MediaProjectionMetricsLogger,
     @ScreenShareMode val defaultSelectedMode: Int = screenShareOptions.first().mode,
-    private val dialog: AlertDialog,
 ) : AdapterView.OnItemSelectedListener {
+    protected lateinit var containerView: View
     private lateinit var warning: TextView
     private lateinit var startButton: TextView
     private lateinit var screenShareModeSpinner: Spinner
@@ -54,9 +53,10 @@
         }
     }
 
-    open fun bind() {
-        warning = dialog.requireViewById(R.id.text_warning)
-        startButton = dialog.requireViewById(android.R.id.button1)
+    open fun bind(view: View) {
+        containerView = view
+        warning = containerView.requireViewById(R.id.text_warning)
+        startButton = containerView.requireViewById(android.R.id.button1)
         initScreenShareOptions()
         createOptionsView(getOptionsViewLayoutId())
     }
@@ -67,15 +67,15 @@
         initScreenShareSpinner()
     }
 
-    /** Sets fields on the dialog that change based on which option is selected. */
+    /** Sets fields on the views that change based on which option is selected. */
     private fun setOptionSpecificFields() {
         warning.text = warningText
         startButton.text = startButtonText
     }
 
     private fun initScreenShareSpinner() {
-        val adapter = OptionsAdapter(dialog.context.applicationContext, screenShareOptions)
-        screenShareModeSpinner = dialog.requireViewById(R.id.screen_share_mode_options)
+        val adapter = OptionsAdapter(containerView.context.applicationContext, screenShareOptions)
+        screenShareModeSpinner = containerView.requireViewById(R.id.screen_share_mode_options)
         screenShareModeSpinner.adapter = adapter
         screenShareModeSpinner.onItemSelectedListener = this
 
@@ -103,10 +103,10 @@
     override fun onNothingSelected(parent: AdapterView<*>?) {}
 
     private val warningText: String
-        get() = dialog.context.getString(selectedScreenShareOption.warningText, appName)
+        get() = containerView.context.getString(selectedScreenShareOption.warningText, appName)
 
     private val startButtonText: String
-        get() = dialog.context.getString(selectedScreenShareOption.startButtonText)
+        get() = containerView.context.getString(selectedScreenShareOption.startButtonText)
 
     fun setStartButtonOnClickListener(listener: View.OnClickListener?) {
         startButton.setOnClickListener { view ->
@@ -121,7 +121,7 @@
 
     private fun createOptionsView(@LayoutRes layoutId: Int?) {
         if (layoutId == null) return
-        val stub = dialog.requireViewById<View>(R.id.options_stub) as ViewStub
+        val stub = containerView.requireViewById<View>(R.id.options_stub) as ViewStub
         stub.layoutResource = layoutId
         stub.inflate()
     }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
index e9b7534..3f14b55e 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
@@ -44,6 +44,7 @@
 import android.inputmethodservice.InputMethodService;
 import android.inputmethodservice.InputMethodService.BackDispositionMode;
 import android.inputmethodservice.InputMethodService.ImeWindowVisibility;
+import android.os.Handler;
 import android.os.RemoteException;
 import android.os.Trace;
 import android.util.Log;
@@ -61,6 +62,7 @@
 import com.android.internal.view.AppearanceRegion;
 import com.android.systemui.Dumpable;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
@@ -182,15 +184,18 @@
     private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     private final StatusBarStateController mStatusBarStateController;
     private DisplayTracker mDisplayTracker;
+    private final Handler mBgHandler;
 
     @Inject
     public TaskbarDelegate(Context context,
             LightBarTransitionsController.Factory lightBarTransitionsControllerFactory,
             StatusBarKeyguardViewManager statusBarKeyguardViewManager,
-            StatusBarStateController statusBarStateController) {
+            StatusBarStateController statusBarStateController,
+            @Background Handler bgHandler) {
         mLightBarTransitionsControllerFactory = lightBarTransitionsControllerFactory;
 
         mContext = context;
+        mBgHandler = bgHandler;
         mDisplayManager = mContext.getSystemService(DisplayManager.class);
         mPipListener = (bounds) -> {
             mEdgeBackGestureHandler.setPipStashExclusionBounds(bounds);
@@ -245,7 +250,9 @@
                 new LightBarTransitionsController.DarkIntensityApplier() {
                     @Override
                     public void applyDarkIntensity(float darkIntensity) {
-                        mOverviewProxyService.onNavButtonsDarkIntensityChanged(darkIntensity);
+                        mBgHandler.post(() -> {
+                            mOverviewProxyService.onNavButtonsDarkIntensityChanged(darkIntensity);
+                        });
                     }
 
                     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index d401b6e..a98b8e5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -784,7 +784,11 @@
 
     private fun updateLongPressEffect(handlesLongClick: Boolean) {
         // The long press effect in the tile can't be updated if it is still running
-        if (longPressEffect?.state != QSLongPressEffect.State.IDLE) return
+        if (
+            longPressEffect?.state != QSLongPressEffect.State.IDLE &&
+                longPressEffect?.state != QSLongPressEffect.State.CLICKED
+        )
+            return
 
         longPressEffect.qsTile?.state?.handlesLongClick = handlesLongClick
         if (handlesLongClick && longPressEffect.initializeEffect(longPressEffectDuration)) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
index ec8d30b..e93cec8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
@@ -41,12 +41,14 @@
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.qs.QSTile;
+import com.android.systemui.plugins.qs.TileDetailsViewModel;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.QsEventLogger;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
+import com.android.systemui.qs.tiles.dialog.ScreenRecordDetailsViewModel;
 import com.android.systemui.res.R;
 import com.android.systemui.screenrecord.RecordingController;
 import com.android.systemui.screenrecord.data.model.ScreenRecordModel;
@@ -54,6 +56,8 @@
 import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
+import java.util.function.Consumer;
+
 import javax.inject.Inject;
 
 /**
@@ -122,17 +126,78 @@
 
     @Override
     protected void handleClick(@Nullable Expandable expandable) {
+        handleClick(() -> showDialog(expandable));
+    }
+
+    private void showDialog(@Nullable Expandable expandable) {
+        final Dialog dialog = mController.createScreenRecordDialog(
+                this::onStartRecordingClicked);
+
+        executeWhenUnlockedKeyguard(() -> {
+            // We animate from the touched view only if we are not on the keyguard, given that if we
+            // are we will dismiss it which will also collapse the shade.
+            boolean shouldAnimateFromExpandable =
+                    expandable != null && !mKeyguardStateController.isShowing();
+
+            if (shouldAnimateFromExpandable) {
+                DialogTransitionAnimator.Controller controller =
+                        expandable.dialogTransitionController(new DialogCuj(
+                                InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+                                INTERACTION_JANK_TAG));
+                if (controller != null) {
+                    mDialogTransitionAnimator.show(dialog,
+                            controller, /* animateBackgroundBoundsChange= */ true);
+                } else {
+                    dialog.show();
+                }
+            } else {
+                dialog.show();
+            }
+        });
+    }
+
+    private void onStartRecordingClicked() {
+        // We dismiss the shade. Since starting the recording will also dismiss the dialog (if
+        // there is one showing), we disable the exit animation which looks weird when it happens
+        // at the same time as the shade collapsing.
+        mDialogTransitionAnimator.disableAllCurrentDialogsExitAnimations();
+        mPanelInteractor.collapsePanels();
+    }
+
+    private void executeWhenUnlockedKeyguard(Runnable dismissActionCallback) {
+        ActivityStarter.OnDismissAction dismissAction = () -> {
+            dismissActionCallback.run();
+
+            int uid = mUserContextProvider.getUserContext().getUserId();
+            mMediaProjectionMetricsLogger.notifyPermissionRequestDisplayed(uid);
+
+            return false;
+        };
+
+        mKeyguardDismissUtil.executeWhenUnlocked(dismissAction, false /* requiresShadeOpen */,
+                true /* afterKeyguardDone */);
+    }
+
+    private void handleClick(Runnable showPromptCallback) {
         if (mController.isStarting()) {
             cancelCountdown();
         } else if (mController.isRecording()) {
             stopRecording();
         } else {
-            mUiHandler.post(() -> showPrompt(expandable));
+            mUiHandler.post(showPromptCallback);
         }
         refreshState();
     }
 
     @Override
+    public boolean getDetailsViewModel(Consumer<TileDetailsViewModel> callback) {
+        handleClick(() ->
+                callback.accept(new ScreenRecordDetailsViewModel())
+        );
+        return true;
+    }
+
+    @Override
     protected void handleUpdateState(BooleanState state, Object arg) {
         boolean isStarting = mController.isStarting();
         boolean isRecording = mController.isRecording();
@@ -178,49 +243,6 @@
         return mContext.getString(R.string.quick_settings_screen_record_label);
     }
 
-    private void showPrompt(@Nullable Expandable expandable) {
-        // We animate from the touched view only if we are not on the keyguard, given that if we
-        // are we will dismiss it which will also collapse the shade.
-        boolean shouldAnimateFromExpandable =
-                expandable != null && !mKeyguardStateController.isShowing();
-
-        // Create the recording dialog that will collapse the shade only if we start the recording.
-        Runnable onStartRecordingClicked = () -> {
-            // We dismiss the shade. Since starting the recording will also dismiss the dialog, we
-            // disable the exit animation which looks weird when it happens at the same time as the
-            // shade collapsing.
-            mDialogTransitionAnimator.disableAllCurrentDialogsExitAnimations();
-            mPanelInteractor.collapsePanels();
-        };
-
-        final Dialog dialog = mController.createScreenRecordDialog(onStartRecordingClicked);
-
-        ActivityStarter.OnDismissAction dismissAction = () -> {
-            if (shouldAnimateFromExpandable) {
-                DialogTransitionAnimator.Controller controller =
-                        expandable.dialogTransitionController(new DialogCuj(
-                                InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
-                                INTERACTION_JANK_TAG));
-                if (controller != null) {
-                    mDialogTransitionAnimator.show(dialog,
-                            controller, /* animateBackgroundBoundsChange= */ true);
-                } else {
-                    dialog.show();
-                }
-            } else {
-                dialog.show();
-            }
-
-            int uid = mUserContextProvider.getUserContext().getUserId();
-            mMediaProjectionMetricsLogger.notifyPermissionRequestDisplayed(uid);
-
-            return false;
-        };
-
-        mKeyguardDismissUtil.executeWhenUnlocked(dismissAction, false /* requiresShadeOpen */,
-                true /* afterKeyguardDone */);
-    }
-
     private void cancelCountdown() {
         Log.d(TAG, "Cancelling countdown");
         mController.cancelCountdown();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java
index 23210ef..340cb68 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java
@@ -373,7 +373,6 @@
         mConnectivityManager.setAirplaneMode(false);
     }
 
-    @VisibleForTesting
     protected int getDefaultDataSubscriptionId() {
         return mSubscriptionManager.getDefaultDataSubscriptionId();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt
new file mode 100644
index 0000000..c64532a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt
@@ -0,0 +1,991 @@
+/*
+ * 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.dialog
+
+import android.app.AlertDialog
+import android.content.Context
+import android.content.DialogInterface
+import android.graphics.drawable.Drawable
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.os.Handler
+import android.telephony.ServiceState
+import android.telephony.SignalStrength
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyDisplayInfo
+import android.text.Html
+import android.text.Layout
+import android.text.TextUtils
+import android.text.method.LinkMovementMethod
+import android.util.Log
+import android.view.View
+import android.view.ViewStub
+import android.view.WindowManager
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.ProgressBar
+import android.widget.Switch
+import android.widget.TextView
+import androidx.annotation.MainThread
+import androidx.annotation.WorkerThread
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
+import androidx.lifecycle.MutableLiveData
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.android.internal.logging.UiEvent
+import com.android.internal.logging.UiEventLogger
+import com.android.internal.telephony.flags.Flags
+import com.android.settingslib.satellite.SatelliteDialogUtils.TYPE_IS_WIFI
+import com.android.settingslib.satellite.SatelliteDialogUtils.mayStartSatelliteWarningDialog
+import com.android.settingslib.wifi.WifiEnterpriseRestrictionUtils
+import com.android.systemui.Prefs
+import com.android.systemui.accessibility.floatingmenu.AnnotationLinkSpan
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.flags.QsDetailedView
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.wifitrackerlib.WifiEntry
+import com.google.common.annotations.VisibleForTesting
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import java.util.concurrent.Executor
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+
+/**
+ * View content for the Internet tile details that handles all UI interactions and state management.
+ *
+ * @param internetDialog non-null if the details should be shown as part of a dialog and null
+ *   otherwise.
+ */
+// TODO: b/377388104 Make this content for details view only.
+class InternetDetailsContentManager
+@AssistedInject
+constructor(
+    private val internetDetailsContentController: InternetDetailsContentController,
+    @Assisted(CAN_CONFIG_MOBILE_DATA) private val canConfigMobileData: Boolean,
+    @Assisted(CAN_CONFIG_WIFI) private val canConfigWifi: Boolean,
+    @Assisted private val coroutineScope: CoroutineScope,
+    @Assisted private var context: Context,
+    @Assisted private var internetDialog: SystemUIDialog?,
+    private val uiEventLogger: UiEventLogger,
+    private val dialogTransitionAnimator: DialogTransitionAnimator,
+    @Main private val handler: Handler,
+    @Background private val backgroundExecutor: Executor,
+    private val keyguard: KeyguardStateController,
+) {
+    // Lifecycle
+    private lateinit var lifecycleRegistry: LifecycleRegistry
+    @VisibleForTesting internal var lifecycleOwner: LifecycleOwner? = null
+    @VisibleForTesting internal val internetContentData = MutableLiveData<InternetContent>()
+    @VisibleForTesting internal var connectedWifiEntry: WifiEntry? = null
+    @VisibleForTesting internal var isProgressBarVisible = false
+
+    // UI Components
+    private lateinit var contentView: View
+    private lateinit var internetDialogTitleView: TextView
+    private lateinit var internetDialogSubTitleView: TextView
+    private lateinit var divider: View
+    private lateinit var progressBar: ProgressBar
+    private lateinit var ethernetLayout: LinearLayout
+    private lateinit var mobileNetworkLayout: LinearLayout
+    private var secondaryMobileNetworkLayout: LinearLayout? = null
+    private lateinit var turnWifiOnLayout: LinearLayout
+    private lateinit var wifiToggleTitleTextView: TextView
+    private lateinit var wifiScanNotifyLayout: LinearLayout
+    private lateinit var wifiScanNotifyTextView: TextView
+    private lateinit var connectedWifiListLayout: LinearLayout
+    private lateinit var connectedWifiIcon: ImageView
+    private lateinit var connectedWifiTitleTextView: TextView
+    private lateinit var connectedWifiSummaryTextView: TextView
+    private lateinit var wifiSettingsIcon: ImageView
+    private lateinit var wifiRecyclerView: RecyclerView
+    private lateinit var seeAllLayout: LinearLayout
+    private lateinit var signalIcon: ImageView
+    private lateinit var mobileTitleTextView: TextView
+    private lateinit var mobileSummaryTextView: TextView
+    private lateinit var airplaneModeSummaryTextView: TextView
+    private lateinit var mobileDataToggle: Switch
+    private lateinit var mobileToggleDivider: View
+    private lateinit var wifiToggle: Switch
+    private lateinit var shareWifiButton: Button
+    private lateinit var airplaneModeButton: Button
+    private var alertDialog: AlertDialog? = null
+    private lateinit var doneButton: Button
+
+    private val canChangeWifiState =
+        WifiEnterpriseRestrictionUtils.isChangeWifiStateAllowed(context)
+    private var wifiNetworkHeight = 0
+    private var backgroundOn: Drawable? = null
+    private var backgroundOff: Drawable? = null
+    private var clickJob: Job? = null
+    private var defaultDataSubId = internetDetailsContentController.defaultDataSubscriptionId
+    @VisibleForTesting
+    internal var adapter = InternetAdapter(internetDetailsContentController, coroutineScope)
+    @VisibleForTesting internal var wifiEntriesCount: Int = 0
+    @VisibleForTesting internal var hasMoreWifiEntries: Boolean = false
+
+    @AssistedFactory
+    interface Factory {
+        fun create(
+            @Assisted(CAN_CONFIG_MOBILE_DATA) canConfigMobileData: Boolean,
+            @Assisted(CAN_CONFIG_WIFI) canConfigWifi: Boolean,
+            coroutineScope: CoroutineScope,
+            context: Context,
+            internetDialog: SystemUIDialog?,
+        ): InternetDetailsContentManager
+    }
+
+    /**
+     * Binds the content manager to the provided content view.
+     *
+     * This method initializes the lifecycle, views, click listeners, and UI of the details content.
+     * It also updates the UI with the current Wi-Fi network information.
+     *
+     * @param contentView The view to which the content manager should be bound.
+     */
+    fun bind(contentView: View) {
+        if (DEBUG) {
+            Log.d(TAG, "Bind InternetDetailsContentManager")
+        }
+
+        this.contentView = contentView
+
+        initializeLifecycle()
+        initializeViews()
+        updateDetailsUI(getStartingInternetContent())
+        initializeAndConfigure()
+    }
+
+    /**
+     * Initializes the LifecycleRegistry if it hasn't been initialized yet. It sets the initial
+     * state of the LifecycleRegistry to Lifecycle.State.CREATED.
+     */
+    fun initializeLifecycle() {
+        if (!::lifecycleRegistry.isInitialized) {
+            lifecycleOwner =
+                object : LifecycleOwner {
+                    override val lifecycle: Lifecycle
+                        get() = lifecycleRegistry
+                }
+            lifecycleRegistry = LifecycleRegistry(lifecycleOwner!!)
+        }
+        lifecycleRegistry.currentState = Lifecycle.State.CREATED
+    }
+
+    private fun initializeViews() {
+        // Set accessibility properties
+        contentView.accessibilityPaneTitle =
+            context.getText(R.string.accessibility_desc_quick_settings)
+
+        // Get dimension resources
+        wifiNetworkHeight =
+            context.resources.getDimensionPixelSize(R.dimen.internet_dialog_wifi_network_height)
+
+        // Initialize LiveData observer
+        internetContentData.observe(lifecycleOwner!!) { internetContent ->
+            updateDetailsUI(internetContent)
+        }
+
+        // Network layouts
+        internetDialogTitleView = contentView.requireViewById(R.id.internet_dialog_title)
+        internetDialogSubTitleView = contentView.requireViewById(R.id.internet_dialog_subtitle)
+        divider = contentView.requireViewById(R.id.divider)
+        progressBar = contentView.requireViewById(R.id.wifi_searching_progress)
+
+        // Set wifi, mobile and ethernet layouts
+        setWifiLayout()
+        setMobileLayout()
+        ethernetLayout = contentView.requireViewById(R.id.ethernet_layout)
+
+        // Done button is only visible for the dialog view
+        doneButton = contentView.requireViewById(R.id.done_button)
+        if (internetDialog == null) {
+            doneButton.visibility = View.GONE
+        } else {
+            // Set done button if qs details view is not enabled.
+            doneButton.setOnClickListener { internetDialog!!.dismiss() }
+        }
+
+        // Share WiFi
+        shareWifiButton = contentView.requireViewById(R.id.share_wifi_button)
+        shareWifiButton.setOnClickListener { view ->
+            if (
+                internetDetailsContentController.mayLaunchShareWifiSettings(
+                    connectedWifiEntry,
+                    view,
+                )
+            ) {
+                uiEventLogger.log(InternetDetailsEvent.SHARE_WIFI_QS_BUTTON_CLICKED)
+            }
+        }
+
+        // Airplane mode
+        airplaneModeButton = contentView.requireViewById(R.id.apm_button)
+        airplaneModeButton.setOnClickListener {
+            internetDetailsContentController.setAirplaneModeDisabled()
+        }
+        airplaneModeSummaryTextView = contentView.requireViewById(R.id.airplane_mode_summary)
+
+        // Background drawables
+        backgroundOn = context.getDrawable(R.drawable.settingslib_switch_bar_bg_on)
+        backgroundOff = context.getDrawable(R.drawable.internet_dialog_selected_effect)
+    }
+
+    private fun setWifiLayout() {
+        // Initialize Wi-Fi related views
+        turnWifiOnLayout = contentView.requireViewById(R.id.turn_on_wifi_layout)
+        wifiToggleTitleTextView = contentView.requireViewById(R.id.wifi_toggle_title)
+        wifiScanNotifyLayout = contentView.requireViewById(R.id.wifi_scan_notify_layout)
+        wifiScanNotifyTextView = contentView.requireViewById(R.id.wifi_scan_notify_text)
+        connectedWifiListLayout = contentView.requireViewById(R.id.wifi_connected_layout)
+        connectedWifiIcon = contentView.requireViewById(R.id.wifi_connected_icon)
+        connectedWifiTitleTextView = contentView.requireViewById(R.id.wifi_connected_title)
+        connectedWifiSummaryTextView = contentView.requireViewById(R.id.wifi_connected_summary)
+        wifiSettingsIcon = contentView.requireViewById(R.id.wifi_settings_icon)
+        wifiToggle = contentView.requireViewById(R.id.wifi_toggle)
+        wifiRecyclerView =
+            contentView.requireViewById<RecyclerView>(R.id.wifi_list_layout).apply {
+                layoutManager = LinearLayoutManager(context)
+                adapter = this@InternetDetailsContentManager.adapter
+            }
+        seeAllLayout = contentView.requireViewById(R.id.see_all_layout)
+
+        // Set click listeners for Wi-Fi related views
+        wifiToggle.setOnClickListener {
+            val isChecked = wifiToggle.isChecked
+            handleWifiToggleClicked(isChecked)
+        }
+        connectedWifiListLayout.setOnClickListener(this::onClickConnectedWifi)
+        seeAllLayout.setOnClickListener(this::onClickSeeMoreButton)
+    }
+
+    private fun setMobileLayout() {
+        // Initialize mobile data related views
+        mobileNetworkLayout = contentView.requireViewById(R.id.mobile_network_layout)
+        signalIcon = contentView.requireViewById(R.id.signal_icon)
+        mobileTitleTextView = contentView.requireViewById(R.id.mobile_title)
+        mobileSummaryTextView = contentView.requireViewById(R.id.mobile_summary)
+        mobileDataToggle = contentView.requireViewById(R.id.mobile_toggle)
+        mobileToggleDivider = contentView.requireViewById(R.id.mobile_toggle_divider)
+
+        // Set click listeners for mobile data related views
+        mobileNetworkLayout.setOnClickListener {
+            val autoSwitchNonDdsSubId: Int =
+                internetDetailsContentController.getActiveAutoSwitchNonDdsSubId()
+            if (autoSwitchNonDdsSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+                showTurnOffAutoDataSwitchDialog(autoSwitchNonDdsSubId)
+            }
+            internetDetailsContentController.connectCarrierNetwork()
+        }
+
+        // Mobile data toggle
+        mobileDataToggle.setOnClickListener {
+            val isChecked = mobileDataToggle.isChecked
+            if (!isChecked && shouldShowMobileDialog()) {
+                mobileDataToggle.isChecked = true
+                showTurnOffMobileDialog()
+            } else if (internetDetailsContentController.isMobileDataEnabled != isChecked) {
+                internetDetailsContentController.setMobileDataEnabled(
+                    context,
+                    defaultDataSubId,
+                    isChecked,
+                    false,
+                )
+            }
+        }
+    }
+
+    /**
+     * This function ensures the component is in the RESUMED state and sets up the internet details
+     * content controller.
+     *
+     * If the component is already in the RESUMED state, this function does nothing.
+     */
+    fun initializeAndConfigure() {
+        // If the current state is RESUMED, it's already initialized.
+        if (lifecycleRegistry.currentState == Lifecycle.State.RESUMED) {
+            return
+        }
+
+        lifecycleRegistry.currentState = Lifecycle.State.RESUMED
+        internetDetailsContentController.onStart(internetDetailsCallback, canConfigWifi)
+        if (!canConfigWifi) {
+            hideWifiViews()
+        }
+    }
+
+    private fun getDialogTitleText(): CharSequence {
+        return internetDetailsContentController.getDialogTitleText()
+    }
+
+    private fun updateDetailsUI(internetContent: InternetContent) {
+        if (DEBUG) {
+            Log.d(TAG, "updateDetailsUI ")
+        }
+        if (QsDetailedView.isEnabled) {
+            internetDialogTitleView.visibility = View.GONE
+            internetDialogSubTitleView.visibility = View.GONE
+        } else {
+            internetDialogTitleView.text = internetContent.internetDialogTitleString
+            internetDialogSubTitleView.text = internetContent.internetDialogSubTitle
+        }
+        airplaneModeButton.visibility =
+            if (internetContent.isAirplaneModeEnabled) View.VISIBLE else View.GONE
+
+        updateEthernetUI(internetContent)
+        updateMobileUI(internetContent)
+        updateWifiUI(internetContent)
+    }
+
+    private fun getStartingInternetContent(): InternetContent {
+        return InternetContent(
+            internetDialogTitleString = getDialogTitleText(),
+            internetDialogSubTitle = getSubtitleText(),
+            isWifiEnabled = internetDetailsContentController.isWifiEnabled,
+            isDeviceLocked = internetDetailsContentController.isDeviceLocked,
+        )
+    }
+
+    private fun getSubtitleText(): String {
+        return internetDetailsContentController.getSubtitleText(isProgressBarVisible).toString()
+    }
+
+    @VisibleForTesting
+    internal fun hideWifiViews() {
+        setProgressBarVisible(false)
+        turnWifiOnLayout.visibility = View.GONE
+        connectedWifiListLayout.visibility = View.GONE
+        wifiRecyclerView.visibility = View.GONE
+        seeAllLayout.visibility = View.GONE
+        shareWifiButton.visibility = View.GONE
+    }
+
+    private fun setProgressBarVisible(visible: Boolean) {
+        if (isProgressBarVisible == visible) {
+            return
+        }
+
+        // Set the indeterminate value from false to true each time to ensure that the progress bar
+        // resets its animation and starts at the leftmost starting point each time it is displayed.
+        isProgressBarVisible = visible
+        progressBar.visibility = if (visible) View.VISIBLE else View.GONE
+        progressBar.isIndeterminate = visible
+        divider.visibility = if (visible) View.GONE else View.VISIBLE
+        internetDialogSubTitleView.text = getSubtitleText()
+    }
+
+    private fun showTurnOffAutoDataSwitchDialog(subId: Int) {
+        var carrierName: CharSequence? = getMobileNetworkTitle(defaultDataSubId)
+        if (TextUtils.isEmpty(carrierName)) {
+            carrierName = getDefaultCarrierName()
+        }
+        alertDialog =
+            AlertDialog.Builder(context)
+                .setTitle(context.getString(R.string.auto_data_switch_disable_title, carrierName))
+                .setMessage(R.string.auto_data_switch_disable_message)
+                .setNegativeButton(R.string.auto_data_switch_dialog_negative_button) { _, _ -> }
+                .setPositiveButton(R.string.auto_data_switch_dialog_positive_button) { _, _ ->
+                    internetDetailsContentController.setAutoDataSwitchMobileDataPolicy(
+                        subId,
+                        /* enable= */ false,
+                    )
+                    secondaryMobileNetworkLayout?.visibility = View.GONE
+                }
+                .create()
+        alertDialog!!.window?.setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG)
+        SystemUIDialog.setShowForAllUsers(alertDialog, true)
+        SystemUIDialog.registerDismissListener(alertDialog)
+        SystemUIDialog.setWindowOnTop(alertDialog, keyguard.isShowing())
+        if (QsDetailedView.isEnabled) {
+            alertDialog!!.show()
+        } else {
+            dialogTransitionAnimator.showFromDialog(alertDialog!!, internetDialog!!, null, false)
+            Log.e(TAG, "Internet dialog is shown with the refactor code")
+        }
+    }
+
+    private fun shouldShowMobileDialog(): Boolean {
+        val mobileDataTurnedOff =
+            Prefs.getBoolean(context, Prefs.Key.QS_HAS_TURNED_OFF_MOBILE_DATA, false)
+        return internetDetailsContentController.isMobileDataEnabled && !mobileDataTurnedOff
+    }
+
+    private fun getMobileNetworkTitle(subId: Int): CharSequence {
+        return internetDetailsContentController.getMobileNetworkTitle(subId)
+    }
+
+    private fun showTurnOffMobileDialog() {
+        val context = contentView.context
+        var carrierName: CharSequence? = getMobileNetworkTitle(defaultDataSubId)
+        val isInService: Boolean =
+            internetDetailsContentController.isVoiceStateInService(defaultDataSubId)
+        if (TextUtils.isEmpty(carrierName) || !isInService) {
+            carrierName = getDefaultCarrierName()
+        }
+        alertDialog =
+            AlertDialog.Builder(context)
+                .setTitle(R.string.mobile_data_disable_title)
+                .setMessage(context.getString(R.string.mobile_data_disable_message, carrierName))
+                .setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int -> }
+                .setPositiveButton(
+                    com.android.internal.R.string.alert_windows_notification_turn_off_action
+                ) { _: DialogInterface?, _: Int ->
+                    internetDetailsContentController.setMobileDataEnabled(
+                        context,
+                        defaultDataSubId,
+                        false,
+                        false,
+                    )
+                    mobileDataToggle.isChecked = false
+                    Prefs.putBoolean(context, Prefs.Key.QS_HAS_TURNED_OFF_MOBILE_DATA, true)
+                }
+                .create()
+        alertDialog!!.window?.setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG)
+        SystemUIDialog.setShowForAllUsers(alertDialog, true)
+        SystemUIDialog.registerDismissListener(alertDialog)
+        SystemUIDialog.setWindowOnTop(alertDialog, keyguard.isShowing())
+        if (QsDetailedView.isEnabled) {
+            alertDialog!!.show()
+        } else {
+            dialogTransitionAnimator.showFromDialog(alertDialog!!, internetDialog!!, null, false)
+        }
+    }
+
+    private fun onClickConnectedWifi(view: View?) {
+        if (connectedWifiEntry == null) {
+            return
+        }
+        internetDetailsContentController.launchWifiDetailsSetting(connectedWifiEntry!!.key, view)
+    }
+
+    private fun onClickSeeMoreButton(view: View?) {
+        internetDetailsContentController.launchNetworkSetting(view)
+    }
+
+    private fun handleWifiToggleClicked(isChecked: Boolean) {
+        if (Flags.oemEnabledSatelliteFlag()) {
+            if (clickJob != null && !clickJob!!.isCompleted) {
+                return
+            }
+            clickJob =
+                mayStartSatelliteWarningDialog(contentView.context, coroutineScope, TYPE_IS_WIFI) {
+                    isAllowClick: Boolean ->
+                    if (isAllowClick) {
+                        setWifiEnabled(isChecked)
+                    } else {
+                        wifiToggle.isChecked = !isChecked
+                    }
+                }
+            return
+        }
+        setWifiEnabled(isChecked)
+    }
+
+    private fun setWifiEnabled(isEnabled: Boolean) {
+        if (internetDetailsContentController.isWifiEnabled == isEnabled) {
+            return
+        }
+        internetDetailsContentController.isWifiEnabled = isEnabled
+    }
+
+    @MainThread
+    private fun updateEthernetUI(internetContent: InternetContent) {
+        ethernetLayout.visibility = if (internetContent.hasEthernet) View.VISIBLE else View.GONE
+    }
+
+    private fun updateWifiUI(internetContent: InternetContent) {
+        if (!canConfigWifi) {
+            return
+        }
+
+        updateWifiToggle(internetContent)
+        updateConnectedWifi(internetContent)
+        updateWifiListAndSeeAll(internetContent)
+        updateWifiScanNotify(internetContent)
+    }
+
+    private fun updateMobileUI(internetContent: InternetContent) {
+        if (!internetContent.shouldUpdateMobileNetwork) {
+            return
+        }
+
+        val isNetworkConnected =
+            internetContent.activeNetworkIsCellular || internetContent.isCarrierNetworkActive
+        // 1. Mobile network should be gone if airplane mode ON or the list of active
+        //    subscriptionId is null.
+        // 2. Carrier network should be gone if airplane mode ON and Wi-Fi is OFF.
+        if (DEBUG) {
+            Log.d(
+                TAG,
+                /*msg = */ "updateMobileUI, isCarrierNetworkActive = " +
+                    internetContent.isCarrierNetworkActive,
+            )
+        }
+
+        if (
+            !internetContent.hasActiveSubIdOnDds &&
+                (!internetContent.isWifiEnabled || !internetContent.isCarrierNetworkActive)
+        ) {
+            mobileNetworkLayout.visibility = View.GONE
+            secondaryMobileNetworkLayout?.visibility = View.GONE
+            return
+        }
+
+        mobileNetworkLayout.visibility = View.VISIBLE
+        mobileDataToggle.setChecked(internetDetailsContentController.isMobileDataEnabled)
+        mobileTitleTextView.text = getMobileNetworkTitle(defaultDataSubId)
+        val summary = getMobileNetworkSummary(defaultDataSubId)
+        if (!TextUtils.isEmpty(summary)) {
+            mobileSummaryTextView.text = Html.fromHtml(summary, Html.FROM_HTML_MODE_LEGACY)
+            mobileSummaryTextView.setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
+            mobileSummaryTextView.visibility = View.VISIBLE
+        } else {
+            mobileSummaryTextView.visibility = View.GONE
+        }
+        backgroundExecutor.execute {
+            val drawable = getSignalStrengthDrawable(defaultDataSubId)
+            handler.post { signalIcon.setImageDrawable(drawable) }
+        }
+
+        mobileDataToggle.visibility = if (canConfigMobileData) View.VISIBLE else View.INVISIBLE
+        mobileToggleDivider.visibility = if (canConfigMobileData) View.VISIBLE else View.INVISIBLE
+        val primaryColor =
+            if (isNetworkConnected) R.color.connected_network_primary_color
+            else R.color.disconnected_network_primary_color
+        mobileToggleDivider.setBackgroundColor(context.getColor(primaryColor))
+
+        // Display the info for the non-DDS if it's actively being used
+        val autoSwitchNonDdsSubId: Int = internetContent.activeAutoSwitchNonDdsSubId
+
+        val nonDdsVisibility =
+            if (autoSwitchNonDdsSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) View.VISIBLE
+            else View.GONE
+
+        val secondaryRes =
+            if (isNetworkConnected) R.style.TextAppearance_InternetDialog_Secondary_Active
+            else R.style.TextAppearance_InternetDialog_Secondary
+        if (nonDdsVisibility == View.VISIBLE) {
+            // non DDS is the currently active sub, set primary visual for it
+            setNonDDSActive(autoSwitchNonDdsSubId)
+        } else {
+            mobileNetworkLayout.background = if (isNetworkConnected) backgroundOn else backgroundOff
+            mobileTitleTextView.setTextAppearance(
+                if (isNetworkConnected) R.style.TextAppearance_InternetDialog_Active
+                else R.style.TextAppearance_InternetDialog
+            )
+            mobileSummaryTextView.setTextAppearance(secondaryRes)
+        }
+
+        secondaryMobileNetworkLayout?.visibility = nonDdsVisibility
+
+        // Set airplane mode to the summary for carrier network
+        if (internetContent.isAirplaneModeEnabled) {
+            airplaneModeSummaryTextView.apply {
+                visibility = View.VISIBLE
+                text = context.getText(R.string.airplane_mode)
+                setTextAppearance(secondaryRes)
+            }
+        } else {
+            airplaneModeSummaryTextView.visibility = View.GONE
+        }
+    }
+
+    private fun setNonDDSActive(autoSwitchNonDdsSubId: Int) {
+        val stub: ViewStub = contentView.findViewById(R.id.secondary_mobile_network_stub)
+        stub.inflate()
+        secondaryMobileNetworkLayout =
+            contentView.findViewById(R.id.secondary_mobile_network_layout)
+        secondaryMobileNetworkLayout?.setOnClickListener { view: View? ->
+            this.onClickConnectedSecondarySub(view)
+        }
+        secondaryMobileNetworkLayout?.background = backgroundOn
+
+        contentView.requireViewById<TextView>(R.id.secondary_mobile_title).apply {
+            text = getMobileNetworkTitle(autoSwitchNonDdsSubId)
+            setTextAppearance(R.style.TextAppearance_InternetDialog_Active)
+        }
+
+        val summary = getMobileNetworkSummary(autoSwitchNonDdsSubId)
+        contentView.requireViewById<TextView>(R.id.secondary_mobile_summary).apply {
+            if (!TextUtils.isEmpty(summary)) {
+                text = Html.fromHtml(summary, Html.FROM_HTML_MODE_LEGACY)
+                breakStrategy = Layout.BREAK_STRATEGY_SIMPLE
+                setTextAppearance(R.style.TextAppearance_InternetDialog_Active)
+            }
+        }
+
+        val secondarySignalIcon: ImageView = contentView.requireViewById(R.id.secondary_signal_icon)
+        backgroundExecutor.execute {
+            val drawable = getSignalStrengthDrawable(autoSwitchNonDdsSubId)
+            handler.post { secondarySignalIcon.setImageDrawable(drawable) }
+        }
+
+        contentView.requireViewById<ImageView>(R.id.secondary_settings_icon).apply {
+            setColorFilter(context.getColor(R.color.connected_network_primary_color))
+        }
+
+        // set secondary visual for default data sub
+        mobileNetworkLayout.background = backgroundOff
+        mobileTitleTextView.setTextAppearance(R.style.TextAppearance_InternetDialog)
+        mobileSummaryTextView.setTextAppearance(R.style.TextAppearance_InternetDialog_Secondary)
+        signalIcon.setColorFilter(context.getColor(R.color.connected_network_secondary_color))
+    }
+
+    @MainThread
+    private fun updateWifiToggle(internetContent: InternetContent) {
+        if (wifiToggle.isChecked != internetContent.isWifiEnabled) {
+            wifiToggle.isChecked = internetContent.isWifiEnabled
+        }
+        if (internetContent.isDeviceLocked) {
+            wifiToggleTitleTextView.setTextAppearance(
+                if ((connectedWifiEntry != null)) R.style.TextAppearance_InternetDialog_Active
+                else R.style.TextAppearance_InternetDialog
+            )
+        }
+        turnWifiOnLayout.background =
+            if ((internetContent.isDeviceLocked && connectedWifiEntry != null)) backgroundOn
+            else null
+
+        if (!canChangeWifiState && wifiToggle.isEnabled) {
+            wifiToggle.isEnabled = false
+            wifiToggleTitleTextView.isEnabled = false
+            contentView.requireViewById<TextView>(R.id.wifi_toggle_summary).apply {
+                isEnabled = false
+                visibility = View.VISIBLE
+            }
+        }
+    }
+
+    @MainThread
+    private fun updateConnectedWifi(internetContent: InternetContent) {
+        if (
+            !internetContent.isWifiEnabled ||
+                connectedWifiEntry == null ||
+                internetContent.isDeviceLocked
+        ) {
+            connectedWifiListLayout.visibility = View.GONE
+            shareWifiButton.visibility = View.GONE
+            return
+        }
+        connectedWifiListLayout.visibility = View.VISIBLE
+        connectedWifiTitleTextView.text = connectedWifiEntry!!.title
+        connectedWifiSummaryTextView.text = connectedWifiEntry!!.getSummary(false)
+        connectedWifiIcon.setImageDrawable(
+            internetDetailsContentController.getInternetWifiDrawable(connectedWifiEntry!!)
+        )
+        wifiSettingsIcon.setColorFilter(context.getColor(R.color.connected_network_primary_color))
+
+        val canShareWifi =
+            internetDetailsContentController.getConfiguratorQrCodeGeneratorIntentOrNull(
+                connectedWifiEntry
+            ) != null
+        shareWifiButton.visibility = if (canShareWifi) View.VISIBLE else View.GONE
+
+        secondaryMobileNetworkLayout?.visibility = View.GONE
+    }
+
+    @MainThread
+    private fun updateWifiListAndSeeAll(internetContent: InternetContent) {
+        if (!internetContent.isWifiEnabled || internetContent.isDeviceLocked) {
+            wifiRecyclerView.visibility = View.GONE
+            seeAllLayout.visibility = View.GONE
+            return
+        }
+        val wifiListMaxCount = getWifiListMaxCount()
+        if (adapter.itemCount > wifiListMaxCount) {
+            hasMoreWifiEntries = true
+        }
+        adapter.setMaxEntriesCount(wifiListMaxCount)
+        val wifiListMinHeight = wifiNetworkHeight * wifiListMaxCount
+        if (wifiRecyclerView.minimumHeight != wifiListMinHeight) {
+            wifiRecyclerView.minimumHeight = wifiListMinHeight
+        }
+        wifiRecyclerView.visibility = View.VISIBLE
+        seeAllLayout.visibility = if (hasMoreWifiEntries) View.VISIBLE else View.INVISIBLE
+    }
+
+    @MainThread
+    private fun updateWifiScanNotify(internetContent: InternetContent) {
+        if (
+            internetContent.isWifiEnabled ||
+                !internetContent.isWifiScanEnabled ||
+                internetContent.isDeviceLocked
+        ) {
+            wifiScanNotifyLayout.visibility = View.GONE
+            return
+        }
+
+        if (TextUtils.isEmpty(wifiScanNotifyTextView.text)) {
+            val linkInfo =
+                AnnotationLinkSpan.LinkInfo(AnnotationLinkSpan.LinkInfo.DEFAULT_ANNOTATION) {
+                    view: View? ->
+                    internetDetailsContentController.launchWifiScanningSetting(view)
+                }
+            wifiScanNotifyTextView.text =
+                AnnotationLinkSpan.linkify(
+                    context.getText(R.string.wifi_scan_notify_message),
+                    linkInfo,
+                )
+            wifiScanNotifyTextView.movementMethod = LinkMovementMethod.getInstance()
+        }
+        wifiScanNotifyLayout.visibility = View.VISIBLE
+    }
+
+    @VisibleForTesting
+    @MainThread
+    internal fun getWifiListMaxCount(): Int {
+        // Use the maximum count of networks to calculate the remaining count for Wi-Fi networks.
+        var count = MAX_NETWORK_COUNT
+        if (ethernetLayout.visibility == View.VISIBLE) {
+            count -= 1
+        }
+        if (mobileNetworkLayout.visibility == View.VISIBLE) {
+            count -= 1
+        }
+
+        // If the remaining count is greater than the maximum count of the Wi-Fi network, the
+        // maximum count of the Wi-Fi network is used.
+        if (count > InternetDetailsContentController.MAX_WIFI_ENTRY_COUNT) {
+            count = InternetDetailsContentController.MAX_WIFI_ENTRY_COUNT
+        }
+        if (connectedWifiListLayout.visibility == View.VISIBLE) {
+            count -= 1
+        }
+        return count
+    }
+
+    private fun getMobileNetworkSummary(subId: Int): String {
+        return internetDetailsContentController.getMobileNetworkSummary(subId)
+    }
+
+    /** For DSDS auto data switch */
+    private fun onClickConnectedSecondarySub(view: View?) {
+        internetDetailsContentController.launchMobileNetworkSettings(view)
+    }
+
+    private fun getSignalStrengthDrawable(subId: Int): Drawable {
+        return internetDetailsContentController.getSignalStrengthDrawable(subId)
+    }
+
+    /**
+     * Unbinds all listeners and resources associated with the view. This method should be called
+     * when the view is no longer needed.
+     */
+    fun unBind() {
+        if (DEBUG) {
+            Log.d(TAG, "unBind")
+        }
+        lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
+        mobileNetworkLayout.setOnClickListener(null)
+        connectedWifiListLayout.setOnClickListener(null)
+        secondaryMobileNetworkLayout?.setOnClickListener(null)
+        seeAllLayout.setOnClickListener(null)
+        wifiToggle.setOnCheckedChangeListener(null)
+        doneButton.setOnClickListener(null)
+        shareWifiButton.setOnClickListener(null)
+        airplaneModeButton.setOnClickListener(null)
+        internetDetailsContentController.onStop()
+    }
+
+    /**
+     * Update the internet details content when receiving the callback.
+     *
+     * @param shouldUpdateMobileNetwork `true` for update the mobile network layout, otherwise
+     *   `false`.
+     */
+    @VisibleForTesting
+    internal fun updateContent(shouldUpdateMobileNetwork: Boolean) {
+        backgroundExecutor.execute {
+            internetContentData.postValue(getInternetContent(shouldUpdateMobileNetwork))
+        }
+    }
+
+    private fun getInternetContent(shouldUpdateMobileNetwork: Boolean): InternetContent {
+        return InternetContent(
+            shouldUpdateMobileNetwork = shouldUpdateMobileNetwork,
+            internetDialogTitleString = getDialogTitleText(),
+            internetDialogSubTitle = getSubtitleText(),
+            activeNetworkIsCellular =
+                if (shouldUpdateMobileNetwork)
+                    internetDetailsContentController.activeNetworkIsCellular()
+                else false,
+            isCarrierNetworkActive =
+                if (shouldUpdateMobileNetwork)
+                    internetDetailsContentController.isCarrierNetworkActive()
+                else false,
+            isAirplaneModeEnabled = internetDetailsContentController.isAirplaneModeEnabled,
+            hasEthernet = internetDetailsContentController.hasEthernet(),
+            isWifiEnabled = internetDetailsContentController.isWifiEnabled,
+            hasActiveSubIdOnDds = internetDetailsContentController.hasActiveSubIdOnDds(),
+            isDeviceLocked = internetDetailsContentController.isDeviceLocked,
+            isWifiScanEnabled = internetDetailsContentController.isWifiScanEnabled(),
+            activeAutoSwitchNonDdsSubId =
+                internetDetailsContentController.getActiveAutoSwitchNonDdsSubId(),
+        )
+    }
+
+    /**
+     * Handles window focus changes. If the activity loses focus and the system UI dialog is
+     * showing, it dismisses the current alert dialog to prevent it from persisting in the
+     * background.
+     *
+     * @param dialog The internet system UI dialog whose focus state has changed.
+     * @param hasFocus True if the window has gained focus, false otherwise.
+     */
+    fun onWindowFocusChanged(dialog: SystemUIDialog, hasFocus: Boolean) {
+        if (alertDialog != null && !alertDialog!!.isShowing) {
+            if (!hasFocus && dialog.isShowing) {
+                dialog.dismiss()
+            }
+        }
+    }
+
+    private fun getDefaultCarrierName(): String? {
+        return context.getString(R.string.mobile_data_disable_message_default_carrier)
+    }
+
+    @VisibleForTesting
+    internal val internetDetailsCallback =
+        object : InternetDetailsContentController.InternetDialogCallback {
+            override fun onRefreshCarrierInfo() {
+                updateContent(shouldUpdateMobileNetwork = true)
+            }
+
+            override fun onSimStateChanged() {
+                updateContent(shouldUpdateMobileNetwork = true)
+            }
+
+            @WorkerThread
+            override fun onCapabilitiesChanged(
+                network: Network?,
+                networkCapabilities: NetworkCapabilities?,
+            ) {
+                updateContent(shouldUpdateMobileNetwork = true)
+            }
+
+            @WorkerThread
+            override fun onLost(network: Network) {
+                updateContent(shouldUpdateMobileNetwork = true)
+            }
+
+            override fun onSubscriptionsChanged(dataSubId: Int) {
+                defaultDataSubId = dataSubId
+                updateContent(shouldUpdateMobileNetwork = true)
+            }
+
+            override fun onServiceStateChanged(serviceState: ServiceState?) {
+                updateContent(shouldUpdateMobileNetwork = true)
+            }
+
+            @WorkerThread
+            override fun onDataConnectionStateChanged(state: Int, networkType: Int) {
+                updateContent(shouldUpdateMobileNetwork = true)
+            }
+
+            override fun onSignalStrengthsChanged(signalStrength: SignalStrength?) {
+                updateContent(shouldUpdateMobileNetwork = true)
+            }
+
+            override fun onUserMobileDataStateChanged(enabled: Boolean) {
+                updateContent(shouldUpdateMobileNetwork = true)
+            }
+
+            override fun onDisplayInfoChanged(telephonyDisplayInfo: TelephonyDisplayInfo?) {
+                updateContent(shouldUpdateMobileNetwork = true)
+            }
+
+            override fun onCarrierNetworkChange(active: Boolean) {
+                updateContent(shouldUpdateMobileNetwork = true)
+            }
+
+            override fun dismissDialog() {
+                if (DEBUG) {
+                    Log.d(TAG, "dismissDialog")
+                }
+                if (internetDialog != null) {
+                    internetDialog!!.dismiss()
+                    internetDialog = null
+                }
+            }
+
+            override fun onAccessPointsChanged(
+                wifiEntries: MutableList<WifiEntry>?,
+                connectedEntry: WifiEntry?,
+                ifHasMoreWifiEntries: Boolean,
+            ) {
+                // Should update the carrier network layout when it is connected under airplane
+                // mode ON.
+                val shouldUpdateCarrierNetwork =
+                    (mobileNetworkLayout.visibility == View.VISIBLE) &&
+                        internetDetailsContentController.isAirplaneModeEnabled
+                handler.post {
+                    connectedWifiEntry = connectedEntry
+                    wifiEntriesCount = wifiEntries?.size ?: 0
+                    hasMoreWifiEntries = ifHasMoreWifiEntries
+                    updateContent(shouldUpdateCarrierNetwork)
+                    adapter.setWifiEntries(wifiEntries, wifiEntriesCount)
+                    adapter.notifyDataSetChanged()
+                }
+            }
+
+            override fun onWifiScan(isScan: Boolean) {
+                setProgressBarVisible(isScan)
+            }
+        }
+
+    enum class InternetDetailsEvent(private val id: Int) : UiEventLogger.UiEventEnum {
+        @UiEvent(doc = "The Internet details became visible on the screen.")
+        INTERNET_DETAILS_VISIBLE(2071),
+        @UiEvent(doc = "The share wifi button is clicked.") SHARE_WIFI_QS_BUTTON_CLICKED(1462);
+
+        override fun getId(): Int {
+            return id
+        }
+    }
+
+    @VisibleForTesting
+    data class InternetContent(
+        val internetDialogTitleString: CharSequence,
+        val internetDialogSubTitle: CharSequence,
+        val isAirplaneModeEnabled: Boolean = false,
+        val hasEthernet: Boolean = false,
+        val shouldUpdateMobileNetwork: Boolean = false,
+        val activeNetworkIsCellular: Boolean = false,
+        val isCarrierNetworkActive: Boolean = false,
+        val isWifiEnabled: Boolean = false,
+        val hasActiveSubIdOnDds: Boolean = false,
+        val isDeviceLocked: Boolean = false,
+        val isWifiScanEnabled: Boolean = false,
+        val activeAutoSwitchNonDdsSubId: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID,
+    )
+
+    companion object {
+        private const val TAG = "InternetDetailsContent"
+        private val DEBUG: Boolean = Log.isLoggable(TAG, Log.DEBUG)
+        private const val MAX_NETWORK_COUNT = 4
+        const val CAN_CONFIG_MOBILE_DATA = "can_config_mobile_data"
+        const val CAN_CONFIG_WIFI = "can_config_wifi"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailedViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
similarity index 100%
rename from packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailedViewModel.kt
rename to packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java
index ee53471..a418b2a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java
@@ -70,6 +70,7 @@
 import com.android.systemui.animation.DialogTransitionAnimator;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.qs.flags.QsDetailedView;
 import com.android.systemui.res.R;
 import com.android.systemui.shade.ShadeDisplayAware;
 import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor;
@@ -207,7 +208,8 @@
             KeyguardStateController keyguardStateController,
             SystemUIDialog.Factory systemUIDialogFactory,
             ShadeDialogContextInteractor shadeDialogContextInteractor) {
-        // TODO: b/377388104 QsDetailedView.assertInLegacyMode();
+        // If `QsDetailedView` is enabled, it should show the details view.
+        QsDetailedView.assertInLegacyMode();
 
         mAboveStatusBar = aboveStatusBar;
         mSystemUIDialogFactory = systemUIDialogFactory;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogManager.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogManager.kt
index 8a54648..5f82e60 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogManager.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.coroutines.newTracingContext
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.flags.QsDetailedView
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
@@ -42,21 +43,24 @@
     @Background private val bgDispatcher: CoroutineDispatcher,
 ) {
     private lateinit var coroutineScope: CoroutineScope
+
     companion object {
         private const val INTERACTION_JANK_TAG = "internet"
         var dialog: SystemUIDialog? = null
     }
 
     /**
-     * Creates a [InternetDialogDelegateLegacy]. The dialog will be animated from [expandable] if
-     * it is not null.
+     * Creates a [InternetDialogDelegateLegacy]. The dialog will be animated from [expandable] if it
+     * is not null.
      */
     fun create(
         aboveStatusBar: Boolean,
         canConfigMobileData: Boolean,
         canConfigWifi: Boolean,
-        expandable: Expandable?
+        expandable: Expandable?,
     ) {
+        // If `QsDetailedView` is enabled, it should show the details view.
+        QsDetailedView.assertInLegacyMode()
         if (dialog != null) {
             if (DEBUG) {
                 Log.d(TAG, "InternetDialog is showing, do not create it twice.")
@@ -64,11 +68,11 @@
             return
         } else {
             coroutineScope = CoroutineScope(bgDispatcher + newTracingContext("InternetDialogScope"))
-            // TODO: b/377388104 check the QsDetailedView flag to use the correct dialogFactory
             dialog =
                 dialogFactory
                     .create(aboveStatusBar, canConfigMobileData, canConfigWifi, coroutineScope)
                     .createDialog()
+
             val controller =
                 expandable?.dialogTransitionController(
                     DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)
@@ -77,10 +81,9 @@
                 dialogTransitionAnimator.show(
                     dialog!!,
                     controller,
-                    animateBackgroundBoundsChange = true
+                    animateBackgroundBoundsChange = true,
                 )
-            }
-                ?: dialog?.show()
+            } ?: dialog?.show()
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt
new file mode 100644
index 0000000..42cb124
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2025 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.dialog
+
+import android.view.LayoutInflater
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import com.android.systemui.plugins.qs.TileDetailsViewModel
+import com.android.systemui.res.R
+
+/** The view model used for the screen record details view in the Quick Settings */
+class ScreenRecordDetailsViewModel() : TileDetailsViewModel() {
+    @Composable
+    override fun GetContentView() {
+        // TODO(b/378514312): Finish implementing this function.
+        AndroidView(
+            modifier = Modifier.fillMaxWidth().heightIn(max = VIEW_MAX_HEIGHT),
+            factory = { context ->
+                // Inflate with the existing dialog xml layout
+                LayoutInflater.from(context).inflate(R.layout.screen_share_dialog, null)
+            },
+        )
+    }
+
+    override fun clickOnSettingsButton() {
+        // No settings button in this tile.
+    }
+
+    override fun getTitle(): String {
+        // TODO(b/388321032): Replace this string with a string in a translatable xml file,
+        return "Screen recording"
+    }
+
+    override fun getSubTitle(): String {
+        // No sub-title in this tile.
+        return ""
+    }
+
+    companion object {
+        private val VIEW_MAX_HEIGHT: Dp = 320.dp
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index c34edc8..30d1f05 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -118,7 +118,8 @@
     override fun addCallback(callback: QSTile.Callback?) {
         callback ?: return
         callbacks.add(callback)
-        state?.let(callback::onStateChanged)
+        state.copyTo(cachedState)
+        state.let(callback::onStateChanged)
     }
 
     override fun removeCallback(callback: QSTile.Callback?) {
@@ -212,9 +213,9 @@
         qsTileViewModel.destroy()
     }
 
-    override fun getState(): QSTile.State =
+    override fun getState(): QSTile.AdapterState =
         qsTileViewModel.currentState?.let { mapState(context, it, qsTileViewModel.config) }
-            ?: QSTile.State()
+            ?: QSTile.AdapterState()
 
     override fun getInstanceId(): InstanceId = qsTileViewModel.config.instanceId
 
@@ -241,7 +242,7 @@
             context: Context,
             viewModelState: QSTileState,
             config: QSTileConfig,
-        ): QSTile.State =
+        ): QSTile.AdapterState =
             // we have to use QSTile.BooleanState to support different side icons
             // which are bound to instanceof QSTile.BooleanState in QSTileView.
             QSTile.AdapterState().apply {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt
index d60f05e..0488962 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt
@@ -90,22 +90,15 @@
                 initialValue = defaultTransitionState,
             )
 
-    fun changeScene(
-        toScene: SceneKey,
-        transitionKey: TransitionKey? = null,
-    ) {
-        dataSource.changeScene(
-            toScene = toScene,
-            transitionKey = transitionKey,
-        )
+    /** Number of currently active transition animations. */
+    val activeTransitionAnimationCount = MutableStateFlow(0)
+
+    fun changeScene(toScene: SceneKey, transitionKey: TransitionKey? = null) {
+        dataSource.changeScene(toScene = toScene, transitionKey = transitionKey)
     }
 
-    fun snapToScene(
-        toScene: SceneKey,
-    ) {
-        dataSource.snapToScene(
-            toScene = toScene,
-        )
+    fun snapToScene(toScene: SceneKey) {
+        dataSource.snapToScene(toScene = toScene)
     }
 
     /**
@@ -116,10 +109,7 @@
      * [overlay] is already shown.
      */
     fun showOverlay(overlay: OverlayKey, transitionKey: TransitionKey? = null) {
-        dataSource.showOverlay(
-            overlay = overlay,
-            transitionKey = transitionKey,
-        )
+        dataSource.showOverlay(overlay = overlay, transitionKey = transitionKey)
     }
 
     /**
@@ -130,10 +120,7 @@
      * if [overlay] is already hidden.
      */
     fun hideOverlay(overlay: OverlayKey, transitionKey: TransitionKey? = null) {
-        dataSource.hideOverlay(
-            overlay = overlay,
-            transitionKey = transitionKey,
-        )
+        dataSource.hideOverlay(overlay = overlay, transitionKey = transitionKey)
     }
 
     /**
@@ -143,11 +130,7 @@
      * This throws if [from] is not currently shown or if [to] is already shown.
      */
     fun replaceOverlay(from: OverlayKey, to: OverlayKey, transitionKey: TransitionKey? = null) {
-        dataSource.replaceOverlay(
-            from = from,
-            to = to,
-            transitionKey = transitionKey,
-        )
+        dataSource.replaceOverlay(from = from, to = to, transitionKey = transitionKey)
     }
 
     /** Sets whether the container is visible. */
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
index 0e6fc36..ba9dc76 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
@@ -47,6 +47,7 @@
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
 
 /**
  * Generic business logic and app state accessors for the scene framework.
@@ -165,12 +166,15 @@
 
     /** Whether the scene container is visible. */
     val isVisible: StateFlow<Boolean> =
-        combine(repository.isVisible, repository.isRemoteUserInputOngoing) {
-                isVisible,
-                isRemoteUserInteractionOngoing ->
+        combine(
+                repository.isVisible,
+                repository.isRemoteUserInputOngoing,
+                repository.activeTransitionAnimationCount,
+            ) { isVisible, isRemoteUserInteractionOngoing, activeTransitionAnimationCount ->
                 isVisibleInternal(
                     raw = isVisible,
                     isRemoteUserInputOngoing = isRemoteUserInteractionOngoing,
+                    activeTransitionAnimationCount = activeTransitionAnimationCount,
                 )
             }
             .stateIn(
@@ -436,8 +440,9 @@
     private fun isVisibleInternal(
         raw: Boolean = repository.isVisible.value,
         isRemoteUserInputOngoing: Boolean = repository.isRemoteUserInputOngoing.value,
+        activeTransitionAnimationCount: Int = repository.activeTransitionAnimationCount.value,
     ): Boolean {
-        return raw || isRemoteUserInputOngoing
+        return raw || isRemoteUserInputOngoing || activeTransitionAnimationCount > 0
     }
 
     /**
@@ -525,4 +530,50 @@
     ): Flow<Map<UserAction, UserActionResult>> {
         return disabledContentInteractor.filteredUserActions(unfiltered)
     }
+
+    /**
+     * Notifies that a transition animation has started.
+     *
+     * The scene container will remain visible while any transition animation is running within it.
+     */
+    fun onTransitionAnimationStart() {
+        repository.activeTransitionAnimationCount.update { current ->
+            (current + 1).also {
+                check(it < 10) {
+                    "Number of active transition animations is too high. Something must be" +
+                        " calling onTransitionAnimationStart too many times!"
+                }
+            }
+        }
+    }
+
+    /**
+     * Notifies that a transition animation has ended.
+     *
+     * The scene container will remain visible while any transition animation is running within it.
+     */
+    fun onTransitionAnimationEnd() {
+        decrementActiveTransitionAnimationCount()
+    }
+
+    /**
+     * Notifies that a transition animation has been canceled.
+     *
+     * The scene container will remain visible while any transition animation is running within it.
+     */
+    fun onTransitionAnimationCancelled() {
+        decrementActiveTransitionAnimationCount()
+    }
+
+    private fun decrementActiveTransitionAnimationCount() {
+        repository.activeTransitionAnimationCount.update { current ->
+            (current - 1).also {
+                check(it >= 0) {
+                    "Number of active transition animations is negative. Something must be" +
+                        " calling onTransitionAnimationEnd or onTransitionAnimationCancelled too" +
+                        " many times!"
+                }
+            }
+        }
+    }
 }
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 8d8c24e..3a23a71 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
@@ -25,6 +25,7 @@
 import com.android.keyguard.AuthInteractionProperties
 import com.android.systemui.CoreStartable
 import com.android.systemui.Flags
+import com.android.systemui.animation.ActivityTransitionAnimator
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
@@ -146,6 +147,7 @@
     private val vibratorHelper: VibratorHelper,
     private val msdlPlayer: MSDLPlayer,
     private val disabledContentInteractor: DisabledContentInteractor,
+    private val activityTransitionAnimator: ActivityTransitionAnimator,
 ) : CoreStartable {
     private val centralSurfaces: CentralSurfaces?
         get() = centralSurfacesOptLazy.get().getOrNull()
@@ -169,6 +171,7 @@
             handleKeyguardEnabledness()
             notifyKeyguardDismissCancelledCallbacks()
             refreshLockscreenEnabled()
+            hydrateActivityTransitionAnimationState()
         } else {
             sceneLogger.logFrameworkEnabled(
                 isEnabled = false,
@@ -929,6 +932,35 @@
         }
     }
 
+    /**
+     * Wires the scene framework to activity transition animations that originate from anywhere. A
+     * subset of these may actually originate from UI inside one of the scenes in the framework.
+     *
+     * Telling the scene framework about ongoing activity transition animations is critical so the
+     * scene framework doesn't make its scene container invisible during a transition.
+     *
+     * As it turns out, making the scene container view invisible during a transition animation
+     * disrupts the animation and causes interaction jank CUJ tracking to ignore reports of the CUJ
+     * ending or being canceled.
+     */
+    private fun hydrateActivityTransitionAnimationState() {
+        activityTransitionAnimator.addListener(
+            object : ActivityTransitionAnimator.Listener {
+                override fun onTransitionAnimationStart() {
+                    sceneInteractor.onTransitionAnimationStart()
+                }
+
+                override fun onTransitionAnimationEnd() {
+                    sceneInteractor.onTransitionAnimationEnd()
+                }
+
+                override fun onTransitionAnimationCancelled() {
+                    sceneInteractor.onTransitionAnimationCancelled()
+                }
+            }
+        )
+    }
+
     companion object {
         private const val TAG = "SceneContainerStartable"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt
index dc0c4dc..2bcd1d1 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt
@@ -24,7 +24,6 @@
 import com.android.systemui.flags.RefactorFlagUtils
 import com.android.systemui.keyguard.KeyguardWmStateRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
-import com.android.systemui.statusbar.phone.PredictiveBackSysUiFlag
 
 /** Helper for reading or using the scene container flag state. */
 object SceneContainerFlag {
@@ -36,8 +35,7 @@
         get() =
             sceneContainer() && // mainAconfigFlag
                 KeyguardWmStateRefactor.isEnabled &&
-                NotificationThrottleHun.isEnabled &&
-                PredictiveBackSysUiFlag.isEnabled
+                NotificationThrottleHun.isEnabled
 
     // NOTE: Changes should also be made in getSecondaryFlags and @EnableSceneContainer
 
@@ -49,7 +47,6 @@
         sequenceOf(
             KeyguardWmStateRefactor.token,
             NotificationThrottleHun.token,
-            PredictiveBackSysUiFlag.token,
             // NOTE: Changes should also be made in isEnabled and @EnableSceneContainer
         )
 
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneJankMonitor.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneJankMonitor.kt
new file mode 100644
index 0000000..48a49c6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneJankMonitor.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2025 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.scene.ui.view
+
+import android.view.View
+import androidx.compose.runtime.getValue
+import com.android.compose.animation.scene.ContentKey
+import com.android.internal.jank.Cuj
+import com.android.internal.jank.Cuj.CujType
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.Hydrator
+import com.android.systemui.scene.shared.model.Scenes
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+/**
+ * Monitors scene transitions and reports the beginning and ending of each scene-related CUJ.
+ *
+ * This general-purpose monitor can be expanded to include other rules that respond to the beginning
+ * and/or ending of transitions and reports jank CUI markers to the [InteractionJankMonitor].
+ */
+class SceneJankMonitor
+@AssistedInject
+constructor(
+    authenticationInteractor: AuthenticationInteractor,
+    private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
+    private val interactionJankMonitor: InteractionJankMonitor,
+) : ExclusiveActivatable() {
+
+    private val hydrator = Hydrator("SceneJankMonitor.hydrator")
+    private val authMethod: AuthenticationMethodModel? by
+        hydrator.hydratedStateOf(
+            traceName = "authMethod",
+            initialValue = null,
+            source = authenticationInteractor.authenticationMethod,
+        )
+
+    override suspend fun onActivated(): Nothing {
+        hydrator.activate()
+    }
+
+    /**
+     * Notifies that a transition is at its start.
+     *
+     * Should be called exactly once each time a new transition starts.
+     */
+    fun onTransitionStart(view: View, from: ContentKey, to: ContentKey, @CujType cuj: Int?) {
+        cuj.orCalculated(from, to) { nonNullCuj -> interactionJankMonitor.begin(view, nonNullCuj) }
+    }
+
+    /**
+     * Notifies that the previous transition is at its end.
+     *
+     * Should be called exactly once each time a transition ends.
+     */
+    fun onTransitionEnd(from: ContentKey, to: ContentKey, @CujType cuj: Int?) {
+        cuj.orCalculated(from, to) { nonNullCuj -> interactionJankMonitor.end(nonNullCuj) }
+    }
+
+    /**
+     * Returns this CUI marker (CUJ identifier), one that's calculated based on other state, or
+     * `null`, if no appropriate CUJ could be calculated.
+     */
+    private fun Int?.orCalculated(
+        from: ContentKey,
+        to: ContentKey,
+        ifNotNull: (nonNullCuj: Int) -> Unit,
+    ) {
+        val thisOrCalculatedCuj = this ?: calculatedCuj(from = from, to = to)
+
+        if (thisOrCalculatedCuj != null) {
+            ifNotNull(thisOrCalculatedCuj)
+        }
+    }
+
+    @CujType
+    private fun calculatedCuj(from: ContentKey, to: ContentKey): Int? {
+        val isDeviceUnlocked = deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked
+        return when {
+            to == Scenes.Bouncer ->
+                when (authMethod) {
+                    is AuthenticationMethodModel.Pin,
+                    is AuthenticationMethodModel.Sim -> Cuj.CUJ_LOCKSCREEN_PIN_APPEAR
+                    is AuthenticationMethodModel.Pattern -> Cuj.CUJ_LOCKSCREEN_PATTERN_APPEAR
+                    is AuthenticationMethodModel.Password -> Cuj.CUJ_LOCKSCREEN_PASSWORD_APPEAR
+                    is AuthenticationMethodModel.None -> null
+                    null -> null
+                }
+            from == Scenes.Bouncer && isDeviceUnlocked ->
+                when (authMethod) {
+                    is AuthenticationMethodModel.Pin,
+                    is AuthenticationMethodModel.Sim -> Cuj.CUJ_LOCKSCREEN_PIN_DISAPPEAR
+                    is AuthenticationMethodModel.Pattern -> Cuj.CUJ_LOCKSCREEN_PATTERN_DISAPPEAR
+                    is AuthenticationMethodModel.Password -> Cuj.CUJ_LOCKSCREEN_PASSWORD_DISAPPEAR
+                    is AuthenticationMethodModel.None -> null
+                    null -> null
+                }
+            else -> null
+        }
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(): SceneJankMonitor
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
index c459068..b8da227 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
@@ -34,6 +34,7 @@
         layoutInsetController: LayoutInsetsController,
         sceneDataSourceDelegator: SceneDataSourceDelegator,
         qsSceneAdapter: Provider<QSSceneAdapter>,
+        sceneJankMonitorFactory: SceneJankMonitor.Factory,
     ) {
         setLayoutInsetsController(layoutInsetController)
         SceneWindowRootViewBinder.bind(
@@ -52,6 +53,7 @@
             },
             dataSourceDelegator = sceneDataSourceDelegator,
             qsSceneAdapter = qsSceneAdapter,
+            sceneJankMonitorFactory = sceneJankMonitorFactory,
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
index f7061d9..7da007c 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
@@ -74,6 +74,7 @@
         onVisibilityChangedInternal: (isVisible: Boolean) -> Unit,
         dataSourceDelegator: SceneDataSourceDelegator,
         qsSceneAdapter: Provider<QSSceneAdapter>,
+        sceneJankMonitorFactory: SceneJankMonitor.Factory,
     ) {
         val unsortedSceneByKey: Map<SceneKey, Scene> = scenes.associateBy { scene -> scene.key }
         val sortedSceneByKey: Map<SceneKey, Scene> =
@@ -133,6 +134,7 @@
                                 dataSourceDelegator = dataSourceDelegator,
                                 qsSceneAdapter = qsSceneAdapter,
                                 containerConfig = containerConfig,
+                                sceneJankMonitorFactory = sceneJankMonitorFactory,
                             )
                             .also { it.id = R.id.scene_container_root_composable }
                     )
@@ -169,6 +171,7 @@
         dataSourceDelegator: SceneDataSourceDelegator,
         qsSceneAdapter: Provider<QSSceneAdapter>,
         containerConfig: SceneContainerConfig,
+        sceneJankMonitorFactory: SceneJankMonitor.Factory,
     ): View {
         return ComposeView(context).apply {
             setContent {
@@ -185,6 +188,7 @@
                             sceneTransitions = containerConfig.transitions,
                             dataSourceDelegator = dataSourceDelegator,
                             qsSceneAdapter = qsSceneAdapter,
+                            sceneJankMonitorFactory = sceneJankMonitorFactory,
                         )
                     }
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt
index e5ff252..7ec523b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt
@@ -19,6 +19,7 @@
 import android.hardware.display.DisplayManager
 import android.os.Bundle
 import android.os.UserHandle
+import android.view.View
 import androidx.annotation.StyleRes
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
@@ -103,7 +104,6 @@
             mediaProjectionMetricsLogger,
             defaultSelectedMode,
             displayManager,
-            dialog,
             controller,
             activityStarter,
             userContextProvider,
@@ -119,6 +119,12 @@
         super<BaseMediaProjectionPermissionDialogDelegate>.onCreate(dialog, savedInstanceState)
         setDialogTitle(R.string.screenrecord_permission_dialog_title)
         dialog.setTitle(R.string.screenrecord_title)
+        setStartButtonOnClickListener { v: View? ->
+            val screenRecordViewBinder: ScreenRecordPermissionViewBinder? =
+                viewBinder as ScreenRecordPermissionViewBinder?
+            screenRecordViewBinder?.startButtonOnClicked()
+            dialog.dismiss()
+        }
         setCancelButtonOnClickListener { dialog.dismiss() }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionViewBinder.kt
index 9f7e1ad..9fcb3df 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionViewBinder.kt
@@ -18,7 +18,6 @@
 
 import android.annotation.SuppressLint
 import android.app.Activity
-import android.app.AlertDialog
 import android.app.PendingIntent
 import android.content.Intent
 import android.hardware.display.DisplayManager
@@ -57,7 +56,6 @@
     mediaProjectionMetricsLogger: MediaProjectionMetricsLogger,
     @ScreenShareMode defaultSelectedMode: Int,
     displayManager: DisplayManager,
-    private val dialog: AlertDialog,
     private val controller: RecordingController,
     private val activityStarter: ActivityStarter,
     private val userContextProvider: UserContextProvider,
@@ -69,56 +67,57 @@
         hostUid = hostUid,
         mediaProjectionMetricsLogger,
         defaultSelectedMode,
-        dialog,
     ) {
     private lateinit var tapsSwitch: Switch
     private lateinit var audioSwitch: Switch
     private lateinit var tapsView: View
     private lateinit var options: Spinner
 
-    override fun bind() {
-        super.bind()
+    override fun bind(view: View) {
+        super.bind(view)
         initRecordOptionsView()
-        setStartButtonOnClickListener { _: View? ->
-            onStartRecordingClicked?.run()
-            if (selectedScreenShareOption.mode == ENTIRE_SCREEN) {
-                requestScreenCapture(
-                    captureTarget = null,
-                    displayId = selectedScreenShareOption.displayId,
-                )
-            }
-            if (selectedScreenShareOption.mode == SINGLE_APP) {
-                val intent = Intent(dialog.context, MediaProjectionAppSelectorActivity::class.java)
-                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        setStartButtonOnClickListener { startButtonOnClicked() }
+    }
 
-                // We can't start activity for result here so we use result receiver to get
-                // the selected target to capture
-                intent.putExtra(
-                    MediaProjectionAppSelectorActivity.EXTRA_CAPTURE_REGION_RESULT_RECEIVER,
-                    CaptureTargetResultReceiver(),
-                )
+    fun startButtonOnClicked() {
+        onStartRecordingClicked?.run()
+        if (selectedScreenShareOption.mode == ENTIRE_SCREEN) {
+            requestScreenCapture(
+                captureTarget = null,
+                displayId = selectedScreenShareOption.displayId,
+            )
+        }
+        if (selectedScreenShareOption.mode == SINGLE_APP) {
+            val intent =
+                Intent(containerView.context, MediaProjectionAppSelectorActivity::class.java)
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
 
-                intent.putExtra(
-                    MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE,
-                    hostUserHandle,
-                )
-                intent.putExtra(MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_UID, hostUid)
-                intent.putExtra(
-                    MediaProjectionAppSelectorActivity.EXTRA_SCREEN_SHARE_TYPE,
-                    MediaProjectionAppSelectorActivity.ScreenShareType.ScreenRecord.name,
-                )
-                activityStarter.startActivity(intent, /* dismissShade= */ true)
-            }
-            dialog.dismiss()
+            // We can't start activity for result here so we use result receiver to get
+            // the selected target to capture
+            intent.putExtra(
+                MediaProjectionAppSelectorActivity.EXTRA_CAPTURE_REGION_RESULT_RECEIVER,
+                CaptureTargetResultReceiver(),
+            )
+
+            intent.putExtra(
+                MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE,
+                hostUserHandle,
+            )
+            intent.putExtra(MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_UID, hostUid)
+            intent.putExtra(
+                MediaProjectionAppSelectorActivity.EXTRA_SCREEN_SHARE_TYPE,
+                MediaProjectionAppSelectorActivity.ScreenShareType.ScreenRecord.name,
+            )
+            activityStarter.startActivity(intent, /* dismissShade= */ true)
         }
     }
 
     @SuppressLint("ClickableViewAccessibility")
     private fun initRecordOptionsView() {
-        audioSwitch = dialog.requireViewById(R.id.screenrecord_audio_switch)
-        tapsSwitch = dialog.requireViewById(R.id.screenrecord_taps_switch)
+        audioSwitch = containerView.requireViewById(R.id.screenrecord_audio_switch)
+        tapsSwitch = containerView.requireViewById(R.id.screenrecord_taps_switch)
 
-        tapsView = dialog.requireViewById(R.id.show_taps)
+        tapsView = containerView.requireViewById(R.id.show_taps)
         updateTapsViewVisibility()
 
         // Add these listeners so that the switch only responds to movement
@@ -126,10 +125,10 @@
         audioSwitch.setOnTouchListener { _, event -> event.action == ACTION_MOVE }
         tapsSwitch.setOnTouchListener { _, event -> event.action == ACTION_MOVE }
 
-        options = dialog.requireViewById(R.id.screen_recording_options)
+        options = containerView.requireViewById(R.id.screen_recording_options)
         val a: ArrayAdapter<*> =
             ScreenRecordingAdapter(
-                dialog.context,
+                containerView.context,
                 android.R.layout.simple_spinner_dropdown_item,
                 MODES,
             )
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt
index e1631cc..bbb13d5 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt
@@ -61,9 +61,18 @@
     /** Callback for notifying of changes. */
     @WeaklyReferencedCallback
     interface Callback {
-        /** Notifies that the current user will be changed. */
+        /**
+         * Same as {@link onBeforeUserSwitching(Int, Runnable)} but the callback will be called
+         * automatically after the completion of this method.
+         */
         fun onBeforeUserSwitching(newUser: Int) {}
 
+        /** Notifies that the current user will be changed. */
+        fun onBeforeUserSwitching(newUser: Int, resultCallback: Runnable) {
+            onBeforeUserSwitching(newUser)
+            resultCallback.run()
+        }
+
         /**
          * Same as {@link onUserChanging(Int, Context, Runnable)} but the callback will be called
          * automatically after the completion of this method.
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
index b7a3aed..a48d4d4 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
@@ -196,8 +196,9 @@
     private fun registerUserSwitchObserver() {
         iActivityManager.registerUserSwitchObserver(
             object : UserSwitchObserver() {
-                override fun onBeforeUserSwitching(newUserId: Int) {
+                override fun onBeforeUserSwitching(newUserId: Int, reply: IRemoteCallback?) {
                     handleBeforeUserSwitching(newUserId)
+                    reply?.sendResult(null)
                 }
 
                 override fun onUserSwitching(newUserId: Int, reply: IRemoteCallback?) {
@@ -236,8 +237,7 @@
         setUserIdInternal(newUserId)
 
         notifySubscribers { callback, resultCallback ->
-                callback.onBeforeUserSwitching(newUserId)
-                resultCallback.run()
+                callback.onBeforeUserSwitching(newUserId, resultCallback)
             }
             .await()
     }
@@ -316,7 +316,7 @@
             val callback = it.callback.get()
             if (callback != null) {
                 it.executor.execute {
-                    traceSection({ "$callback" }) { action(callback) { latch.countDown() } }
+                    traceSection({ "UserTrackerImpl::$callback" }) { action(callback) { latch.countDown() } }
                 }
             } else {
                 latch.countDown()
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index e168025..c4306d3 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -49,7 +49,6 @@
 import android.animation.ValueAnimator;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.content.ContentResolver;
 import android.content.res.Resources;
 import android.graphics.Color;
 import android.graphics.Insets;
@@ -58,28 +57,23 @@
 import android.graphics.RenderEffect;
 import android.graphics.Shader;
 import android.os.Bundle;
-import android.os.Handler;
 import android.os.Trace;
-import android.os.UserManager;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.util.MathUtils;
 import android.view.HapticFeedbackConstants;
 import android.view.InputDevice;
-import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.View;
 import android.view.View.AccessibilityDelegate;
 import android.view.ViewConfiguration;
 import android.view.ViewPropertyAnimator;
-import android.view.ViewStub;
 import android.view.ViewTreeObserver;
 import android.view.WindowInsets;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityNodeInfo;
-import android.view.animation.Interpolator;
 
 import com.android.app.animation.Interpolators;
 import com.android.internal.annotations.VisibleForTesting;
@@ -105,19 +99,17 @@
 import com.android.systemui.doze.DozeLog;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.dump.DumpsysTableLogger;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.fragments.FragmentService;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
-import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingObserver;
 import com.android.systemui.keyguard.shared.model.ClockSize;
 import com.android.systemui.keyguard.shared.model.Edge;
 import com.android.systemui.keyguard.shared.model.TransitionState;
 import com.android.systemui.keyguard.shared.model.TransitionStep;
 import com.android.systemui.keyguard.ui.binder.KeyguardLongPressViewBinder;
+import com.android.systemui.keyguard.ui.transitions.BlurConfig;
 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel;
 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager;
@@ -162,7 +154,6 @@
 import com.android.systemui.statusbar.notification.ViewGroupFadeHelper;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor;
-import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
 import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
 import com.android.systemui.statusbar.notification.headsup.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.notification.headsup.OnHeadsUpChangedListener;
@@ -174,10 +165,8 @@
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
-import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor;
-import com.android.systemui.statusbar.phone.BounceInterpolator;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
@@ -254,7 +243,7 @@
      * Whether the Shade should animate to reflect Back gesture progress.
      * To minimize latency at runtime, we cache this, else we'd be reading it every time
      * updateQsExpansion() is called... and it's called very often.
-     *
+     * <p>
      * Whenever we change this flag, SysUI is restarted, so it's never going to be "stale".
      */
 
@@ -285,8 +274,6 @@
     private final ConfigurationController mConfigurationController;
     private final Provider<FlingAnimationUtils.Builder> mFlingAnimationUtilsBuilder;
     private final NotificationStackScrollLayoutController mNotificationStackScrollLayoutController;
-    private final LayoutInflater mLayoutInflater;
-    private final FeatureFlags mFeatureFlags;
     private final AccessibilityManager mAccessibilityManager;
     private final NotificationWakeUpCoordinator mWakeUpCoordinator;
     private final PulseExpansionHandler mPulseExpansionHandler;
@@ -311,7 +298,6 @@
     private final DozeLog mDozeLog;
     /** Whether or not the NotificationPanelView can be expanded or collapsed with a drag. */
     private final boolean mNotificationsDragEnabled;
-    private final Interpolator mBounceInterpolator;
     private final NotificationShadeWindowController mNotificationShadeWindowController;
     private final ShadeExpansionStateManager mShadeExpansionStateManager;
     private final ShadeRepository mShadeRepository;
@@ -321,7 +307,6 @@
     private final NotificationGutsManager mGutsManager;
     private final AlternateBouncerInteractor mAlternateBouncerInteractor;
     private final QuickSettingsControllerImpl mQsController;
-    private final NaturalScrollingSettingObserver mNaturalScrollingSettingObserver;
     private final TouchHandler mTouchHandler = new TouchHandler();
 
     private long mDownTime;
@@ -436,7 +421,6 @@
                     mPanelAlphaAnimator.getProperty(), Interpolators.ALPHA_IN);
 
     private final CommandQueue mCommandQueue;
-    private final UserManager mUserManager;
     private final MediaDataManager mMediaDataManager;
     @PanelState
     private int mCurrentPanelState = STATE_CLOSED;
@@ -462,7 +446,6 @@
     private boolean mIsGestureNavigation;
     private int mOldLayoutDirection;
 
-    private final ContentResolver mContentResolver;
     private float mMinFraction;
 
     private final KeyguardMediaController mKeyguardMediaController;
@@ -475,7 +458,6 @@
     private int mSplitShadeScrimTransitionDistance;
 
     private final NotificationListContainer mNotificationListContainer;
-    private final NotificationStackSizeCalculator mNotificationStackSizeCalculator;
     private final NPVCDownEventState.Buffer mLastDownEvents;
     private final KeyguardClockInteractor mKeyguardClockInteractor;
     private float mMinExpandHeight;
@@ -530,8 +512,6 @@
     private final KeyguardInteractor mKeyguardInteractor;
     private final PowerInteractor mPowerInteractor;
     private final CoroutineDispatcher mMainDispatcher;
-    private boolean mIsAnyMultiShadeExpanded;
-    private boolean mForceFlingAnimationForTest = false;
     private final SplitShadeStateController mSplitShadeStateController;
     private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */,
             mNextCollapseSpeedUpFactor, false /* expandBecauseOfFalsing */);
@@ -550,9 +530,6 @@
 
     @Inject
     public NotificationPanelViewController(NotificationPanelView view,
-            @Main Handler handler,
-            @ShadeDisplayAware LayoutInflater layoutInflater,
-            FeatureFlags featureFlags,
             NotificationWakeUpCoordinator coordinator,
             PulseExpansionHandler pulseExpansionHandler,
             DynamicPrivacyController dynamicPrivacyController,
@@ -584,7 +561,6 @@
             KeyguardStatusBarViewComponent.Factory keyguardStatusBarViewComponentFactory,
             LockscreenShadeTransitionController lockscreenShadeTransitionController,
             ScrimController scrimController,
-            UserManager userManager,
             MediaDataManager mediaDataManager,
             NotificationShadeDepthController notificationShadeDepthController,
             AmbientState ambientState,
@@ -595,7 +571,6 @@
             QuickSettingsControllerImpl quickSettingsController,
             FragmentService fragmentService,
             IStatusBarService statusBarService,
-            ContentResolver contentResolver,
             ShadeHeaderController shadeHeaderController,
             ScreenOffAnimationController screenOffAnimationController,
             LockscreenGestureLogger lockscreenGestureLogger,
@@ -606,7 +581,6 @@
             KeyguardUnlockAnimationController keyguardUnlockAnimationController,
             KeyguardIndicationController keyguardIndicationController,
             NotificationListContainer notificationListContainer,
-            NotificationStackSizeCalculator notificationStackSizeCalculator,
             UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
             SystemClock systemClock,
             KeyguardClockInteractor keyguardClockInteractor,
@@ -625,7 +599,6 @@
             SplitShadeStateController splitShadeStateController,
             PowerInteractor powerInteractor,
             KeyguardClockPositionAlgorithm keyguardClockPositionAlgorithm,
-            NaturalScrollingSettingObserver naturalScrollingSettingObserver,
             MSDLPlayer msdlPlayer,
             BrightnessMirrorShowingInteractor brightnessMirrorShowingInteractor) {
         SceneContainerFlag.assertInLegacyMode();
@@ -651,7 +624,6 @@
         mKeyguardInteractor = keyguardInteractor;
         mPowerInteractor = powerInteractor;
         mClockPositionAlgorithm = keyguardClockPositionAlgorithm;
-        mNaturalScrollingSettingObserver = naturalScrollingSettingObserver;
         mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
             @Override
             public void onViewAttachedToWindow(View v) {
@@ -691,7 +663,6 @@
                 .setY2(0.84f)
                 .build();
         mLatencyTracker = latencyTracker;
-        mBounceInterpolator = new BounceInterpolator();
         mFalsingManager = falsingManager;
         mDozeLog = dozeLog;
         mNotificationsDragEnabled = mResources.getBoolean(
@@ -708,13 +679,11 @@
         mMediaHierarchyManager = mediaHierarchyManager;
         mNotificationsQSContainerController = notificationsQSContainerController;
         mNotificationListContainer = notificationListContainer;
-        mNotificationStackSizeCalculator = notificationStackSizeCalculator;
         mNavigationBarController = navigationBarController;
         mNotificationsQSContainerController.init();
         mNotificationStackScrollLayoutController = notificationStackScrollLayoutController;
         mKeyguardStatusBarViewComponentFactory = keyguardStatusBarViewComponentFactory;
         mDepthController = notificationShadeDepthController;
-        mContentResolver = contentResolver;
         mFragmentService = fragmentService;
         mStatusBarService = statusBarService;
         mSplitShadeStateController = splitShadeStateController;
@@ -722,8 +691,6 @@
                 mSplitShadeStateController.shouldUseSplitNotificationShade(mResources);
         mView.setWillNotDraw(!DEBUG_DRAWABLE);
         mShadeHeaderController = shadeHeaderController;
-        mLayoutInflater = layoutInflater;
-        mFeatureFlags = featureFlags;
         mAnimateBack = predictiveBackAnimateShade();
         mFalsingCollector = falsingCollector;
         mWakeUpCoordinator = coordinator;
@@ -736,7 +703,6 @@
         mPulseExpansionHandler = pulseExpansionHandler;
         mDozeParameters = dozeParameters;
         mScrimController = scrimController;
-        mUserManager = userManager;
         mMediaDataManager = mediaDataManager;
         mTapAgainViewController = tapAgainViewController;
         mSysUiState = sysUiState;
@@ -889,7 +855,7 @@
 
         // Dreaming->Lockscreen
         collectFlow(mView, mDreamingToLockscreenTransitionViewModel.getLockscreenAlpha(),
-                setDreamLockscreenTransitionAlpha(mNotificationStackScrollLayoutController),
+                setDreamLockscreenTransitionAlpha(),
                 mMainDispatcher);
 
         collectFlow(mView, mKeyguardTransitionInteractor.transition(
@@ -949,13 +915,12 @@
         if (!com.android.systemui.Flags.bouncerUiRevamp()) return;
 
         if (isBouncerShowing && isExpanded()) {
-            // Blur the shade much lesser than the background surface so that the surface is
-            // distinguishable from the background.
-            float shadeBlurEffect = mDepthController.getMaxBlurRadiusPx() / 3;
+            float shadeBlurEffect = BlurConfig.maxBlurRadiusToNotificationPanelBlurRadius(
+                    mDepthController.getMaxBlurRadiusPx());
             mView.setRenderEffect(RenderEffect.createBlurEffect(
                     shadeBlurEffect,
                     shadeBlurEffect,
-                    Shader.TileMode.MIRROR));
+                    Shader.TileMode.CLAMP));
         } else {
             mView.setRenderEffect(null);
         }
@@ -963,28 +928,31 @@
 
     @Override
     public void updateResources() {
-        Trace.beginSection("NSSLC#updateResources");
-        final boolean newSplitShadeEnabled =
-                mSplitShadeStateController.shouldUseSplitNotificationShade(mResources);
-        final boolean splitShadeChanged = mSplitShadeEnabled != newSplitShadeEnabled;
-        mSplitShadeEnabled = newSplitShadeEnabled;
-        mQsController.updateResources();
-        mNotificationsQSContainerController.updateResources();
-        updateKeyguardStatusViewAlignment(/* animate= */false);
-        mKeyguardMediaController.refreshMediaPosition(
-                "NotificationPanelViewController.updateResources");
+        try {
+            Trace.beginSection("NSSLC#updateResources");
+            final boolean newSplitShadeEnabled =
+                    mSplitShadeStateController.shouldUseSplitNotificationShade(mResources);
+            final boolean splitShadeChanged = mSplitShadeEnabled != newSplitShadeEnabled;
+            mSplitShadeEnabled = newSplitShadeEnabled;
+            mQsController.updateResources();
+            mNotificationsQSContainerController.updateResources();
+            updateKeyguardStatusViewAlignment();
+            mKeyguardMediaController.refreshMediaPosition(
+                    "NotificationPanelViewController.updateResources");
 
-        if (splitShadeChanged) {
-            if (isPanelVisibleBecauseOfHeadsUp()) {
-                // workaround for b/324642496, because HUNs set state to OPENING
-                onPanelStateChanged(STATE_CLOSED);
+            if (splitShadeChanged) {
+                if (isPanelVisibleBecauseOfHeadsUp()) {
+                    // workaround for b/324642496, because HUNs set state to OPENING
+                    onPanelStateChanged(STATE_CLOSED);
+                }
+                onSplitShadeEnabledChanged();
             }
-            onSplitShadeEnabledChanged();
-        }
 
-        mSplitShadeFullTransitionDistance =
-                mResources.getDimensionPixelSize(R.dimen.split_shade_full_transition_distance);
-        Trace.endSection();
+            mSplitShadeFullTransitionDistance =
+                    mResources.getDimensionPixelSize(R.dimen.split_shade_full_transition_distance);
+        } finally {
+            Trace.endSection();
+        }
     }
 
     private void onSplitShadeEnabledChanged() {
@@ -1011,29 +979,6 @@
         mQsController.updateQsState();
     }
 
-    private View reInflateStub(int viewId, int stubId, int layoutId, boolean enabled) {
-        View view = mView.findViewById(viewId);
-        if (view != null) {
-            int index = mView.indexOfChild(view);
-            mView.removeView(view);
-            if (enabled) {
-                view = mLayoutInflater.inflate(layoutId, mView, false);
-                mView.addView(view, index);
-            } else {
-                // Add the stub back so we can re-inflate it again if necessary
-                ViewStub stub = new ViewStub(mView.getContext(), layoutId);
-                stub.setId(stubId);
-                mView.addView(stub, index);
-                view = null;
-            }
-        } else if (enabled) {
-            // It's possible the stub was never inflated if the configuration changed
-            ViewStub stub = mView.findViewById(stubId);
-            view = stub.inflate();
-        }
-        return view;
-    }
-
     @VisibleForTesting
     void reInflateViews() {
         debugLog("reInflateViews");
@@ -1042,11 +987,6 @@
                 mStatusBarStateController.getInterpolatedDozeAmount());
     }
 
-    @VisibleForTesting
-    boolean isFlinging() {
-        return mIsFlinging;
-    }
-
     /** Sets a listener to be notified when the shade starts opening or finishes closing. */
     public void setOpenCloseListener(OpenCloseListener openCloseListener) {
         SceneContainerFlag.assertInLegacyMode();
@@ -1096,8 +1036,6 @@
      * @param forceClockUpdate Should the clock be updated even when not on keyguard
      */
     private void positionClockAndNotifications(boolean forceClockUpdate) {
-        boolean animate = !SceneContainerFlag.isEnabled()
-                && mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending();
         int stackScrollerPadding;
         boolean onKeyguard = isKeyguardShowing();
 
@@ -1120,14 +1058,14 @@
         mNotificationStackScrollLayoutController.setIntrinsicPadding(stackScrollerPadding);
 
         mStackScrollerMeasuringPass++;
-        requestScrollerTopPaddingUpdate(animate);
+        requestScrollerTopPaddingUpdate();
         mStackScrollerMeasuringPass = 0;
         mAnimateNextPositionUpdate = false;
     }
 
     private void updateClockAppearance() {
         mKeyguardClockInteractor.setClockSize(computeDesiredClockSize());
-        updateKeyguardStatusViewAlignment(/* animate= */true);
+        updateKeyguardStatusViewAlignment();
 
         float darkAmount =
                 mScreenOffAnimationController.shouldExpandNotifications()
@@ -1146,10 +1084,6 @@
     }
 
     private ClockSize computeDesiredClockSize() {
-        if (shouldForceSmallClock()) {
-            return ClockSize.SMALL;
-        }
-
         if (mSplitShadeEnabled) {
             return computeDesiredClockSizeForSplitShade();
         }
@@ -1174,17 +1108,9 @@
         return ClockSize.LARGE;
     }
 
-    private boolean shouldForceSmallClock() {
-        return mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ENABLE_LANDSCAPE)
-                && !isOnAod()
-                // True on small landscape screens
-                && mResources.getBoolean(R.bool.force_small_clock_on_lockscreen);
-    }
-
-    private void updateKeyguardStatusViewAlignment(boolean animate) {
+    private void updateKeyguardStatusViewAlignment() {
         boolean shouldBeCentered = shouldKeyguardStatusViewBeCentered();
         mKeyguardUnfoldTransition.ifPresent(t -> t.setStatusViewCentered(shouldBeCentered));
-        mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered);
     }
 
     private boolean shouldKeyguardStatusViewBeCentered() {
@@ -1214,14 +1140,8 @@
     }
 
     private boolean hasVisibleNotifications() {
-        if (FooterViewRefactor.isEnabled()) {
-            return mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()
-                    || mMediaDataManager.hasActiveMediaOrRecommendation();
-        } else {
-            return mNotificationStackScrollLayoutController
-                    .getVisibleNotificationCount() != 0
-                    || mMediaDataManager.hasActiveMediaOrRecommendation();
-        }
+        return mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()
+                || mMediaDataManager.hasActiveMediaOrRecommendation();
     }
 
     @Override
@@ -1464,7 +1384,7 @@
                 }
             }
         });
-        if (!mScrimController.isScreenOn() && !mForceFlingAnimationForTest) {
+        if (!mScrimController.isScreenOn()) {
             animator.setDuration(1);
         }
         setAnimator(animator);
@@ -1472,16 +1392,11 @@
     }
 
     @VisibleForTesting
-    void setForceFlingAnimationForTest(boolean force) {
-        mForceFlingAnimationForTest = force;
-    }
-
-    @VisibleForTesting
     void onFlingEnd(boolean cancelled) {
         mIsFlinging = false;
         mExpectingSynthesizedDown = false;
         // No overshoot when the animation ends
-        setOverExpansionInternal(0, false /* isFromGesture */);
+        setOverExpansionInternal(0);
         setAnimator(null);
         mKeyguardStateController.notifyPanelFlingEnd();
         if (!cancelled) {
@@ -1572,7 +1487,7 @@
     }
 
     /** Return whether a touch is near the gesture handle at the bottom of screen */
-    boolean isInGestureNavHomeHandleArea(float x, float y) {
+    boolean isInGestureNavHomeHandleArea(float y) {
         return mIsGestureNavigation && y > mView.getHeight() - mNavigationBarBottomHeight;
     }
 
@@ -1605,7 +1520,7 @@
      * There are two scenarios behind this function call. First, input focus transfer has
      * successfully happened and this view already received synthetic DOWN event.
      * (mExpectingSynthesizedDown == false). Do nothing.
-     *
+     * <p>
      * Second, before input focus transfer finished, user may have lifted finger in previous window
      * and this window never received synthetic DOWN event. (mExpectingSynthesizedDown == true). In
      * this case, we use the velocity to trigger fling event.
@@ -1766,7 +1681,7 @@
         return mBarState == KEYGUARD;
     }
 
-    void requestScrollerTopPaddingUpdate(boolean animate) {
+    void requestScrollerTopPaddingUpdate() {
         if (!SceneContainerFlag.isEnabled()) {
             float padding = mQsController.calculateNotificationsTopPadding(mIsExpandingOrCollapsing,
                     getKeyguardNotificationStaticPadding(), mExpandedFraction);
@@ -2041,11 +1956,6 @@
     }
 
     @VisibleForTesting
-    void setTouchSlopExceeded(boolean isTouchSlopExceeded) {
-        mTouchSlopExceeded = isTouchSlopExceeded;
-    }
-
-    @VisibleForTesting
     void setOverExpansion(float overExpansion) {
         if (overExpansion == mOverExpansion) {
             return;
@@ -2218,9 +2128,6 @@
     @Override
     public void setBouncerShowing(boolean bouncerShowing) {
         mBouncerShowing = bouncerShowing;
-        if (!FooterViewRefactor.isEnabled()) {
-            mNotificationStackScrollLayoutController.updateShowEmptyShadeView();
-        }
         updateVisibility();
     }
 
@@ -2397,7 +2304,7 @@
         final float dozeAmount = dozing ? 1 : 0;
         mStatusBarStateController.setAndInstrumentDozeAmount(mView, dozeAmount, animate);
 
-        updateKeyguardStatusViewAlignment(animate);
+        updateKeyguardStatusViewAlignment();
     }
 
     @Override
@@ -2416,7 +2323,7 @@
         }
         mNotificationStackScrollLayoutController.setPulsing(pulsing, animatePulse);
 
-        updateKeyguardStatusViewAlignment(/* animate= */ true);
+        updateKeyguardStatusViewAlignment();
     }
 
     public void performHapticFeedback(int constant) {
@@ -2982,8 +2889,7 @@
         mIsSpringBackAnimation = true;
         ValueAnimator animator = ValueAnimator.ofFloat(mOverExpansion, 0);
         animator.addUpdateListener(
-                animation -> setOverExpansionInternal((float) animation.getAnimatedValue(),
-                        false /* isFromGesture */));
+                animation -> setOverExpansionInternal((float) animation.getAnimatedValue()));
         animator.setDuration(SHADE_OPEN_SPRING_BACK_DURATION);
         animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
         animator.addListener(new AnimatorListenerAdapter() {
@@ -3075,19 +2981,10 @@
      * Set the current overexpansion
      *
      * @param overExpansion the amount of overexpansion to apply
-     * @param isFromGesture is this amount from a gesture and needs to be rubberBanded?
      */
-    private void setOverExpansionInternal(float overExpansion, boolean isFromGesture) {
-        if (!isFromGesture) {
-            mLastGesturedOverExpansion = -1;
-            setOverExpansion(overExpansion);
-        } else if (mLastGesturedOverExpansion != overExpansion) {
-            mLastGesturedOverExpansion = overExpansion;
-            final float heightForFullOvershoot = mView.getHeight() / 3.0f;
-            float newExpansion = MathUtils.saturate(overExpansion / heightForFullOvershoot);
-            newExpansion = Interpolators.getOvershootInterpolation(newExpansion);
-            setOverExpansion(newExpansion * mPanelFlingOvershootAmount * 2.0f);
-        }
+    private void setOverExpansionInternal(float overExpansion) {
+        mLastGesturedOverExpansion = -1;
+        setOverExpansion(overExpansion);
     }
 
     /** Sets the expanded height relative to a number from 0 to 1. */
@@ -3183,29 +3080,6 @@
     }
 
     /**
-     * Phase 2: Bounce down.
-     */
-    private void startUnlockHintAnimationPhase2(final Runnable onAnimationFinished) {
-        ValueAnimator animator = createHeightAnimator(getMaxPanelHeight());
-        animator.setDuration(450);
-        animator.setInterpolator(mBounceInterpolator);
-        animator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                setAnimator(null);
-                onAnimationFinished.run();
-                updateExpansionAndVisibility();
-            }
-        });
-        animator.start();
-        setAnimator(animator);
-    }
-
-    private ValueAnimator createHeightAnimator(float targetHeight) {
-        return createHeightAnimator(targetHeight, 0.0f /* performOvershoot */);
-    }
-
-    /**
      * Create an animator that can also overshoot
      *
      * @param targetHeight    the target height
@@ -3225,7 +3099,7 @@
                                 mPanelFlingOvershootAmount * overshootAmount,
                                 Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
                                         animator.getAnimatedFraction()));
-                        setOverExpansionInternal(expansion, false /* isFromGesture */);
+                        setOverExpansionInternal(expansion);
                     }
                     setExpandedHeightInternal((float) animation.getAnimatedValue());
                 });
@@ -3280,8 +3154,8 @@
         mFixedDuration = NO_FIXED_DURATION;
     }
 
-    boolean postToView(Runnable action) {
-        return mView.post(action);
+    void postToView(Runnable action) {
+        mView.post(action);
     }
 
     /** Sends an external (e.g. Status Bar) intercept touch event to the Shade touch handler. */
@@ -3360,7 +3234,7 @@
         return mShadeExpansionStateManager;
     }
 
-    void onQsExpansionChanged(boolean expanded) {
+    void onQsExpansionChanged() {
         updateExpandedHeightToMaxHeight();
         updateSystemUiStateFlags();
         NavigationBarView navigationBarView =
@@ -3372,7 +3246,7 @@
 
     @VisibleForTesting
     void onQsSetExpansionHeightCalled(boolean qsFullyExpanded) {
-        requestScrollerTopPaddingUpdate(false);
+        requestScrollerTopPaddingUpdate();
         mKeyguardStatusBarViewController.updateViewState();
         int barState = getBarState();
         if (barState == SHADE_LOCKED || barState == KEYGUARD) {
@@ -3413,7 +3287,7 @@
 
     private void onExpansionHeightSetToMax(boolean requestPaddingUpdate) {
         if (requestPaddingUpdate) {
-            requestScrollerTopPaddingUpdate(false /* animate */);
+            requestScrollerTopPaddingUpdate();
         }
         updateExpandedHeightToMaxHeight();
     }
@@ -3437,7 +3311,7 @@
                             ? (ExpandableNotificationRow) firstChildNotGone : null;
             if (firstRow != null && (view == firstRow || (firstRow.getNotificationParent()
                     == firstRow))) {
-                requestScrollerTopPaddingUpdate(false /* animate */);
+                requestScrollerTopPaddingUpdate();
             }
             updateExpandedHeightToMaxHeight();
         }
@@ -3517,7 +3391,6 @@
                 boolean animatingUnlockedShadeToKeyguardBypass
         ) {
             boolean goingToFullShade = mStatusBarStateController.goingToFullShade();
-            boolean keyguardFadingAway = mKeyguardStateController.isKeyguardFadingAway();
             int oldState = mBarState;
             boolean keyguardShowing = statusBarState == KEYGUARD;
 
@@ -3738,17 +3611,13 @@
         }
         if (state == STATE_CLOSED) {
             mQsController.setExpandImmediate(false);
-            // Close the status bar in the next frame so we can show the end of the
-            // animation.
-            if (!mIsAnyMultiShadeExpanded) {
-                mView.post(mMaybeHideExpandedRunnable);
-            }
+            // Close the status bar in the next frame so we can show the end of the animation.
+            mView.post(mMaybeHideExpandedRunnable);
         }
         mCurrentPanelState = state;
     }
 
-    private Consumer<Float> setDreamLockscreenTransitionAlpha(
-            NotificationStackScrollLayoutController stackScroller) {
+    private Consumer<Float> setDreamLockscreenTransitionAlpha() {
         return (Float alpha) -> {
             // Also animate the status bar's alpha during transitions between the lockscreen and
             // dreams.
@@ -4285,4 +4154,3 @@
         }
     }
 }
-
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
index c88e7b8..d058372 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
@@ -86,7 +86,6 @@
 import com.android.systemui.statusbar.QsFrameTranslateController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor;
-import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
 import com.android.systemui.statusbar.notification.stack.AmbientState;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
@@ -96,8 +95,8 @@
 import com.android.systemui.statusbar.phone.LightBarController;
 import com.android.systemui.statusbar.phone.LockscreenGestureLogger;
 import com.android.systemui.statusbar.phone.ScrimController;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.phone.ShadeTouchableRegionManager;
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.policy.CastController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.SplitShadeStateController;
@@ -115,9 +114,7 @@
 import javax.inject.Inject;
 import javax.inject.Provider;
 
-/** Handles QuickSettings touch handling, expansion and animation state
- * TODO (b/264460656) make this dumpable
- */
+/** Handles QuickSettings touch handling, expansion and animation state. */
 @SysUISingleton
 public class QuickSettingsControllerImpl implements QuickSettingsController, Dumpable {
     public static final String TAG = "QuickSettingsController";
@@ -295,7 +292,6 @@
     private ValueAnimator mSizeChangeAnimator;
 
     private ExpansionHeightListener mExpansionHeightListener;
-    private QsStateUpdateListener mQsStateUpdateListener;
     private ApplyClippingImmediatelyListener mApplyClippingImmediatelyListener;
     private FlingQsWithoutClickListener mFlingQsWithoutClickListener;
     private ExpansionHeightSetToMaxListener mExpansionHeightSetToMaxListener;
@@ -402,10 +398,6 @@
         mExpansionHeightListener = listener;
     }
 
-    void setQsStateUpdateListener(QsStateUpdateListener listener) {
-        mQsStateUpdateListener = listener;
-    }
-
     void setApplyClippingImmediatelyListener(ApplyClippingImmediatelyListener listener) {
         mApplyClippingImmediatelyListener = listener;
     }
@@ -563,7 +555,7 @@
         }
         // TODO (b/265193930): remove dependency on NPVC
         // Let's reject anything at the very bottom around the home handle in gesture nav
-        if (mPanelViewControllerLazy.get().isInGestureNavHomeHandleArea(x, y)) {
+        if (mPanelViewControllerLazy.get().isInGestureNavHomeHandleArea(y)) {
             return false;
         }
         return y <= mNotificationStackScrollLayoutController.getBottomMostNotificationBottom()
@@ -805,7 +797,7 @@
         if (changed) {
             mShadeRepository.setLegacyIsQsExpanded(expanded);
             updateQsState();
-            mPanelViewControllerLazy.get().onQsExpansionChanged(expanded);
+            mPanelViewControllerLazy.get().onQsExpansionChanged();
             mShadeLog.logQsExpansionChanged("QS Expansion Changed.", expanded,
                     getMinExpansionHeight(), getMaxExpansionHeight(),
                     mStackScrollerOverscrolling, mAnimatorExpand, mAnimating);
@@ -1022,16 +1014,6 @@
     }
 
     void updateQsState() {
-        if (!FooterViewRefactor.isEnabled()) {
-            // Update full screen state; note that this will be true if the QS panel is only
-            // partially expanded, and that is fixed with the footer view refactor.
-            setQsFullScreen(/* qsFullScreen = */ getExpanded() && !mSplitShadeEnabled);
-        }
-
-        if (mQsStateUpdateListener != null) {
-            mQsStateUpdateListener.onQsStateUpdated(getExpanded(), mStackScrollerOverscrolling);
-        }
-
         if (mQs == null) return;
         mQs.setExpanded(getExpanded());
     }
@@ -1094,10 +1076,8 @@
         // Update the light bar
         mLightBarController.setQsExpanded(mFullyExpanded);
 
-        if (FooterViewRefactor.isEnabled()) {
-            // Update full screen state
-            setQsFullScreen(/* qsFullScreen = */ mFullyExpanded && !mSplitShadeEnabled);
-        }
+        // Update full screen state
+        setQsFullScreen(/* qsFullScreen = */ mFullyExpanded && !mSplitShadeEnabled);
     }
 
     float getLockscreenShadeDragProgress() {
@@ -1212,7 +1192,7 @@
     /**
      * Applies clipping to quick settings, notifications layout and
      * updates bounds of the notifications background (notifications scrim).
-     *
+     * <p>
      * The parameters are bounds of the notifications area rectangle, this function
      * calculates bounds for the QS clipping based on the notifications bounds.
      */
@@ -2268,10 +2248,8 @@
                     setExpansionHeight(qsHeight);
                 }
 
-                boolean hasNotifications = FooterViewRefactor.isEnabled()
-                        ? mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()
-                        : mNotificationStackScrollLayoutController.getVisibleNotificationCount()
-                                != 0;
+                boolean hasNotifications =
+                        mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue();
                 if (!hasNotifications && !mMediaDataManager.hasActiveMediaOrRecommendation()) {
                     // No notifications are visible, let's animate to the height of qs instead
                     if (isQsFragmentCreated()) {
@@ -2406,10 +2384,6 @@
         void onQsSetExpansionHeightCalled(boolean qsFullyExpanded);
     }
 
-    interface QsStateUpdateListener {
-        void onQsStateUpdated(boolean qsExpanded, boolean isStackScrollerOverscrolling);
-    }
-
     interface ApplyClippingImmediatelyListener {
         void onQsClippingImmediatelyApplied(boolean clipStatusView, Rect lastQsClipBounds,
                 int top, boolean qsFragmentCreated, boolean qsVisible);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
index 63e8ba8..7476420 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
@@ -37,12 +37,13 @@
 import com.android.systemui.res.R
 import com.android.systemui.scene.ui.view.WindowRootView
 import com.android.systemui.shade.data.repository.MutableShadeDisplaysRepository
-import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor
-import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractorImpl
 import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
 import com.android.systemui.shade.data.repository.ShadeDisplaysRepositoryImpl
 import com.android.systemui.shade.display.ShadeDisplayPolicyModule
+import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor
+import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractorImpl
 import com.android.systemui.shade.domain.interactor.ShadeDisplaysInteractor
+import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractor
 import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
 import com.android.systemui.statusbar.phone.ConfigurationControllerImpl
 import com.android.systemui.statusbar.phone.ConfigurationForwarder
@@ -276,6 +277,8 @@
 @Module
 internal interface OptionalShadeDisplayAwareBindings {
     @BindsOptionalOf fun bindOptionalOfWindowRootView(): WindowRootView
+
+    @BindsOptionalOf fun bindOptionalOShadeExpandedStateInteractor(): ShadeExpandedStateInteractor
 }
 
 /**
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt
index 4d35d0e..e358dce 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt
@@ -24,7 +24,6 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.scene.ui.view.WindowRootView
-import com.android.systemui.shade.ShadeDisplayChangeLatencyTracker.Companion.TIMEOUT
 import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
 import com.android.systemui.util.kotlin.getOrNull
 import java.util.Optional
@@ -33,7 +32,6 @@
 import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Job
-import kotlinx.coroutines.cancel
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.filter
@@ -135,7 +133,7 @@
 
     private companion object {
         const val TAG = "ShadeDisplayLatency"
-        val t = TrackTracer(trackName = TAG)
+        val t = TrackTracer(trackName = TAG, trackGroup = "shade")
         val TIMEOUT = 3.seconds
         const val SHADE_MOVE_ACTION = LatencyTracker.ACTION_SHADE_WINDOW_DISPLAY_CHANGE
     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
index 359ddd8..5fab889 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
@@ -18,13 +18,16 @@
 
 import android.annotation.IntDef
 import android.os.Trace
+import android.os.Trace.TRACE_TAG_APP as TRACE_TAG
 import android.util.Log
 import androidx.annotation.FloatRange
+import com.android.app.tracing.TraceStateLogger
+import com.android.app.tracing.TrackGroupUtils.trackGroup
+import com.android.app.tracing.coroutines.TrackTracer
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.util.Compile
 import java.util.concurrent.CopyOnWriteArrayList
 import javax.inject.Inject
-import android.os.Trace.TRACE_TAG_APP as TRACE_TAG
 
 /**
  * A class responsible for managing the notification panel's current state.
@@ -38,6 +41,8 @@
     private val expansionListeners = CopyOnWriteArrayList<ShadeExpansionListener>()
     private val stateListeners = CopyOnWriteArrayList<ShadeStateListener>()
 
+    private val stateLogger = TraceStateLogger(trackGroup("shade", TRACK_NAME))
+
     @PanelState private var state: Int = STATE_CLOSED
     @FloatRange(from = 0.0, to = 1.0) private var fraction: Float = 0f
     private var expanded: Boolean = false
@@ -75,7 +80,7 @@
     fun onPanelExpansionChanged(
         @FloatRange(from = 0.0, to = 1.0) fraction: Float,
         expanded: Boolean,
-        tracking: Boolean
+        tracking: Boolean,
     ) {
         require(!fraction.isNaN()) { "fraction cannot be NaN" }
         val oldState = state
@@ -113,11 +118,8 @@
         )
 
         if (Trace.isTagEnabled(TRACE_TAG)) {
-            Trace.traceCounter(TRACE_TAG, "panel_expansion", (fraction * 100).toInt())
-            if (state != oldState) {
-                Trace.asyncTraceForTrackEnd(TRACE_TAG, TRACK_NAME, 0)
-                Trace.asyncTraceForTrackBegin(TRACE_TAG, TRACK_NAME, state.panelStateToString(), 0)
-            }
+            TrackTracer.instantForGroup("shade", "panel_expansion", fraction)
+            stateLogger.log(state.panelStateToString())
         }
 
         val expansionChangeEvent = ShadeExpansionChangeEvent(fraction, expanded, tracking)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
index 2348a11..b9df9f86 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
@@ -35,6 +35,8 @@
 import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractorSceneContainerImpl
 import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractor
 import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractorImpl
+import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractor
+import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractorImpl
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl
 import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl
@@ -176,4 +178,10 @@
     @Binds
     @SysUISingleton
     abstract fun bindShadeModeInteractor(impl: ShadeModeInteractorImpl): ShadeModeInteractor
+
+    @Binds
+    @SysUISingleton
+    abstract fun bindShadeExpandedStateInteractor(
+        impl: ShadeExpandedStateInteractorImpl
+    ): ShadeExpandedStateInteractor
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt
new file mode 100644
index 0000000..2705cda
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade
+
+import com.android.app.tracing.TraceStateLogger
+import com.android.app.tracing.TrackGroupUtils.trackGroup
+import com.android.app.tracing.coroutines.TrackTracer.Companion.instantForGroup
+import com.android.app.tracing.coroutines.launchTraced
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class ShadeStateTraceLogger
+@Inject
+constructor(
+    private val shadeInteractor: ShadeInteractor,
+    private val shadeDisplaysRepository: ShadeDisplaysRepository,
+    @Application private val scope: CoroutineScope,
+) : CoreStartable {
+    override fun start() {
+        scope.launchTraced("ShadeStateTraceLogger") {
+            launch {
+                val stateLogger = createTraceStateLogger("isShadeLayoutWide")
+                shadeInteractor.isShadeLayoutWide.collect { stateLogger.log(it.toString()) }
+            }
+            launch {
+                val stateLogger = createTraceStateLogger("shadeMode")
+                shadeInteractor.shadeMode.collect { stateLogger.log(it.toString()) }
+            }
+            launch {
+                shadeInteractor.shadeExpansion.collect {
+                    instantForGroup(TRACK_GROUP_NAME, "shadeExpansion", it)
+                }
+            }
+            launch {
+                shadeDisplaysRepository.displayId.collect {
+                    instantForGroup(TRACK_GROUP_NAME, "displayId", it)
+                }
+            }
+        }
+    }
+
+    private fun createTraceStateLogger(trackName: String): TraceStateLogger {
+        return TraceStateLogger(trackGroup(TRACK_GROUP_NAME, trackName))
+    }
+
+    private companion object {
+        const val TRACK_GROUP_NAME = "shade"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt
index a36c56e..9a9fc46 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt
@@ -27,7 +27,7 @@
  * them across various threads' logs.
  */
 object ShadeTraceLogger {
-    private val t = TrackTracer(trackName = "ShadeTraceLogger")
+    val t = TrackTracer(trackName = "ShadeTraceLogger", trackGroup = "shade")
 
     @JvmStatic
     fun logOnMovedToDisplay(displayId: Int, config: Configuration) {
@@ -44,8 +44,11 @@
         t.instant { "moveShadeWindowTo(displayId=$displayId)" }
     }
 
-    @JvmStatic
-    fun traceReparenting(r: () -> Unit) {
+    suspend fun traceReparenting(r: suspend () -> Unit) {
         t.traceAsync({ "reparenting" }) { r() }
     }
+
+    inline fun traceWaitForExpansion(expansion: Float, r: () -> Unit) {
+        t.traceAsync({ "waiting for shade expansion to match $expansion" }) { r() }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
index e191120..3449e81 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
@@ -41,6 +41,7 @@
 import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
 import com.android.systemui.scene.ui.composable.Overlay
 import com.android.systemui.scene.ui.composable.Scene
+import com.android.systemui.scene.ui.view.SceneJankMonitor
 import com.android.systemui.scene.ui.view.SceneWindowRootView
 import com.android.systemui.scene.ui.view.WindowRootView
 import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
@@ -89,6 +90,7 @@
             layoutInsetController: NotificationInsetsController,
             sceneDataSourceDelegator: Provider<SceneDataSourceDelegator>,
             qsSceneAdapter: Provider<QSSceneAdapter>,
+            sceneJankMonitorFactory: SceneJankMonitor.Factory,
         ): WindowRootView {
             return if (SceneContainerFlag.isEnabled) {
                 checkNoSceneDuplicates(scenesProvider.get())
@@ -104,6 +106,7 @@
                     layoutInsetController = layoutInsetController,
                     sceneDataSourceDelegator = sceneDataSourceDelegator.get(),
                     qsSceneAdapter = qsSceneAdapter,
+                    sceneJankMonitorFactory = sceneJankMonitorFactory,
                 )
                 sceneWindowRootView
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt
index c4de78b..570a785 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt
@@ -40,4 +40,9 @@
     @IntoMap
     @ClassKey(ShadeStartable::class)
     abstract fun provideShadeStartable(startable: ShadeStartable): CoreStartable
+
+    @Binds
+    @IntoMap
+    @ClassKey(ShadeStateTraceLogger::class)
+    abstract fun provideShadeStateTraceLogger(startable: ShadeStateTraceLogger): CoreStartable
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
index ef62d2d..a2edd3ab 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
@@ -15,11 +15,18 @@
  */
 package com.android.systemui.shade.data.repository
 
+import android.annotation.SuppressLint
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
 
 /** Data for the shade, mostly related to expansion of the shade and quick settings. */
 interface ShadeRepository {
@@ -36,7 +43,7 @@
      * Information about the currently running fling animation, or null if no fling animation is
      * running.
      */
-    val currentFling: StateFlow<FlingInfo?>
+    val currentFling: SharedFlow<FlingInfo?>
 
     /**
      * The amount the lockscreen shade has dragged down by the user, [0-1]. 0 means fully collapsed,
@@ -180,7 +187,8 @@
 
 /** Business logic for shade interactions */
 @SysUISingleton
-class ShadeRepositoryImpl @Inject constructor() : ShadeRepository {
+class ShadeRepositoryImpl @Inject constructor(@Background val backgroundScope: CoroutineScope) :
+    ShadeRepository {
     private val _qsExpansion = MutableStateFlow(0f)
     @Deprecated("Use ShadeInteractor.qsExpansion instead")
     override val qsExpansion: StateFlow<Float> = _qsExpansion.asStateFlow()
@@ -193,8 +201,13 @@
     override val udfpsTransitionToFullShadeProgress: StateFlow<Float> =
         _udfpsTransitionToFullShadeProgress.asStateFlow()
 
-    private val _currentFling: MutableStateFlow<FlingInfo?> = MutableStateFlow(null)
-    override val currentFling: StateFlow<FlingInfo?> = _currentFling.asStateFlow()
+    /**
+     * Must be a SharedFlow, since the fling is by definition an event and dropping it has extreme
+     * consequences in some cases (for example, keyguard uses this to decide when to unlock).
+     */
+    @SuppressLint("SharedFlowCreation")
+    override val currentFling: MutableSharedFlow<FlingInfo?> =
+        MutableSharedFlow(replay = 2, onBufferOverflow = BufferOverflow.DROP_OLDEST)
 
     private val _legacyShadeExpansion = MutableStateFlow(0f)
     @Deprecated("Use ShadeInteractor.shadeExpansion instead")
@@ -294,7 +307,7 @@
     }
 
     override fun setCurrentFling(info: FlingInfo?) {
-        _currentFling.value = info
+        backgroundScope.launch { currentFling.emit(info) }
     }
 
     @Deprecated("Should only be called by NPVC and tests")
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/FakeShadeExpandedStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/FakeShadeExpandedStateInteractor.kt
new file mode 100644
index 0000000..eab0016
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/FakeShadeExpandedStateInteractor.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.domain.interactor
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+/** Fake [ShadeExpandedStateInteractor] for tests. */
+class FakeShadeExpandedStateInteractor : ShadeExpandedStateInteractor {
+
+    private val mutableExpandedElement =
+        MutableStateFlow<ShadeExpandedStateInteractor.ShadeElement?>(null)
+    override val currentlyExpandedElement: StateFlow<ShadeExpandedStateInteractor.ShadeElement?>
+        get() = mutableExpandedElement
+
+    fun setState(state: ShadeExpandedStateInteractor.ShadeElement?) {
+        mutableExpandedElement.value = state
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt
index be561b1..691a383 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
 import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
 import com.android.window.flags.Flags
+import java.util.Optional
 import javax.inject.Inject
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
@@ -47,8 +48,19 @@
     @Background private val bgScope: CoroutineScope,
     @Main private val mainThreadContext: CoroutineContext,
     private val shadeDisplayChangeLatencyTracker: ShadeDisplayChangeLatencyTracker,
+    shadeExpandedInteractor: Optional<ShadeExpandedStateInteractor>,
 ) : CoreStartable {
 
+    private val shadeExpandedInteractor =
+        shadeExpandedInteractor.orElse(null)
+            ?: error(
+                """
+            ShadeExpandedStateInteractor must be provided for ShadeDisplaysInteractor to work.
+            If it is not, it means this is being instantiated in a SystemUI variant that shouldn't.
+            """
+                    .trimIndent()
+            )
+
     override fun start() {
         ShadeWindowGoesAround.isUnexpectedlyInLegacyMode()
         bgScope.launchTraced(TAG) {
@@ -78,9 +90,12 @@
             withContext(mainThreadContext) {
                 traceReparenting {
                     shadeDisplayChangeLatencyTracker.onShadeDisplayChanging(destinationId)
+                    val expandedElement = shadeExpandedInteractor.currentlyExpandedElement.value
+                    expandedElement?.collapse(reason = "Shade window move")
                     reparentToDisplayId(id = destinationId)
+                    expandedElement?.expand(reason = "Shade window move")
+                    checkContextDisplayMatchesExpected(destinationId)
                 }
-                checkContextDisplayMatchesExpected(destinationId)
             }
         } catch (e: IllegalStateException) {
             Log.e(
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractor.kt
new file mode 100644
index 0000000..dd3abee
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractor.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.shade.ShadeTraceLogger.traceWaitForExpansion
+import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractor.ShadeElement
+import com.android.systemui.shade.shared.flag.DualShade
+import com.android.systemui.util.kotlin.Utils.Companion.combineState
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeout
+
+/**
+ * Wrapper around [ShadeInteractor] to facilitate expansion and collapse of Notifications and quick
+ * settings.
+ *
+ * Specifially created to simplify [ShadeDisplaysInteractor] logic.
+ *
+ * NOTE: with [SceneContainerFlag] or [DualShade] disabled, [currentlyExpandedElement] will always
+ * return null!
+ */
+interface ShadeExpandedStateInteractor {
+    /** Returns the expanded [ShadeElement]. If none is, returns null. */
+    val currentlyExpandedElement: StateFlow<ShadeElement?>
+
+    /** An element from the shade window that can be expanded or collapsed. */
+    abstract class ShadeElement {
+        /** Expands the shade element, returning when the expansion is done */
+        abstract suspend fun expand(reason: String)
+
+        /** Collapses the shade element, returning when the collapse is done. */
+        abstract suspend fun collapse(reason: String)
+    }
+}
+
+@SysUISingleton
+class ShadeExpandedStateInteractorImpl
+@Inject
+constructor(
+    private val shadeInteractor: ShadeInteractor,
+    @Background private val bgScope: CoroutineScope,
+) : ShadeExpandedStateInteractor {
+
+    private val notificationElement = NotificationElement()
+    private val qsElement = QSElement()
+
+    override val currentlyExpandedElement: StateFlow<ShadeElement?> =
+        if (SceneContainerFlag.isEnabled) {
+            combineState(
+                shadeInteractor.isShadeAnyExpanded,
+                shadeInteractor.isQsExpanded,
+                bgScope,
+                SharingStarted.Eagerly,
+            ) { isShadeAnyExpanded, isQsExpanded ->
+                when {
+                    isShadeAnyExpanded -> notificationElement
+                    isQsExpanded -> qsElement
+                    else -> null
+                }
+            }
+        } else {
+            MutableStateFlow(null)
+        }
+
+    inner class NotificationElement : ShadeElement() {
+        override suspend fun expand(reason: String) {
+            shadeInteractor.expandNotificationsShade(reason)
+            shadeInteractor.shadeExpansion.waitUntil(1f)
+        }
+
+        override suspend fun collapse(reason: String) {
+            shadeInteractor.collapseNotificationsShade(reason)
+            shadeInteractor.shadeExpansion.waitUntil(0f)
+        }
+    }
+
+    inner class QSElement : ShadeElement() {
+        override suspend fun expand(reason: String) {
+            shadeInteractor.expandQuickSettingsShade(reason)
+            shadeInteractor.qsExpansion.waitUntil(1f)
+        }
+
+        override suspend fun collapse(reason: String) {
+            shadeInteractor.collapseQuickSettingsShade(reason)
+            shadeInteractor.qsExpansion.waitUntil(0f)
+        }
+    }
+
+    private suspend fun StateFlow<Float>.waitUntil(f: Float) {
+        // it's important to not do this in the main thread otherwise it will block any rendering.
+        withContext(bgScope.coroutineContext) {
+            withTimeout(1.seconds) { traceWaitForExpansion(expansion = f) { first { it == f } } }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
index 37989f5..2885ce8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
@@ -11,13 +11,13 @@
 import android.graphics.PorterDuffXfermode
 import android.graphics.RadialGradient
 import android.graphics.Shader
-import android.os.Trace
 import android.util.AttributeSet
 import android.util.MathUtils.lerp
 import android.view.MotionEvent
 import android.view.View
 import android.view.animation.PathInterpolator
 import com.android.app.animation.Interpolators
+import com.android.app.tracing.coroutines.TrackTracer
 import com.android.keyguard.logging.ScrimLogger
 import com.android.systemui.shade.TouchLogger
 import com.android.systemui.statusbar.LightRevealEffect.Companion.getPercentPastThreshold
@@ -321,9 +321,8 @@
                 }
                 revealEffect.setRevealAmountOnScrim(value, this)
                 updateScrimOpaque()
-                Trace.traceCounter(
-                    Trace.TRACE_TAG_APP,
-                    "light_reveal_amount $logString",
+                TrackTracer.instantForGroup(
+                    "scrim", { "light_reveal_amount $logString" },
                     (field * 100).toInt()
                 )
                 invalidate()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index 239257d..10b726b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -61,11 +61,13 @@
 import com.android.systemui.Flags;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Application;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlagsClassic;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
 import com.android.systemui.recents.OverviewProxyService;
@@ -78,6 +80,7 @@
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.ListenerSet;
+import com.android.systemui.util.kotlin.JavaAdapterKt;
 import com.android.systemui.util.settings.SecureSettings;
 
 import dagger.Lazy;
@@ -88,9 +91,14 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
 
 import javax.inject.Inject;
 
+import kotlinx.coroutines.CoroutineScope;
+
 /**
  * Handles keeping track of the current user, profiles, and various things related to hiding
  * contents, redacting notifications, and the lockscreen.
@@ -111,6 +119,9 @@
     private static final Uri SHOW_PRIVATE_LOCKSCREEN =
             Settings.Secure.getUriFor(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
 
+    private static final long LOCK_TIME_FOR_SENSITIVE_REDACTION_MS =
+            TimeUnit.MINUTES.toMillis(10);
+
     private final Lazy<NotificationVisibilityProvider> mVisibilityProviderLazy;
     private final Lazy<CommonNotifCollection> mCommonNotifCollectionLazy;
     private final DevicePolicyManager mDevicePolicyManager;
@@ -284,7 +295,15 @@
     protected final SparseArray<UserInfo> mCurrentProfiles = new SparseArray<>();
     protected final SparseArray<UserInfo> mCurrentManagedProfiles = new SparseArray<>();
 
+    // The last lock time. Uses currentTimeMillis
+    @VisibleForTesting
+    protected final AtomicLong mLastLockTime = new AtomicLong(-1);
+    // Whether or not the device is locked
+    @VisibleForTesting
+    protected final AtomicBoolean mLocked = new AtomicBoolean(true);
+
     protected int mCurrentUserId = 0;
+
     protected NotificationPresenter mPresenter;
     protected ContentObserver mLockscreenSettingsObserver;
     protected ContentObserver mSettingsObserver;
@@ -311,7 +330,10 @@
             DumpManager dumpManager,
             LockPatternUtils lockPatternUtils,
             FeatureFlagsClassic featureFlags,
-            Lazy<DeviceUnlockedInteractor> deviceUnlockedInteractorLazy) {
+            Lazy<DeviceUnlockedInteractor> deviceUnlockedInteractorLazy,
+            Lazy<KeyguardInteractor> keyguardInteractor,
+            @Application CoroutineScope coroutineScope
+    ) {
         mContext = context;
         mMainExecutor = mainExecutor;
         mBackgroundExecutor = backgroundExecutor;
@@ -341,6 +363,19 @@
         if (keyguardPrivateNotifications()) {
             init();
         }
+
+        // To avoid dependency injection cycle, finish constructing this object before using the
+        // KeyguardInteractor. The CoroutineScope will only be null in tests.
+        if (LockscreenOtpRedaction.isEnabled() && coroutineScope != null) {
+            mMainExecutor.execute(() -> JavaAdapterKt.collectFlow(coroutineScope,
+                    keyguardInteractor.get().isKeyguardDismissible(),
+                    unlocked -> {
+                        if (!unlocked) {
+                            mLastLockTime.set(System.currentTimeMillis());
+                        }
+                        mLocked.set(!unlocked);
+                    }));
+        }
     }
 
     public void setUpWithPresenter(NotificationPresenter presenter) {
@@ -443,7 +478,7 @@
         mCurrentUserId = mUserTracker.getUserId(); // in case we reg'd receiver too late
         updateCurrentProfilesCache();
 
-        // Set  up
+        // Set up
         mBackgroundExecutor.execute(() -> {
             @SuppressLint("MissingPermission") List<UserInfo> users = mUserManager.getUsers();
             for (int i = users.size() - 1; i >= 0; i--) {
@@ -667,8 +702,6 @@
                 !userAllowsPrivateNotificationsInPublic(mCurrentUserId);
         boolean isNotifForManagedProfile = mCurrentManagedProfiles.contains(userId);
         boolean isNotifUserRedacted = !userAllowsPrivateNotificationsInPublic(userId);
-        boolean isNotifSensitive = LockscreenOtpRedaction.isEnabled()
-                && ent.getRanking() != null && ent.getRanking().hasSensitiveContent();
 
         // redact notifications if the current user is redacting notifications or the notification
         // contains sensitive content. However if the notification is associated with a managed
@@ -689,12 +722,45 @@
         if (keyguardPrivateNotifications() && !mKeyguardAllowingNotifications) {
             return REDACTION_TYPE_PUBLIC;
         }
-        if (isNotifSensitive) {
+
+        if (shouldShowSensitiveContentRedactedView(ent)) {
             return REDACTION_TYPE_SENSITIVE_CONTENT;
         }
         return REDACTION_TYPE_NONE;
     }
 
+    /*
+     * We show the sensitive content redaction view if
+     * 1. The feature is enabled
+     * 2. The device is locked
+     * 3. The notification has the `hasSensitiveContent` ranking variable set to true
+     * 4. The device has been locked for at least LOCK_TIME_FOR_SENSITIVE_REDACTION_MS
+     * 5. The notification arrived since the last lock time
+     */
+    private boolean shouldShowSensitiveContentRedactedView(NotificationEntry ent) {
+        if (!LockscreenOtpRedaction.isEnabled()) {
+            return false;
+        }
+
+        if (!mLocked.get()) {
+            return false;
+        }
+
+        if (ent.getRanking() == null || !ent.getRanking().hasSensitiveContent()) {
+            return false;
+        }
+
+        long lastLockedTime = mLastLockTime.get();
+        if (ent.getSbn().getPostTime() < lastLockedTime) {
+            return false;
+        }
+
+        if ((System.currentTimeMillis() - lastLockedTime) < LOCK_TIME_FOR_SENSITIVE_REDACTION_MS) {
+            return false;
+        }
+        return true;
+    }
+
     private boolean packageHasVisibilityOverride(String key) {
         if (mCommonNotifCollectionLazy.get() == null) {
             Log.wtf(TAG, "mEntryManager was null!", new Throwable());
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
index e83cded..38f7c39 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
@@ -22,7 +22,6 @@
 import android.content.Context
 import android.content.res.Configuration
 import android.os.SystemClock
-import android.os.Trace
 import android.util.IndentingPrintWriter
 import android.util.Log
 import android.util.MathUtils
@@ -33,7 +32,9 @@
 import androidx.dynamicanimation.animation.SpringAnimation
 import androidx.dynamicanimation.animation.SpringForce
 import com.android.app.animation.Interpolators
+import com.android.app.tracing.coroutines.TrackTracer
 import com.android.systemui.Dumpable
+import com.android.systemui.Flags.spatialModelAppPushback
 import com.android.systemui.animation.ShadeInterpolation
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -52,7 +53,9 @@
 import com.android.systemui.util.WallpaperController
 import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor
 import com.android.systemui.window.flag.WindowBlurFlag
+import com.android.wm.shell.appzoomout.AppZoomOut
 import java.io.PrintWriter
+import java.util.Optional
 import javax.inject.Inject
 import kotlin.math.max
 import kotlin.math.sign
@@ -79,6 +82,7 @@
     private val splitShadeStateController: SplitShadeStateController,
     private val windowRootViewBlurInteractor: WindowRootViewBlurInteractor,
     @Application private val applicationScope: CoroutineScope,
+    private val appZoomOutOptional: Optional<AppZoomOut>,
     dumpManager: DumpManager,
     configurationController: ConfigurationController,
 ) : ShadeExpansionListener, Dumpable {
@@ -263,7 +267,7 @@
             updateScheduled = false
             val (blur, zoomOutFromShadeRadius) = computeBlurAndZoomOut()
             val opaque = shouldBlurBeOpaque
-            Trace.traceCounter(Trace.TRACE_TAG_APP, "shade_blur_radius", blur)
+            TrackTracer.instantForGroup("shade", "shade_blur_radius", blur)
             blurUtils.applyBlur(root.viewRootImpl, blur, opaque)
             onBlurApplied(blur, zoomOutFromShadeRadius)
         }
@@ -271,6 +275,13 @@
     private fun onBlurApplied(appliedBlurRadius: Int, zoomOutFromShadeRadius: Float) {
         lastAppliedBlur = appliedBlurRadius
         wallpaperController.setNotificationShadeZoom(zoomOutFromShadeRadius)
+        if (spatialModelAppPushback()) {
+            appZoomOutOptional.ifPresent { appZoomOut ->
+                appZoomOut.setProgress(
+                    zoomOutFromShadeRadius
+                )
+            }
+        }
         listeners.forEach {
             it.onWallpaperZoomOutChanged(zoomOutFromShadeRadius)
             it.onBlurRadiusChanged(appliedBlurRadius)
@@ -384,7 +395,7 @@
             windowRootViewBlurInteractor.onBlurAppliedEvent.collect { appliedBlurRadius ->
                 if (updateScheduled) {
                     // Process the blur applied event only if we scheduled the update
-                    Trace.traceCounter(Trace.TRACE_TAG_APP, "shade_blur_radius", appliedBlurRadius)
+                    TrackTracer.instantForGroup("shade", "shade_blur_radius", appliedBlurRadius)
                     updateScheduled = false
                     onBlurApplied(appliedBlurRadius, zoomOutCalculatedFromShadeRadius)
                 } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 48cf7a83..155049f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -23,6 +23,7 @@
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Rect;
+import android.os.Bundle;
 import android.util.AttributeSet;
 import android.util.IndentingPrintWriter;
 import android.util.MathUtils;
@@ -30,6 +31,7 @@
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
 import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 import android.view.animation.Interpolator;
 import android.view.animation.PathInterpolator;
 
@@ -1014,12 +1016,24 @@
     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
         super.onInitializeAccessibilityNodeInfo(info);
         if (mInteractive) {
+            // Add two accessibility actions that both performs expanding the notification shade
             info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
-            AccessibilityNodeInfo.AccessibilityAction unlock
-                    = new AccessibilityNodeInfo.AccessibilityAction(
+
+            AccessibilityAction seeAll = new AccessibilityAction(
                     AccessibilityNodeInfo.ACTION_CLICK,
-                    getContext().getString(R.string.accessibility_overflow_action));
-            info.addAction(unlock);
+                    getContext().getString(R.string.accessibility_overflow_action)
+            );
+            info.addAction(seeAll);
+        }
+    }
+
+    @Override
+    public boolean performAccessibilityAction(int action, Bundle args) {
+        // override ACTION_EXPAND with ACTION_CLICK
+        if (action == AccessibilityNodeInfo.ACTION_EXPAND) {
+            return super.performAccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, args);
+        } else {
+            return super.performAccessibilityAction(action, args);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index a7ad462..ead8f6a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -37,6 +37,7 @@
 import androidx.annotation.NonNull;
 
 import com.android.app.animation.Interpolators;
+import com.android.app.tracing.coroutines.TrackTracer;
 import com.android.compose.animation.scene.OverlayKey;
 import com.android.compose.animation.scene.SceneKey;
 import com.android.internal.annotations.GuardedBy;
@@ -671,7 +672,7 @@
     }
 
     private void recordHistoricalState(int newState, int lastState, boolean upcoming) {
-        Trace.traceCounter(Trace.TRACE_TAG_APP, "statusBarState", newState);
+        TrackTracer.instantForGroup("statusBar", "state", newState);
         mHistoryIndex = (mHistoryIndex + 1) % HISTORY_SIZE;
         HistoricalState state = mHistoricalRecords[mHistoryIndex];
         state.mNewState = newState;
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 de08e38..86954d5 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
@@ -18,7 +18,6 @@
 
 import android.view.View
 import com.android.internal.jank.InteractionJankMonitor
-import com.android.systemui.Flags
 import com.android.systemui.animation.ActivityTransitionAnimator
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
@@ -64,18 +63,12 @@
                     is OngoingCallModel.InCallWithVisibleApp -> OngoingActivityChipModel.Hidden()
                     is OngoingCallModel.InCall -> {
                         val icon =
-                            if (
-                                Flags.statusBarCallChipNotificationIcon() &&
-                                    state.notificationIconView != null
-                            ) {
+                            if (state.notificationIconView != null) {
                                 StatusBarConnectedDisplays.assertInLegacyMode()
                                 OngoingActivityChipModel.ChipIcon.StatusBarView(
                                     state.notificationIconView
                                 )
-                            } else if (
-                                StatusBarConnectedDisplays.isEnabled &&
-                                    Flags.statusBarCallChipNotificationIcon()
-                            ) {
+                            } else if (StatusBarConnectedDisplays.isEnabled) {
                                 OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon(
                                     state.notificationKey
                                 )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
index 2f6431b..ec3a5b2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
+import com.android.systemui.statusbar.notification.domain.model.TopPinnedState
 import com.android.systemui.statusbar.notification.headsup.PinnedStatus
 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
 import javax.inject.Inject
@@ -60,7 +61,7 @@
 
     /** Converts the notification to the [OngoingActivityChipModel] object. */
     private fun NotificationChipModel.toActivityChipModel(
-        headsUpState: PinnedStatus
+        headsUpState: TopPinnedState
     ): OngoingActivityChipModel.Shown {
         StatusBarNotifChips.assertInNewMode()
         val icon =
@@ -87,8 +88,12 @@
                 }
             }
 
-        if (headsUpState == PinnedStatus.PinnedByUser) {
-            // If the user tapped the chip to show the HUN, we want to just show the icon because
+        val isShowingHeadsUpFromChipTap =
+            headsUpState is TopPinnedState.Pinned &&
+                headsUpState.status == PinnedStatus.PinnedByUser &&
+                headsUpState.key == this.key
+        if (isShowingHeadsUpFromChipTap) {
+            // If the user tapped this chip to show the HUN, we want to just show the icon because
             // the HUN will show the rest of the information.
             return OngoingActivityChipModel.Shown.IconOnly(icon, colors, onClickListener)
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
index 059e69a..b0fa9d8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
@@ -25,6 +25,7 @@
 import android.widget.FrameLayout
 import android.widget.ImageView
 import android.widget.TextView
+import androidx.annotation.UiThread
 import com.android.systemui.common.ui.binder.IconViewBinder
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.StatusBarIconView
@@ -38,24 +39,24 @@
 /** Binder for ongoing activity chip views. */
 object OngoingActivityChipBinder {
     /** Binds the given [chipModel] data to the given [chipView]. */
-    fun bind(chipModel: OngoingActivityChipModel, chipView: View, iconViewStore: IconViewStore?) {
-        val chipContext = chipView.context
-        val chipDefaultIconView: ImageView =
-            chipView.requireViewById(R.id.ongoing_activity_chip_icon)
-        val chipTimeView: ChipChronometer =
-            chipView.requireViewById(R.id.ongoing_activity_chip_time)
-        val chipTextView: TextView = chipView.requireViewById(R.id.ongoing_activity_chip_text)
-        val chipShortTimeDeltaView: DateTimeView =
-            chipView.requireViewById(R.id.ongoing_activity_chip_short_time_delta)
-        val chipBackgroundView: ChipBackgroundContainer =
-            chipView.requireViewById(R.id.ongoing_activity_chip_background)
+    fun bind(
+        chipModel: OngoingActivityChipModel,
+        viewBinding: OngoingActivityChipViewBinding,
+        iconViewStore: IconViewStore?,
+    ) {
+        val chipContext = viewBinding.rootView.context
+        val chipDefaultIconView = viewBinding.defaultIconView
+        val chipTimeView = viewBinding.timeView
+        val chipTextView = viewBinding.textView
+        val chipShortTimeDeltaView = viewBinding.shortTimeDeltaView
+        val chipBackgroundView = viewBinding.backgroundView
 
         when (chipModel) {
             is OngoingActivityChipModel.Shown -> {
                 // Data
                 setChipIcon(chipModel, chipBackgroundView, chipDefaultIconView, iconViewStore)
                 setChipMainContent(chipModel, chipTextView, chipTimeView, chipShortTimeDeltaView)
-                chipView.setOnClickListener(chipModel.onClickListener)
+                viewBinding.rootView.setOnClickListener(chipModel.onClickListener)
                 updateChipPadding(
                     chipModel,
                     chipBackgroundView,
@@ -65,7 +66,7 @@
                 )
 
                 // Accessibility
-                setChipAccessibility(chipModel, chipView, chipBackgroundView)
+                setChipAccessibility(chipModel, viewBinding.rootView, chipBackgroundView)
 
                 // Colors
                 val textColor = chipModel.colors.text(chipContext)
@@ -83,6 +84,85 @@
         }
     }
 
+    /** Stores [rootView] and relevant child views in an object for easy reference. */
+    fun createBinding(rootView: View): OngoingActivityChipViewBinding {
+        return OngoingActivityChipViewBinding(
+            rootView = rootView,
+            timeView = rootView.requireViewById(R.id.ongoing_activity_chip_time),
+            textView = rootView.requireViewById(R.id.ongoing_activity_chip_text),
+            shortTimeDeltaView =
+                rootView.requireViewById(R.id.ongoing_activity_chip_short_time_delta),
+            defaultIconView = rootView.requireViewById(R.id.ongoing_activity_chip_icon),
+            backgroundView = rootView.requireViewById(R.id.ongoing_activity_chip_background),
+        )
+    }
+
+    /**
+     * Resets any width restrictions that were placed on the primary chip's contents.
+     *
+     * Should be used when the user's screen bounds changed because there may now be more room in
+     * the status bar to show additional content.
+     */
+    fun resetPrimaryChipWidthRestrictions(
+        primaryChipViewBinding: OngoingActivityChipViewBinding,
+        currentPrimaryChipViewModel: OngoingActivityChipModel,
+    ) {
+        if (currentPrimaryChipViewModel is OngoingActivityChipModel.Hidden) {
+            return
+        }
+        resetChipMainContentWidthRestrictions(
+            primaryChipViewBinding,
+            currentPrimaryChipViewModel as OngoingActivityChipModel.Shown,
+        )
+    }
+
+    /**
+     * Resets any width restrictions that were placed on the secondary chip and its contents.
+     *
+     * Should be used when the user's screen bounds changed because there may now be more room in
+     * the status bar to show additional content.
+     */
+    fun resetSecondaryChipWidthRestrictions(
+        secondaryChipViewBinding: OngoingActivityChipViewBinding,
+        currentSecondaryChipModel: OngoingActivityChipModel,
+    ) {
+        if (currentSecondaryChipModel is OngoingActivityChipModel.Hidden) {
+            return
+        }
+        secondaryChipViewBinding.rootView.resetWidthRestriction()
+        resetChipMainContentWidthRestrictions(
+            secondaryChipViewBinding,
+            currentSecondaryChipModel as OngoingActivityChipModel.Shown,
+        )
+    }
+
+    private fun resetChipMainContentWidthRestrictions(
+        viewBinding: OngoingActivityChipViewBinding,
+        model: OngoingActivityChipModel.Shown,
+    ) {
+        when (model) {
+            is OngoingActivityChipModel.Shown.Text -> viewBinding.textView.resetWidthRestriction()
+            is OngoingActivityChipModel.Shown.Timer -> viewBinding.timeView.resetWidthRestriction()
+            is OngoingActivityChipModel.Shown.ShortTimeDelta ->
+                viewBinding.shortTimeDeltaView.resetWidthRestriction()
+            is OngoingActivityChipModel.Shown.IconOnly,
+            is OngoingActivityChipModel.Shown.Countdown -> {}
+        }
+    }
+
+    /**
+     * Resets any width restrictions that were placed on the given view.
+     *
+     * Should be used when the user's screen bounds changed because there may now be more room in
+     * the status bar to show additional content.
+     */
+    @UiThread
+    fun View.resetWidthRestriction() {
+        // View needs to be visible in order to be re-measured
+        visibility = View.VISIBLE
+        forceLayout()
+    }
+
     private fun setChipIcon(
         chipModel: OngoingActivityChipModel.Shown,
         backgroundView: ChipBackgroundContainer,
@@ -242,8 +322,9 @@
         chipTimeView: ChipChronometer,
         chipShortTimeDeltaView: DateTimeView,
     ) {
-        if (chipModel.icon != null) {
-            if (chipModel.icon is OngoingActivityChipModel.ChipIcon.StatusBarView) {
+        val icon = chipModel.icon
+        if (icon != null) {
+            if (iconRequiresEmbeddedPadding(icon)) {
                 // If the icon is a custom [StatusBarIconView], then it should've come from
                 // `Notification.smallIcon`, which is required to embed its own paddings. We need to
                 // adjust the other paddings to make everything look good :)
@@ -265,6 +346,10 @@
         }
     }
 
+    private fun iconRequiresEmbeddedPadding(icon: OngoingActivityChipModel.ChipIcon) =
+        icon is OngoingActivityChipModel.ChipIcon.StatusBarView ||
+            icon is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon
+
     private fun View.setTextPaddingForEmbeddedPaddingIcon() {
         val newPaddingEnd =
             context.resources.getDimensionPixelSize(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipViewBinding.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipViewBinding.kt
new file mode 100644
index 0000000..1814b74
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipViewBinding.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2025 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.binder
+
+import android.view.View
+import android.widget.ImageView
+import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
+import com.android.systemui.statusbar.chips.ui.view.ChipChronometer
+import com.android.systemui.statusbar.chips.ui.view.ChipDateTimeView
+import com.android.systemui.statusbar.chips.ui.view.ChipTextView
+
+/** Stores bound views for a given chip. */
+data class OngoingActivityChipViewBinding(
+    val rootView: View,
+    val timeView: ChipChronometer,
+    val textView: ChipTextView,
+    val shortTimeDeltaView: ChipDateTimeView,
+    val defaultIconView: ImageView,
+    val backgroundView: ChipBackgroundContainer,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipText.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipText.kt
new file mode 100644
index 0000000..3d768d2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipText.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips.ui.compose
+
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.rememberTextMeasurer
+import com.android.systemui.res.R
+
+/**
+ * Renders text within a status bar chip. The text is only displayed if more than 50% of its width
+ * can fit inside the bounds of the chip. If there is any overflow,
+ * [R.dimen.ongoing_activity_chip_text_fading_edge_length] is used to fade out the edge of the text.
+ */
+@Composable
+fun ChipText(
+    text: String,
+    backgroundColor: Color,
+    modifier: Modifier = Modifier,
+    color: Color = Color.Unspecified,
+    style: TextStyle = LocalTextStyle.current,
+    minimumVisibleRatio: Float = 0.5f,
+) {
+    val density = LocalDensity.current
+    val textMeasurer = rememberTextMeasurer()
+
+    val textFadeLength =
+        dimensionResource(id = R.dimen.ongoing_activity_chip_text_fading_edge_length)
+    val maxTextWidthDp = dimensionResource(id = R.dimen.ongoing_activity_chip_max_text_width)
+    val maxTextWidthPx = with(density) { maxTextWidthDp.toPx() }
+
+    val textLayoutResult = remember(text, style) { textMeasurer.measure(text, style) }
+    val willOverflowWidth = textLayoutResult.size.width > maxTextWidthPx
+
+    if (isSufficientlyVisible(maxTextWidthPx, minimumVisibleRatio, textLayoutResult)) {
+        Text(
+            text = text,
+            style = style,
+            softWrap = false,
+            color = color,
+            modifier =
+                modifier
+                    .sizeIn(maxWidth = maxTextWidthDp)
+                    .then(
+                        if (willOverflowWidth) {
+                            Modifier.overflowFadeOut(
+                                with(density) { textFadeLength.roundToPx() },
+                                backgroundColor,
+                            )
+                        } else {
+                            Modifier
+                        }
+                    ),
+        )
+    }
+}
+
+private fun Modifier.overflowFadeOut(fadeLength: Int, color: Color): Modifier = drawWithContent {
+    drawContent()
+
+    val brush =
+        Brush.horizontalGradient(
+            colors = listOf(Color.Transparent, color),
+            startX = size.width - fadeLength,
+            endX = size.width,
+        )
+    drawRect(
+        brush = brush,
+        topLeft = Offset(size.width - fadeLength, 0f),
+        size = Size(fadeLength.toFloat(), size.height),
+    )
+}
+
+/**
+ * Returns `true` if at least [minimumVisibleRatio] of the text width fits within the given
+ * [maxAvailableWidthPx].
+ */
+@Composable
+private fun isSufficientlyVisible(
+    maxAvailableWidthPx: Float,
+    minimumVisibleRatio: Float,
+    textLayoutResult: TextLayoutResult,
+): Boolean {
+    val widthPx = textLayoutResult.size.width
+
+    return (maxAvailableWidthPx / widthPx) > minimumVisibleRatio
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChronometerText.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChronometerText.kt
index a747abb..1c14d33 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChronometerText.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChronometerText.kt
@@ -28,17 +28,11 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureResult
-import androidx.compose.ui.layout.MeasureScope
-import androidx.compose.ui.node.LayoutModifierNode
-import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.constrain
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.statusbar.chips.ui.compose.modifiers.neverDecreaseWidth
 import kotlinx.coroutines.delay
 
 /** Platform-optimized interface for getting current time */
@@ -97,35 +91,3 @@
         modifier = modifier.neverDecreaseWidth(),
     )
 }
-
-/** A modifier that ensures the width of the content only increases and never decreases. */
-private fun Modifier.neverDecreaseWidth(): Modifier {
-    return this.then(neverDecreaseWidthElement)
-}
-
-private data object neverDecreaseWidthElement : ModifierNodeElement<NeverDecreaseWidthNode>() {
-    override fun create(): NeverDecreaseWidthNode {
-        return NeverDecreaseWidthNode()
-    }
-
-    override fun update(node: NeverDecreaseWidthNode) {
-        error("This should never be called")
-    }
-}
-
-private class NeverDecreaseWidthNode : Modifier.Node(), LayoutModifierNode {
-    private var minWidth = 0
-
-    override fun MeasureScope.measure(
-        measurable: Measurable,
-        constraints: Constraints,
-    ): MeasureResult {
-        val placeable = measurable.measure(Constraints(minWidth = minWidth).constrain(constraints))
-        val width = placeable.width
-        val height = placeable.height
-
-        minWidth = maxOf(minWidth, width)
-
-        return layout(width, height) { placeable.place(0, 0) }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt
new file mode 100644
index 0000000..1be5842
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt
@@ -0,0 +1,208 @@
+/*
+ * 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.compose
+
+import android.content.res.ColorStateList
+import android.view.ViewGroup
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import com.android.systemui.common.ui.compose.Icon
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.chips.ui.compose.modifiers.neverDecreaseWidth
+import com.android.systemui.statusbar.chips.ui.model.ColorsModel
+import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+
+@Composable
+fun OngoingActivityChip(model: OngoingActivityChipModel.Shown, modifier: Modifier = Modifier) {
+    val context = LocalContext.current
+    val isClickable = model.onClickListener != null
+    val hasEmbeddedIcon = model.icon is OngoingActivityChipModel.ChipIcon.StatusBarView
+
+    // Use a Box with `fillMaxHeight` to create a larger click surface for the chip. The visible
+    // height of the chip is determined by the height of the background of the Row below.
+    Box(
+        contentAlignment = Alignment.Center,
+        modifier =
+            modifier
+                .fillMaxHeight()
+                .clickable(
+                    enabled = isClickable,
+                    onClick = {
+                        // TODO(b/372657935): Implement click actions.
+                    },
+                ),
+    ) {
+        Row(
+            horizontalArrangement = Arrangement.Center,
+            verticalAlignment = Alignment.CenterVertically,
+            modifier =
+                Modifier.clip(
+                        RoundedCornerShape(
+                            dimensionResource(id = R.dimen.ongoing_activity_chip_corner_radius)
+                        )
+                    )
+                    .height(dimensionResource(R.dimen.ongoing_appops_chip_height))
+                    .widthIn(
+                        min =
+                            if (isClickable) {
+                                dimensionResource(id = R.dimen.min_clickable_item_size)
+                            } else {
+                                0.dp
+                            }
+                    )
+                    .background(Color(model.colors.background(context).defaultColor))
+                    .padding(
+                        horizontal =
+                            if (hasEmbeddedIcon) {
+                                0.dp
+                            } else {
+                                dimensionResource(id = R.dimen.ongoing_activity_chip_side_padding)
+                            }
+                    ),
+        ) {
+            model.icon?.let { ChipIcon(viewModel = it, colors = model.colors) }
+
+            val isIconOnly = model is OngoingActivityChipModel.Shown.IconOnly
+            val isTextOnly = model.icon == null
+            if (!isIconOnly) {
+                ChipContent(
+                    viewModel = model,
+                    modifier =
+                        Modifier.padding(
+                            start =
+                                if (isTextOnly || hasEmbeddedIcon) {
+                                    0.dp
+                                } else {
+                                    dimensionResource(
+                                        id = R.dimen.ongoing_activity_chip_icon_text_padding
+                                    )
+                                },
+                            end =
+                                if (hasEmbeddedIcon) {
+                                    dimensionResource(
+                                        id =
+                                            R.dimen
+                                                .ongoing_activity_chip_text_end_padding_for_embedded_padding_icon
+                                    )
+                                } else {
+                                    0.dp
+                                },
+                        ),
+                )
+            }
+        }
+    }
+}
+
+@Composable
+private fun ChipIcon(
+    viewModel: OngoingActivityChipModel.ChipIcon,
+    colors: ColorsModel,
+    modifier: Modifier = Modifier,
+) {
+    val context = LocalContext.current
+
+    when (viewModel) {
+        is OngoingActivityChipModel.ChipIcon.StatusBarView -> {
+            val originalIcon = viewModel.impl
+            val iconSizePx =
+                context.resources.getDimensionPixelSize(
+                    R.dimen.ongoing_activity_chip_embedded_padding_icon_size
+                )
+            AndroidView(
+                modifier = modifier,
+                factory = { _ ->
+                    originalIcon.apply {
+                        layoutParams = ViewGroup.LayoutParams(iconSizePx, iconSizePx)
+                        imageTintList = ColorStateList.valueOf(colors.text(context))
+                    }
+                },
+            )
+        }
+
+        is OngoingActivityChipModel.ChipIcon.SingleColorIcon -> {
+            Icon(
+                icon = viewModel.impl,
+                tint = Color(colors.text(context)),
+                modifier =
+                    modifier.size(dimensionResource(id = R.dimen.ongoing_activity_chip_icon_size)),
+            )
+        }
+
+        // TODO(b/372657935): Add recommended architecture implementation for
+        // StatusBarNotificationIcons
+        is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon -> {}
+    }
+}
+
+@Composable
+private fun ChipContent(viewModel: OngoingActivityChipModel.Shown, modifier: Modifier = Modifier) {
+    val context = LocalContext.current
+    when (viewModel) {
+        is OngoingActivityChipModel.Shown.Timer -> {
+            ChronometerText(
+                startTimeMillis = viewModel.startTimeMs,
+                style = MaterialTheme.typography.labelLarge,
+                color = Color(viewModel.colors.text(context)),
+                modifier = modifier,
+            )
+        }
+
+        is OngoingActivityChipModel.Shown.Countdown -> {
+            ChipText(
+                text = viewModel.secondsUntilStarted.toString(),
+                color = Color(viewModel.colors.text(context)),
+                style = MaterialTheme.typography.labelLarge,
+                modifier = modifier.neverDecreaseWidth(),
+                backgroundColor = Color(viewModel.colors.background(context).defaultColor),
+            )
+        }
+
+        is OngoingActivityChipModel.Shown.Text -> {
+            ChipText(
+                text = viewModel.text,
+                color = Color(viewModel.colors.text(context)),
+                style = MaterialTheme.typography.labelLarge,
+                modifier = modifier,
+                backgroundColor = Color(viewModel.colors.background(context).defaultColor),
+            )
+        }
+
+        is OngoingActivityChipModel.Shown.ShortTimeDelta -> {
+            // TODO(b/372657935): Implement ShortTimeDelta content in compose.
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt
new file mode 100644
index 0000000..85ea087
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips.ui.compose
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel
+import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+
+@Composable
+fun OngoingActivityChips(chips: MultipleOngoingActivityChipsModel, modifier: Modifier = Modifier) {
+    Row(
+        // TODO(b/372657935): Remove magic numbers for padding and spacing.
+        modifier = modifier.fillMaxHeight().padding(horizontal = 6.dp),
+        verticalAlignment = Alignment.CenterVertically,
+        horizontalArrangement = Arrangement.spacedBy(8.dp),
+    ) {
+        // TODO(b/372657935): Make sure chips are only shown when there is enough horizontal
+        // space.
+        if (chips.primary is OngoingActivityChipModel.Shown) {
+            OngoingActivityChip(model = chips.primary)
+        }
+        if (chips.secondary is OngoingActivityChipModel.Shown) {
+            OngoingActivityChip(model = chips.secondary)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/modifiers/NeverDecreaseWidth.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/modifiers/NeverDecreaseWidth.kt
new file mode 100644
index 0000000..505a5fc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/modifiers/NeverDecreaseWidth.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2025 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.compose.modifiers
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.constrain
+
+/** A modifier that ensures the width of the content only increases and never decreases. */
+fun Modifier.neverDecreaseWidth(): Modifier {
+    return this.then(neverDecreaseWidthElement)
+}
+
+private data object neverDecreaseWidthElement : ModifierNodeElement<NeverDecreaseWidthNode>() {
+    override fun create(): NeverDecreaseWidthNode {
+        return NeverDecreaseWidthNode()
+    }
+
+    override fun update(node: NeverDecreaseWidthNode) {
+        error("This should never be called")
+    }
+}
+
+private class NeverDecreaseWidthNode : Modifier.Node(), LayoutModifierNode {
+    private var minWidth = 0
+
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints,
+    ): MeasureResult {
+        val placeable = measurable.measure(Constraints(minWidth = minWidth).constrain(constraints))
+        val width = placeable.width
+        val height = placeable.height
+
+        minWidth = maxOf(minWidth, width)
+
+        return layout(width, height) { placeable.place(0, 0) }
+    }
+}
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 c81e8e2..956d99e 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
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.chips.ui.model
 
 import android.view.View
-import com.android.systemui.Flags
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
@@ -130,10 +129,6 @@
          */
         data class StatusBarView(val impl: StatusBarIconView) : ChipIcon {
             init {
-                check(Flags.statusBarCallChipNotificationIcon()) {
-                    "OngoingActivityChipModel.ChipIcon.StatusBarView created even though " +
-                        "Flags.statusBarCallChipNotificationIcon is not enabled"
-                }
                 StatusBarConnectedDisplays.assertInLegacyMode()
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt
index ff3061e..7b4b79d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt
@@ -33,10 +33,8 @@
  *    that wide. This means the chip may get larger over time (e.g. in the transition from 59:59 to
  *    1:00:00), but never smaller.
  * 2) Hiding the text if the time gets too long for the space available. Once the text has been
- *    hidden, it remains hidden for the duration of the activity.
- *
- * Note that if the text was too big in portrait mode, resulting in the text being hidden, then the
- * text will also be hidden in landscape (even if there is enough space for it in landscape).
+ *    hidden, it remains hidden for the duration of the activity (or until [resetWidthRestriction]
+ *    is called).
  */
 class ChipChronometer
 @JvmOverloads
@@ -51,12 +49,23 @@
     private var shouldHideText: Boolean = false
 
     override fun setBase(base: Long) {
-        // These variables may have changed during the previous activity, so re-set them before the
-        // new activity starts.
+        resetWidthRestriction()
+        super.setBase(base)
+    }
+
+    /**
+     * Resets any width restrictions that were placed on the chronometer.
+     *
+     * Should be used when the user's screen bounds changed because there may now be more room in
+     * the status bar to show additional content.
+     */
+    @UiThread
+    fun resetWidthRestriction() {
         minimumTextWidth = 0
         shouldHideText = false
+        // View needs to be visible in order to be re-measured
         visibility = VISIBLE
-        super.setBase(base)
+        forceLayout()
     }
 
     /** Sets whether this view should hide its text or not. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index 254b792..d327fc2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -16,8 +16,6 @@
 
 package com.android.systemui.statusbar.dagger;
 
-import static com.android.systemui.Flags.predictiveBackAnimateDialogs;
-
 import android.content.Context;
 import android.os.Handler;
 import android.os.RemoteException;
@@ -28,7 +26,6 @@
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.CoreStartable;
 import com.android.systemui.animation.ActivityTransitionAnimator;
-import com.android.systemui.animation.AnimationFeatureFlags;
 import com.android.systemui.animation.DialogTransitionAnimator;
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.dagger.SysUISingleton;
@@ -226,8 +223,7 @@
             IDreamManager dreamManager,
             KeyguardStateController keyguardStateController,
             Lazy<AlternateBouncerInteractor> alternateBouncerInteractor,
-            InteractionJankMonitor interactionJankMonitor,
-            AnimationFeatureFlags animationFeatureFlags) {
+            InteractionJankMonitor interactionJankMonitor) {
         DialogTransitionAnimator.Callback callback = new DialogTransitionAnimator.Callback() {
             @Override
             public boolean isDreaming() {
@@ -249,19 +245,6 @@
                 return alternateBouncerInteractor.get().canShowAlternateBouncerForFingerprint();
             }
         };
-        return new DialogTransitionAnimator(
-                mainExecutor, callback, interactionJankMonitor, animationFeatureFlags);
-    }
-
-    /** */
-    @Provides
-    @SysUISingleton
-    static AnimationFeatureFlags provideAnimationFeatureFlags() {
-        return new AnimationFeatureFlags() {
-            @Override
-            public boolean isPredictiveBackQsDialogAnim() {
-                return predictiveBackAnimateDialogs();
-            }
-        };
+        return new DialogTransitionAnimator(mainExecutor, callback, interactionJankMonitor);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
index 46c84fbc..351cdc8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
@@ -27,18 +27,20 @@
 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
 import com.android.systemui.statusbar.data.StatusBarDataLayerModule
 import com.android.systemui.statusbar.data.repository.LightBarControllerStore
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProviderImpl
+import com.android.systemui.statusbar.layout.ui.viewmodel.StatusBarContentInsetsViewModel
 import com.android.systemui.statusbar.phone.AutoHideController
 import com.android.systemui.statusbar.phone.AutoHideControllerImpl
 import com.android.systemui.statusbar.phone.LightBarController
 import com.android.systemui.statusbar.phone.LightBarControllerImpl
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProviderImpl
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLog
 import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
 import com.android.systemui.statusbar.phone.ongoingcall.domain.interactor.OngoingCallInteractor
 import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.ui.StatusBarUiLayerModule
 import com.android.systemui.statusbar.ui.SystemBarUtilsProxyImpl
 import com.android.systemui.statusbar.window.MultiDisplayStatusBarWindowControllerStore
 import com.android.systemui.statusbar.window.SingleDisplayStatusBarWindowControllerStore
@@ -60,7 +62,14 @@
  *   ([com.android.systemui.statusbar.pipeline.dagger.StatusBarPipelineModule],
  *   [com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule], etc.).
  */
-@Module(includes = [StatusBarDataLayerModule::class, SystemBarUtilsProxyImpl.Module::class])
+@Module(
+    includes =
+        [
+            StatusBarDataLayerModule::class,
+            StatusBarUiLayerModule::class,
+            SystemBarUtilsProxyImpl.Module::class,
+        ]
+)
 interface StatusBarModule {
 
     @Binds
@@ -91,9 +100,7 @@
         @SysUISingleton
         @IntoMap
         @ClassKey(OngoingCallController::class)
-        fun ongoingCallController(
-            controller: OngoingCallController
-        ): CoreStartable =
+        fun ongoingCallController(controller: OngoingCallController): CoreStartable =
             if (StatusBarChipsModernization.isEnabled) {
                 CoreStartable.NOP
             } else {
@@ -104,9 +111,7 @@
         @SysUISingleton
         @IntoMap
         @ClassKey(OngoingCallInteractor::class)
-        fun ongoingCallInteractor(
-            interactor: OngoingCallInteractor
-        ): CoreStartable =
+        fun ongoingCallInteractor(interactor: OngoingCallInteractor): CoreStartable =
             if (StatusBarChipsModernization.isEnabled) {
                 interactor
             } else {
@@ -173,5 +178,13 @@
         ): StatusBarContentInsetsProvider {
             return factory.create(context, configurationController, sysUICutoutProvider)
         }
+
+        @Provides
+        @SysUISingleton
+        fun contentInsetsViewModel(
+            insetsProvider: StatusBarContentInsetsProvider
+        ): StatusBarContentInsetsViewModel {
+            return StatusBarContentInsetsViewModel(insetsProvider)
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/model/StatusBarAppearance.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/model/StatusBarAppearance.kt
index 0cd31d0..b7b91fa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/data/model/StatusBarAppearance.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/model/StatusBarAppearance.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.statusbar.data.model
 
 import com.android.internal.view.AppearanceRegion
-import com.android.systemui.statusbar.phone.BoundsPair
+import com.android.systemui.statusbar.layout.BoundsPair
 
 /** Keeps track of various parameters coordinating the appearance of the status bar. */
 data class StatusBarAppearance(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStore.kt
index 554c46f..5ea1211 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStore.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStore.kt
@@ -28,8 +28,8 @@
 import com.android.systemui.display.data.repository.PerDisplayStoreImpl
 import com.android.systemui.display.data.repository.SingleDisplayStore
 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProviderImpl
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProviderImpl
 import dagger.Lazy
 import dagger.Module
 import dagger.Provides
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt
index 22c37df..7fa9f0e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt
@@ -33,9 +33,9 @@
 import com.android.systemui.statusbar.core.StatusBarInitializer.OnStatusBarViewInitializedListener
 import com.android.systemui.statusbar.data.model.StatusBarAppearance
 import com.android.systemui.statusbar.data.model.StatusBarMode
-import com.android.systemui.statusbar.phone.BoundsPair
-import com.android.systemui.statusbar.phone.LetterboxAppearanceCalculator
-import com.android.systemui.statusbar.phone.StatusBarBoundsProvider
+import com.android.systemui.statusbar.layout.BoundsPair
+import com.android.systemui.statusbar.layout.LetterboxAppearanceCalculator
+import com.android.systemui.statusbar.layout.StatusBarBoundsProvider
 import com.android.systemui.statusbar.phone.fragment.dagger.HomeStatusBarComponent
 import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
 import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingCallRepository
@@ -209,9 +209,7 @@
     override val ongoingProcessRequiresStatusBarVisible =
         _ongoingProcessRequiresStatusBarVisible.asStateFlow()
 
-    override fun setOngoingProcessRequiresStatusBarVisible(
-        requiredVisible: Boolean
-    ) {
+    override fun setOngoingProcessRequiresStatusBarVisible(requiredVisible: Boolean) {
         _ongoingProcessRequiresStatusBarVisible.value = requiredVisible
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
index f7bc23c..63410d746 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
@@ -41,8 +41,8 @@
 import com.android.systemui.statusbar.events.PrivacyDotCorner.BottomRight
 import com.android.systemui.statusbar.events.PrivacyDotCorner.TopLeft
 import com.android.systemui.statusbar.events.PrivacyDotCorner.TopRight
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsChangedListener
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.leak.RotationUtils
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
index 1038ad4..70632b3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
@@ -36,8 +36,8 @@
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
 import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsChangedListener
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
 import com.android.systemui.statusbar.window.StatusBarWindowController
 import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
 import com.android.systemui.util.animation.AnimationUtil.Companion.frames
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt
index 4e68bee..e3e77e1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.media.controls.data.repository.MediaFilterRepository
 import com.android.systemui.media.controls.shared.model.MediaCommonModel
 import com.android.systemui.media.controls.shared.model.MediaData
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.featurepods.media.shared.model.MediaControlChipModel
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -71,5 +72,10 @@
 }
 
 private fun MediaData.toMediaControlChipModel(): MediaControlChipModel {
-    return MediaControlChipModel(appIcon = this.appIcon, appName = this.app, songName = this.song)
+    return MediaControlChipModel(
+        appIcon = this.appIcon,
+        appName = this.app,
+        songName = this.song,
+        playOrPause = this.semanticActions?.getActionById(R.id.actionPlayPause),
+    )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/shared/model/MediaControlChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/shared/model/MediaControlChipModel.kt
index 4035667..2e47c1e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/shared/model/MediaControlChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/shared/model/MediaControlChipModel.kt
@@ -17,10 +17,12 @@
 package com.android.systemui.statusbar.featurepods.media.shared.model
 
 import android.graphics.drawable.Icon
+import com.android.systemui.media.controls.shared.model.MediaAction
 
 /** Model used to display a media control chip in the status bar. */
 data class MediaControlChipModel(
     val appIcon: Icon?,
     val appName: String?,
     val songName: CharSequence?,
+    val playOrPause: MediaAction?,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt
index 2aea7d8..19acb2e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.statusbar.featurepods.media.domain.interactor.MediaControlChipInteractor
 import com.android.systemui.statusbar.featurepods.media.shared.model.MediaControlChipModel
+import com.android.systemui.statusbar.featurepods.popups.shared.model.HoverBehavior
 import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipId
 import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel
 import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.StatusBarPopupChipViewModel
@@ -33,6 +34,7 @@
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
 
 /**
  * [StatusBarPopupChipViewModel] for a media control chip in the status bar. This view model is
@@ -54,40 +56,51 @@
      */
     override val chip: StateFlow<PopupChipModel> =
         mediaControlChipInteractor.mediaControlModel
-            .map { mediaControlModel -> toPopupChipModel(mediaControlModel, applicationContext) }
+            .map { mediaControlModel -> toPopupChipModel(mediaControlModel) }
             .stateIn(
                 backgroundScope,
                 SharingStarted.WhileSubscribed(),
                 PopupChipModel.Hidden(PopupChipId.MediaControl),
             )
-}
 
-private fun toPopupChipModel(model: MediaControlChipModel?, context: Context): PopupChipModel {
-    if (model == null || model.songName.isNullOrEmpty()) {
-        return PopupChipModel.Hidden(PopupChipId.MediaControl)
-    }
+    private fun toPopupChipModel(model: MediaControlChipModel?): PopupChipModel {
+        if (model == null || model.songName.isNullOrEmpty()) {
+            return PopupChipModel.Hidden(PopupChipId.MediaControl)
+        }
 
-    val contentDescription = model.appName?.let { ContentDescription.Loaded(description = it) }
-    return PopupChipModel.Shown(
-        chipId = PopupChipId.MediaControl,
-        icon =
-            model.appIcon?.loadDrawable(context)?.let {
+        val contentDescription = model.appName?.let { ContentDescription.Loaded(description = it) }
+
+        val defaultIcon =
+            model.appIcon?.loadDrawable(applicationContext)?.let {
                 Icon.Loaded(drawable = it, contentDescription = contentDescription)
             }
                 ?: Icon.Resource(
                     res = com.android.internal.R.drawable.ic_audio_media,
                     contentDescription = contentDescription,
-                ),
-        hoverIcon =
-            Icon.Resource(
-                res = com.android.internal.R.drawable.ic_media_pause,
-                contentDescription = null,
-            ),
-        chipText = model.songName.toString(),
-        isToggled = false,
-        // TODO(b/385202114): Show a popup containing the media carousal when the chip is toggled.
-        onToggle = {},
-        // TODO(b/385202193): Add support for clicking on the icon on a media chip.
-        onIconPressed = {},
-    )
+                )
+        return PopupChipModel.Shown(
+            chipId = PopupChipId.MediaControl,
+            icon = defaultIcon,
+            chipText = model.songName.toString(),
+            isToggled = false,
+            // TODO(b/385202114): Show a popup containing the media carousal when the chip is
+            // toggled.
+            onToggle = {},
+            hoverBehavior = createHoverBehavior(model),
+        )
+    }
+
+    private fun createHoverBehavior(model: MediaControlChipModel): HoverBehavior {
+        val playOrPause = model.playOrPause ?: return HoverBehavior.None
+        val icon = playOrPause.icon ?: return HoverBehavior.None
+        val action = playOrPause.action ?: return HoverBehavior.None
+
+        val contentDescription =
+            ContentDescription.Loaded(description = playOrPause.contentDescription.toString())
+
+        return HoverBehavior.Button(
+            icon = Icon.Loaded(drawable = icon, contentDescription = contentDescription),
+            onIconPressed = { backgroundScope.launch { action.run() } },
+        )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt
index e7e3d02..683b9716 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt
@@ -26,6 +26,18 @@
     data object MediaControl : PopupChipId("MediaControl")
 }
 
+/** Defines the behavior of the chip when hovered over. */
+sealed interface HoverBehavior {
+    /** No specific hover behavior. The default icon will be shown. */
+    data object None : HoverBehavior
+
+    /**
+     * Shows a button on hover with the given [icon] and executes [onIconPressed] when the icon is
+     * pressed.
+     */
+    data class Button(val icon: Icon, val onIconPressed: () -> Unit) : HoverBehavior
+}
+
 /** Model for individual status bar popup chips. */
 sealed class PopupChipModel {
     abstract val logName: String
@@ -40,15 +52,10 @@
         override val chipId: PopupChipId,
         /** Default icon displayed on the chip */
         val icon: Icon,
-        /**
-         * Icon to be displayed if the chip is hovered. i.e. the mouse pointer is inside the bounds
-         * of the chip.
-         */
-        val hoverIcon: Icon,
         val chipText: String,
         val isToggled: Boolean = false,
         val onToggle: () -> Unit,
-        val onIconPressed: () -> Unit,
+        val hoverBehavior: HoverBehavior = HoverBehavior.None,
     ) : PopupChipModel() {
         override val logName = "Shown(id=$chipId, toggled=$isToggled)"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/compose/StatusBarPopupChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/compose/StatusBarPopupChip.kt
index 1a775d7..34bef9d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/compose/StatusBarPopupChip.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/compose/StatusBarPopupChip.kt
@@ -25,7 +25,6 @@
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.widthIn
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.foundation.shape.RoundedCornerShape
@@ -42,7 +41,9 @@
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
+import com.android.compose.modifiers.thenIf
 import com.android.systemui.common.ui.compose.Icon
+import com.android.systemui.statusbar.featurepods.popups.shared.model.HoverBehavior
 import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel
 
 /**
@@ -52,52 +53,59 @@
  */
 @Composable
 fun StatusBarPopupChip(model: PopupChipModel.Shown, modifier: Modifier = Modifier) {
-    val interactionSource = remember { MutableInteractionSource() }
-    val isHovered by interactionSource.collectIsHoveredAsState()
+    val hasHoverBehavior = model.hoverBehavior !is HoverBehavior.None
+    val hoverInteractionSource = remember { MutableInteractionSource() }
+    val isHovered by hoverInteractionSource.collectIsHoveredAsState()
     val isToggled = model.isToggled
 
+    val chipBackgroundColor =
+        if (isToggled) {
+            MaterialTheme.colorScheme.primaryContainer
+        } else {
+            MaterialTheme.colorScheme.surfaceContainerHighest
+        }
     Surface(
         shape = RoundedCornerShape(16.dp),
         modifier =
             modifier
-                .hoverable(interactionSource = interactionSource)
-                .padding(vertical = 4.dp)
                 .widthIn(max = 120.dp)
+                .padding(vertical = 4.dp)
                 .animateContentSize()
-                .clickable(onClick = { model.onToggle() }),
-        color =
-            if (isToggled) {
-                MaterialTheme.colorScheme.primaryContainer
-            } else {
-                MaterialTheme.colorScheme.surfaceContainerHighest
-            },
+                .thenIf(hasHoverBehavior) { Modifier.hoverable(hoverInteractionSource) }
+                .clickable { model.onToggle() },
+        color = chipBackgroundColor,
     ) {
         Row(
             modifier = Modifier.padding(start = 4.dp, end = 8.dp),
             verticalAlignment = Alignment.CenterVertically,
             horizontalArrangement = Arrangement.spacedBy(4.dp),
         ) {
-            val currentIcon = if (isHovered) model.hoverIcon else model.icon
-            val backgroundColor =
-                if (isToggled) {
-                    MaterialTheme.colorScheme.primary
-                } else {
-                    MaterialTheme.colorScheme.primaryContainer
-                }
-
+            val iconColor =
+                if (isHovered) chipBackgroundColor else contentColorFor(chipBackgroundColor)
+            val hoverBehavior = model.hoverBehavior
+            val iconBackgroundColor = contentColorFor(chipBackgroundColor)
+            val iconInteractionSource = remember { MutableInteractionSource() }
             Icon(
-                icon = currentIcon,
+                icon =
+                    when {
+                        isHovered && hoverBehavior is HoverBehavior.Button -> hoverBehavior.icon
+                        else -> model.icon
+                    },
                 modifier =
-                    Modifier.background(color = backgroundColor, shape = CircleShape)
-                        .clickable(
-                            role = Role.Button,
-                            onClick = model.onIconPressed,
-                            indication = ripple(),
-                            interactionSource = remember { MutableInteractionSource() },
-                        )
-                        .padding(2.dp)
-                        .size(18.dp),
-                tint = contentColorFor(backgroundColor),
+                    Modifier.thenIf(isHovered) {
+                            Modifier.padding(3.dp)
+                                .background(color = iconBackgroundColor, shape = CircleShape)
+                        }
+                        .thenIf(hoverBehavior is HoverBehavior.Button) {
+                            Modifier.clickable(
+                                role = Role.Button,
+                                onClick = (hoverBehavior as HoverBehavior.Button).onIconPressed,
+                                indication = ripple(),
+                                interactionSource = iconInteractionSource,
+                            )
+                        }
+                        .padding(3.dp),
+                tint = iconColor,
             )
 
             Text(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/layout/LetterboxAppearanceCalculator.kt
similarity index 87%
rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/layout/LetterboxAppearanceCalculator.kt
index 231a8c6..1469fe7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/layout/LetterboxAppearanceCalculator.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
 
 import android.annotation.ColorInt
 import android.content.Context
@@ -39,7 +39,7 @@
 ) {
     override fun toString(): String {
         val appearanceString =
-                ViewDebug.flagsToString(InsetsFlags::class.java, "appearance", appearance)
+            ViewDebug.flagsToString(InsetsFlags::class.java, "appearance", appearance)
         return "LetterboxAppearance{$appearanceString, $appearanceRegions}"
     }
 }
@@ -57,14 +57,16 @@
     private val letterboxBackgroundProvider: LetterboxBackgroundProvider,
 ) : Dumpable {
 
-    private val darkAppearanceIconColor = context.getColor(
-        // For a dark background status bar, use a *light* icon color.
-        com.android.settingslib.R.color.light_mode_icon_color_single_tone
-    )
-    private val lightAppearanceIconColor = context.getColor(
-        // For a light background status bar, use a *dark* icon color.
-        com.android.settingslib.R.color.dark_mode_icon_color_single_tone
-    )
+    private val darkAppearanceIconColor =
+        context.getColor(
+            // For a dark background status bar, use a *light* icon color.
+            com.android.settingslib.R.color.light_mode_icon_color_single_tone
+        )
+    private val lightAppearanceIconColor =
+        context.getColor(
+            // For a light background status bar, use a *dark* icon color.
+            com.android.settingslib.R.color.dark_mode_icon_color_single_tone
+        )
 
     init {
         dumpManager.registerCriticalDumpable(this)
@@ -85,7 +87,11 @@
         lastAppearanceRegions = originalAppearanceRegions
         lastLetterboxes = letterboxes
         return getLetterboxAppearanceInternal(
-                letterboxes, originalAppearance, originalAppearanceRegions, statusBarBounds)
+                letterboxes,
+                originalAppearance,
+                originalAppearanceRegions,
+                statusBarBounds,
+            )
             .also { lastLetterboxAppearance = it }
     }
 
@@ -118,7 +124,7 @@
 
     private fun getAppearanceRegions(
         originalAppearanceRegions: List<AppearanceRegion>,
-        letterboxes: List<LetterboxDetails>
+        letterboxes: List<LetterboxDetails>,
     ): List<AppearanceRegion> {
         return sanitizeAppearanceRegions(originalAppearanceRegions, letterboxes) +
             getAllOuterAppearanceRegions(letterboxes)
@@ -126,7 +132,7 @@
 
     private fun sanitizeAppearanceRegions(
         originalAppearanceRegions: List<AppearanceRegion>,
-        letterboxes: List<LetterboxDetails>
+        letterboxes: List<LetterboxDetails>,
     ): List<AppearanceRegion> =
         originalAppearanceRegions.map { appearanceRegion ->
             val matchingLetterbox =
@@ -138,17 +144,20 @@
                 // full bounds of its window.
                 // Here we want the bounds to be only for the inner bounds of the letterboxed app.
                 AppearanceRegion(
-                    appearanceRegion.appearance, matchingLetterbox.letterboxInnerBounds)
+                    appearanceRegion.appearance,
+                    matchingLetterbox.letterboxInnerBounds,
+                )
             }
         }
 
     private fun originalAppearanceWithScrim(
         @Appearance originalAppearance: Int,
-        originalAppearanceRegions: List<AppearanceRegion>
+        originalAppearanceRegions: List<AppearanceRegion>,
     ): LetterboxAppearance {
         return LetterboxAppearance(
             originalAppearance or APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS,
-            originalAppearanceRegions)
+            originalAppearanceRegions,
+        )
     }
 
     @Appearance
@@ -215,7 +224,9 @@
            lastAppearanceRegion: $lastAppearanceRegions,
            lastLetterboxes: $lastLetterboxes,
            lastLetterboxAppearance: $lastLetterboxAppearance
-       """.trimIndent())
+       """
+                .trimIndent()
+        )
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/layout/LetterboxBackgroundProvider.kt
similarity index 93%
rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProvider.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/layout/LetterboxBackgroundProvider.kt
index 34c7059e..3d8ced1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/layout/LetterboxBackgroundProvider.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
 
 import android.annotation.ColorInt
 import android.app.WallpaperManager
@@ -49,9 +49,7 @@
         private set
 
     private val wallpaperColorsListener =
-        WallpaperManager.OnColorsChangedListener { _, _ ->
-            fetchBackgroundColorInfo()
-        }
+        WallpaperManager.OnColorsChangedListener { _, _ -> fetchBackgroundColorInfo() }
 
     override fun start() {
         fetchBackgroundColorInfo()
@@ -75,6 +73,8 @@
             """
            letterboxBackgroundColor: ${Color.valueOf(letterboxBackgroundColor)}
            isLetterboxBackgroundMultiColored: $isLetterboxBackgroundMultiColored
-       """.trimIndent())
+       """
+                .trimIndent()
+        )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarBoundsProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/layout/StatusBarBoundsProvider.kt
similarity index 98%
rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarBoundsProvider.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/layout/StatusBarBoundsProvider.kt
index 3ac0bac..ac5b037 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarBoundsProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/layout/StatusBarBoundsProvider.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
 
 import android.graphics.Rect
 import android.view.View
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProvider.kt
similarity index 99%
rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProvider.kt
index 41db5f4..f7a9094 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProvider.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
 
 import android.annotation.Px
 import android.content.Context
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModel.kt
new file mode 100644
index 0000000..03c0748
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModel.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2025 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.layout.ui.viewmodel
+
+import android.graphics.Rect
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsChangedListener
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.onStart
+
+/** A recommended architecture version of [StatusBarContentInsetsProvider]. */
+class StatusBarContentInsetsViewModel(
+    private val statusBarContentInsetsProvider: StatusBarContentInsetsProvider
+) {
+    /** Emits the status bar content area for the given rotation in absolute bounds. */
+    val contentArea: Flow<Rect> =
+        conflatedCallbackFlow {
+                val listener =
+                    object : StatusBarContentInsetsChangedListener {
+                        override fun onStatusBarContentInsetsChanged() {
+                            trySend(
+                                statusBarContentInsetsProvider
+                                    .getStatusBarContentAreaForCurrentRotation()
+                            )
+                        }
+                    }
+                statusBarContentInsetsProvider.addCallback(listener)
+                awaitClose { statusBarContentInsetsProvider.removeCallback(listener) }
+            }
+            .onStart {
+                emit(statusBarContentInsetsProvider.getStatusBarContentAreaForCurrentRotation())
+            }
+            .distinctUntilChanged()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelStore.kt
new file mode 100644
index 0000000..d2dccc4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelStore.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2025 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.layout.ui.viewmodel
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.display.data.repository.DisplayRepository
+import com.android.systemui.display.data.repository.PerDisplayStore
+import com.android.systemui.display.data.repository.PerDisplayStoreImpl
+import com.android.systemui.display.data.repository.SingleDisplayStore
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore
+import dagger.Lazy
+import dagger.Module
+import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+
+/** Provides per-display instances of [StatusBarContentInsetsViewModel]. */
+interface StatusBarContentInsetsViewModelStore : PerDisplayStore<StatusBarContentInsetsViewModel>
+
+@SysUISingleton
+class MultiDisplayStatusBarContentInsetsViewModelStore
+@Inject
+constructor(
+    @Background backgroundApplicationScope: CoroutineScope,
+    displayRepository: DisplayRepository,
+    private val statusBarContentInsetsProviderStore: StatusBarContentInsetsProviderStore,
+) :
+    StatusBarContentInsetsViewModelStore,
+    PerDisplayStoreImpl<StatusBarContentInsetsViewModel>(
+        backgroundApplicationScope,
+        displayRepository,
+    ) {
+
+    override fun createInstanceForDisplay(displayId: Int): StatusBarContentInsetsViewModel? {
+        val insetsProvider =
+            statusBarContentInsetsProviderStore.forDisplay(displayId) ?: return null
+        return StatusBarContentInsetsViewModel(insetsProvider)
+    }
+
+    override val instanceClass = StatusBarContentInsetsViewModel::class.java
+}
+
+@SysUISingleton
+class SingleDisplayStatusBarContentInsetsViewModelStore
+@Inject
+constructor(statusBarContentInsetsViewModel: StatusBarContentInsetsViewModel) :
+    StatusBarContentInsetsViewModelStore,
+    PerDisplayStore<StatusBarContentInsetsViewModel> by SingleDisplayStore(
+        defaultInstance = statusBarContentInsetsViewModel
+    )
+
+@Module
+object StatusBarContentInsetsViewModelStoreModule {
+    @Provides
+    @SysUISingleton
+    @IntoMap
+    @ClassKey(StatusBarContentInsetsViewModelStore::class)
+    fun storeAsCoreStartable(
+        multiDisplayLazy: Lazy<MultiDisplayStatusBarContentInsetsViewModelStore>
+    ): CoreStartable {
+        return if (StatusBarConnectedDisplays.isEnabled) {
+            return multiDisplayLazy.get()
+        } else {
+            CoreStartable.NOP
+        }
+    }
+
+    @Provides
+    @SysUISingleton
+    fun store(
+        singleDisplayLazy: Lazy<SingleDisplayStatusBarContentInsetsViewModelStore>,
+        multiDisplayLazy: Lazy<MultiDisplayStatusBarContentInsetsViewModelStore>,
+    ): StatusBarContentInsetsViewModelStore {
+        return if (StatusBarConnectedDisplays.isEnabled) {
+            multiDisplayLazy.get()
+        } else {
+            singleDisplayLazy.get()
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinator.kt
index 90212ed..034a4fd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinator.kt
@@ -36,7 +36,7 @@
 internal constructor(private val notifLiveDataStoreImpl: NotifLiveDataStoreImpl) : CoreCoordinator {
 
     override fun attach(pipeline: NotifPipeline) {
-        pipeline.addOnAfterRenderListListener { entries, _ -> onAfterRenderList(entries) }
+        pipeline.addOnAfterRenderListListener { entries -> onAfterRenderList(entries) }
     }
 
     override fun dumpPipeline(d: PipelineDumper) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index c7535ec..eb5a370 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -112,20 +112,20 @@
         if (StatusBarNotifChips.isEnabled) {
             applicationScope.launch {
                 statusBarNotificationChipsInteractor.promotedNotificationChipTapEvent.collect {
-                    showPromotedNotificationHeadsUp(it)
+                    onPromotedNotificationChipTapEvent(it)
                 }
             }
         }
     }
 
     /**
-     * Shows the promoted notification with the given [key] as heads-up.
+     * Updates the heads-up state based on which promoted notification with the given [key] was
+     * tapped.
      *
      * Must be run on the main thread.
      */
-    private fun showPromotedNotificationHeadsUp(key: String) {
+    private fun onPromotedNotificationChipTapEvent(key: String) {
         StatusBarNotifChips.assertInNewMode()
-        mLogger.logShowPromotedNotificationHeadsUp(key)
 
         val entry = notifCollection.getEntry(key)
         if (entry == null) {
@@ -135,22 +135,29 @@
         // TODO(b/364653005): Validate that the given key indeed matches a promoted notification,
         // not just any notification.
 
+        val isCurrentlyHeadsUp = mHeadsUpManager.isHeadsUpEntry(entry.key)
         val posted =
             PostedEntry(
                 entry,
                 wasAdded = false,
                 wasUpdated = false,
-                // Force-set this notification to show heads-up.
-                shouldHeadsUpEver = true,
-                shouldHeadsUpAgain = true,
+                // We want the chip to act as a toggle, so if the chip's notification is currently
+                // showing as heads up, then we should stop showing it.
+                shouldHeadsUpEver = !isCurrentlyHeadsUp,
+                shouldHeadsUpAgain = !isCurrentlyHeadsUp,
                 isPinnedByUser = true,
-                isHeadsUpEntry = mHeadsUpManager.isHeadsUpEntry(entry.key),
+                isHeadsUpEntry = isCurrentlyHeadsUp,
                 isBinding = isEntryBinding(entry),
             )
+        if (isCurrentlyHeadsUp) {
+            mLogger.logHidePromotedNotificationHeadsUp(key)
+        } else {
+            mLogger.logShowPromotedNotificationHeadsUp(key)
+        }
 
         mExecutor.execute {
             mPostedEntries[entry.key] = posted
-            mNotifPromoter.invalidateList("showPromotedNotificationHeadsUp: ${entry.logKey}")
+            mNotifPromoter.invalidateList("onPromotedNotificationChipTapEvent: ${entry.logKey}")
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
index e443a04..5141aa3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
@@ -148,6 +148,15 @@
         )
     }
 
+    fun logHidePromotedNotificationHeadsUp(key: String) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            { str1 = key },
+            { "requesting promoted entry to hide heads up: $str1" },
+        )
+    }
+
     fun logPromotedNotificationForHeadsUpNotFound(key: String) {
         buffer.log(
             TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
index 32de65b..1cb2366 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
@@ -23,11 +23,9 @@
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl
-import com.android.systemui.statusbar.notification.collection.render.NotifStackController
-import com.android.systemui.statusbar.notification.collection.render.NotifStats
+import com.android.systemui.statusbar.notification.data.model.NotifStats
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor
-import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT
 import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController
 import javax.inject.Inject
@@ -43,7 +41,8 @@
     private val groupExpansionManagerImpl: GroupExpansionManagerImpl,
     private val renderListInteractor: RenderNotificationListInteractor,
     private val activeNotificationsInteractor: ActiveNotificationsInteractor,
-    private val sensitiveNotificationProtectionController: SensitiveNotificationProtectionController,
+    private val sensitiveNotificationProtectionController:
+        SensitiveNotificationProtectionController,
 ) : Coordinator {
 
     override fun attach(pipeline: NotifPipeline) {
@@ -51,14 +50,10 @@
         groupExpansionManagerImpl.attach(pipeline)
     }
 
-    private fun onAfterRenderList(entries: List<ListEntry>, controller: NotifStackController) =
+    private fun onAfterRenderList(entries: List<ListEntry>) =
         traceSection("StackCoordinator.onAfterRenderList") {
             val notifStats = calculateNotifStats(entries)
-            if (FooterViewRefactor.isEnabled) {
-                activeNotificationsInteractor.setNotifStats(notifStats)
-            } else {
-                controller.setNotifStats(notifStats)
-            }
+            activeNotificationsInteractor.setNotifStats(notifStats)
             renderListInteractor.setRenderedList(entries)
         }
 
@@ -87,7 +82,6 @@
             }
         }
         return NotifStats(
-            numActiveNotifs = entries.size,
             hasNonClearableAlertingNotifs = hasNonClearableAlertingNotifs,
             hasClearableAlertingNotifs = hasClearableAlertingNotifs,
             hasNonClearableSilentNotifs = hasNonClearableSilentNotifs,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java
index a34d033..c58b3fe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java
@@ -33,7 +33,6 @@
 import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer;
 import com.android.systemui.statusbar.notification.collection.coordinator.NotifCoordinators;
 import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl;
-import com.android.systemui.statusbar.notification.collection.render.NotifStackController;
 import com.android.systemui.statusbar.notification.collection.render.RenderStageManager;
 import com.android.systemui.statusbar.notification.collection.render.ShadeViewManager;
 import com.android.systemui.statusbar.notification.collection.render.ShadeViewManagerFactory;
@@ -89,8 +88,7 @@
     public void initialize(
             NotificationListener notificationService,
             NotificationRowBinderImpl rowBinder,
-            NotificationListContainer listContainer,
-            NotifStackController stackController) {
+            NotificationListContainer listContainer) {
         mDumpManager.registerDumpable("NotifPipeline", this);
 
         mNotificationService = notificationService;
@@ -102,7 +100,7 @@
         mNotifPluggableCoordinators.attach(mPipelineWrapper);
 
         // Wire up pipeline
-        mShadeViewManager = mShadeViewManagerFactory.create(listContainer, stackController);
+        mShadeViewManager = mShadeViewManagerFactory.create(listContainer);
         mShadeViewManager.attach(mRenderStageManager);
         mRenderStageManager.attach(mListBuilder);
         mListBuilder.attach(mNotifCollection);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java
index b5a0f7a..ac450c0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java
@@ -20,7 +20,6 @@
 
 import com.android.systemui.statusbar.notification.collection.ListEntry;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
-import com.android.systemui.statusbar.notification.collection.render.NotifStackController;
 
 import java.util.List;
 
@@ -31,9 +30,6 @@
      *
      * @param entries The current list of top-level entries. Note that this is a live view into the
      * current list and will change whenever the pipeline is rerun.
-     * @param controller An object for setting state on the shade.
      */
-    void onAfterRenderList(
-            @NonNull List<ListEntry> entries,
-            @NonNull NotifStackController controller);
+    void onAfterRenderList(@NonNull List<ListEntry> entries);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt
deleted file mode 100644
index a37937a..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.notification.collection.render
-
-import javax.inject.Inject
-
-/** An interface by which the pipeline can make updates to the notification root view. */
-interface NotifStackController {
-    /** Provides stats about the list of notifications attached to the shade */
-    fun setNotifStats(stats: NotifStats)
-}
-
-/** Data provided to the NotificationRootController whenever the pipeline runs */
-data class NotifStats(
-    // TODO(b/293167744): The count can be removed from here when we remove the FooterView flag.
-    val numActiveNotifs: Int,
-    val hasNonClearableAlertingNotifs: Boolean,
-    val hasClearableAlertingNotifs: Boolean,
-    val hasNonClearableSilentNotifs: Boolean,
-    val hasClearableSilentNotifs: Boolean
-) {
-    companion object {
-        @JvmStatic val empty = NotifStats(0, false, false, false, false)
-    }
-}
-
-/**
- * An implementation of NotifStackController which provides default, no-op implementations of each
- * method. This is used by ArcSystemUI so that that implementation can opt-in to overriding methods,
- * rather than forcing us to add no-op implementations in their implementation every time a method
- * is added.
- */
-open class DefaultNotifStackController @Inject constructor() : NotifStackController {
-    override fun setNotifStats(stats: NotifStats) {}
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt
index 410b78b..8284022 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt
@@ -37,12 +37,6 @@
     fun onRenderList(notifList: List<ListEntry>)
 
     /**
-     * Provides an interface for the pipeline to update the overall shade. This will be called at
-     * most once for each time [onRenderList] is called.
-     */
-    fun getStackController(): NotifStackController
-
-    /**
      * Provides an interface for the pipeline to update individual groups. This will be called at
      * most once for each group in the most recent call to [onRenderList].
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt
index 9d3b098..21e6837 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt
@@ -50,7 +50,7 @@
         traceSection("RenderStageManager.onRenderList") {
             val viewRenderer = viewRenderer ?: return
             viewRenderer.onRenderList(notifList)
-            dispatchOnAfterRenderList(viewRenderer, notifList)
+            dispatchOnAfterRenderList(notifList)
             dispatchOnAfterRenderGroups(viewRenderer, notifList)
             dispatchOnAfterRenderEntries(viewRenderer, notifList)
             viewRenderer.onDispatchComplete()
@@ -85,15 +85,9 @@
             dump("onAfterRenderEntryListeners", onAfterRenderEntryListeners)
         }
 
-    private fun dispatchOnAfterRenderList(
-        viewRenderer: NotifViewRenderer,
-        entries: List<ListEntry>,
-    ) {
+    private fun dispatchOnAfterRenderList(entries: List<ListEntry>) {
         traceSection("RenderStageManager.dispatchOnAfterRenderList") {
-            val stackController = viewRenderer.getStackController()
-            onAfterRenderListListeners.forEach { listener ->
-                listener.onAfterRenderList(entries, stackController)
-            }
+            onAfterRenderListListeners.forEach { listener -> listener.onAfterRenderList(entries) }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt
index 3c838e5..72316bf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt
@@ -41,7 +41,6 @@
 constructor(
     @ShadeDisplayAware context: Context,
     @Assisted listContainer: NotificationListContainer,
-    @Assisted private val stackController: NotifStackController,
     mediaContainerController: MediaContainerController,
     featureManager: NotificationSectionsFeatureManager,
     sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider,
@@ -83,8 +82,6 @@
                 }
             }
 
-            override fun getStackController(): NotifStackController = stackController
-
             override fun getGroupController(group: GroupEntry): NotifGroupController =
                 viewBarn.requireGroupController(group.requireSummary)
 
@@ -95,8 +92,5 @@
 
 @AssistedFactory
 interface ShadeViewManagerFactory {
-    fun create(
-        listContainer: NotificationListContainer,
-        stackController: NotifStackController,
-    ): ShadeViewManager
+    fun create(listContainer: NotificationListContainer): ShadeViewManager
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/model/NotifStats.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/model/NotifStats.kt
new file mode 100644
index 0000000..d7fd702
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/model/NotifStats.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.data.model
+
+/** Information about the current list of notifications. */
+data class NotifStats(
+    val hasNonClearableAlertingNotifs: Boolean,
+    val hasClearableAlertingNotifs: Boolean,
+    val hasNonClearableSilentNotifs: Boolean,
+    val hasClearableSilentNotifs: Boolean,
+) {
+    companion object {
+        @JvmStatic
+        val empty =
+            NotifStats(
+                hasNonClearableAlertingNotifs = false,
+                hasClearableAlertingNotifs = false,
+                hasNonClearableSilentNotifs = false,
+                hasClearableSilentNotifs = false,
+            )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt
index 2b9e493..70f06eb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt
@@ -16,7 +16,7 @@
 package com.android.systemui.statusbar.notification.data.repository
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.notification.collection.render.NotifStats
+import com.android.systemui.statusbar.notification.data.model.NotifStats
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore.Key
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationEntryModel
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
index 6b93ee1..0c040c8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
@@ -18,7 +18,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
-import com.android.systemui.statusbar.notification.collection.render.NotifStats
+import com.android.systemui.statusbar.notification.data.model.NotifStats
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
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 75c7d2d..6140c92 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
@@ -25,6 +25,7 @@
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository
 import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
+import com.android.systemui.statusbar.notification.domain.model.TopPinnedState
 import com.android.systemui.statusbar.notification.headsup.PinnedStatus
 import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey
 import javax.inject.Inject
@@ -98,21 +99,39 @@
         }
     }
 
-    /** What [PinnedStatus] does the top row have? */
-    private val topPinnedStatus: Flow<PinnedStatus> =
+    /** What [PinnedStatus] and key does the top row have? */
+    private val topPinnedState: Flow<TopPinnedState> =
         headsUpRepository.activeHeadsUpRows.flatMapLatest { rows ->
             if (rows.isNotEmpty()) {
-                combine(rows.map { it.pinnedStatus }) { pinnedStatus ->
-                    pinnedStatus.firstOrNull { it.isPinned } ?: PinnedStatus.NotPinned
+                // For each row, emits a (key, pinnedStatus) pair each time any row's
+                // `pinnedStatus` changes
+                val toCombine: List<Flow<Pair<String, PinnedStatus>>> =
+                    rows.map { row -> row.pinnedStatus.map { status -> row.key to status } }
+                combine(toCombine) { pairs ->
+                    val topPinnedRow: Pair<String, PinnedStatus>? =
+                        pairs.firstOrNull { it.second.isPinned }
+                    if (topPinnedRow != null) {
+                        TopPinnedState.Pinned(
+                            key = topPinnedRow.first,
+                            status = topPinnedRow.second,
+                        )
+                    } else {
+                        TopPinnedState.NothingPinned
+                    }
                 }
             } else {
-                // if the set is empty, there are no flows to combine
-                flowOf(PinnedStatus.NotPinned)
+                flowOf(TopPinnedState.NothingPinned)
             }
         }
 
     /** Are there any pinned heads up rows to display? */
-    val hasPinnedRows: Flow<Boolean> = topPinnedStatus.map { it.isPinned }
+    val hasPinnedRows: Flow<Boolean> =
+        topPinnedState.map {
+            when (it) {
+                is TopPinnedState.Pinned -> it.status.isPinned
+                is TopPinnedState.NothingPinned -> false
+            }
+        }
 
     val isHeadsUpOrAnimatingAway: Flow<Boolean> by lazy {
         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
@@ -142,13 +161,25 @@
             }
         }
 
-    /** Emits the pinned notification state as it relates to the status bar. */
-    val statusBarHeadsUpState: Flow<PinnedStatus> =
-        combine(topPinnedStatus, canShowHeadsUp) { topPinnedStatus, canShowHeadsUp ->
+    /**
+     * Emits the pinned notification state as it relates to the status bar. Includes both the pinned
+     * status and key of the notification that's pinned (if there is a pinned notification).
+     */
+    val statusBarHeadsUpState: Flow<TopPinnedState> =
+        combine(topPinnedState, canShowHeadsUp) { topPinnedState, canShowHeadsUp ->
             if (canShowHeadsUp) {
-                topPinnedStatus
+                topPinnedState
             } else {
-                PinnedStatus.NotPinned
+                TopPinnedState.NothingPinned
+            }
+        }
+
+    /** Emits the pinned notification status as it relates to the status bar. */
+    val statusBarHeadsUpStatus: Flow<PinnedStatus> =
+        statusBarHeadsUpState.map {
+            when (it) {
+                is TopPinnedState.Pinned -> it.status
+                is TopPinnedState.NothingPinned -> PinnedStatus.NotPinned
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
index 042389f..fd5973e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt
@@ -25,7 +25,6 @@
 import android.service.notification.StatusBarNotification
 import android.util.ArrayMap
 import com.android.app.tracing.traceSection
-import com.android.systemui.Flags
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.ListEntry
@@ -132,12 +131,6 @@
     }
 
     private fun NotificationEntry.toModel(): ActiveNotificationModel {
-        val statusBarChipIcon =
-            if (Flags.statusBarCallChipNotificationIcon()) {
-                icons.statusBarChipIcon
-            } else {
-                null
-            }
         val promotedContent =
             if (PromotedNotificationContentModel.featureFlagEnabled()) {
                 promotedNotificationContentModel
@@ -158,7 +151,7 @@
             aodIcon = icons.aodIcon?.sourceIcon,
             shelfIcon = icons.shelfIcon?.sourceIcon,
             statusBarIcon = icons.statusBarIcon?.sourceIcon,
-            statusBarChipIconView = statusBarChipIcon,
+            statusBarChipIconView = icons.statusBarChipIcon,
             uid = sbn.uid,
             packageName = sbn.packageName,
             contentIntent = sbn.notification.contentIntent,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/model/TopPinnedState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/model/TopPinnedState.kt
new file mode 100644
index 0000000..51c448a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/model/TopPinnedState.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2025 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.domain.model
+
+import com.android.systemui.statusbar.notification.headsup.PinnedStatus
+
+/** A class representing the state of the top pinned row. */
+sealed interface TopPinnedState {
+    /** Nothing is pinned. */
+    data object NothingPinned : TopPinnedState
+
+    /**
+     * The top pinned row is a notification with the given key and status.
+     *
+     * @property status must have [PinnedStatus.isPinned] as true.
+     */
+    data class Pinned(val key: String, val status: PinnedStatus) : TopPinnedState {
+        init {
+            check(status.isPinned)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt
index fbec640..7e2361f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt
@@ -26,7 +26,6 @@
 import com.android.systemui.statusbar.notification.NotificationActivityStarter.SettingsIntent
 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
 import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
-import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterMessageViewModel
 import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
 import com.android.systemui.util.kotlin.FlowDumperImpl
@@ -35,7 +34,6 @@
 import java.util.Locale
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.flowOf
@@ -57,9 +55,7 @@
     dumpManager: DumpManager,
 ) : FlowDumperImpl(dumpManager) {
     val areNotificationsHiddenInShade: Flow<Boolean> by lazy {
-        if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
-            flowOf(false)
-        } else if (ModesEmptyShadeFix.isEnabled) {
+        if (ModesEmptyShadeFix.isEnabled) {
             zenModeInteractor.areNotificationsHiddenInShade
                 .dumpWhileCollecting("areNotificationsHiddenInShade")
                 .flowOn(bgDispatcher)
@@ -70,15 +66,10 @@
         }
     }
 
-    val hasFilteredOutSeenNotifications: StateFlow<Boolean> by lazy {
-        if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
-            MutableStateFlow(false)
-        } else {
-            seenNotificationsInteractor.hasFilteredOutSeenNotifications.dumpValue(
-                "hasFilteredOutSeenNotifications"
-            )
-        }
-    }
+    val hasFilteredOutSeenNotifications: StateFlow<Boolean> =
+        seenNotificationsInteractor.hasFilteredOutSeenNotifications.dumpValue(
+            "hasFilteredOutSeenNotifications"
+        )
 
     val text: Flow<String> by lazy {
         if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/FooterViewRefactor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/FooterViewRefactor.kt
deleted file mode 100644
index 7e6044e..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/FooterViewRefactor.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.notification.footer.shared
-
-import com.android.systemui.Flags
-import com.android.systemui.flags.FlagToken
-import com.android.systemui.flags.RefactorFlagUtils
-
-/** Helper for reading or using the FooterView refactor flag state. */
-@Suppress("NOTHING_TO_INLINE")
-object FooterViewRefactor {
-    /** The aconfig flag name */
-    const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR
-
-    /** A token used for dependency declaration */
-    val token: FlagToken
-        get() = FlagToken(FLAG_NAME, isEnabled)
-
-    /** Is the refactor enabled */
-    @JvmStatic
-    inline val isEnabled
-        get() = Flags.notificationsFooterViewRefactor()
-
-    /**
-     * 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/statusbar/notification/footer/ui/view/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
index d258898..a670f69 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
@@ -41,7 +41,6 @@
 
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.notification.ColorUpdateLogger;
-import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
 import com.android.systemui.statusbar.notification.footer.shared.NotifRedesignFooter;
 import com.android.systemui.statusbar.notification.row.FooterViewButton;
 import com.android.systemui.statusbar.notification.row.StackScrollerDecorView;
@@ -63,16 +62,9 @@
     private FooterViewButton mSettingsButton;
     private FooterViewButton mHistoryButton;
     private boolean mShouldBeHidden;
-    private boolean mShowHistory;
-    // String cache, for performance reasons.
-    // Reading them from a Resources object can be quite slow sometimes.
-    private String mManageNotificationText;
-    private String mManageNotificationHistoryText;
 
     // Footer label
     private TextView mSeenNotifsFooterTextView;
-    private String mSeenNotifsFilteredText;
-    private Drawable mSeenNotifsFilteredIcon;
 
     private @StringRes int mClearAllButtonTextId;
     private @StringRes int mClearAllButtonDescriptionId;
@@ -159,8 +151,8 @@
         IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal);
         super.dump(pw, args);
         DumpUtilsKt.withIncreasedIndent(pw, () -> {
+            // TODO: b/375010573 - update dumps for redesign
             pw.println("visibility: " + DumpUtilsKt.visibilityString(getVisibility()));
-            pw.println("manageButton showHistory: " + mShowHistory);
             pw.println("manageButton visibility: "
                     + DumpUtilsKt.visibilityString(mClearAllButton.getVisibility()));
             pw.println("dismissButton visibility: "
@@ -170,7 +162,6 @@
 
     /** Set the text label for the "Clear all" button. */
     public void setClearAllButtonText(@StringRes int textId) {
-        if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) return;
         if (mClearAllButtonTextId == textId) {
             return; // nothing changed
         }
@@ -187,9 +178,6 @@
 
     /** Set the accessibility content description for the "Clear all" button. */
     public void setClearAllButtonDescription(@StringRes int contentDescriptionId) {
-        if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
-            return;
-        }
         if (mClearAllButtonDescriptionId == contentDescriptionId) {
             return; // nothing changed
         }
@@ -207,7 +195,6 @@
     /** Set the text label for the "Manage"/"History" button. */
     public void setManageOrHistoryButtonText(@StringRes int textId) {
         NotifRedesignFooter.assertInLegacyMode();
-        if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) return;
         if (mManageOrHistoryButtonTextId == textId) {
             return; // nothing changed
         }
@@ -226,9 +213,6 @@
     /** Set the accessibility content description for the "Clear all" button. */
     public void setManageOrHistoryButtonDescription(@StringRes int contentDescriptionId) {
         NotifRedesignFooter.assertInLegacyMode();
-        if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
-            return;
-        }
         if (mManageOrHistoryButtonDescriptionId == contentDescriptionId) {
             return; // nothing changed
         }
@@ -247,7 +231,6 @@
 
     /** Set the string for a message to be shown instead of the buttons. */
     public void setMessageString(@StringRes int messageId) {
-        if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) return;
         if (mMessageStringId == messageId) {
             return; // nothing changed
         }
@@ -265,7 +248,6 @@
 
     /** Set the icon to be shown before the message (see {@link #setMessageString(int)}). */
     public void setMessageIcon(@DrawableRes int iconId) {
-        if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) return;
         if (mMessageIconId == iconId) {
             return; // nothing changed
         }
@@ -303,32 +285,17 @@
             mManageOrHistoryButton = findViewById(R.id.manage_text);
         }
         mSeenNotifsFooterTextView = findViewById(R.id.unlock_prompt_footer);
-        if (!FooterViewRefactor.isEnabled()) {
-            updateResources();
-        }
         updateContent();
         updateColors();
     }
 
     /** Show a message instead of the footer buttons. */
     public void setFooterLabelVisible(boolean isVisible) {
-        // In the refactored code, hiding the buttons is handled in the FooterViewModel
-        if (FooterViewRefactor.isEnabled()) {
-            if (isVisible) {
-                mSeenNotifsFooterTextView.setVisibility(View.VISIBLE);
-            } else {
-                mSeenNotifsFooterTextView.setVisibility(View.GONE);
-            }
+        // Note: hiding the buttons is handled in the FooterViewModel
+        if (isVisible) {
+            mSeenNotifsFooterTextView.setVisibility(View.VISIBLE);
         } else {
-            if (isVisible) {
-                mManageOrHistoryButton.setVisibility(View.GONE);
-                mClearAllButton.setVisibility(View.GONE);
-                mSeenNotifsFooterTextView.setVisibility(View.VISIBLE);
-            } else {
-                mManageOrHistoryButton.setVisibility(View.VISIBLE);
-                mClearAllButton.setVisibility(View.VISIBLE);
-                mSeenNotifsFooterTextView.setVisibility(View.GONE);
-            }
+            mSeenNotifsFooterTextView.setVisibility(View.GONE);
         }
     }
 
@@ -359,10 +326,8 @@
 
     /** Set onClickListener for the clear all (end) button. */
     public void setClearAllButtonClickListener(OnClickListener listener) {
-        if (FooterViewRefactor.isEnabled()) {
-            if (mClearAllButtonClickListener == listener) return;
-            mClearAllButtonClickListener = listener;
-        }
+        if (mClearAllButtonClickListener == listener) return;
+        mClearAllButtonClickListener = listener;
         mClearAllButton.setOnClickListener(listener);
     }
 
@@ -379,62 +344,17 @@
                 || touchY > mContent.getY() + mContent.getHeight();
     }
 
-    /** Show "History" instead of "Manage" on the start button. */
-    public void showHistory(boolean showHistory) {
-        FooterViewRefactor.assertInLegacyMode();
-        if (mShowHistory == showHistory) {
-            return;
-        }
-        mShowHistory = showHistory;
-        updateContent();
-    }
-
     private void updateContent() {
-        if (FooterViewRefactor.isEnabled()) {
-            updateClearAllButtonText();
-            updateClearAllButtonDescription();
+        updateClearAllButtonText();
+        updateClearAllButtonDescription();
 
-            if (!NotifRedesignFooter.isEnabled()) {
-                updateManageOrHistoryButtonText();
-                updateManageOrHistoryButtonDescription();
-            }
-
-            updateMessageString();
-            updateMessageIcon();
-        } else {
-            // NOTE: Prior to the refactor, `updateResources` set the class properties to the right
-            // string values. It was always being called together with `updateContent`, which
-            // deals with actually associating those string values with the correct views
-            // (buttons or text).
-            // In the new code, the resource IDs are being set in the view binder (through
-            // setMessageString and similar setters). The setters themselves now deal with
-            // updating both the resource IDs and the views where appropriate (as in, calling
-            // `updateMessageString` when the resource ID changes). This eliminates the need for
-            // `updateResources`, which will eventually be removed. There are, however, still
-            // situations in which we want to update the views even if the resource IDs didn't
-            // change, such as configuration changes.
-            if (mShowHistory) {
-                mManageOrHistoryButton.setText(mManageNotificationHistoryText);
-                mManageOrHistoryButton.setContentDescription(mManageNotificationHistoryText);
-            } else {
-                mManageOrHistoryButton.setText(mManageNotificationText);
-                mManageOrHistoryButton.setContentDescription(mManageNotificationText);
-            }
-
-            mClearAllButton.setText(R.string.clear_all_notifications_text);
-            mClearAllButton.setContentDescription(
-                    mContext.getString(R.string.accessibility_clear_all));
-
-            mSeenNotifsFooterTextView.setText(mSeenNotifsFilteredText);
-            mSeenNotifsFooterTextView
-                    .setCompoundDrawablesRelative(mSeenNotifsFilteredIcon, null, null, null);
+        if (!NotifRedesignFooter.isEnabled()) {
+            updateManageOrHistoryButtonText();
+            updateManageOrHistoryButtonDescription();
         }
-    }
 
-    /** Whether the start button shows "History" (true) or "Manage" (false). */
-    public boolean isHistoryShown() {
-        FooterViewRefactor.assertInLegacyMode();
-        return mShowHistory;
+        updateMessageString();
+        updateMessageIcon();
     }
 
     @Override
@@ -445,9 +365,6 @@
         }
         super.onConfigurationChanged(newConfig);
         updateColors();
-        if (!FooterViewRefactor.isEnabled()) {
-            updateResources();
-        }
         updateContent();
     }
 
@@ -502,18 +419,6 @@
         }
     }
 
-    private void updateResources() {
-        FooterViewRefactor.assertInLegacyMode();
-        mManageNotificationText = getContext().getString(R.string.manage_notifications_text);
-        mManageNotificationHistoryText = getContext()
-                .getString(R.string.manage_notifications_history_text);
-        int unlockIconSize = getResources()
-                .getDimensionPixelSize(R.dimen.notifications_unseen_footer_icon_size);
-        mSeenNotifsFilteredText = getContext().getString(R.string.unlock_to_see_notif_text);
-        mSeenNotifsFilteredIcon = getContext().getDrawable(R.drawable.ic_friction_lock_closed);
-        mSeenNotifsFilteredIcon.setBounds(0, 0, unlockIconSize, unlockIconSize);
-    }
-
     @Override
     @NonNull
     public ExpandableViewState createExpandableViewState() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
index e724935..5696e9f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
@@ -27,7 +27,6 @@
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
 import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
-import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
 import com.android.systemui.util.kotlin.sample
 import com.android.systemui.util.ui.AnimatableEvent
@@ -144,6 +143,7 @@
         )
 }
 
+// TODO: b/293167744 - remove this, use new viewmodel style
 @Module
 object FooterViewModelModule {
     @Provides
@@ -153,18 +153,13 @@
         notificationSettingsInteractor: Provider<NotificationSettingsInteractor>,
         seenNotificationsInteractor: Provider<SeenNotificationsInteractor>,
         shadeInteractor: Provider<ShadeInteractor>,
-    ): Optional<FooterViewModel> {
-        return if (FooterViewRefactor.isEnabled) {
-            Optional.of(
-                FooterViewModel(
-                    activeNotificationsInteractor.get(),
-                    notificationSettingsInteractor.get(),
-                    seenNotificationsInteractor.get(),
-                    shadeInteractor.get(),
-                )
+    ): Optional<FooterViewModel> =
+        Optional.of(
+            FooterViewModel(
+                activeNotificationsInteractor.get(),
+                notificationSettingsInteractor.get(),
+                seenNotificationsInteractor.get(),
+                shadeInteractor.get(),
             )
-        } else {
-            Optional.empty()
-        }
-    }
+        )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
index b56a838..31375cc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
@@ -162,9 +162,7 @@
             val sbIcon = iconBuilder.createIconView(entry)
             sbIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
             val sbChipIcon: StatusBarIconView?
-            if (
-                Flags.statusBarCallChipNotificationIcon() && !StatusBarConnectedDisplays.isEnabled
-            ) {
+            if (!StatusBarConnectedDisplays.isEnabled) {
                 sbChipIcon = iconBuilder.createIconView(entry)
                 sbChipIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
             } else {
@@ -186,7 +184,7 @@
 
             try {
                 setIcon(entry, normalIconDescriptor, sbIcon)
-                if (Flags.statusBarCallChipNotificationIcon() && sbChipIcon != null) {
+                if (sbChipIcon != null) {
                     setIcon(entry, normalIconDescriptor, sbChipIcon)
                 }
                 setIcon(entry, sensitiveIconDescriptor, shelfIcon)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt
index 2c5d9c2..3c2051f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt
@@ -20,7 +20,6 @@
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption
 import com.android.systemui.statusbar.NotificationPresenter
 import com.android.systemui.statusbar.notification.NotificationActivityStarter
-import com.android.systemui.statusbar.notification.collection.render.NotifStackController
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer
 
 /**
@@ -33,7 +32,6 @@
     fun initialize(
         presenter: NotificationPresenter,
         listContainer: NotificationListContainer,
-        stackController: NotifStackController,
         notificationActivityStarter: NotificationActivityStarter,
     )
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
index ea6a60b..0a9899e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
@@ -34,7 +34,6 @@
 import com.android.systemui.statusbar.notification.collection.init.NotifPipelineInitializer
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
-import com.android.systemui.statusbar.notification.collection.render.NotifStackController
 import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
 import com.android.systemui.statusbar.notification.logging.NotificationLogger
 import com.android.systemui.statusbar.notification.row.NotifBindPipelineInitializer
@@ -76,7 +75,6 @@
     override fun initialize(
         presenter: NotificationPresenter,
         listContainer: NotificationListContainer,
-        stackController: NotifStackController,
         notificationActivityStarter: NotificationActivityStarter,
     ) {
         notificationListener.registerAsSystemService()
@@ -101,7 +99,7 @@
 
         notifPipelineInitializer
             .get()
-            .initialize(notificationListener, notificationRowBinder, listContainer, stackController)
+            .initialize(notificationListener, notificationRowBinder, listContainer)
 
         targetSdkResolver.initialize(notifPipeline.get())
         notificationsMediaManager.setUpWithPresenter(presenter)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt
index 148b3f0..92d96f9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt
@@ -21,7 +21,6 @@
 import com.android.systemui.statusbar.NotificationListener
 import com.android.systemui.statusbar.NotificationPresenter
 import com.android.systemui.statusbar.notification.NotificationActivityStarter
-import com.android.systemui.statusbar.notification.collection.render.NotifStackController
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer
 import javax.inject.Inject
 
@@ -35,7 +34,6 @@
     override fun initialize(
         presenter: NotificationPresenter,
         listContainer: NotificationListContainer,
-        stackController: NotifStackController,
         notificationActivityStarter: NotificationActivityStarter,
     ) {
         // Always connect the listener even if notification-handling is disabled. Being a listener
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
index c6832bc..cc4be57 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
@@ -20,7 +20,6 @@
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.SystemClock;
-import android.os.Trace;
 import android.service.notification.NotificationListenerService;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -29,6 +28,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.app.tracing.coroutines.TrackTracer;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.statusbar.IStatusBarService;
@@ -152,8 +152,8 @@
 
             mExpansionStateLogger.onVisibilityChanged(
                     mTmpCurrentlyVisibleNotifications, mTmpCurrentlyVisibleNotifications);
-            Trace.traceCounter(Trace.TRACE_TAG_APP, "Notifications [Active]", N);
-            Trace.traceCounter(Trace.TRACE_TAG_APP, "Notifications [Visible]",
+            TrackTracer.instantForGroup("Notifications", "Active", N);
+            TrackTracer.instantForGroup("Notifications", "Visible",
                     mCurrentlyVisibleNotifications.size());
 
             recycleAllVisibilityObjects(mTmpNoLongerVisibleNotifications);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt
index 10e67a4..640d364 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt
@@ -25,7 +25,7 @@
 import android.app.NotificationManager.IMPORTANCE_UNSPECIFIED
 import android.content.Context
 import android.graphics.drawable.Drawable
-import android.text.TextUtils
+import android.text.TextUtils.isEmpty
 import android.transition.AutoTransition
 import android.transition.Transition
 import android.transition.TransitionManager
@@ -37,13 +37,10 @@
 import android.widget.Switch
 import android.widget.TextView
 import com.android.settingslib.Utils
-
 import com.android.systemui.res.R
 import com.android.systemui.util.Assert
 
-/**
- * Half-shelf for notification channel controls
- */
+/** Half-shelf for notification channel controls */
 class ChannelEditorListView(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) {
     lateinit var controller: ChannelEditorDialogController
     var appIcon: Drawable? = null
@@ -84,23 +81,21 @@
 
         val transition = AutoTransition()
         transition.duration = 200
-        transition.addListener(object : Transition.TransitionListener {
-            override fun onTransitionEnd(p0: Transition?) {
-                notifySubtreeAccessibilityStateChangedIfNeeded()
-            }
+        transition.addListener(
+            object : Transition.TransitionListener {
+                override fun onTransitionEnd(p0: Transition?) {
+                    notifySubtreeAccessibilityStateChangedIfNeeded()
+                }
 
-            override fun onTransitionResume(p0: Transition?) {
-            }
+                override fun onTransitionResume(p0: Transition?) {}
 
-            override fun onTransitionPause(p0: Transition?) {
-            }
+                override fun onTransitionPause(p0: Transition?) {}
 
-            override fun onTransitionCancel(p0: Transition?) {
-            }
+                override fun onTransitionCancel(p0: Transition?) {}
 
-            override fun onTransitionStart(p0: Transition?) {
+                override fun onTransitionStart(p0: Transition?) {}
             }
-        })
+        )
         TransitionManager.beginDelayedTransition(this, transition)
 
         // Remove any rows
@@ -130,8 +125,9 @@
 
     private fun updateAppControlRow(enabled: Boolean) {
         appControlRow.iconView.setImageDrawable(appIcon)
-        appControlRow.channelName.text = context.resources
-                .getString(R.string.notification_channel_dialog_title, appName)
+        val title = context.resources.getString(R.string.notification_channel_dialog_title, appName)
+        appControlRow.channelName.text = title
+        appControlRow.switch.contentDescription = title
         appControlRow.switch.isChecked = enabled
         appControlRow.switch.setOnCheckedChangeListener { _, b ->
             controller.proposeSetAppNotificationsEnabled(b)
@@ -164,8 +160,8 @@
     var gentle = false
 
     init {
-        highlightColor = Utils.getColorAttrDefaultColor(
-                context, android.R.attr.colorControlHighlight)
+        highlightColor =
+            Utils.getColorAttrDefaultColor(context, android.R.attr.colorControlHighlight)
     }
 
     var channel: NotificationChannel? = null
@@ -182,17 +178,16 @@
         switch = requireViewById(R.id.toggle)
         switch.setOnCheckedChangeListener { _, b ->
             channel?.let {
-                controller.proposeEditForChannel(it,
-                        if (b) it.originalImportance.coerceAtLeast(IMPORTANCE_LOW)
-                        else IMPORTANCE_NONE)
+                controller.proposeEditForChannel(
+                    it,
+                    if (b) it.originalImportance.coerceAtLeast(IMPORTANCE_LOW) else IMPORTANCE_NONE,
+                )
             }
         }
         setOnClickListener { switch.toggle() }
     }
 
-    /**
-     * Play an animation that highlights this row
-     */
+    /** Play an animation that highlights this row */
     fun playHighlight() {
         // Use 0 for the start value because our background is given to us by our parent
         val fadeInLoop = ValueAnimator.ofObject(ArgbEvaluator(), 0, highlightColor)
@@ -211,17 +206,21 @@
 
         channelName.text = nc.name ?: ""
 
-        nc.group?.let { groupId ->
-            channelDescription.text = controller.groupNameForId(groupId)
-        }
+        nc.group?.let { groupId -> channelDescription.text = controller.groupNameForId(groupId) }
 
-        if (nc.group == null || TextUtils.isEmpty(channelDescription.text)) {
+        if (nc.group == null || isEmpty(channelDescription.text)) {
             channelDescription.visibility = View.GONE
         } else {
             channelDescription.visibility = View.VISIBLE
         }
 
         switch.isChecked = nc.importance != IMPORTANCE_NONE
+        switch.contentDescription =
+            if (isEmpty(channelDescription.text)) {
+                channelName.text
+            } else {
+                "${channelName.text} ${channelDescription.text}"
+            }
     }
 
     private fun updateImportance() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 7e3d004..95604c1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -1267,6 +1267,9 @@
         }
         if (mExpandedWhenPinned) {
             return Math.max(getMaxExpandHeight(), getHeadsUpHeight());
+        } else if (android.app.Flags.compactHeadsUpNotification()
+                && getShowingLayout().isHUNCompact()) {
+            return getHeadsUpHeight();
         } else if (atLeastMinHeight) {
             return Math.max(getCollapsedHeight(), getHeadsUpHeight());
         } else {
@@ -3680,6 +3683,10 @@
         return super.disallowSingleClick(event);
     }
 
+    // TODO: b/388470175 - Although this does get triggered when a notification
+    // is expanded by the system (e.g. the first notication in the shade), it
+    // will not be when a notification is collapsed by the system (such as when
+    // the shade is closed).
     private void onExpansionChanged(boolean userAction, boolean wasExpanded) {
         boolean nowExpanded = isExpanded();
         if (mIsSummaryWithChildren && (!mIsMinimized || wasExpanded)) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt
new file mode 100644
index 0000000..e27ff7d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.ColorFilter
+import android.graphics.Paint
+import android.graphics.PixelFormat
+import android.graphics.drawable.Drawable
+
+/**
+ * A background style for smarter-smart-actions.
+ *
+ * TODO(b/383567383) implement final UX
+ */
+class MagicActionBackgroundDrawable(context: Context) : Drawable() {
+
+    private var _alpha: Int = 255
+    private var _colorFilter: ColorFilter? = null
+    private val paint =
+        Paint().apply {
+            color = context.getColor(com.android.internal.R.color.materialColorPrimaryContainer)
+        }
+
+    override fun draw(canvas: Canvas) {
+        canvas.drawRect(bounds, paint)
+    }
+
+    override fun setAlpha(alpha: Int) {
+        _alpha = alpha
+        invalidateSelf()
+    }
+
+    override fun setColorFilter(colorFilter: ColorFilter?) {
+        _colorFilter = colorFilter
+        invalidateSelf()
+    }
+
+    override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index 7c44eae..70e27a9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -16,8 +16,6 @@
 
 package com.android.systemui.statusbar.notification.row;
 
-import static android.app.Flags.notificationsRedesignTemplates;
-
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
 import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT;
 import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED;
@@ -481,16 +479,15 @@
                     logger.logAsyncTaskProgress(entryForLogging,
                             "creating low-priority group summary remote view");
                     result.mNewMinimizedGroupHeaderView =
-                            builder.makeLowPriorityContentView(/* useRegularSubtext = */ true,
-                                    /* highlightExpander = */ notificationsRedesignTemplates());
+                            builder.makeLowPriorityContentView(true /* useRegularSubtext */);
                 }
             }
             setNotifsViewsInflaterFactory(result, row, notifLayoutInflaterFactoryProvider);
             result.packageContext = packageContext;
             result.headsUpStatusBarText = builder.getHeadsUpStatusBarText(
-                    /* showingPublic = */ false);
+                    false /* showingPublic */);
             result.headsUpStatusBarTextPublic = builder.getHeadsUpStatusBarText(
-                    /* showingPublic = */ true);
+                    true /* showingPublic */);
 
             return result;
         });
@@ -1139,8 +1136,7 @@
     private static RemoteViews createContentView(Notification.Builder builder,
             boolean isMinimized, boolean useLarge) {
         if (isMinimized) {
-            return builder.makeLowPriorityContentView(/* useRegularSubtext = */ false,
-                    /* highlightExpander = */ false);
+            return builder.makeLowPriorityContentView(false /* useRegularSubtext */);
         }
         return builder.createContentView(useLarge);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 786d7d9..0d29981 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -207,6 +207,8 @@
     private boolean mContentAnimating;
     private UiEventLogger mUiEventLogger;
 
+    private boolean mIsHUNCompact;
+
     public NotificationContentView(Context context, AttributeSet attrs) {
         super(context, attrs);
         mHybridGroupManager = new HybridGroupManager(getContext());
@@ -543,6 +545,7 @@
         if (child == null) {
             mHeadsUpChild = null;
             mHeadsUpWrapper = null;
+            mIsHUNCompact = false;
             if (mTransformationStartVisibleType == VISIBLE_TYPE_HEADSUP) {
                 mTransformationStartVisibleType = VISIBLE_TYPE_NONE;
             }
@@ -556,8 +559,9 @@
         mHeadsUpWrapper = NotificationViewWrapper.wrap(getContext(), child,
                 mContainingNotification);
 
-        if (Flags.compactHeadsUpNotification()
-                && mHeadsUpWrapper instanceof NotificationCompactHeadsUpTemplateViewWrapper) {
+        mIsHUNCompact = Flags.compactHeadsUpNotification()
+                && mHeadsUpWrapper instanceof NotificationCompactHeadsUpTemplateViewWrapper;
+        if (mIsHUNCompact) {
             logCompactHUNShownEvent();
         }
 
@@ -902,6 +906,10 @@
         }
     }
 
+    public boolean isHUNCompact() {
+        return mIsHUNCompact;
+    }
+
     private boolean isGroupExpanded() {
         return mContainingNotification.isGroupExpanded();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
index ae9b69c..c619b17 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
@@ -16,7 +16,6 @@
 package com.android.systemui.statusbar.notification.row
 
 import android.annotation.SuppressLint
-import android.app.Flags.notificationsRedesignTemplates
 import android.app.Notification
 import android.app.Notification.MessagingStyle
 import android.content.Context
@@ -888,10 +887,7 @@
                             entryForLogging,
                             "creating low-priority group summary remote view",
                         )
-                        builder.makeLowPriorityContentView(
-                            /* useRegularSubtext = */ true,
-                            /* highlightExpander = */ notificationsRedesignTemplates(),
-                        )
+                        builder.makeLowPriorityContentView(true /* useRegularSubtext */)
                     } else null
                 NewRemoteViews(
                         contracted = contracted,
@@ -1661,10 +1657,7 @@
             useLarge: Boolean,
         ): RemoteViews {
             return if (isMinimized) {
-                builder.makeLowPriorityContentView(
-                    /* useRegularSubtext = */ false,
-                    /* highlightExpander = */ false,
-                )
+                builder.makeLowPriorityContentView(false /* useRegularSubtext */)
             } else builder.createContentView(useLarge)
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index e477c74..8e48065 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -568,8 +568,7 @@
                 builder = Notification.Builder.recoverBuilder(getContext(),
                         notification.getNotification());
             }
-            header = builder.makeLowPriorityContentView(true /* useRegularSubtext */,
-                    notificationsRedesignTemplates() /* highlightExpander */);
+            header = builder.makeLowPriorityContentView(true /* useRegularSubtext */);
             if (mMinimizedGroupHeader == null) {
                 mMinimizedGroupHeader = (NotificationHeaderView) header.apply(getContext(),
                         this);
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 9766f9e..76591ac 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
@@ -108,7 +108,6 @@
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
 import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix;
 import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView;
-import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView;
 import com.android.systemui.statusbar.notification.headsup.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.notification.headsup.HeadsUpUtil;
@@ -703,9 +702,6 @@
         if (!ModesEmptyShadeFix.isEnabled()) {
             inflateEmptyShadeView();
         }
-        if (!FooterViewRefactor.isEnabled()) {
-            inflateFooterView();
-        }
     }
 
     /**
@@ -741,22 +737,12 @@
     }
 
     void reinflateViews() {
-        if (!FooterViewRefactor.isEnabled()) {
-            inflateFooterView();
-            updateFooter();
-        }
         if (!ModesEmptyShadeFix.isEnabled()) {
             inflateEmptyShadeView();
         }
         mSectionsManager.reinflateViews();
     }
 
-    public void setIsRemoteInputActive(boolean isActive) {
-        FooterViewRefactor.assertInLegacyMode();
-        mIsRemoteInputActive = isActive;
-        updateFooter();
-    }
-
     void sendRemoteInputRowBottomBound(Float bottom) {
         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
         if (bottom != null) {
@@ -766,43 +752,6 @@
         mScrollViewFields.sendRemoteInputRowBottomBound(bottom);
     }
 
-    /** Setter for filtered notifs, to be removed with the FooterViewRefactor flag. */
-    public void setHasFilteredOutSeenNotifications(boolean hasFilteredOutSeenNotifications) {
-        FooterViewRefactor.assertInLegacyMode();
-        mHasFilteredOutSeenNotifications = hasFilteredOutSeenNotifications;
-    }
-
-    @VisibleForTesting
-    public void updateFooter() {
-        FooterViewRefactor.assertInLegacyMode();
-        if (mFooterView == null || mController == null) {
-            return;
-        }
-        final boolean showHistory = mController.isHistoryEnabled();
-        final boolean showDismissView = shouldShowDismissView();
-
-        updateFooterView(shouldShowFooterView(showDismissView)/* visible */,
-                showDismissView /* showDismissView */,
-                showHistory/* showHistory */);
-    }
-
-    private boolean shouldShowDismissView() {
-        FooterViewRefactor.assertInLegacyMode();
-        return mController.hasActiveClearableNotifications(ROWS_ALL);
-    }
-
-    private boolean shouldShowFooterView(boolean showDismissView) {
-        FooterViewRefactor.assertInLegacyMode();
-        return (showDismissView || mController.getVisibleNotificationCount() > 0)
-                && mIsCurrentUserSetup // see: b/193149550
-                && !onKeyguard()
-                && mUpcomingStatusBarState != StatusBarState.KEYGUARD
-                // quick settings don't affect notifications when not in full screen
-                && (getQsExpansionFraction() != 1 || !mQsFullScreen)
-                && !mScreenOffAnimationController.shouldHideNotificationsFooter()
-                && !mIsRemoteInputActive;
-    }
-
     void updateBgColor() {
         for (int i = 0; i < getChildCount(); i++) {
             View child = getChildAt(i);
@@ -1861,9 +1810,6 @@
      */
     private float getAppearEndPosition() {
         SceneContainerFlag.assertInLegacyMode();
-        if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
-            return getAppearEndPositionLegacy();
-        }
 
         int appearPosition = mAmbientState.getStackTopMargin();
         if (mEmptyShadeView.getVisibility() == GONE) {
@@ -1883,32 +1829,6 @@
         return appearPosition + (onKeyguard() ? getTopPadding() : getIntrinsicPadding());
     }
 
-    /**
-     * The version of {@code getAppearEndPosition} that uses the notif count. The view shouldn't
-     * need to know about that, so we want to phase this out with the footer view refactor.
-     */
-    private float getAppearEndPositionLegacy() {
-        FooterViewRefactor.assertInLegacyMode();
-
-        int appearPosition = mAmbientState.getStackTopMargin();
-        int visibleNotifCount = mController.getVisibleNotificationCount();
-        if (mEmptyShadeView.getVisibility() == GONE && visibleNotifCount > 0) {
-            if (isHeadsUpTransition()
-                    || (mInHeadsUpPinnedMode && !mAmbientState.isDozing())) {
-                if (mShelf.getVisibility() != GONE && visibleNotifCount > 1) {
-                    appearPosition += mShelf.getIntrinsicHeight() + mPaddingBetweenElements;
-                }
-                appearPosition += getTopHeadsUpPinnedHeight()
-                        + getPositionInLinearLayout(mAmbientState.getTrackedHeadsUpRow());
-            } else if (mShelf.getVisibility() != GONE) {
-                appearPosition += mShelf.getIntrinsicHeight();
-            }
-        } else {
-            appearPosition = mEmptyShadeView.getHeight();
-        }
-        return appearPosition + (onKeyguard() ? getTopPadding() : getIntrinsicPadding());
-    }
-
     private boolean isHeadsUpTransition() {
         return mAmbientState.getTrackedHeadsUpRow() != null;
     }
@@ -1928,8 +1848,7 @@
             // This can't use expansion fraction as that goes only from 0 to 1. Also when
             // appear fraction for HUN is 0, expansion fraction will be already around 0.2-0.3
             // and that makes translation jump immediately.
-            float appearEndPosition = FooterViewRefactor.isEnabled() ? getAppearEndPosition()
-                    : getAppearEndPositionLegacy();
+            float appearEndPosition = getAppearEndPosition();
             float appearStartPosition = getAppearStartPosition();
             float hunAppearFraction = (height - appearStartPosition)
                     / (appearEndPosition - appearStartPosition);
@@ -2663,12 +2582,6 @@
     }
 
     @Override
-    public int getHeadsUpInset() {
-        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0;
-        return mHeadsUpInset;
-    }
-
-    @Override
     public int getStackBottomInset() {
         return mPaddingBetweenElements + mShelf.getIntrinsicHeight();
     }
@@ -4854,15 +4767,6 @@
         }
     }
 
-    /**
-     * Returns whether or not a History button is shown in the footer. If there is no footer, then
-     * this will return false.
-     **/
-    public boolean isHistoryShown() {
-        FooterViewRefactor.assertInLegacyMode();
-        return mFooterView != null && mFooterView.isHistoryShown();
-    }
-
     /** Bind the {@link FooterView} to the NSSL. */
     public void setFooterView(@NonNull FooterView footerView) {
         int index = -1;
@@ -4872,18 +4776,6 @@
         }
         mFooterView = footerView;
         addView(mFooterView, index);
-        if (!FooterViewRefactor.isEnabled()) {
-            if (mManageButtonClickListener != null) {
-                mFooterView.setManageButtonClickListener(mManageButtonClickListener);
-            }
-            mFooterView.setClearAllButtonClickListener(v -> {
-                if (mFooterClearAllListener != null) {
-                    mFooterClearAllListener.onClearAll();
-                }
-                clearNotifications(ROWS_ALL, true /* closeShade */);
-                footerView.setClearAllButtonVisible(false /* visible */, true /* animate */);
-            });
-        }
     }
 
     public void setEmptyShadeView(EmptyShadeView emptyShadeView) {
@@ -4896,13 +4788,6 @@
         addView(mEmptyShadeView, index);
     }
 
-    /** Legacy version, should be removed with the footer refactor flag. */
-    public void updateEmptyShadeView(boolean visible, boolean areNotificationsHiddenInShade) {
-        FooterViewRefactor.assertInLegacyMode();
-        updateEmptyShadeView(visible, areNotificationsHiddenInShade,
-                mHasFilteredOutSeenNotifications);
-    }
-
     /** Trigger an update for the empty shade resources and visibility. */
     public void updateEmptyShadeView(boolean visible, boolean areNotificationsHiddenInShade,
             boolean hasFilteredOutSeenNotifications) {
@@ -4955,18 +4840,6 @@
         return mEmptyShadeView.isVisible();
     }
 
-    public void updateFooterView(boolean visible, boolean showDismissView, boolean showHistory) {
-        FooterViewRefactor.assertInLegacyMode();
-        if (mFooterView == null || mNotificationStackSizeCalculator == null) {
-            return;
-        }
-        boolean animate = mIsExpanded && mAnimationsEnabled;
-        mFooterView.setVisible(visible, animate);
-        mFooterView.showHistory(showHistory);
-        mFooterView.setClearAllButtonVisible(showDismissView, animate);
-        mFooterView.setFooterLabelVisible(mHasFilteredOutSeenNotifications);
-    }
-
     @VisibleForTesting
     public void setClearAllInProgress(boolean clearAllInProgress) {
         mClearAllInProgress = clearAllInProgress;
@@ -5250,10 +5123,8 @@
 
     public void setQsFullScreen(boolean qsFullScreen) {
         SceneContainerFlag.assertInLegacyMode();
-        if (FooterViewRefactor.isEnabled()) {
-            if (qsFullScreen == mQsFullScreen) {
-                return;  // no change
-            }
+        if (qsFullScreen == mQsFullScreen) {
+            return;  // no change
         }
         mQsFullScreen = qsFullScreen;
         updateAlgorithmLayoutMinHeight();
@@ -5272,8 +5143,6 @@
 
     public void setQsExpansionFraction(float qsExpansionFraction) {
         SceneContainerFlag.assertInLegacyMode();
-        boolean footerAffected = getQsExpansionFraction() != qsExpansionFraction
-                && (getQsExpansionFraction() == 1 || qsExpansionFraction == 1);
         mQsExpansionFraction = qsExpansionFraction;
         updateUseRoundedRectClipping();
 
@@ -5282,9 +5151,6 @@
         if (getOwnScrollY() > 0) {
             setOwnScrollY((int) MathUtils.lerp(getOwnScrollY(), 0, getQsExpansionFraction()));
         }
-        if (!FooterViewRefactor.isEnabled() && footerAffected) {
-            updateFooter();
-        }
     }
 
     @VisibleForTesting
@@ -5462,14 +5328,6 @@
         requestChildrenUpdate();
     }
 
-    void setUpcomingStatusBarState(int upcomingStatusBarState) {
-        FooterViewRefactor.assertInLegacyMode();
-        mUpcomingStatusBarState = upcomingStatusBarState;
-        if (mUpcomingStatusBarState != mStatusBarState) {
-            updateFooter();
-        }
-    }
-
     void onStatePostChange(boolean fromShadeLocked) {
         boolean onKeyguard = onKeyguard();
 
@@ -5478,9 +5336,6 @@
         }
 
         setExpandingEnabled(!onKeyguard);
-        if (!FooterViewRefactor.isEnabled()) {
-            updateFooter();
-        }
         requestChildrenUpdate();
         onUpdateRowStates();
         updateVisibility();
@@ -5496,8 +5351,7 @@
         if (mEmptyShadeView == null || mEmptyShadeView.getVisibility() == GONE) {
             return getMinExpansionHeight();
         } else {
-            return FooterViewRefactor.isEnabled() ? getAppearEndPosition()
-                    : getAppearEndPositionLegacy();
+            return getAppearEndPosition();
         }
     }
 
@@ -5589,12 +5443,6 @@
                     for (int i = 0; i < childCount; i++) {
                         ExpandableView child = getChildAtIndex(i);
                         child.dump(pw, args);
-                        if (!FooterViewRefactor.isEnabled()) {
-                            if (child instanceof FooterView) {
-                                DumpUtilsKt.withIncreasedIndent(pw,
-                                        () -> dumpFooterViewVisibility(pw));
-                            }
-                        }
                         pw.println();
                     }
                     int transientViewCount = getTransientViewCount();
@@ -5621,45 +5469,6 @@
         pw.append(" bottomRadius=").println(mBgCornerRadii[4]);
     }
 
-    private void dumpFooterViewVisibility(IndentingPrintWriter pw) {
-        FooterViewRefactor.assertInLegacyMode();
-        final boolean showDismissView = shouldShowDismissView();
-
-        pw.println("showFooterView: " + shouldShowFooterView(showDismissView));
-        DumpUtilsKt.withIncreasedIndent(
-                pw,
-                () -> {
-                    pw.println("showDismissView: " + showDismissView);
-                    DumpUtilsKt.withIncreasedIndent(
-                            pw,
-                            () -> {
-                                pw.println(
-                                        "hasActiveClearableNotifications: "
-                                                + mController.hasActiveClearableNotifications(
-                                                        ROWS_ALL));
-                            });
-                    pw.println();
-                    pw.println("showHistory: " + mController.isHistoryEnabled());
-                    pw.println();
-                    pw.println(
-                            "visibleNotificationCount: "
-                                    + mController.getVisibleNotificationCount());
-                    pw.println("mIsCurrentUserSetup: " + mIsCurrentUserSetup);
-                    pw.println("onKeyguard: " + onKeyguard());
-                    pw.println("mUpcomingStatusBarState: " + mUpcomingStatusBarState);
-                    if (!SceneContainerFlag.isEnabled()) {
-                        pw.println("QsExpansionFraction: " + getQsExpansionFraction());
-                    }
-                    pw.println("mQsFullScreen: " + mQsFullScreen);
-                    pw.println(
-                            "mScreenOffAnimationController"
-                                    + ".shouldHideNotificationsFooter: "
-                                    + mScreenOffAnimationController
-                                            .shouldHideNotificationsFooter());
-                    pw.println("mIsRemoteInputActive: " + mIsRemoteInputActive);
-                });
-    }
-
     public boolean isFullyHidden() {
         return mAmbientState.isFullyHidden();
     }
@@ -5770,14 +5579,6 @@
         clearNotifications(ROWS_GENTLE, closeShade, hideSilentSection);
     }
 
-    /** Legacy version of clearNotifications below. Uses the old data source for notif stats. */
-    void clearNotifications(@SelectedRows int selection, boolean closeShade) {
-        FooterViewRefactor.assertInLegacyMode();
-        final boolean hideSilentSection = !mController.hasNotifications(
-                ROWS_GENTLE, false /* clearable */);
-        clearNotifications(selection, closeShade, hideSilentSection);
-    }
-
     /**
      * Collects a list of visible rows, and animates them away in a staggered fashion as if they
      * were dismissed. Notifications are dismissed in the backend via onClearAllAnimationsEnd.
@@ -5832,25 +5633,6 @@
         return canChildBeCleared(row) && matchesSelection(row, selection);
     }
 
-    /**
-     * Register a {@link View.OnClickListener} to be invoked when the Manage button is clicked.
-     */
-    public void setManageButtonClickListener(@Nullable OnClickListener listener) {
-        FooterViewRefactor.assertInLegacyMode();
-        mManageButtonClickListener = listener;
-        if (mFooterView != null) {
-            mFooterView.setManageButtonClickListener(mManageButtonClickListener);
-        }
-    }
-
-    @VisibleForTesting
-    protected void inflateFooterView() {
-        FooterViewRefactor.assertInLegacyMode();
-        FooterView footerView = (FooterView) LayoutInflater.from(mContext).inflate(
-                R.layout.status_bar_notification_footer, this, false);
-        setFooterView(footerView);
-    }
-
     private void inflateEmptyShadeView() {
         ModesEmptyShadeFix.assertInLegacyMode();
 
@@ -6097,11 +5879,6 @@
         mHighPriorityBeforeSpeedBump = highPriorityBeforeSpeedBump;
     }
 
-    void setFooterClearAllListener(FooterClearAllListener listener) {
-        FooterViewRefactor.assertInLegacyMode();
-        mFooterClearAllListener = listener;
-    }
-
     void setClearAllFinishedWhilePanelExpandedRunnable(Runnable runnable) {
         mClearAllFinishedWhilePanelExpandedRunnable = runnable;
     }
@@ -6400,17 +6177,6 @@
     }
 
     /**
-     * Sets whether the current user is set up, which is required to show the footer (b/193149550)
-     */
-    public void setCurrentUserSetup(boolean isCurrentUserSetup) {
-        FooterViewRefactor.assertInLegacyMode();
-        if (mIsCurrentUserSetup != isCurrentUserSetup) {
-            mIsCurrentUserSetup = isCurrentUserSetup;
-            updateFooter();
-        }
-    }
-
-    /**
      * Sets a {@link StackStateLogger} which is notified as the {@link StackStateAnimator} updates
      * the views.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index a33a9ed..c717e3b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -29,15 +29,14 @@
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnEmptySpaceClickListener;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnOverscrollTopChangedListener;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL;
-import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_GENTLE;
-import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_HIGH_PRIORITY;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.SelectedRows;
 import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_STANDARD;
-import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
 
 import android.animation.ObjectAnimator;
 import android.content.res.Configuration;
 import android.graphics.Point;
+import android.graphics.RenderEffect;
+import android.graphics.Shader;
 import android.os.Trace;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -64,14 +63,10 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.ExpandHelper;
 import com.android.systemui.Gefingerpoken;
-import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.classifier.Classifier;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository;
-import com.android.systemui.keyguard.shared.model.KeyguardState;
-import com.android.systemui.keyguard.shared.model.TransitionStep;
 import com.android.systemui.media.controls.ui.controller.KeyguardMediaController;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
@@ -92,18 +87,13 @@
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager.UserChangedListener;
-import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShelf;
 import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.ColorUpdateLogger;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
-import com.android.systemui.statusbar.notification.headsup.HeadsUpNotificationViewControllerEmptyImpl;
-import com.android.systemui.statusbar.notification.headsup.HeadsUpTouchHelper;
-import com.android.systemui.statusbar.notification.headsup.HeadsUpTouchHelper.HeadsUpNotificationViewController;
 import com.android.systemui.statusbar.notification.LaunchAnimationParameters;
-import com.android.systemui.statusbar.notification.NotificationActivityStarter;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
 import com.android.systemui.statusbar.notification.collection.EntryWithDismissStats;
 import com.android.systemui.statusbar.notification.collection.NotifCollection;
@@ -116,13 +106,12 @@
 import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider;
 import com.android.systemui.statusbar.notification.collection.provider.VisibilityLocationProviderDelegator;
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
-import com.android.systemui.statusbar.notification.collection.render.NotifStackController;
-import com.android.systemui.statusbar.notification.collection.render.NotifStats;
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
-import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
-import com.android.systemui.statusbar.notification.dagger.SilentHeader;
-import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor;
-import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
+import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
+import com.android.systemui.statusbar.notification.headsup.HeadsUpNotificationViewControllerEmptyImpl;
+import com.android.systemui.statusbar.notification.headsup.HeadsUpTouchHelper;
+import com.android.systemui.statusbar.notification.headsup.HeadsUpTouchHelper.HeadsUpNotificationViewController;
+import com.android.systemui.statusbar.notification.headsup.OnHeadsUpChangedListener;
 import com.android.systemui.statusbar.notification.init.NotificationsController;
 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
@@ -137,13 +126,8 @@
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
-import com.android.systemui.statusbar.policy.DeviceProvisionedController;
-import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
-import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
-import com.android.systemui.statusbar.notification.headsup.OnHeadsUpChangedListener;
 import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController;
 import com.android.systemui.statusbar.policy.SplitShadeStateController;
-import com.android.systemui.statusbar.policy.ZenModeController;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.util.Compile;
 import com.android.systemui.util.settings.SecureSettings;
@@ -179,10 +163,8 @@
     private HeadsUpTouchHelper mHeadsUpTouchHelper;
     private final NotificationRoundnessManager mNotificationRoundnessManager;
     private final TunerService mTunerService;
-    private final DeviceProvisionedController mDeviceProvisionedController;
     private final DynamicPrivacyController mDynamicPrivacyController;
     private final ConfigurationController mConfigurationController;
-    private final ZenModeController mZenModeController;
     private final MetricsLogger mMetricsLogger;
     private final ColorUpdateLogger mColorUpdateLogger;
 
@@ -193,7 +175,6 @@
     private final NotifPipeline mNotifPipeline;
     private final NotifCollection mNotifCollection;
     private final UiEventLogger mUiEventLogger;
-    private final NotificationRemoteInputManager mRemoteInputManager;
     private final VisibilityLocationProviderDelegator mVisibilityLocationProviderDelegator;
     private final ShadeController mShadeController;
     private final Provider<WindowRootView> mWindowRootView;
@@ -201,9 +182,7 @@
     private final SysuiStatusBarStateController mStatusBarStateController;
     private final KeyguardBypassController mKeyguardBypassController;
     private final PowerInteractor mPowerInteractor;
-    private final PrimaryBouncerInteractor mPrimaryBouncerInteractor;
     private final NotificationLockscreenUserManager mLockscreenUserManager;
-    private final SectionHeaderController mSilentHeaderController;
     private final LockscreenShadeTransitionController mLockscreenShadeTransitionController;
     private final InteractionJankMonitor mJankMonitor;
     private final NotificationStackSizeCalculator mNotificationStackSizeCalculator;
@@ -211,8 +190,6 @@
     private final NotificationStackScrollLogger mLogger;
 
     private final GroupExpansionManager mGroupExpansionManager;
-    private final SeenNotificationsInteractor mSeenNotificationsInteractor;
-    private final KeyguardTransitionRepository mKeyguardTransitionRepo;
     private NotificationStackScrollLayout mView;
     private TouchHandler mTouchHandler;
     private NotificationSwipeHelper mSwipeHelper;
@@ -220,7 +197,6 @@
     private Boolean mHistoryEnabled;
     private int mBarState;
     private HeadsUpAppearanceController mHeadsUpAppearanceController;
-    private boolean mIsInTransitionToAod = false;
 
     private final NotificationTargetsHelper mNotificationTargetsHelper;
     private final SecureSettings mSecureSettings;
@@ -235,11 +211,6 @@
 
     private final NotificationListContainerImpl mNotificationListContainer =
             new NotificationListContainerImpl();
-    private final NotifStackController mNotifStackController =
-            new NotifStackControllerImpl();
-
-    @Nullable
-    private NotificationActivityStarter mNotificationActivityStarter;
 
     @VisibleForTesting
     final View.OnAttachStateChangeListener mOnAttachStateChangeListener =
@@ -248,9 +219,6 @@
                 public void onViewAttachedToWindow(View v) {
                     mColorUpdateLogger.logTriggerEvent("NSSLC.onViewAttachedToWindow()");
                     mConfigurationController.addCallback(mConfigurationListener);
-                    if (!FooterViewRefactor.isEnabled()) {
-                        mZenModeController.addCallback(mZenModeControllerCallback);
-                    }
                     final int newBarState = mStatusBarStateController.getState();
                     if (newBarState != mBarState) {
                         mStateListener.onStateChanged(newBarState);
@@ -264,9 +232,6 @@
                 public void onViewDetachedFromWindow(View v) {
                     mColorUpdateLogger.logTriggerEvent("NSSLC.onViewDetachedFromWindow()");
                     mConfigurationController.removeCallback(mConfigurationListener);
-                    if (!FooterViewRefactor.isEnabled()) {
-                        mZenModeController.removeCallback(mZenModeControllerCallback);
-                    }
                     mStatusBarStateController.removeCallback(mStateListener);
                 }
             };
@@ -287,28 +252,6 @@
     @Nullable
     private ObjectAnimator mHideAlphaAnimator = null;
 
-    private final DeviceProvisionedListener mDeviceProvisionedListener =
-            new DeviceProvisionedListener() {
-                @Override
-                public void onDeviceProvisionedChanged() {
-                    updateCurrentUserIsSetup();
-                }
-
-                @Override
-                public void onUserSwitched() {
-                    updateCurrentUserIsSetup();
-                }
-
-                @Override
-                public void onUserSetupChanged() {
-                    updateCurrentUserIsSetup();
-                }
-
-                private void updateCurrentUserIsSetup() {
-                    mView.setCurrentUserSetup(mDeviceProvisionedController.isCurrentUserSetup());
-                }
-            };
-
     private final Runnable mSensitiveStateChangedListener = new Runnable() {
         @Override
         public void run() {
@@ -318,20 +261,10 @@
         }
     };
 
-    private final DynamicPrivacyController.Listener mDynamicPrivacyControllerListener = () -> {
-        if (!FooterViewRefactor.isEnabled()) {
-            // Let's update the footer once the notifications have been updated (in the next frame)
-            mView.post(this::updateFooter);
-        }
-    };
-
     @VisibleForTesting
     final ConfigurationListener mConfigurationListener = new ConfigurationListener() {
         @Override
         public void onDensityOrFontScaleChanged() {
-            if (!FooterViewRefactor.isEnabled()) {
-                updateShowEmptyShadeView();
-            }
             mView.reinflateViews();
         }
 
@@ -351,10 +284,6 @@
             mView.updateBgColor();
             mView.updateDecorViews();
             mView.reinflateViews();
-            if (!FooterViewRefactor.isEnabled()) {
-                updateShowEmptyShadeView();
-                updateFooter();
-            }
         }
 
         @Override
@@ -363,7 +292,6 @@
         }
     };
 
-    private NotifStats mNotifStats = NotifStats.getEmpty();
     private float mMaxAlphaForKeyguard = 1.0f;
     private String mMaxAlphaForKeyguardSource = "constructor";
     private float mMaxAlphaForUnhide = 1.0f;
@@ -401,19 +329,9 @@
                 }
 
                 @Override
-                public void onUpcomingStateChanged(int newState) {
-                    if (!FooterViewRefactor.isEnabled()) {
-                        mView.setUpcomingStatusBarState(newState);
-                    }
-                }
-
-                @Override
                 public void onStatePostChange() {
                     updateSensitivenessWithAnimation(mStatusBarStateController.goingToFullShade());
                     mView.onStatePostChange(mStatusBarStateController.fromShadeLocked());
-                    if (!FooterViewRefactor.isEnabled()) {
-                        updateImportantForAccessibility();
-                    }
                 }
             };
 
@@ -422,9 +340,6 @@
         public void onUserChanged(int userId) {
             updateSensitivenessWithAnimation(false);
             mHistoryEnabled = null;
-            if (!FooterViewRefactor.isEnabled()) {
-                updateFooter();
-            }
         }
     };
 
@@ -656,7 +571,7 @@
                                 == null) {
                             mHeadsUpManager.removeNotification(
                                     row.getEntry().getSbn().getKey(),
-                                    /* removeImmediately= */ true ,
+                                    /* removeImmediately= */ true,
                                     /* reason= */ "onChildSnappedBack"
                             );
                         }
@@ -714,14 +629,6 @@
                 }
             };
 
-    private final ZenModeController.Callback mZenModeControllerCallback =
-            new ZenModeController.Callback() {
-                @Override
-                public void onZenChanged(int zen) {
-                    updateShowEmptyShadeView();
-                }
-            };
-
     @Inject
     public NotificationStackScrollLayoutController(
             NotificationStackScrollLayout view,
@@ -734,16 +641,12 @@
             Provider<IStatusBarService> statusBarService,
             NotificationRoundnessManager notificationRoundnessManager,
             TunerService tunerService,
-            DeviceProvisionedController deviceProvisionedController,
             DynamicPrivacyController dynamicPrivacyController,
             @ShadeDisplayAware ConfigurationController configurationController,
             SysuiStatusBarStateController statusBarStateController,
             KeyguardMediaController keyguardMediaController,
             KeyguardBypassController keyguardBypassController,
             PowerInteractor powerInteractor,
-            PrimaryBouncerInteractor primaryBouncerInteractor,
-            KeyguardTransitionRepository keyguardTransitionRepo,
-            ZenModeController zenModeController,
             NotificationLockscreenUserManager lockscreenUserManager,
             MetricsLogger metricsLogger,
             ColorUpdateLogger colorUpdateLogger,
@@ -752,14 +655,11 @@
             FalsingManager falsingManager,
             NotificationSwipeHelper.Builder notificationSwipeHelperBuilder,
             GroupExpansionManager groupManager,
-            @SilentHeader SectionHeaderController silentHeaderController,
             NotifPipeline notifPipeline,
             NotifCollection notifCollection,
             LockscreenShadeTransitionController lockscreenShadeTransitionController,
             UiEventLogger uiEventLogger,
-            NotificationRemoteInputManager remoteInputManager,
             VisibilityLocationProviderDelegator visibilityLocationProviderDelegator,
-            SeenNotificationsInteractor seenNotificationsInteractor,
             NotificationListViewBinder viewBinder,
             ShadeController shadeController,
             Provider<WindowRootView> windowRootView,
@@ -775,7 +675,6 @@
             SensitiveNotificationProtectionController sensitiveNotificationProtectionController,
             WallpaperInteractor wallpaperInteractor) {
         mView = view;
-        mKeyguardTransitionRepo = keyguardTransitionRepo;
         mViewBinder = viewBinder;
         mStackStateLogger = stackLogger;
         mLogger = logger;
@@ -795,15 +694,12 @@
         }
         mNotificationRoundnessManager = notificationRoundnessManager;
         mTunerService = tunerService;
-        mDeviceProvisionedController = deviceProvisionedController;
         mDynamicPrivacyController = dynamicPrivacyController;
         mConfigurationController = configurationController;
         mStatusBarStateController = statusBarStateController;
         mKeyguardMediaController = keyguardMediaController;
         mKeyguardBypassController = keyguardBypassController;
         mPowerInteractor = powerInteractor;
-        mPrimaryBouncerInteractor = primaryBouncerInteractor;
-        mZenModeController = zenModeController;
         mLockscreenUserManager = lockscreenUserManager;
         mMetricsLogger = metricsLogger;
         mColorUpdateLogger = colorUpdateLogger;
@@ -815,13 +711,10 @@
         mJankMonitor = jankMonitor;
         mNotificationStackSizeCalculator = notificationStackSizeCalculator;
         mGroupExpansionManager = groupManager;
-        mSilentHeaderController = silentHeaderController;
         mNotifPipeline = notifPipeline;
         mNotifCollection = notifCollection;
         mUiEventLogger = uiEventLogger;
-        mRemoteInputManager = remoteInputManager;
         mVisibilityLocationProviderDelegator = visibilityLocationProviderDelegator;
-        mSeenNotificationsInteractor = seenNotificationsInteractor;
         mShadeController = shadeController;
         mWindowRootView = windowRootView;
         mNotificationTargetsHelper = notificationTargetsHelper;
@@ -850,18 +743,7 @@
         mView.setClearAllAnimationListener(this::onAnimationEnd);
         mView.setClearAllListener((selection) -> mUiEventLogger.log(
                 NotificationPanelEvent.fromSelection(selection)));
-        if (!FooterViewRefactor.isEnabled()) {
-            mView.setFooterClearAllListener(() ->
-                    mMetricsLogger.action(MetricsEvent.ACTION_DISMISS_ALL_NOTES));
-            mView.setIsRemoteInputActive(mRemoteInputManager.isRemoteInputActive());
-            mRemoteInputManager.addControllerCallback(new RemoteInputController.Callback() {
-                @Override
-                public void onRemoteInputActive(boolean active) {
-                    mView.setIsRemoteInputActive(active);
-                }
-            });
-        }
-        mView.setClearAllFinishedWhilePanelExpandedRunnable(()-> {
+        mView.setClearAllFinishedWhilePanelExpandedRunnable(() -> {
             final Runnable doCollapseRunnable = () ->
                     mShadeController.animateCollapseShade(CommandQueue.FLAG_EXCLUDE_NONE);
             mView.postDelayed(doCollapseRunnable, /* delayMillis = */ DELAY_BEFORE_SHADE_CLOSE);
@@ -889,19 +771,11 @@
         mView.setKeyguardBypassEnabled(mKeyguardBypassController.getBypassEnabled());
         mKeyguardBypassController
                 .registerOnBypassStateChangedListener(mView::setKeyguardBypassEnabled);
-        if (!FooterViewRefactor.isEnabled()) {
-            mView.setManageButtonClickListener(v -> {
-                if (mNotificationActivityStarter != null) {
-                    mNotificationActivityStarter.startHistoryIntent(v, mView.isHistoryShown());
-                }
-            });
-        }
 
         if (!SceneContainerFlag.isEnabled()) {
             mHeadsUpManager.addListener(mOnHeadsUpChangedListener);
         }
         mHeadsUpManager.setAnimationStateHandler(mView::setHeadsUpGoingAwayAnimationsAllowed);
-        mDynamicPrivacyController.addListener(mDynamicPrivacyControllerListener);
 
         mLockscreenShadeTransitionController.setStackScroller(this);
 
@@ -914,9 +788,6 @@
                     switch (key) {
                         case Settings.Secure.NOTIFICATION_HISTORY_ENABLED:
                             mHistoryEnabled = null;  // invalidate
-                            if (!FooterViewRefactor.isEnabled()) {
-                                updateFooter();
-                            }
                             break;
                         case HIGH_PRIORITY:
                             mView.setHighPriorityBeforeSpeedBump("1".equals(newValue));
@@ -938,12 +809,6 @@
             return kotlin.Unit.INSTANCE;
         });
 
-        if (!FooterViewRefactor.isEnabled()) {
-            // attach callback, and then call it to update mView immediately
-            mDeviceProvisionedController.addCallback(mDeviceProvisionedListener);
-            mDeviceProvisionedListener.onDeviceProvisionedChanged();
-        }
-
         if (screenshareNotificationHiding()) {
             mSensitiveNotificationProtectionController
                     .registerSensitiveStateListener(mSensitiveStateChangedListener);
@@ -953,20 +818,12 @@
             mOnAttachStateChangeListener.onViewAttachedToWindow(mView);
         }
         mView.addOnAttachStateChangeListener(mOnAttachStateChangeListener);
-        if (!FooterViewRefactor.isEnabled()) {
-            mSilentHeaderController.setOnClearSectionClickListener(v -> clearSilentNotifications());
-        }
 
         mGroupExpansionManager.registerGroupExpansionChangeListener(
                 (changedRow, expanded) -> mView.onGroupExpandChanged(changedRow, expanded));
 
         mViewBinder.bindWhileAttached(mView, this);
 
-        if (!FooterViewRefactor.isEnabled()) {
-            collectFlow(mView, mKeyguardTransitionRepo.getTransitions(),
-                    this::onKeyguardTransitionChanged);
-        }
-
         mView.setWallpaperInteractor(mWallpaperInteractor);
     }
 
@@ -1168,11 +1025,6 @@
         return mView != null && mView.isAddOrRemoveAnimationPending();
     }
 
-    public int getVisibleNotificationCount() {
-        FooterViewRefactor.assertInLegacyMode();
-        return mNotifStats.getNumActiveNotifs();
-    }
-
     public boolean isHistoryEnabled() {
         Boolean historyEnabled = mHistoryEnabled;
         if (historyEnabled == null) {
@@ -1284,9 +1136,6 @@
 
     public void setQsFullScreen(boolean fullScreen) {
         mView.setQsFullScreen(fullScreen);
-        if (!FooterViewRefactor.isEnabled()) {
-            updateShowEmptyShadeView();
-        }
     }
 
     public void setScrollingEnabled(boolean enabled) {
@@ -1390,6 +1239,22 @@
         updateAlpha();
     }
 
+    /**
+     * Applies a blur effect to the view.
+     *
+     * @param blurRadius Radius of blur
+     */
+    public void setBlurRadius(float blurRadius) {
+        if (blurRadius > 0.0f) {
+            mView.setRenderEffect(RenderEffect.createBlurEffect(
+                    blurRadius,
+                    blurRadius,
+                    Shader.TileMode.CLAMP));
+        } else {
+            mView.setRenderEffect(null);
+        }
+    }
+
     private void updateAlpha() {
         if (mView != null) {
             mView.setAlpha(Math.min(Math.min(mMaxAlphaFromView, mMaxAlphaForKeyguard),
@@ -1464,64 +1329,12 @@
     }
 
     /**
-     * Set the visibility of the view, and propagate it to specific children.
+     * Set the visibility of the view.
      *
      * @param visible either the view is visible or not.
      */
     public void updateVisibility(boolean visible) {
         mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
-
-        // Refactor note: the empty shade's visibility doesn't seem to actually depend on the
-        // parent visibility (so this update seemingly doesn't do anything). Therefore, this is not
-        // modeled in the refactored code.
-        if (!FooterViewRefactor.isEnabled() && mView.getVisibility() == View.VISIBLE) {
-            // Synchronize EmptyShadeView visibility with the parent container.
-            updateShowEmptyShadeView();
-            updateImportantForAccessibility();
-        }
-    }
-
-    /**
-     * Update whether we should show the empty shade view ("no notifications" in the shade).
-     * <p>
-     * When in split mode, notifications are always visible regardless of the state of the
-     * QuickSettings panel. That being the case, empty view is always shown if the other conditions
-     * are true.
-     */
-    public void updateShowEmptyShadeView() {
-        FooterViewRefactor.assertInLegacyMode();
-
-        Trace.beginSection("NSSLC.updateShowEmptyShadeView");
-
-        final boolean shouldShow = getVisibleNotificationCount() == 0
-                && !mView.isQsFullScreen()
-                // Hide empty shade view when in transition to AOD.
-                // That avoids "No Notifications" to blink when transitioning to AOD.
-                // For more details, see: b/228790482
-                && !mIsInTransitionToAod
-                // Don't show any notification content if the bouncer is showing. See b/267060171.
-                && !mPrimaryBouncerInteractor.isBouncerShowing();
-
-        mView.updateEmptyShadeView(shouldShow, mZenModeController.areNotificationsHiddenInShade());
-
-        Trace.endSection();
-    }
-
-    /**
-     * Update the importantForAccessibility of NotificationStackScrollLayout.
-     * <p>
-     * We want the NSSL to be unimportant for accessibility when there's no
-     * notifications in it while the device is on lock screen, to avoid unlablel NSSL view.
-     * Otherwise, we want it to be important for accessibility to enable accessibility
-     * auto-scrolling in NSSL.
-     */
-    public void updateImportantForAccessibility() {
-        FooterViewRefactor.assertInLegacyMode();
-        if (getVisibleNotificationCount() == 0 && mView.onKeyguard()) {
-            mView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
-        } else {
-            mView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
-        }
     }
 
     public boolean isShowingEmptyShadeView() {
@@ -1577,34 +1390,6 @@
         mView.setPulsing(pulsing, animatePulse);
     }
 
-    /**
-     * Return whether there are any clearable notifications
-     */
-    public boolean hasActiveClearableNotifications(@SelectedRows int selection) {
-        FooterViewRefactor.assertInLegacyMode();
-        return hasNotifications(selection, true /* clearable */);
-    }
-
-    public boolean hasNotifications(@SelectedRows int selection, boolean isClearable) {
-        FooterViewRefactor.assertInLegacyMode();
-        boolean hasAlertingMatchingClearable = isClearable
-                ? mNotifStats.getHasClearableAlertingNotifs()
-                : mNotifStats.getHasNonClearableAlertingNotifs();
-        boolean hasSilentMatchingClearable = isClearable
-                ? mNotifStats.getHasClearableSilentNotifs()
-                : mNotifStats.getHasNonClearableSilentNotifs();
-        switch (selection) {
-            case ROWS_GENTLE:
-                return hasSilentMatchingClearable;
-            case ROWS_HIGH_PRIORITY:
-                return hasAlertingMatchingClearable;
-            case ROWS_ALL:
-                return hasSilentMatchingClearable || hasAlertingMatchingClearable;
-            default:
-                throw new IllegalStateException("Bad selection: " + selection);
-        }
-    }
-
     /** Sets whether the NSSL is displayed over the unoccluded Lockscreen. */
     public void setOnLockscreen(boolean isOnLockscreen) {
         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
@@ -1637,9 +1422,6 @@
                 }
                 mHeadsUpManager.setRemoteInputActive(entry, remoteInputActive);
                 entry.notifyHeightChanged(true /* needsAnimation */);
-                if (!FooterViewRefactor.isEnabled()) {
-                    updateFooter();
-                }
             }
 
             public void lockScrollTo(NotificationEntry entry) {
@@ -1662,13 +1444,6 @@
         };
     }
 
-    public void updateFooter() {
-        FooterViewRefactor.assertInLegacyMode();
-        Trace.beginSection("NSSLC.updateFooter");
-        mView.updateFooter();
-        Trace.endSection();
-    }
-
     public void onUpdateRowStates() {
         mView.onUpdateRowStates();
     }
@@ -1695,18 +1470,10 @@
         return mView.getTransientViewCount();
     }
 
-    public View getTransientView(int i) {
-        return mView.getTransientView(i);
-    }
-
     public NotificationStackScrollLayout getView() {
         return mView;
     }
 
-    public float calculateGapHeight(ExpandableView previousView, ExpandableView child, int count) {
-        return mView.calculateGapHeight(previousView, child, count);
-    }
-
     NotificationRoundnessManager getNotificationRoundnessManager() {
         return mNotificationRoundnessManager;
     }
@@ -1715,10 +1482,6 @@
         return mNotificationListContainer;
     }
 
-    public NotifStackController getNotifStackController() {
-        return mNotifStackController;
-    }
-
     public void resetCheckSnoozeLeavebehind() {
         mView.resetCheckSnoozeLeavebehind();
     }
@@ -1772,13 +1535,6 @@
         return NotificationSwipeHelper.isTouchInView(event, view);
     }
 
-    public void clearSilentNotifications() {
-        FooterViewRefactor.assertInLegacyMode();
-        // Leave the shade open if there will be other notifs left over to clear
-        final boolean closeShade = !hasActiveClearableNotifications(ROWS_HIGH_PRIORITY);
-        mView.clearNotifications(ROWS_GENTLE, closeShade);
-    }
-
     private void onAnimationEnd(List<ExpandableNotificationRow> viewsToRemove,
             @SelectedRows int selectedRows) {
         if (selectedRows == ROWS_ALL) {
@@ -1880,10 +1636,6 @@
         mView.animateNextTopPaddingChange();
     }
 
-    public void setNotificationActivityStarter(NotificationActivityStarter activityStarter) {
-        mNotificationActivityStarter = activityStarter;
-    }
-
     public NotificationTargetsHelper getNotificationTargetsHelper() {
         return mNotificationTargetsHelper;
     }
@@ -1898,18 +1650,6 @@
     }
 
     @VisibleForTesting
-    void onKeyguardTransitionChanged(TransitionStep transitionStep) {
-        FooterViewRefactor.assertInLegacyMode();
-        boolean isTransitionToAod = transitionStep.getTo().equals(KeyguardState.AOD)
-                && (transitionStep.getFrom().equals(KeyguardState.GONE)
-                || transitionStep.getFrom().equals(KeyguardState.OCCLUDED));
-        if (mIsInTransitionToAod != isTransitionToAod) {
-            mIsInTransitionToAod = isTransitionToAod;
-            updateShowEmptyShadeView();
-        }
-    }
-
-    @VisibleForTesting
     TouchHandler getTouchHandler() {
         return mTouchHandler;
     }
@@ -2288,22 +2028,4 @@
                     && !mSwipeHelper.isSwiping();
         }
     }
-
-    private class NotifStackControllerImpl implements NotifStackController {
-        @Override
-        public void setNotifStats(@NonNull NotifStats notifStats) {
-            FooterViewRefactor.assertInLegacyMode();
-            mNotifStats = notifStats;
-
-            if (!FooterViewRefactor.isEnabled()) {
-                mView.setHasFilteredOutSeenNotifications(
-                        mSeenNotificationsInteractor
-                                .getHasFilteredOutSeenNotifications().getValue());
-
-                updateFooter();
-                updateShowEmptyShadeView();
-                updateImportantForAccessibility();
-            }
-        }
-    }
 }
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 1653029..06b989a 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
@@ -35,7 +35,6 @@
 import com.android.systemui.statusbar.NotificationShelf;
 import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView;
-import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -463,26 +462,23 @@
                 if (v == ambientState.getShelf()) {
                     continue;
                 }
-                if (FooterViewRefactor.isEnabled()) {
-                    if (v instanceof EmptyShadeView) {
-                        emptyShadeVisible = true;
-                    }
-                    if (v instanceof FooterView footerView) {
-                        if (emptyShadeVisible || notGoneIndex == 0) {
-                            // if the empty shade is visible or the footer is the first visible
-                            // view, we're in a transitory state so let's leave the footer alone.
-                            if (Flags.notificationsFooterVisibilityFix()
-                                    && !SceneContainerFlag.isEnabled()) {
-                                // ...except for the hidden state, to prevent it from flashing on
-                                // the screen (this piece is copied from updateChild, and is not
-                                // necessary in flexiglass).
-                                if (footerView.shouldBeHidden()
-                                        || !ambientState.isShadeExpanded()) {
-                                    footerView.getViewState().hidden = true;
-                                }
+                if (v instanceof EmptyShadeView) {
+                    emptyShadeVisible = true;
+                }
+                if (v instanceof FooterView footerView) {
+                    if (emptyShadeVisible || notGoneIndex == 0) {
+                        // if the empty shade is visible or the footer is the first visible
+                        // view, we're in a transitory state so let's leave the footer alone.
+                        if (Flags.notificationsFooterVisibilityFix()
+                                && !SceneContainerFlag.isEnabled()) {
+                            // ...except for the hidden state, to prevent it from flashing on
+                            // the screen (this piece is copied from updateChild, and is not
+                            // necessary in flexiglass).
+                            if (footerView.shouldBeHidden() || !ambientState.isShadeExpanded()) {
+                                footerView.getViewState().hidden = true;
                             }
-                            continue;
                         }
+                        continue;
                     }
                 }
 
@@ -699,44 +695,28 @@
                 viewEnd, /* hunMax */ ambientState.getMaxHeadsUpTranslation()
         );
         if (view instanceof FooterView) {
-            if (FooterViewRefactor.isEnabled()) {
-                if (SceneContainerFlag.isEnabled()) {
-                    final float footerEnd =
-                            stackTop + viewState.getYTranslation() + view.getIntrinsicHeight();
-                    final boolean noSpaceForFooter = footerEnd > ambientState.getStackCutoff();
-                    ((FooterView.FooterViewState) viewState).hideContent =
-                            noSpaceForFooter || (ambientState.isClearAllInProgress()
-                                    && !hasNonClearableNotifs(algorithmState));
-                } else {
-                    // TODO(b/333445519): shouldBeHidden should reflect whether the shade is closed
-                    //  already, so we shouldn't need to use ambientState here. However,
-                    //  currently it doesn't get updated quickly enough and can cause the footer to
-                    //  flash when closing the shade. As such, we temporarily also check the
-                    //  ambientState directly.
-                    if (((FooterView) view).shouldBeHidden() || !ambientState.isShadeExpanded()) {
-                        viewState.hidden = true;
-                    } else {
-                        final float footerEnd = algorithmState.mCurrentExpandedYPosition
-                                + view.getIntrinsicHeight();
-                        final boolean noSpaceForFooter =
-                                footerEnd > ambientState.getStackEndHeight();
-                        ((FooterView.FooterViewState) viewState).hideContent =
-                                noSpaceForFooter || (ambientState.isClearAllInProgress()
-                                        && !hasNonClearableNotifs(algorithmState));
-                    }
-                }
+            if (SceneContainerFlag.isEnabled()) {
+                final float footerEnd =
+                        stackTop + viewState.getYTranslation() + view.getIntrinsicHeight();
+                final boolean noSpaceForFooter = footerEnd > ambientState.getStackCutoff();
+                ((FooterView.FooterViewState) viewState).hideContent =
+                        noSpaceForFooter || (ambientState.isClearAllInProgress()
+                                && !hasNonClearableNotifs(algorithmState));
             } else {
-                final boolean shadeClosed = !ambientState.isShadeExpanded();
-                final boolean isShelfShowing = algorithmState.firstViewInShelf != null;
-                if (shadeClosed) {
+                // TODO(b/333445519): shouldBeHidden should reflect whether the shade is closed
+                //  already, so we shouldn't need to use ambientState here. However,
+                //  currently it doesn't get updated quickly enough and can cause the footer to
+                //  flash when closing the shade. As such, we temporarily also check the
+                //  ambientState directly.
+                if (((FooterView) view).shouldBeHidden() || !ambientState.isShadeExpanded()) {
                     viewState.hidden = true;
                 } else {
                     final float footerEnd = algorithmState.mCurrentExpandedYPosition
                             + view.getIntrinsicHeight();
-                    final boolean noSpaceForFooter = footerEnd > ambientState.getStackEndHeight();
+                    final boolean noSpaceForFooter =
+                            footerEnd > ambientState.getStackEndHeight();
                     ((FooterView.FooterViewState) viewState).hideContent =
-                            isShelfShowing || noSpaceForFooter
-                                    || (ambientState.isClearAllInProgress()
+                            noSpaceForFooter || (ambientState.isClearAllInProgress()
                                     && !hasNonClearableNotifs(algorithmState));
                 }
             }
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 5249a6d..d302fb6 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
@@ -106,9 +106,6 @@
     /** Sets whether the view is displayed in pulsing mode. */
     fun setPulsing(pulsing: Boolean, animated: Boolean)
 
-    /** Gets the inset for HUNs when they are not visible */
-    fun getHeadsUpInset(): Int
-
     /**
      * Signals that any open Notification guts should be closed, as scene container is handling
      * touch events.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt
index 53749ff..c8c798d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.stack.ui.view
 
-import android.os.Trace
 import android.service.notification.NotificationListenerService
 import androidx.annotation.VisibleForTesting
+import com.android.app.tracing.coroutines.TrackTracer
 import com.android.internal.statusbar.IStatusBarService
 import com.android.internal.statusbar.NotificationVisibility
 import com.android.systemui.dagger.SysUISingleton
@@ -183,8 +183,8 @@
 
             maybeLogVisibilityChanges(newlyVisible, noLongerVisible, activeNotifCount)
             updateExpansionStates(newlyVisible, noLongerVisible)
-            Trace.traceCounter(Trace.TRACE_TAG_APP, "Notifications [Active]", activeNotifCount)
-            Trace.traceCounter(Trace.TRACE_TAG_APP, "Notifications [Visible]", newVisibilities.size)
+            TrackTracer.instantForGroup("Notifications", "Active", activeNotifCount)
+            TrackTracer.instantForGroup("Notifications", "Visible", newVisibilities.size)
 
             lastLoggedVisibilities.clear()
             lastLoggedVisibilities.putAll(newVisibilities)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index b456168..1d7e658 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -40,7 +40,6 @@
 import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView
 import com.android.systemui.statusbar.notification.emptyshade.ui.viewbinder.EmptyShadeViewBinder
 import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.EmptyShadeViewModel
-import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.footer.shared.NotifRedesignFooter
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
 import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder
@@ -108,25 +107,20 @@
                 launch { bindShelf(shelf) }
                 bindHideList(viewController, viewModel, hiderTracker)
 
-                if (FooterViewRefactor.isEnabled) {
-                    val hasNonClearableSilentNotifications: StateFlow<Boolean> =
-                        viewModel.hasNonClearableSilentNotifications.stateIn(this)
-                    launch { reinflateAndBindFooter(view, hasNonClearableSilentNotifications) }
-                    launch {
-                        if (ModesEmptyShadeFix.isEnabled) {
-                            reinflateAndBindEmptyShade(view)
-                        } else {
-                            bindEmptyShadeLegacy(viewModel.emptyShadeViewFactory.create(), view)
-                        }
+                val hasNonClearableSilentNotifications: StateFlow<Boolean> =
+                    viewModel.hasNonClearableSilentNotifications.stateIn(this)
+                launch { reinflateAndBindFooter(view, hasNonClearableSilentNotifications) }
+                launch {
+                    if (ModesEmptyShadeFix.isEnabled) {
+                        reinflateAndBindEmptyShade(view)
+                    } else {
+                        bindEmptyShadeLegacy(viewModel.emptyShadeViewFactory.create(), view)
                     }
-                    launch {
-                        bindSilentHeaderClickListener(view, hasNonClearableSilentNotifications)
-                    }
-                    launch {
-                        viewModel.isImportantForAccessibility.collect { isImportantForAccessibility
-                            ->
-                            view.setImportantForAccessibilityYesNo(isImportantForAccessibility)
-                        }
+                }
+                launch { bindSilentHeaderClickListener(view, hasNonClearableSilentNotifications) }
+                launch {
+                    viewModel.isImportantForAccessibility.collect { isImportantForAccessibility ->
+                        view.setImportantForAccessibilityYesNo(isImportantForAccessibility)
                     }
                 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index ea71460..3ea4d48 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -28,7 +28,6 @@
 import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
-import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
@@ -81,9 +80,6 @@
 
                             controller.setOverExpansion(0f)
                             controller.setOverScrollAmount(0)
-                            if (!FooterViewRefactor.isEnabled) {
-                                controller.updateFooter()
-                            }
                         }
                     }
                 }
@@ -183,6 +179,10 @@
                         }
                     }
 
+                    if (Flags.bouncerUiRevamp()) {
+                        launch { viewModel.blurRadius.collect { controller.setBlurRadius(it) } }
+                    }
+
                     if (communalSettingsInteractor.isCommunalFlagEnabled()) {
                         launch {
                             viewModel.glanceableHubAlpha.collect {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
index 38390e7..fcc671a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
@@ -25,7 +25,6 @@
 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
 import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
 import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.EmptyShadeViewModel
-import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
 import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey
 import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel
@@ -75,46 +74,37 @@
      * we want it to be important for accessibility to enable accessibility auto-scrolling in NSSL.
      * See b/242235264 for more details.
      */
-    val isImportantForAccessibility: Flow<Boolean> by lazy {
-        if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
-            flowOf(true)
-        } else {
-            combine(
-                    activeNotificationsInteractor.areAnyNotificationsPresent,
-                    notificationStackInteractor.isShowingOnLockscreen,
-                ) { hasNotifications, isShowingOnLockscreen ->
-                    hasNotifications || !isShowingOnLockscreen
-                }
-                .distinctUntilChanged()
-                .dumpWhileCollecting("isImportantForAccessibility")
-                .flowOn(bgDispatcher)
-        }
-    }
+    val isImportantForAccessibility: Flow<Boolean> =
+        combine(
+                activeNotificationsInteractor.areAnyNotificationsPresent,
+                notificationStackInteractor.isShowingOnLockscreen,
+            ) { hasNotifications, isShowingOnLockscreen ->
+                hasNotifications || !isShowingOnLockscreen
+            }
+            .distinctUntilChanged()
+            .dumpWhileCollecting("isImportantForAccessibility")
+            .flowOn(bgDispatcher)
 
     val shouldShowEmptyShadeView: Flow<Boolean> by lazy {
         ModesEmptyShadeFix.assertInLegacyMode()
-        if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
-            flowOf(false)
-        } else {
-            combine(
-                    activeNotificationsInteractor.areAnyNotificationsPresent,
-                    shadeInteractor.isQsFullscreen,
-                    notificationStackInteractor.isShowingOnLockscreen,
-                ) { hasNotifications, isQsFullScreen, isShowingOnLockscreen ->
-                    when {
-                        hasNotifications -> false
-                        isQsFullScreen -> false
-                        // Do not show the empty shade if the lockscreen is visible (including AOD
-                        // b/228790482 and bouncer b/267060171), except if the shade is opened on
-                        // top.
-                        isShowingOnLockscreen -> false
-                        else -> true
-                    }
+        combine(
+                activeNotificationsInteractor.areAnyNotificationsPresent,
+                shadeInteractor.isQsFullscreen,
+                notificationStackInteractor.isShowingOnLockscreen,
+            ) { hasNotifications, isQsFullScreen, isShowingOnLockscreen ->
+                when {
+                    hasNotifications -> false
+                    isQsFullScreen -> false
+                    // Do not show the empty shade if the lockscreen is visible (including AOD
+                    // b/228790482 and bouncer b/267060171), except if the shade is opened on
+                    // top.
+                    isShowingOnLockscreen -> false
+                    else -> true
                 }
-                .distinctUntilChanged()
-                .dumpWhileCollecting("shouldShowEmptyShadeView")
-                .flowOn(bgDispatcher)
-        }
+            }
+            .distinctUntilChanged()
+            .dumpWhileCollecting("shouldShowEmptyShadeView")
+            .flowOn(bgDispatcher)
     }
 
     val shouldShowEmptyShadeViewAnimated: Flow<AnimatedValue<Boolean>> by lazy {
@@ -164,18 +154,14 @@
      */
     val shouldHideFooterView: Flow<Boolean> by lazy {
         SceneContainerFlag.assertInLegacyMode()
-        if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
-            flowOf(false)
-        } else {
-            // When the shade is closed, the footer is still present in the list, but not visible.
-            // This prevents the footer from being shown when a HUN is present, while still allowing
-            // the footer to be counted as part of the shade for measurements.
-            shadeInteractor.shadeExpansion
-                .map { it == 0f }
-                .distinctUntilChanged()
-                .dumpWhileCollecting("shouldHideFooterView")
-                .flowOn(bgDispatcher)
-        }
+        // When the shade is closed, the footer is still present in the list, but not visible.
+        // This prevents the footer from being shown when a HUN is present, while still allowing
+        // the footer to be counted as part of the shade for measurements.
+        shadeInteractor.shadeExpansion
+            .map { it == 0f }
+            .distinctUntilChanged()
+            .dumpWhileCollecting("shouldHideFooterView")
+            .flowOn(bgDispatcher)
     }
 
     /**
@@ -188,68 +174,64 @@
      */
     val shouldIncludeFooterView: Flow<AnimatedValue<Boolean>> by lazy {
         SceneContainerFlag.assertInLegacyMode()
-        if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
-            flowOf(AnimatedValue.NotAnimating(false))
-        } else {
-            combine(
-                    activeNotificationsInteractor.areAnyNotificationsPresent,
-                    userSetupInteractor.isUserSetUp,
-                    notificationStackInteractor.isShowingOnLockscreen,
-                    shadeInteractor.isQsFullscreen,
-                    remoteInputInteractor.isRemoteInputActive,
-                ) {
-                    hasNotifications,
-                    isUserSetUp,
-                    isShowingOnLockscreen,
-                    qsFullScreen,
-                    isRemoteInputActive ->
-                    when {
-                        !hasNotifications -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
-                        // Hide the footer until the user setup is complete, to prevent access
-                        // to settings (b/193149550).
-                        !isUserSetUp -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
-                        // Do not show the footer if the lockscreen is visible (incl. AOD),
-                        // except if the shade is opened on top. See also b/219680200.
-                        // Do not animate, as that makes the footer appear briefly when
-                        // transitioning between the shade and keyguard.
-                        isShowingOnLockscreen -> VisibilityChange.DISAPPEAR_WITHOUT_ANIMATION
-                        // Do not show the footer if quick settings are fully expanded (except
-                        // for the foldable split shade view). See b/201427195 && b/222699879.
-                        qsFullScreen -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
-                        // Hide the footer if remote input is active (i.e. user is replying to a
-                        // notification). See b/75984847.
-                        isRemoteInputActive -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
-                        else -> VisibilityChange.APPEAR_WITH_ANIMATION
-                    }
+        combine(
+                activeNotificationsInteractor.areAnyNotificationsPresent,
+                userSetupInteractor.isUserSetUp,
+                notificationStackInteractor.isShowingOnLockscreen,
+                shadeInteractor.isQsFullscreen,
+                remoteInputInteractor.isRemoteInputActive,
+            ) {
+                hasNotifications,
+                isUserSetUp,
+                isShowingOnLockscreen,
+                qsFullScreen,
+                isRemoteInputActive ->
+                when {
+                    !hasNotifications -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
+                    // Hide the footer until the user setup is complete, to prevent access
+                    // to settings (b/193149550).
+                    !isUserSetUp -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
+                    // Do not show the footer if the lockscreen is visible (incl. AOD),
+                    // except if the shade is opened on top. See also b/219680200.
+                    // Do not animate, as that makes the footer appear briefly when
+                    // transitioning between the shade and keyguard.
+                    isShowingOnLockscreen -> VisibilityChange.DISAPPEAR_WITHOUT_ANIMATION
+                    // Do not show the footer if quick settings are fully expanded (except
+                    // for the foldable split shade view). See b/201427195 && b/222699879.
+                    qsFullScreen -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
+                    // Hide the footer if remote input is active (i.e. user is replying to a
+                    // notification). See b/75984847.
+                    isRemoteInputActive -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
+                    else -> VisibilityChange.APPEAR_WITH_ANIMATION
                 }
-                .distinctUntilChanged(
-                    // Equivalent unless visibility changes
-                    areEquivalent = { a: VisibilityChange, b: VisibilityChange ->
-                        a.visible == b.visible
-                    }
-                )
-                // Should we animate the visibility change?
-                .sample(
-                    // TODO(b/322167853): This check is currently duplicated in FooterViewModel,
-                    //  but instead it should be a field in ShadeAnimationInteractor.
-                    combine(
-                            shadeInteractor.isShadeFullyExpanded,
-                            shadeInteractor.isShadeTouchable,
-                            ::Pair,
-                        )
-                        .onStart { emit(Pair(false, false)) }
-                ) { visibilityChange, (isShadeFullyExpanded, animationsEnabled) ->
-                    // Animate if the shade is interactive, but NOT on the lockscreen. Having
-                    // animations enabled while on the lockscreen makes the footer appear briefly
-                    // when transitioning between the shade and keyguard.
-                    val shouldAnimate =
-                        isShadeFullyExpanded && animationsEnabled && visibilityChange.canAnimate
-                    AnimatableEvent(visibilityChange.visible, shouldAnimate)
+            }
+            .distinctUntilChanged(
+                // Equivalent unless visibility changes
+                areEquivalent = { a: VisibilityChange, b: VisibilityChange ->
+                    a.visible == b.visible
                 }
-                .toAnimatedValueFlow()
-                .dumpWhileCollecting("shouldIncludeFooterView")
-                .flowOn(bgDispatcher)
-        }
+            )
+            // Should we animate the visibility change?
+            .sample(
+                // TODO(b/322167853): This check is currently duplicated in FooterViewModel,
+                //  but instead it should be a field in ShadeAnimationInteractor.
+                combine(
+                        shadeInteractor.isShadeFullyExpanded,
+                        shadeInteractor.isShadeTouchable,
+                        ::Pair,
+                    )
+                    .onStart { emit(Pair(false, false)) }
+            ) { visibilityChange, (isShadeFullyExpanded, animationsEnabled) ->
+                // Animate if the shade is interactive, but NOT on the lockscreen. Having
+                // animations enabled while on the lockscreen makes the footer appear briefly
+                // when transitioning between the shade and keyguard.
+                val shouldAnimate =
+                    isShadeFullyExpanded && animationsEnabled && visibilityChange.canAnimate
+                AnimatableEvent(visibilityChange.visible, shouldAnimate)
+            }
+            .toAnimatedValueFlow()
+            .dumpWhileCollecting("shouldIncludeFooterView")
+            .flowOn(bgDispatcher)
     }
 
     // This flow replaces shouldHideFooterView+shouldIncludeFooterView in flexiglass.
@@ -328,25 +310,15 @@
         APPEAR_WITH_ANIMATION(visible = true, canAnimate = true),
     }
 
-    val hasClearableAlertingNotifications: Flow<Boolean> by lazy {
-        if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
-            flowOf(false)
-        } else {
-            activeNotificationsInteractor.hasClearableAlertingNotifications.dumpWhileCollecting(
-                "hasClearableAlertingNotifications"
-            )
-        }
-    }
+    val hasClearableAlertingNotifications: Flow<Boolean> =
+        activeNotificationsInteractor.hasClearableAlertingNotifications.dumpWhileCollecting(
+            "hasClearableAlertingNotifications"
+        )
 
-    val hasNonClearableSilentNotifications: Flow<Boolean> by lazy {
-        if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
-            flowOf(false)
-        } else {
-            activeNotificationsInteractor.hasNonClearableSilentNotifications.dumpWhileCollecting(
-                "hasNonClearableSilentNotifications"
-            )
-        }
-    }
+    val hasNonClearableSilentNotifications: Flow<Boolean> =
+        activeNotificationsInteractor.hasNonClearableSilentNotifications.dumpWhileCollecting(
+            "hasNonClearableSilentNotifications"
+        )
 
     val topHeadsUpRow: Flow<HeadsUpRowKey?> by lazy {
         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index fc8c70f..f0455fc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -42,6 +42,7 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE
 import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED
+import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToPrimaryBouncerTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
@@ -154,6 +155,7 @@
     private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
     private val primaryBouncerToLockscreenTransitionViewModel:
         PrimaryBouncerToLockscreenTransitionViewModel,
+    private val primaryBouncerTransitions: Set<@JvmSuppressWildcards PrimaryBouncerTransition>,
     aodBurnInViewModel: AodBurnInViewModel,
     private val communalSceneInteractor: CommunalSceneInteractor,
     // Lazy because it's only used in the SceneContainer + Dual Shade configuration.
@@ -562,7 +564,7 @@
             lockscreenToDreamingTransitionViewModel.lockscreenAlpha,
             lockscreenToGoneTransitionViewModel.notificationAlpha(viewState),
             lockscreenToOccludedTransitionViewModel.lockscreenAlpha,
-            lockscreenToPrimaryBouncerTransitionViewModel.lockscreenAlpha,
+            lockscreenToPrimaryBouncerTransitionViewModel.notificationAlpha,
             alternateBouncerToPrimaryBouncerTransitionViewModel.notificationAlpha,
             occludedToAodTransitionViewModel.lockscreenAlpha,
             occludedToGoneTransitionViewModel.notificationAlpha(viewState),
@@ -626,6 +628,12 @@
             .dumpWhileCollecting("keyguardAlpha")
     }
 
+    val blurRadius =
+        primaryBouncerTransitions
+            .map { transition -> transition.notificationBlurRadius }
+            .merge()
+            .dumpWhileCollecting("blurRadius")
+
     /**
      * Returns a flow of the expected alpha while running a LOCKSCREEN<->GLANCEABLE_HUB or
      * DREAMING<->GLANCEABLE_HUB transition or idle on the hub.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index 6f29f61..afc5bc6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -750,7 +750,7 @@
                     BiometricUnlockSource.Companion.fromBiometricSourceType(biometricSourceType)
             );
         } else if (biometricSourceType == BiometricSourceType.FINGERPRINT
-                && mUpdateMonitor.isUdfpsSupported()) {
+                && mUpdateMonitor.isOpticalUdfpsSupported()) {
             long currUptimeMillis = mSystemClock.uptimeMillis();
             if (currUptimeMillis - mLastFpFailureUptimeMillis < mConsecutiveFpFailureThreshold) {
                 mNumConsecutiveFpFailures += 1;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index aba30d2..b146b92 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -851,10 +851,6 @@
         mLightRevealScrim = lightRevealScrim;
 
         mViewCaptureAwareWindowManager = viewCaptureAwareWindowManager;
-
-        if (PredictiveBackSysUiFlag.isEnabled()) {
-            mContext.getApplicationInfo().setEnableOnBackInvokedCallback(true);
-        }
     }
 
     private void initBubbles(Bubbles bubbles) {
@@ -1491,14 +1487,11 @@
         mActivityTransitionAnimator.setCallback(mActivityTransitionAnimatorCallback);
         mActivityTransitionAnimator.addListener(mActivityTransitionAnimatorListener);
         mRemoteInputManager.addControllerCallback(mNotificationShadeWindowController);
-        mStackScrollerController.setNotificationActivityStarter(
-                mNotificationActivityStarterLazy.get());
         mGutsManager.setNotificationActivityStarter(mNotificationActivityStarterLazy.get());
         mShadeController.setNotificationPresenter(mPresenterLazy.get());
         mNotificationsController.initialize(
                 mPresenterLazy.get(),
                 mNotifListContainer,
-                mStackScrollerController.getNotifStackController(),
                 mNotificationActivityStarterLazy.get());
         mWindowRootViewVisibilityInteractor.setUp(mPresenterLazy.get(), mNotificationsController);
     }
@@ -3031,9 +3024,6 @@
         public void onConfigChanged(Configuration newConfig) {
             updateResources();
             updateDisplaySize(); // populates mDisplayMetrics
-            if (PredictiveBackSysUiFlag.isEnabled()) {
-                mContext.getApplicationInfo().setEnableOnBackInvokedCallback(true);
-            }
 
             if (DEBUG) {
                 Log.v(TAG, "configuration changed: " + mContext.getResources().getConfiguration());
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
index 6a77988..a339bc9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
@@ -47,6 +47,7 @@
 import com.android.systemui.battery.BatteryMeterView;
 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
 import com.android.systemui.res.R;
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider;
 import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange;
 import com.android.systemui.statusbar.phone.ui.TintedIconManager;
 import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
index 6028f17..4c2bfe5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
@@ -64,6 +64,7 @@
 import com.android.systemui.statusbar.disableflags.DisableStateTracker;
 import com.android.systemui.statusbar.events.SystemStatusAnimationCallback;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider;
 import com.android.systemui.statusbar.notification.AnimatableProperty;
 import com.android.systemui.statusbar.notification.PropertyAnimator;
 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxModule.kt
index 2e3f0d0..1e6a0f8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxModule.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.statusbar.phone
 
 import com.android.systemui.CoreStartable
+import com.android.systemui.statusbar.layout.LetterboxBackgroundProvider
 import dagger.Binds
 import dagger.Module
 import dagger.multibindings.ClassKey
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarControllerImpl.java
index ca0c1ac9..0a28551 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarControllerImpl.java
@@ -44,6 +44,7 @@
 import com.android.systemui.statusbar.data.repository.DarkIconDispatcherStore;
 import com.android.systemui.statusbar.data.repository.StatusBarModePerDisplayRepository;
 import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore;
+import com.android.systemui.statusbar.layout.BoundsPair;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.util.Compile;
 import com.android.systemui.util.kotlin.JavaAdapterKt;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
index ccd1b6c..aa13089 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
@@ -46,6 +46,7 @@
 import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator
 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
 import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
 import com.android.systemui.statusbar.policy.Clock
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.window.StatusBarWindowStateController
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PredictiveBackSysUiFlag.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PredictiveBackSysUiFlag.kt
deleted file mode 100644
index 74d6ba5..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PredictiveBackSysUiFlag.kt
+++ /dev/null
@@ -1,53 +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.phone
-
-import com.android.systemui.Flags
-import com.android.systemui.flags.FlagToken
-import com.android.systemui.flags.RefactorFlagUtils
-
-/** Helper for reading or using the predictive back flag state. */
-@Suppress("NOTHING_TO_INLINE")
-object PredictiveBackSysUiFlag {
-    /** The aconfig flag name */
-    const val FLAG_NAME = Flags.FLAG_PREDICTIVE_BACK_SYSUI
-
-    /** A token used for dependency declaration */
-    val token: FlagToken
-        get() = FlagToken(FLAG_NAME, isEnabled)
-
-    /** Is the refactor enabled */
-    @JvmStatic
-    inline val isEnabled
-        get() = Flags.predictiveBackSysui()
-
-    /**
-     * 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/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index 324db79..d43fed0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -43,6 +43,7 @@
 import androidx.annotation.FloatRange;
 import androidx.annotation.Nullable;
 
+import com.android.app.tracing.coroutines.TrackTracer;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.colorextraction.ColorExtractor.GradientColors;
 import com.android.internal.graphics.ColorUtils;
@@ -554,7 +555,7 @@
 
         final ScrimState oldState = mState;
         mState = state;
-        Trace.traceCounter(Trace.TRACE_TAG_APP, "scrim_state", mState.ordinal());
+        TrackTracer.instantForGroup("scrim", "state", mState.ordinal());
 
         if (mCallback != null) {
             mCallback.onCancelled();
@@ -1279,10 +1280,9 @@
                 tint = getDebugScrimTint(scrimView);
             }
 
-            Trace.traceCounter(Trace.TRACE_TAG_APP, getScrimName(scrimView) + "_alpha",
+            TrackTracer.instantForGroup("scrim", getScrimName(scrimView) + "_alpha",
                     (int) (alpha * 255));
-
-            Trace.traceCounter(Trace.TRACE_TAG_APP, getScrimName(scrimView) + "_tint",
+            TrackTracer.instantForGroup("scrim", getScrimName(scrimView) + "_tint",
                     Color.alpha(tint));
             scrimView.setTint(tint);
             if (!mIsBouncerToGoneTransitionRunning) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
index 198859a..8dcb663 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
@@ -17,8 +17,8 @@
 package com.android.systemui.statusbar.phone;
 
 import android.graphics.Color;
-import android.os.Trace;
 
+import com.android.app.tracing.coroutines.TrackTracer;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.res.R;
 import com.android.systemui.scrim.ScrimView;
@@ -425,11 +425,11 @@
             tint = scrim == mScrimInFront ? ScrimController.DEBUG_FRONT_TINT
                     : ScrimController.DEBUG_BEHIND_TINT;
         }
-        Trace.traceCounter(Trace.TRACE_TAG_APP,
+        TrackTracer.instantForGroup("scrim",
                 scrim == mScrimInFront ? "front_scrim_alpha" : "back_scrim_alpha",
                 (int) (alpha * 255));
 
-        Trace.traceCounter(Trace.TRACE_TAG_APP,
+        TrackTracer.instantForGroup("scrim",
                 scrim == mScrimInFront ? "front_scrim_tint" : "back_scrim_tint",
                 Color.alpha(tint));
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 3749b96..8443edd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -18,7 +18,6 @@
 
 import static android.view.WindowInsets.Type.navigationBars;
 
-import static com.android.systemui.Flags.predictiveBackAnimateBouncer;
 import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN;
 import static com.android.systemui.plugins.ActivityStarter.OnDismissAction;
 import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
@@ -328,7 +327,6 @@
     private float mQsExpansion;
 
     final Set<KeyguardViewManagerCallback> mCallbacks = new HashSet<>();
-    private boolean mIsBackAnimationEnabled;
     private final UdfpsOverlayInteractor mUdfpsOverlayInteractor;
     private final ActivityStarter mActivityStarter;
 
@@ -434,7 +432,6 @@
                 .map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null);
         mAlternateBouncerInteractor = alternateBouncerInteractor;
         mBouncerInteractor = bouncerInteractor;
-        mIsBackAnimationEnabled = predictiveBackAnimateBouncer();
         mUdfpsOverlayInteractor = udfpsOverlayInteractor;
         mActivityStarter = activityStarter;
         mKeyguardTransitionInteractor = keyguardTransitionInteractor;
@@ -630,7 +627,7 @@
 
     private boolean shouldPlayBackAnimation() {
         // Suppress back animation when bouncer shouldn't be dismissed on back invocation.
-        return !needsFullscreenBouncer() && mIsBackAnimationEnabled;
+        return !needsFullscreenBouncer();
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
index 03324d2..c47ed17 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
@@ -16,8 +16,6 @@
 
 package com.android.systemui.statusbar.phone;
 
-import static com.android.systemui.Flags.predictiveBackAnimateDialogs;
-
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.content.BroadcastReceiver;
@@ -285,15 +283,13 @@
         for (int i = 0; i < mOnCreateRunnables.size(); i++) {
             mOnCreateRunnables.get(i).run();
         }
-        if (predictiveBackAnimateDialogs()) {
-            View targetView = getWindow().getDecorView();
-            DialogKt.registerAnimationOnBackInvoked(
-                    /* dialog = */ this,
-                    /* targetView = */ targetView,
-                    /* backAnimationSpec= */mDelegate.getBackAnimationSpec(
-                            () -> targetView.getResources().getDisplayMetrics())
-            );
-        }
+        View targetView = getWindow().getDecorView();
+        DialogKt.registerAnimationOnBackInvoked(
+                /* dialog = */ this,
+                /* targetView = */ targetView,
+                /* backAnimationSpec= */mDelegate.getBackAnimationSpec(
+                        () -> targetView.getResources().getDisplayMetrics())
+        );
     }
 
     private void updateWindowSize() {
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 c31e34c5..e622d8f 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
@@ -81,6 +81,7 @@
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinder;
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarVisibilityChangeListener;
 import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel;
+import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.HomeStatusBarViewModelFactory;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.window.StatusBarWindowController;
 import com.android.systemui.statusbar.window.StatusBarWindowControllerStore;
@@ -142,6 +143,8 @@
     private StatusBarVisibilityModel mLastModifiedVisibility =
             StatusBarVisibilityModel.createDefaultModel();
     private DarkIconManager mDarkIconManager;
+    private HomeStatusBarViewModel mHomeStatusBarViewModel;
+
     private final HomeStatusBarComponent.Factory mHomeStatusBarComponentFactory;
     private final CommandQueue mCommandQueue;
     private final CollapsedStatusBarFragmentLogger mCollapsedStatusBarFragmentLogger;
@@ -151,8 +154,8 @@
     private final ShadeExpansionStateManager mShadeExpansionStateManager;
     private final StatusBarIconController mStatusBarIconController;
     private final CarrierConfigTracker mCarrierConfigTracker;
-    private final HomeStatusBarViewModel mHomeStatusBarViewModel;
     private final HomeStatusBarViewBinder mHomeStatusBarViewBinder;
+    private final HomeStatusBarViewModelFactory mHomeStatusBarViewModelFactory;
     private final StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
     private final DarkIconManager.Factory mDarkIconManagerFactory;
     private final SecureSettings mSecureSettings;
@@ -256,7 +259,7 @@
             ShadeExpansionStateManager shadeExpansionStateManager,
             StatusBarIconController statusBarIconController,
             DarkIconManager.Factory darkIconManagerFactory,
-            HomeStatusBarViewModel homeStatusBarViewModel,
+            HomeStatusBarViewModelFactory homeStatusBarViewModelFactory,
             HomeStatusBarViewBinder homeStatusBarViewBinder,
             StatusBarHideIconsForBouncerManager statusBarHideIconsForBouncerManager,
             KeyguardStateController keyguardStateController,
@@ -281,7 +284,7 @@
         mAnimationScheduler = animationScheduler;
         mShadeExpansionStateManager = shadeExpansionStateManager;
         mStatusBarIconController = statusBarIconController;
-        mHomeStatusBarViewModel = homeStatusBarViewModel;
+        mHomeStatusBarViewModelFactory = homeStatusBarViewModelFactory;
         mHomeStatusBarViewBinder = homeStatusBarViewBinder;
         mStatusBarHideIconsForBouncerManager = statusBarHideIconsForBouncerManager;
         mDarkIconManagerFactory = darkIconManagerFactory;
@@ -410,6 +413,7 @@
         mCarrierConfigTracker.addCallback(mCarrierConfigCallback);
         mCarrierConfigTracker.addDefaultDataSubscriptionChangedListener(mDefaultDataListener);
 
+        mHomeStatusBarViewModel = mHomeStatusBarViewModelFactory.create(displayId);
         mHomeStatusBarViewBinder.bind(
                 view.getContext().getDisplayId(),
                 mStatusBar,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/HomeStatusBarComponent.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/HomeStatusBarComponent.java
index 5837752..7207d0a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/HomeStatusBarComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/HomeStatusBarComponent.java
@@ -21,13 +21,13 @@
 import com.android.systemui.dagger.qualifiers.RootView;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController;
+import com.android.systemui.statusbar.layout.StatusBarBoundsProvider;
 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
 import com.android.systemui.statusbar.phone.LegacyLightsOutNotifController;
 import com.android.systemui.statusbar.phone.PhoneStatusBarTransitions;
 import com.android.systemui.statusbar.phone.PhoneStatusBarView;
 import com.android.systemui.statusbar.phone.PhoneStatusBarViewController;
-import com.android.systemui.statusbar.phone.StatusBarBoundsProvider;
 import com.android.systemui.statusbar.phone.StatusBarDemoMode;
 import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment;
 import com.android.systemui.statusbar.window.StatusBarWindowController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarStartablesModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarStartablesModule.kt
index ba91814..b56a9a1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarStartablesModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarStartablesModule.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.statusbar.phone.fragment.dagger
 
-import com.android.systemui.statusbar.phone.StatusBarBoundsProvider
+import com.android.systemui.statusbar.layout.StatusBarBoundsProvider
 import dagger.Binds
 import dagger.Module
 import dagger.multibindings.IntoSet
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
index c57cede..f56c2d5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
@@ -18,8 +18,6 @@
 
 import android.app.ActivityManager
 import android.app.IActivityManager
-import android.app.Notification
-import android.app.Notification.CallStyle.CALL_TYPE_ONGOING
 import android.app.PendingIntent
 import android.app.UidObserver
 import android.content.Context
@@ -44,9 +42,6 @@
 import com.android.systemui.statusbar.chips.ui.view.ChipChronometer
 import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore
 import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler
-import com.android.systemui.statusbar.notification.collection.NotificationEntry
-import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
-import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
 import com.android.systemui.statusbar.notification.shared.CallType
@@ -60,7 +55,9 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 
-/** A controller to handle the ongoing call chip in the collapsed status bar.
+/**
+ * A controller to handle the ongoing call chip in the collapsed status bar.
+ *
  * @deprecated Use [OngoingCallInteractor] instead, which follows recommended architecture patterns
  */
 @Deprecated("Use OngoingCallInteractor instead")
@@ -71,7 +68,6 @@
     @Application private val scope: CoroutineScope,
     private val context: Context,
     private val ongoingCallRepository: OngoingCallRepository,
-    private val notifCollection: CommonNotifCollection,
     private val activeNotificationsInteractor: ActiveNotificationsInteractor,
     private val systemClock: SystemClock,
     private val activityStarter: ActivityStarter,
@@ -90,105 +86,24 @@
 
     private val mListeners: MutableList<OngoingCallListener> = mutableListOf()
     private val uidObserver = CallAppUidObserver()
-    private val notifListener =
-        object : NotifCollectionListener {
-            // Temporary workaround for b/178406514 for testing purposes.
-            //
-            // b/178406514 means that posting an incoming call notif then updating it to an ongoing
-            // call notif does not work (SysUI never receives the update). This workaround allows us
-            // to trigger the ongoing call chip when an ongoing call notif is *added* rather than
-            // *updated*, allowing us to test the chip.
-            //
-            // TODO(b/183229367): Remove this function override when b/178406514 is fixed.
-            override fun onEntryAdded(entry: NotificationEntry) {
-                onEntryUpdated(entry, true)
-            }
-
-            override fun onEntryUpdated(entry: NotificationEntry) {
-                StatusBarUseReposForCallChip.assertInLegacyMode()
-                // We have a new call notification or our existing call notification has been
-                // updated.
-                // TODO(b/183229367): This likely won't work if you take a call from one app then
-                //  switch to a call from another app.
-                if (
-                    callNotificationInfo == null && isCallNotification(entry) ||
-                        (entry.sbn.key == callNotificationInfo?.key)
-                ) {
-                    val newOngoingCallInfo =
-                        CallNotificationInfo(
-                            entry.sbn.key,
-                            entry.sbn.notification.getWhen(),
-                            // In this old listener pattern, we don't have access to the
-                            // notification icon.
-                            notificationIconView = null,
-                            entry.sbn.notification.contentIntent,
-                            entry.sbn.uid,
-                            entry.sbn.notification.extras.getInt(
-                                Notification.EXTRA_CALL_TYPE,
-                                -1,
-                            ) == CALL_TYPE_ONGOING,
-                            statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false,
-                        )
-                    if (newOngoingCallInfo == callNotificationInfo) {
-                        return
-                    }
-
-                    callNotificationInfo = newOngoingCallInfo
-                    if (newOngoingCallInfo.isOngoing) {
-                        logger.log(
-                            TAG,
-                            LogLevel.DEBUG,
-                            { str1 = newOngoingCallInfo.key },
-                            { "Call notif *is* ongoing -> showing chip. key=$str1" },
-                        )
-                        updateChip()
-                    } else {
-                        logger.log(
-                            TAG,
-                            LogLevel.DEBUG,
-                            { str1 = newOngoingCallInfo.key },
-                            { "Call notif not ongoing -> hiding chip. key=$str1" },
-                        )
-                        removeChip()
-                    }
-                }
-            }
-
-            override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
-                if (entry.sbn.key == callNotificationInfo?.key) {
-                    logger.log(
-                        TAG,
-                        LogLevel.DEBUG,
-                        { str1 = entry.sbn.key },
-                        { "Call notif removed -> hiding chip. key=$str1" },
-                    )
-                    removeChip()
-                }
-            }
-        }
 
     override fun start() {
-        if (StatusBarChipsModernization.isEnabled)
-            return
+        if (StatusBarChipsModernization.isEnabled) return
 
         dumpManager.registerDumpable(this)
 
-        if (Flags.statusBarUseReposForCallChip()) {
-            scope.launch {
-                // Listening to [ActiveNotificationsInteractor] instead of using
-                // [NotifCollectionListener#onEntryUpdated] is better for two reasons:
-                // 1. ActiveNotificationsInteractor automatically filters the notification list to
-                // just notifications for the current user, which ensures we don't show a call chip
-                // for User 1's call while User 2 is active (see b/328584859).
-                // 2. ActiveNotificationsInteractor only emits notifications that are currently
-                // present in the shade, which means we know we've already inflated the icon that we
-                // might use for the call chip (see b/354930838).
-                activeNotificationsInteractor.ongoingCallNotification.collect {
-                    updateInfoFromNotifModel(it)
-                }
+        scope.launch {
+            // Listening to [ActiveNotificationsInteractor] instead of using
+            // [NotifCollectionListener#onEntryUpdated] is better for two reasons:
+            // 1. ActiveNotificationsInteractor automatically filters the notification list to
+            // just notifications for the current user, which ensures we don't show a call chip
+            // for User 1's call while User 2 is active (see b/328584859).
+            // 2. ActiveNotificationsInteractor only emits notifications that are currently
+            // present in the shade, which means we know we've already inflated the icon that we
+            // might use for the call chip (see b/354930838).
+            activeNotificationsInteractor.ongoingCallNotification.collect {
+                updateInfoFromNotifModel(it)
             }
-        } else {
-            notifCollection.addCollectionListener(notifListener)
         }
 
         scope.launch {
@@ -244,21 +159,12 @@
             logger.log(
                 TAG,
                 LogLevel.DEBUG,
-                {
-                    bool1 = Flags.statusBarCallChipNotificationIcon()
-                    bool2 = currentInfo.notificationIconView != null
-                },
-                { "Creating OngoingCallModel.InCall. notifIconFlag=$bool1 hasIcon=$bool2" },
+                { bool1 = currentInfo.notificationIconView != null },
+                { "Creating OngoingCallModel.InCall. hasIcon=$bool1" },
             )
-            val icon =
-                if (Flags.statusBarCallChipNotificationIcon()) {
-                    currentInfo.notificationIconView
-                } else {
-                    null
-                }
             return OngoingCallModel.InCall(
                 startTimeMs = currentInfo.callStartTime,
-                notificationIconView = icon,
+                notificationIconView = currentInfo.notificationIconView,
                 intent = currentInfo.intent,
                 notificationKey = currentInfo.key,
             )
@@ -597,8 +503,4 @@
     }
 }
 
-private fun isCallNotification(entry: NotificationEntry): Boolean {
-    return entry.sbn.notification.isStyle(Notification.CallStyle::class.java)
-}
-
 private const val TAG = OngoingCallRepository.TAG
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarUseReposForCallChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarUseReposForCallChip.kt
deleted file mode 100644
index 4bdd90e..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarUseReposForCallChip.kt
+++ /dev/null
@@ -1,53 +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.phone.ongoingcall
-
-import com.android.systemui.Flags
-import com.android.systemui.flags.FlagToken
-import com.android.systemui.flags.RefactorFlagUtils
-
-/** Helper for reading or using the status bar use repos for call chip flag state. */
-@Suppress("NOTHING_TO_INLINE")
-object StatusBarUseReposForCallChip {
-    /** The aconfig flag name */
-    const val FLAG_NAME = Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP
-
-    /** A token used for dependency declaration */
-    val token: FlagToken
-        get() = FlagToken(FLAG_NAME, isEnabled)
-
-    /** Is the refactor enabled */
-    @JvmStatic
-    inline val isEnabled
-        get() = Flags.statusBarUseReposForCallChip()
-
-    /**
-     * 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/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index 96666d8..c71162a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -56,8 +56,8 @@
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinder
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinderImpl
-import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel
-import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModelImpl
+import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.HomeStatusBarViewModelFactory
+import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModelImpl.HomeStatusBarViewModelFactoryImpl
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositorySwitcher
@@ -148,7 +148,9 @@
     abstract fun bindCarrierConfigStartable(impl: CarrierConfigCoreStartable): CoreStartable
 
     @Binds
-    abstract fun homeStatusBarViewModel(impl: HomeStatusBarViewModelImpl): HomeStatusBarViewModel
+    abstract fun homeStatusBarViewModelFactory(
+        impl: HomeStatusBarViewModelFactoryImpl
+    ): HomeStatusBarViewModelFactory
 
     @Binds
     abstract fun homeStatusBarViewBinder(impl: HomeStatusBarViewBinderImpl): HomeStatusBarViewBinder
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
index 7e06c35..31d6d86d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
@@ -41,9 +41,11 @@
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ConnectedDisplaysStatusBarNotificationIconViewStore
 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
 import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment
+import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
 import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel
 import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.VisibilityModel
 import javax.inject.Inject
+import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.launch
 
 /**
@@ -106,32 +108,39 @@
                 }
 
                 if (NotificationsLiveDataStoreRefactor.isEnabled) {
-                    val displayId = view.display.displayId
                     val lightsOutView: View = view.requireViewById(R.id.notification_lights_out)
                     launch {
-                        viewModel.areNotificationsLightsOut(displayId).collect { show ->
+                        viewModel.areNotificationsLightsOut.collect { show ->
                             animateLightsOutView(lightsOutView, show)
                         }
                     }
                 }
 
-                if (Flags.statusBarScreenSharingChips() && !StatusBarNotifChips.isEnabled) {
-                    val primaryChipView: View =
-                        view.requireViewById(R.id.ongoing_activity_chip_primary)
+                if (
+                    Flags.statusBarScreenSharingChips() &&
+                        !StatusBarNotifChips.isEnabled &&
+                        !StatusBarChipsModernization.isEnabled
+                ) {
+                    val primaryChipViewBinding =
+                        OngoingActivityChipBinder.createBinding(
+                            view.requireViewById(R.id.ongoing_activity_chip_primary)
+                        )
                     launch {
                         viewModel.primaryOngoingActivityChip.collect { primaryChipModel ->
                             OngoingActivityChipBinder.bind(
                                 primaryChipModel,
-                                primaryChipView,
+                                primaryChipViewBinding,
                                 iconViewStore,
                             )
                             if (StatusBarRootModernization.isEnabled) {
                                 when (primaryChipModel) {
                                     is OngoingActivityChipModel.Shown ->
-                                        primaryChipView.show(shouldAnimateChange = true)
+                                        primaryChipViewBinding.rootView.show(
+                                            shouldAnimateChange = true
+                                        )
 
                                     is OngoingActivityChipModel.Hidden ->
-                                        primaryChipView.hide(
+                                        primaryChipViewBinding.rootView.hide(
                                             state = View.GONE,
                                             shouldAnimateChange = primaryChipModel.shouldAnimate,
                                         )
@@ -157,29 +166,39 @@
                     }
                 }
 
-                if (Flags.statusBarScreenSharingChips() && StatusBarNotifChips.isEnabled) {
-                    val primaryChipView: View =
-                        view.requireViewById(R.id.ongoing_activity_chip_primary)
-                    val secondaryChipView: View =
-                        view.requireViewById(R.id.ongoing_activity_chip_secondary)
+                if (
+                    Flags.statusBarScreenSharingChips() &&
+                        StatusBarNotifChips.isEnabled &&
+                        !StatusBarChipsModernization.isEnabled
+                ) {
+                    // Create view bindings here so we don't keep re-fetching child views each time
+                    // the chip model changes.
+                    val primaryChipViewBinding =
+                        OngoingActivityChipBinder.createBinding(
+                            view.requireViewById(R.id.ongoing_activity_chip_primary)
+                        )
+                    val secondaryChipViewBinding =
+                        OngoingActivityChipBinder.createBinding(
+                            view.requireViewById(R.id.ongoing_activity_chip_secondary)
+                        )
                     launch {
-                        viewModel.ongoingActivityChips.collect { chips ->
+                        viewModel.ongoingActivityChips.collectLatest { chips ->
                             OngoingActivityChipBinder.bind(
                                 chips.primary,
-                                primaryChipView,
+                                primaryChipViewBinding,
                                 iconViewStore,
                             )
-                            // TODO(b/364653005): Don't show the secondary chip if there isn't
-                            // enough space for it.
                             OngoingActivityChipBinder.bind(
                                 chips.secondary,
-                                secondaryChipView,
+                                secondaryChipViewBinding,
                                 iconViewStore,
                             )
 
                             if (StatusBarRootModernization.isEnabled) {
-                                primaryChipView.adjustVisibility(chips.primary.toVisibilityModel())
-                                secondaryChipView.adjustVisibility(
+                                primaryChipViewBinding.rootView.adjustVisibility(
+                                    chips.primary.toVisibilityModel()
+                                )
+                                secondaryChipViewBinding.rootView.adjustVisibility(
                                     chips.secondary.toVisibilityModel()
                                 )
                             } else {
@@ -192,6 +211,18 @@
                                     shouldAnimate = true,
                                 )
                             }
+
+                            viewModel.contentArea.collect { _ ->
+                                OngoingActivityChipBinder.resetPrimaryChipWidthRestrictions(
+                                    primaryChipViewBinding,
+                                    viewModel.ongoingActivityChips.value.primary,
+                                )
+                                OngoingActivityChipBinder.resetSecondaryChipWidthRestrictions(
+                                    secondaryChipViewBinding,
+                                    viewModel.ongoingActivityChips.value.secondary,
+                                )
+                                view.requestLayout()
+                            }
                         }
                     }
                 }
@@ -209,7 +240,7 @@
                     StatusBarOperatorNameViewBinder.bind(
                         operatorNameView,
                         viewModel.operatorNameViewModel,
-                        viewModel::areaTint,
+                        viewModel.areaTint,
                     )
                     launch {
                         viewModel.shouldShowOperatorNameView.collect {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt
index b7744d3..5dd76f4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt
@@ -32,19 +32,16 @@
     fun bind(
         operatorFrameView: View,
         viewModel: StatusBarOperatorNameViewModel,
-        areaTint: (Int) -> Flow<StatusBarTintColor>,
+        areaTint: Flow<StatusBarTintColor>,
     ) {
         operatorFrameView.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.STARTED) {
-                val displayId = operatorFrameView.display.displayId
-
                 val operatorNameText =
                     operatorFrameView.requireViewById<TextView>(R.id.operator_name)
                 launch { viewModel.operatorName.collect { operatorNameText.text = it } }
 
                 launch {
-                    val tint = areaTint(displayId)
-                    tint.collect { statusBarTintColors ->
+                    areaTint.collect { statusBarTintColors ->
                         operatorNameText.setTextColor(
                             statusBarTintColors.tint(operatorNameText.viewBoundsOnScreen())
                         )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt
index 7243ba7..71e1918 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt
@@ -24,6 +24,7 @@
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -33,9 +34,11 @@
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.app.tracing.coroutines.launchTraced as launch
+import com.android.compose.theme.PlatformTheme
 import com.android.keyguard.AlphaOptimizedLinearLayout
 import com.android.systemui.plugins.DarkIconDispatcher
 import com.android.systemui.res.R
+import com.android.systemui.statusbar.chips.ui.compose.OngoingActivityChips
 import com.android.systemui.statusbar.data.repository.DarkIconDispatcherStore
 import com.android.systemui.statusbar.events.domain.interactor.SystemStatusEventAnimationInteractor
 import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips
@@ -53,13 +56,14 @@
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinder
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarVisibilityChangeListener
 import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel
+import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.HomeStatusBarViewModelFactory
 import javax.inject.Inject
 
 /** Factory to simplify the dependency management for [StatusBarRoot] */
 class StatusBarRootFactory
 @Inject
 constructor(
-    private val homeStatusBarViewModel: HomeStatusBarViewModel,
+    private val homeStatusBarViewModelFactory: HomeStatusBarViewModelFactory,
     private val homeStatusBarViewBinder: HomeStatusBarViewBinder,
     private val notificationIconsBinder: NotificationIconContainerStatusBarViewBinder,
     private val darkIconManagerFactory: DarkIconManager.Factory,
@@ -70,13 +74,14 @@
 ) {
     fun create(root: ViewGroup, andThen: (ViewGroup) -> Unit): ComposeView {
         val composeView = ComposeView(root.context)
+        val displayId = root.context.displayId
         val darkIconDispatcher =
             darkIconDispatcherStore.forDisplay(root.context.displayId) ?: return composeView
         composeView.apply {
             setContent {
                 StatusBarRoot(
                     parent = root,
-                    statusBarViewModel = homeStatusBarViewModel,
+                    statusBarViewModel = homeStatusBarViewModelFactory.create(displayId),
                     statusBarViewBinder = homeStatusBarViewBinder,
                     notificationIconsBinder = notificationIconsBinder,
                     darkIconManagerFactory = darkIconManagerFactory,
@@ -158,17 +163,64 @@
                             darkIconDispatcher,
                         )
                     iconController.addIconGroup(darkIconManager)
+
+                    if (StatusBarChipsModernization.isEnabled) {
+                        val startSideExceptHeadsUp =
+                            phoneStatusBarView.requireViewById<LinearLayout>(
+                                R.id.status_bar_start_side_except_heads_up
+                            )
+
+                        val composeView =
+                            ComposeView(context).apply {
+                                layoutParams =
+                                    LinearLayout.LayoutParams(
+                                        LinearLayout.LayoutParams.WRAP_CONTENT,
+                                        LinearLayout.LayoutParams.WRAP_CONTENT,
+                                    )
+
+                                setViewCompositionStrategy(
+                                    ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
+                                )
+
+                                setContent {
+                                    PlatformTheme {
+                                        val chips by
+                                            statusBarViewModel.ongoingActivityChips
+                                                .collectAsStateWithLifecycle()
+                                        OngoingActivityChips(chips = chips)
+                                    }
+                                }
+                            }
+
+                        // Add the composable container for ongoingActivityChips before the
+                        // notification_icon_area to maintain the same ordering for ongoing activity
+                        // chips in the status bar layout.
+                        val notificationIconAreaIndex =
+                            startSideExceptHeadsUp.indexOfChild(
+                                startSideExceptHeadsUp.findViewById(R.id.notification_icon_area)
+                            )
+                        startSideExceptHeadsUp.addView(composeView, notificationIconAreaIndex)
+                    }
+
                     HomeStatusBarIconBlockListBinder.bind(
                         statusIconContainer,
                         darkIconManager,
                         statusBarViewModel.iconBlockList,
                     )
 
-                    if (!StatusBarChipsModernization.isEnabled) {
+                    if (StatusBarChipsModernization.isEnabled) {
+                        // Make sure the primary chip is hidden when StatusBarChipsModernization is
+                        // enabled. OngoingActivityChips will be shown in a composable container
+                        // when this flag is enabled.
+                        phoneStatusBarView
+                            .requireViewById<View>(R.id.ongoing_activity_chip_primary)
+                            .visibility = View.GONE
+                    } else {
                         ongoingCallController.setChipView(
                             phoneStatusBarView.requireViewById(R.id.ongoing_activity_chip_primary)
                         )
                     }
+
                     // For notifications, first inflate the [NotificationIconContainer]
                     val notificationIconArea =
                         phoneStatusBarView.requireViewById<ViewGroup>(R.id.notification_icon_area)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt
index c9cc173..d9d9a29 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt
@@ -19,7 +19,6 @@
 import android.annotation.ColorInt
 import android.graphics.Rect
 import android.view.View
-import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -44,6 +43,7 @@
 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle
 import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel
 import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.StatusBarPopupChipsViewModel
+import com.android.systemui.statusbar.layout.ui.viewmodel.StatusBarContentInsetsViewModelStore
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
 import com.android.systemui.statusbar.notification.headsup.PinnedStatus
@@ -53,7 +53,9 @@
 import com.android.systemui.statusbar.pipeline.shared.domain.interactor.HomeStatusBarIconBlockListInteractor
 import com.android.systemui.statusbar.pipeline.shared.domain.interactor.HomeStatusBarInteractor
 import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.VisibilityModel
-import javax.inject.Inject
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
@@ -118,6 +120,7 @@
     val shouldShowOperatorNameView: Flow<Boolean>
     val isClockVisible: Flow<VisibilityModel>
     val isNotificationIconContainerVisible: Flow<VisibilityModel>
+
     /**
      * Pair of (system info visibility, event animation state). The animation state can be used to
      * respond to the system event chip animations. In all cases, system info visibility correctly
@@ -128,6 +131,9 @@
     /** Which icons to block from the home status bar */
     val iconBlockList: Flow<List<String>>
 
+    /** This status bar's current content area for the given rotation in absolute bounds. */
+    val contentArea: Flow<Rect>
+
     /**
      * Apps can request a low profile mode [android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE] where
      * status bar and navigation icons dim. In this mode, a notification dot appears where the
@@ -137,13 +143,13 @@
      * whether there are notifications when the device is in
      * [android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE].
      */
-    fun areNotificationsLightsOut(displayId: Int): Flow<Boolean>
+    val areNotificationsLightsOut: Flow<Boolean>
 
     /**
-     * Given a displayId, returns a flow of [StatusBarTintColor], a functional interface that will
-     * allow a view to calculate its correct tint depending on location
+     * A flow of [StatusBarTintColor], a functional interface that will allow a view to calculate
+     * its correct tint depending on location
      */
-    fun areaTint(displayId: Int): Flow<StatusBarTintColor>
+    val areaTint: Flow<StatusBarTintColor>
 
     /** Models the current visibility for a specific child view of status bar. */
     data class VisibilityModel(
@@ -157,17 +163,22 @@
         val baseVisibility: VisibilityModel,
         val animationState: SystemEventAnimationState,
     )
+
+    /** Interface for the assisted factory, to allow for providing a fake in tests */
+    interface HomeStatusBarViewModelFactory {
+        fun create(displayId: Int): HomeStatusBarViewModel
+    }
 }
 
-@SysUISingleton
 class HomeStatusBarViewModelImpl
-@Inject
+@AssistedInject
 constructor(
+    @Assisted thisDisplayId: Int,
     homeStatusBarInteractor: HomeStatusBarInteractor,
     homeStatusBarIconBlockListInteractor: HomeStatusBarIconBlockListInteractor,
-    private val lightsOutInteractor: LightsOutInteractor,
-    private val notificationsInteractor: ActiveNotificationsInteractor,
-    private val darkIconInteractor: DarkIconInteractor,
+    lightsOutInteractor: LightsOutInteractor,
+    notificationsInteractor: ActiveNotificationsInteractor,
+    darkIconInteractor: DarkIconInteractor,
     headsUpNotificationInteractor: HeadsUpNotificationInteractor,
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
     keyguardInteractor: KeyguardInteractor,
@@ -178,6 +189,7 @@
     ongoingActivityChipsViewModel: OngoingActivityChipsViewModel,
     statusBarPopupChipsViewModel: StatusBarPopupChipsViewModel,
     animations: SystemStatusEventAnimationInteractor,
+    statusBarContentInsetsViewModelStore: StatusBarContentInsetsViewModelStore,
     @Application coroutineScope: CoroutineScope,
 ) : HomeStatusBarViewModel {
     override val isTransitioningFromLockscreenToOccluded: StateFlow<Boolean> =
@@ -211,22 +223,22 @@
             }
             .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), initialValue = false)
 
-    override fun areNotificationsLightsOut(displayId: Int): Flow<Boolean> =
+    override val areNotificationsLightsOut: Flow<Boolean> =
         if (NotificationsLiveDataStoreRefactor.isUnexpectedlyInLegacyMode()) {
             emptyFlow()
         } else {
             combine(
                     notificationsInteractor.areAnyNotificationsPresent,
-                    lightsOutInteractor.isLowProfile(displayId) ?: flowOf(false),
+                    lightsOutInteractor.isLowProfile(thisDisplayId) ?: flowOf(false),
                 ) { hasNotifications, isLowProfile ->
                     hasNotifications && isLowProfile
                 }
                 .distinctUntilChanged()
         }
 
-    override fun areaTint(displayId: Int): Flow<StatusBarTintColor> =
+    override val areaTint: Flow<StatusBarTintColor> =
         darkIconInteractor
-            .darkState(displayId)
+            .darkState(thisDisplayId)
             .map { (areas: Collection<Rect>, tint: Int) ->
                 StatusBarTintColor { viewBounds: Rect ->
                     if (DarkIconDispatcher.isInAreas(areas, viewBounds)) {
@@ -283,11 +295,12 @@
     override val shouldShowOperatorNameView: Flow<Boolean> =
         combine(
             shouldHomeStatusBarBeVisible,
-            headsUpNotificationInteractor.statusBarHeadsUpState,
+            headsUpNotificationInteractor.statusBarHeadsUpStatus,
             homeStatusBarInteractor.visibilityViaDisableFlags,
             homeStatusBarInteractor.shouldShowOperatorName,
-        ) { shouldStatusBarBeVisible, headsUpState, visibilityViaDisableFlags, shouldShowOperator ->
-            val hideForHeadsUp = headsUpState == PinnedStatus.PinnedBySystem
+        ) { shouldStatusBarBeVisible, headsUpStatus, visibilityViaDisableFlags, shouldShowOperator
+            ->
+            val hideForHeadsUp = headsUpStatus == PinnedStatus.PinnedBySystem
             shouldStatusBarBeVisible &&
                 !hideForHeadsUp &&
                 visibilityViaDisableFlags.isSystemInfoAllowed &&
@@ -297,10 +310,10 @@
     override val isClockVisible: Flow<VisibilityModel> =
         combine(
             shouldHomeStatusBarBeVisible,
-            headsUpNotificationInteractor.statusBarHeadsUpState,
+            headsUpNotificationInteractor.statusBarHeadsUpStatus,
             homeStatusBarInteractor.visibilityViaDisableFlags,
-        ) { shouldStatusBarBeVisible, headsUpState, visibilityViaDisableFlags ->
-            val hideClockForHeadsUp = headsUpState == PinnedStatus.PinnedBySystem
+        ) { shouldStatusBarBeVisible, headsUpStatus, visibilityViaDisableFlags ->
+            val hideClockForHeadsUp = headsUpStatus == PinnedStatus.PinnedBySystem
             val showClock =
                 shouldStatusBarBeVisible &&
                     visibilityViaDisableFlags.isClockAllowed &&
@@ -356,6 +369,10 @@
     override val iconBlockList: Flow<List<String>> =
         homeStatusBarIconBlockListInteractor.iconBlockList
 
+    override val contentArea: Flow<Rect> =
+        statusBarContentInsetsViewModelStore.forDisplay(thisDisplayId)?.contentArea
+            ?: flowOf(Rect(0, 0, 0, 0))
+
     @View.Visibility
     private fun Boolean.toVisibleOrGone(): Int {
         return if (this) View.VISIBLE else View.GONE
@@ -364,6 +381,13 @@
     // Similar to the above, but uses INVISIBLE in place of GONE
     @View.Visibility
     private fun Boolean.toVisibleOrInvisible(): Int = if (this) View.VISIBLE else View.INVISIBLE
+
+    /** Inject this to create the display-dependent view model */
+    @AssistedFactory
+    interface HomeStatusBarViewModelFactoryImpl :
+        HomeStatusBarViewModel.HomeStatusBarViewModelFactory {
+        override fun create(displayId: Int): HomeStatusBarViewModelImpl
+    }
 }
 
 /** Lookup the color for a given view in the status bar */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
index 31cae79..81d06a8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
@@ -32,6 +32,7 @@
 
 import androidx.annotation.VisibleForTesting;
 
+import com.android.app.tracing.coroutines.TrackTracer;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
@@ -241,7 +242,7 @@
 
     private void setKeyguardFadingAway(boolean keyguardFadingAway) {
         if (mKeyguardFadingAway != keyguardFadingAway) {
-            Trace.traceCounter(Trace.TRACE_TAG_APP, "keyguardFadingAway",
+            TrackTracer.instantForGroup("keyguard", "FadingAway",
                     keyguardFadingAway ? 1 : 0);
             mKeyguardFadingAway = keyguardFadingAway;
             invokeForEachCallback(Callback::onKeyguardFadingAwayChanged);
@@ -356,7 +357,7 @@
     @Override
     public void notifyKeyguardGoingAway(boolean keyguardGoingAway) {
         if (mKeyguardGoingAway != keyguardGoingAway) {
-            Trace.traceCounter(Trace.TRACE_TAG_APP, "keyguardGoingAway",
+            Trace.traceCounter(Trace.TRACE_TAG_APP, "keyguard##GoingAway",
                     keyguardGoingAway ? 1 : 0);
             mKeyguardGoingAway = keyguardGoingAway;
             mKeyguardInteractorLazy.get().setIsKeyguardGoingAway(keyguardGoingAway);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt
index 56c9e9a..cb26679 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt
@@ -41,6 +41,7 @@
 import android.view.accessibility.AccessibilityNodeInfo
 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
 import android.widget.Button
+import com.android.systemui.Flags
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.res.R
 import com.android.systemui.shared.system.ActivityManagerWrapper
@@ -52,6 +53,7 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.headsup.HeadsUpManager
 import com.android.systemui.statusbar.notification.logging.NotificationLogger
+import com.android.systemui.statusbar.notification.row.MagicActionBackgroundDrawable
 import com.android.systemui.statusbar.phone.KeyguardDismissUtil
 import com.android.systemui.statusbar.policy.InflatedSmartReplyState.SuppressedActions
 import com.android.systemui.statusbar.policy.SmartReplyView.SmartActions
@@ -400,6 +402,15 @@
             .apply {
                 text = action.title
 
+                if (Flags.notificationMagicActionsTreatment()) {
+                    if (
+                        smartActions.fromAssistant &&
+                            action.extras.getBoolean(Notification.Action.EXTRA_IS_MAGIC, false)
+                    ) {
+                        background = MagicActionBackgroundDrawable(parent.context)
+                    }
+                }
+
                 // We received the Icon from the application - so use the Context of the application
                 // to
                 // reference icon resources.
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 12ed647..fdc2d8d 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,8 @@
 
 package com.android.systemui.statusbar.policy.domain.interactor
 
-import android.app.NotificationManager.INTERRUPTION_FILTER_NONE
 import android.content.Context
+import android.media.AudioManager
 import android.provider.Settings
 import android.provider.Settings.Secure.ZEN_DURATION_FOREVER
 import android.provider.Settings.Secure.ZEN_DURATION_PROMPT
@@ -29,6 +29,7 @@
 import com.android.settingslib.notification.modes.ZenIcon
 import com.android.settingslib.notification.modes.ZenIconLoader
 import com.android.settingslib.notification.modes.ZenMode
+import com.android.settingslib.volume.shared.model.AudioStream
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.modes.shared.ModesUi
 import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository
@@ -67,6 +68,17 @@
     deviceProvisioningRepository: DeviceProvisioningRepository,
     userSetupRepository: UserSetupRepository,
 ) {
+    /**
+     * List of predicates to determine if the [ZenMode] blocks an audio stream. Typical use case
+     * would be: `zenModeByStreamPredicates[stream](zenMode)`
+     */
+    private val zenModeByStreamPredicates =
+        mapOf<Int, (ZenMode) -> Boolean>(
+            AudioManager.STREAM_MUSIC to { it.policy.priorityCategoryMedia == STATE_DISALLOW },
+            AudioManager.STREAM_ALARM to { it.policy.priorityCategoryAlarms == STATE_DISALLOW },
+            AudioManager.STREAM_SYSTEM to { it.policy.priorityCategorySystem == STATE_DISALLOW },
+        )
+
     val isZenAvailable: Flow<Boolean> =
         combine(
             deviceProvisioningRepository.isDeviceProvisioned,
@@ -125,21 +137,16 @@
             .flowOn(bgDispatcher)
             .distinctUntilChanged()
 
-    val activeModesBlockingEverything: Flow<ActiveZenModes> = getFilteredActiveModesFlow { mode ->
-        mode.interruptionFilter == INTERRUPTION_FILTER_NONE
-    }
+    fun canBeBlockedByZenMode(stream: AudioStream): Boolean =
+        zenModeByStreamPredicates.containsKey(stream.value)
 
-    val activeModesBlockingMedia: Flow<ActiveZenModes> = getFilteredActiveModesFlow { mode ->
-        mode.policy.priorityCategoryMedia == STATE_DISALLOW
-    }
-
-    val activeModesBlockingAlarms: Flow<ActiveZenModes> = getFilteredActiveModesFlow { mode ->
-        mode.policy.priorityCategoryAlarms == STATE_DISALLOW
-    }
-
-    private fun getFilteredActiveModesFlow(predicate: (ZenMode) -> Boolean): Flow<ActiveZenModes> {
+    fun activeModesBlockingStream(stream: AudioStream): Flow<ActiveZenModes> {
+        val isBlockingStream = zenModeByStreamPredicates[stream.value]
+        require(isBlockingStream != null) {
+            "$stream is unsupported. Use canBeBlockedByZenMode to check if the stream can be affected by the Zen Mode."
+        }
         return modes
-            .map { modes -> modes.filter { mode -> predicate(mode) } }
+            .map { modes -> modes.filter { isBlockingStream(it) } }
             .map { modes -> buildActiveZenModes(modes) }
             .flowOn(bgDispatcher)
             .distinctUntilChanged()
@@ -194,7 +201,6 @@
                         )
                         null
                     }
-
                     ZEN_DURATION_FOREVER -> null
                     else -> Duration.ofMinutes(zenDuration.toLong())
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ui/StatusBarUiLayerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ui/StatusBarUiLayerModule.kt
new file mode 100644
index 0000000..8e81d78
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ui/StatusBarUiLayerModule.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2025 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.ui
+
+import com.android.systemui.statusbar.layout.ui.viewmodel.StatusBarContentInsetsViewModelStoreModule
+import dagger.Module
+
+@Module(includes = [StatusBarContentInsetsViewModelStoreModule::class])
+object StatusBarUiLayerModule
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt
index 6175ea1..a98a9e0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt
@@ -60,7 +60,7 @@
 
     private val showingHeadsUpStatusBar: Flow<Boolean> =
         if (SceneContainerFlag.isEnabled) {
-            headsUpNotificationInteractor.statusBarHeadsUpState.map { it.isPinned }
+            headsUpNotificationInteractor.statusBarHeadsUpStatus.map { it.isPinned }
         } else {
             flowOf(false)
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt
index 1e043ec..ecfcb29 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt
@@ -23,7 +23,7 @@
 import com.android.systemui.animation.ActivityTransitionAnimator
 import com.android.systemui.fragments.FragmentHostManager
 import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
 import java.util.Optional
 
 /** Encapsulates all logic for the status bar window state management. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
index 848e91d..8518acb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
@@ -58,7 +58,7 @@
 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays;
 import com.android.systemui.statusbar.core.StatusBarRootModernization;
 import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController;
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider;
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider;
 import com.android.systemui.statusbar.window.StatusBarWindowModule.InternalWindowViewInflater;
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
 import com.android.systemui.unfold.util.JankMonitorTransitionProgressListener;
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
index ae32b7a..bce55cb 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
@@ -50,7 +50,7 @@
         )
     GestureTutorialScreen(
         screenConfig = screenConfig,
-        gestureUiStateFlow = viewModel.gestureUiState,
+        tutorialStateFlow = viewModel.tutorialState,
         motionEventConsumer = {
             easterEggGestureViewModel.accept(it)
             viewModel.handleEvent(it)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
index 73c54af..284e23e 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
@@ -18,7 +18,6 @@
 
 import android.view.MotionEvent
 import androidx.activity.compose.BackHandler
-import androidx.annotation.RawRes
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.layout.Box
@@ -27,77 +26,21 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.pointer.pointerInteropFilter
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.systemui.inputdevice.tutorial.ui.composable.ActionTutorialContent
 import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.NotStarted
 import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig
-import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.Finished
-import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.NotStarted
-import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState
 import kotlinx.coroutines.flow.Flow
 
-sealed interface GestureUiState {
-    data object NotStarted : GestureUiState
-
-    data class Finished(@RawRes val successAnimation: Int) : GestureUiState
-
-    data class InProgress(
-        val progress: Float = 0f,
-        val progressStartMarker: String,
-        val progressEndMarker: String,
-    ) : GestureUiState
-
-    data object Error : GestureUiState
-}
-
-fun GestureState.toGestureUiState(
-    progressStartMarker: String,
-    progressEndMarker: String,
-    successAnimation: Int,
-): GestureUiState {
-    return when (this) {
-        GestureState.NotStarted -> NotStarted
-        is GestureState.InProgress ->
-            GestureUiState.InProgress(this.progress, progressStartMarker, progressEndMarker)
-        is GestureState.Finished -> GestureUiState.Finished(successAnimation)
-        GestureState.Error -> GestureUiState.Error
-    }
-}
-
-fun GestureUiState.toTutorialActionState(previousState: TutorialActionState): TutorialActionState {
-    return when (this) {
-        NotStarted -> TutorialActionState.NotStarted
-        is GestureUiState.InProgress -> {
-            val inProgress =
-                TutorialActionState.InProgress(
-                    progress = progress,
-                    startMarker = progressStartMarker,
-                    endMarker = progressEndMarker,
-                )
-            if (
-                previousState is TutorialActionState.InProgressAfterError ||
-                    previousState is TutorialActionState.Error
-            ) {
-                return TutorialActionState.InProgressAfterError(inProgress)
-            } else {
-                return inProgress
-            }
-        }
-        is Finished -> TutorialActionState.Finished(successAnimation)
-        GestureUiState.Error -> TutorialActionState.Error
-    }
-}
-
 @Composable
 fun GestureTutorialScreen(
     screenConfig: TutorialScreenConfig,
-    gestureUiStateFlow: Flow<GestureUiState>,
+    tutorialStateFlow: Flow<TutorialActionState>,
     motionEventConsumer: (MotionEvent) -> Boolean,
     easterEggTriggeredFlow: Flow<Boolean>,
     onEasterEggFinished: () -> Unit,
@@ -106,25 +49,21 @@
 ) {
     BackHandler(onBack = onBack)
     val easterEggTriggered by easterEggTriggeredFlow.collectAsStateWithLifecycle(false)
-    val gestureState by gestureUiStateFlow.collectAsStateWithLifecycle(NotStarted)
+    val tutorialState by tutorialStateFlow.collectAsStateWithLifecycle(NotStarted)
     TouchpadGesturesHandlingBox(
         motionEventConsumer,
-        gestureState,
+        tutorialState,
         easterEggTriggered,
         onEasterEggFinished,
     ) {
-        var lastState: TutorialActionState by remember {
-            mutableStateOf(TutorialActionState.NotStarted)
-        }
-        lastState = gestureState.toTutorialActionState(lastState)
-        ActionTutorialContent(lastState, onDoneButtonClicked, screenConfig)
+        ActionTutorialContent(tutorialState, onDoneButtonClicked, screenConfig)
     }
 }
 
 @Composable
 private fun TouchpadGesturesHandlingBox(
     motionEventConsumer: (MotionEvent) -> Boolean,
-    gestureState: GestureUiState,
+    tutorialState: TutorialActionState,
     easterEggTriggered: Boolean,
     onEasterEggFinished: () -> Unit,
     modifier: Modifier = Modifier,
@@ -150,7 +89,7 @@
                 .pointerInteropFilter(
                     onTouchEvent = { event ->
                         // FINISHED is the final state so we don't need to process touches anymore
-                        if (gestureState is Finished) {
+                        if (tutorialState is TutorialActionState.Finished) {
                             false
                         } else {
                             motionEventConsumer(event)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
index 4f1f40d..4acdb60 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
@@ -49,7 +49,7 @@
         )
     GestureTutorialScreen(
         screenConfig = screenConfig,
-        gestureUiStateFlow = viewModel.gestureUiState,
+        tutorialStateFlow = viewModel.tutorialState,
         motionEventConsumer = {
             easterEggGestureViewModel.accept(it)
             viewModel.handleEvent(it)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
index 6c9e26c..8dd53a7 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
@@ -50,7 +50,7 @@
         )
     GestureTutorialScreen(
         screenConfig = screenConfig,
-        gestureUiStateFlow = viewModel.gestureUiState,
+        tutorialStateFlow = viewModel.tutorialState,
         motionEventConsumer = {
             easterEggGestureViewModel.accept(it)
             viewModel.handleEvent(it)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModel.kt
index 8e53669a..7a3d4d1 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModel.kt
@@ -17,12 +17,12 @@
 package com.android.systemui.touchpad.tutorial.ui.viewmodel
 
 import android.view.MotionEvent
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState
 import com.android.systemui.res.R
-import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState
-import com.android.systemui.touchpad.tutorial.ui.composable.toGestureUiState
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureDirection
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted
 import com.android.systemui.touchpad.tutorial.ui.gesture.handleTouchpadMotionEvent
 import com.android.systemui.util.kotlin.pairwiseBy
 import kotlinx.coroutines.flow.Flow
@@ -30,21 +30,26 @@
 class BackGestureScreenViewModel(val gestureRecognizer: GestureRecognizerAdapter) :
     TouchpadTutorialScreenViewModel {
 
-    override val gestureUiState: Flow<GestureUiState> =
-        gestureRecognizer.gestureState.pairwiseBy(GestureState.NotStarted) { previous, current ->
-            toGestureUiState(current, previous)
-        }
+    override val tutorialState: Flow<TutorialActionState> =
+        gestureRecognizer.gestureState
+            .pairwiseBy(NotStarted) { previous, current ->
+                current to toAnimationProperties(current, previous)
+            }
+            .mapToTutorialState()
 
     override fun handleEvent(event: MotionEvent): Boolean {
         return gestureRecognizer.handleTouchpadMotionEvent(event)
     }
 
-    private fun toGestureUiState(current: GestureState, previous: GestureState): GestureUiState {
+    private fun toAnimationProperties(
+        current: GestureState,
+        previous: GestureState,
+    ): TutorialAnimationProperties {
         val (startMarker, endMarker) =
             if (current is InProgress && current.direction == GestureDirection.LEFT) {
                 "gesture to L" to "end progress L"
             } else "gesture to R" to "end progress R"
-        return current.toGestureUiState(
+        return TutorialAnimationProperties(
             progressStartMarker = startMarker,
             progressEndMarker = endMarker,
             successAnimation = successAnimation(previous),
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModel.kt
index 9d6f568..c75d44f 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModel.kt
@@ -17,9 +17,8 @@
 package com.android.systemui.touchpad.tutorial.ui.viewmodel
 
 import android.view.MotionEvent
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState
 import com.android.systemui.res.R
-import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState
-import com.android.systemui.touchpad.tutorial.ui.composable.toGestureUiState
 import com.android.systemui.touchpad.tutorial.ui.gesture.handleTouchpadMotionEvent
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.map
@@ -27,14 +26,17 @@
 class HomeGestureScreenViewModel(private val gestureRecognizer: GestureRecognizerAdapter) :
     TouchpadTutorialScreenViewModel {
 
-    override val gestureUiState: Flow<GestureUiState> =
-        gestureRecognizer.gestureState.map {
-            it.toGestureUiState(
-                progressStartMarker = "drag with gesture",
-                progressEndMarker = "release playback realtime",
-                successAnimation = R.raw.trackpad_home_success,
-            )
-        }
+    override val tutorialState: Flow<TutorialActionState> =
+        gestureRecognizer.gestureState
+            .map {
+                it to
+                    TutorialAnimationProperties(
+                        progressStartMarker = "drag with gesture",
+                        progressEndMarker = "release playback realtime",
+                        successAnimation = R.raw.trackpad_home_success,
+                    )
+            }
+            .mapToTutorialState()
 
     override fun handleEvent(event: MotionEvent): Boolean {
         return gestureRecognizer.handleTouchpadMotionEvent(event)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModel.kt
index 9752858..9fab5f3 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModel.kt
@@ -17,9 +17,8 @@
 package com.android.systemui.touchpad.tutorial.ui.viewmodel
 
 import android.view.MotionEvent
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState
 import com.android.systemui.res.R
-import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState
-import com.android.systemui.touchpad.tutorial.ui.composable.toGestureUiState
 import com.android.systemui.touchpad.tutorial.ui.gesture.handleTouchpadMotionEvent
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.map
@@ -27,14 +26,17 @@
 class RecentAppsGestureScreenViewModel(private val gestureRecognizer: GestureRecognizerAdapter) :
     TouchpadTutorialScreenViewModel {
 
-    override val gestureUiState: Flow<GestureUiState> =
-        gestureRecognizer.gestureState.map {
-            it.toGestureUiState(
-                progressStartMarker = "drag with gesture",
-                progressEndMarker = "onPause",
-                successAnimation = R.raw.trackpad_recent_apps_success,
-            )
-        }
+    override val tutorialState: Flow<TutorialActionState> =
+        gestureRecognizer.gestureState
+            .map {
+                it to
+                    TutorialAnimationProperties(
+                        progressStartMarker = "drag with gesture",
+                        progressEndMarker = "onPause",
+                        successAnimation = R.raw.trackpad_recent_apps_success,
+                    )
+            }
+            .mapToTutorialState()
 
     override fun handleEvent(event: MotionEvent): Boolean {
         return gestureRecognizer.handleTouchpadMotionEvent(event)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialScreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialScreenViewModel.kt
index 31e953d..3b6e3c7 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialScreenViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialScreenViewModel.kt
@@ -17,11 +17,62 @@
 package com.android.systemui.touchpad.tutorial.ui.viewmodel
 
 import android.view.MotionEvent
-import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState
+import androidx.annotation.RawRes
+import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.Finished
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
 
 interface TouchpadTutorialScreenViewModel {
-    val gestureUiState: Flow<GestureUiState>
+    val tutorialState: Flow<TutorialActionState>
 
     fun handleEvent(event: MotionEvent): Boolean
 }
+
+data class TutorialAnimationProperties(
+    val progressStartMarker: String,
+    val progressEndMarker: String,
+    @RawRes val successAnimation: Int,
+)
+
+fun Flow<Pair<GestureState, TutorialAnimationProperties>>.mapToTutorialState():
+    Flow<TutorialActionState> {
+    return flow<TutorialActionState> {
+        var lastState: TutorialActionState = TutorialActionState.NotStarted
+        collect { (gestureState, animationProperties) ->
+            val newState = gestureState.toTutorialActionState(animationProperties, lastState)
+            lastState = newState
+            emit(newState)
+        }
+    }
+}
+
+fun GestureState.toTutorialActionState(
+    properties: TutorialAnimationProperties,
+    previousState: TutorialActionState,
+): TutorialActionState {
+    return when (this) {
+        NotStarted -> TutorialActionState.NotStarted
+        is InProgress -> {
+            val inProgress =
+                TutorialActionState.InProgress(
+                    progress = progress,
+                    startMarker = properties.progressStartMarker,
+                    endMarker = properties.progressEndMarker,
+                )
+            if (
+                previousState is TutorialActionState.InProgressAfterError ||
+                    previousState is TutorialActionState.Error
+            ) {
+                TutorialActionState.InProgressAfterError(inProgress)
+            } else {
+                inProgress
+            }
+        }
+        is Finished -> TutorialActionState.Finished(properties.successAnimation)
+        GestureState.Error -> TutorialActionState.Error
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTraceLogger.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTraceLogger.kt
index 6597097..7d3966b 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTraceLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTraceLogger.kt
@@ -17,8 +17,9 @@
 
 import android.content.Context
 import android.hardware.devicestate.DeviceStateManager
-import android.os.Trace
 import com.android.app.tracing.TraceStateLogger
+import com.android.app.tracing.coroutines.TrackTracer
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -29,7 +30,6 @@
 import javax.inject.Inject
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
-import com.android.app.tracing.coroutines.launchTraced as launch
 import kotlinx.coroutines.plus
 
 /**
@@ -45,7 +45,7 @@
     @Application applicationScope: CoroutineScope,
     @Background private val coroutineContext: CoroutineContext,
     private val deviceStateRepository: DeviceStateRepository,
-    private val deviceStateManager: DeviceStateManager
+    private val deviceStateManager: DeviceStateManager,
 ) : CoreStartable {
     private val isFoldable: Boolean = isDeviceFoldable(context.resources, deviceStateManager)
 
@@ -61,7 +61,7 @@
 
         bgScope.launch {
             foldStateRepository.hingeAngle.collect {
-                Trace.traceCounter(Trace.TRACE_TAG_APP, "hingeAngle", it.toInt())
+                TrackTracer.instantForGroup("unfold", "hingeAngle", it.toInt())
             }
         }
         bgScope.launch {
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt
index d9a2e95..a88b127a 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt
@@ -17,10 +17,14 @@
 package com.android.systemui.util.kotlin
 
 import android.content.Context
+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.filter
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
 
 class Utils {
     companion object {
@@ -32,6 +36,7 @@
 
         fun <A, B, C, D> toQuad(a: A, bcd: Triple<B, C, D>) =
             Quad(a, bcd.first, bcd.second, bcd.third)
+
         fun <A, B, C, D> toQuad(abc: Triple<A, B, C>, d: D) =
             Quad(abc.first, abc.second, abc.third, d)
 
@@ -51,7 +56,7 @@
                 bcdefg.third,
                 bcdefg.fourth,
                 bcdefg.fifth,
-                bcdefg.sixth
+                bcdefg.sixth,
             )
 
         /**
@@ -81,7 +86,7 @@
         fun <A, B, C, D> Flow<A>.sample(
             b: Flow<B>,
             c: Flow<C>,
-            d: Flow<D>
+            d: Flow<D>,
         ): Flow<Quad<A, B, C, D>> {
             return this.sample(combine(b, c, d, ::Triple), ::toQuad)
         }
@@ -134,6 +139,20 @@
         ): Flow<Septuple<A, B, C, D, E, F, G>> {
             return this.sample(combine(b, c, d, e, f, g, ::Sextuple), ::toSeptuple)
         }
+
+        /**
+         * Combines 2 state flows, applying [transform] between the initial values to set the
+         * initial value of the resulting StateFlow.
+         */
+        fun <A, B, R> combineState(
+            f1: StateFlow<A>,
+            f2: StateFlow<B>,
+            scope: CoroutineScope,
+            sharingStarted: SharingStarted,
+            transform: (A, B) -> R,
+        ): StateFlow<R> =
+            combine(f1, f2) { a, b -> transform(a, b) }
+                .stateIn(scope, sharingStarted, transform(f1.value, f2.value))
     }
 }
 
@@ -144,7 +163,7 @@
     val second: B,
     val third: C,
     val fourth: D,
-    val fifth: E
+    val fifth: E,
 )
 
 data class Sextuple<A, B, C, D, E, F>(
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt
index fa108842..3b0c8a6 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt
@@ -46,7 +46,7 @@
 constructor(
     private val volumeDialogController: VolumeDialogController,
     @VolumeDialogPlugin private val coroutineScope: CoroutineScope,
-    @Background private val bgHandler: Handler,
+    @Background private val bgHandler: Handler?,
 ) {
 
     @SuppressLint("SharedFlowCreation") // event-bus needed
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt
index e8d19dd..96630ca 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt
@@ -51,6 +51,8 @@
 import kotlinx.coroutines.launch
 
 private const val CLOSE_DRAWER_DELAY = 300L
+// Ensure roundness and color of button is updated when progress is changed by a minimum fraction.
+private const val BUTTON_MIN_VISIBLE_CHANGE = 0.05F
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @VolumeDialogScope
@@ -58,12 +60,12 @@
 @Inject
 constructor(private val viewModel: VolumeDialogRingerDrawerViewModel) {
     private val roundnessSpringForce =
-        SpringForce(0F).apply {
+        SpringForce(1F).apply {
             stiffness = 800F
             dampingRatio = 0.6F
         }
     private val colorSpringForce =
-        SpringForce(0F).apply {
+        SpringForce(1F).apply {
             stiffness = 3800F
             dampingRatio = 1F
         }
@@ -257,30 +259,35 @@
             // We only need to execute on roundness animation end and volume dialog background
             // progress update once because these changes should be applied once on volume dialog
             // background and ringer drawer views.
-            val selectedCornerRadius = (selectedButton.background as GradientDrawable).cornerRadius
-            if (selectedCornerRadius.toInt() != selectedButtonUiModel.cornerRadius) {
-                selectedButton.animateTo(
-                    selectedButtonUiModel,
-                    if (uiModel.currentButtonIndex == count - 1) {
-                        onProgressChanged
-                    } else {
-                        { _, _ -> }
-                    },
-                )
-            }
-            val unselectedCornerRadius =
-                (unselectedButton.background as GradientDrawable).cornerRadius
-            if (unselectedCornerRadius.toInt() != unselectedButtonUiModel.cornerRadius) {
-                unselectedButton.animateTo(
-                    unselectedButtonUiModel,
-                    if (previousIndex == count - 1) {
-                        onProgressChanged
-                    } else {
-                        { _, _ -> }
-                    },
-                )
-            }
             coroutineScope {
+                val selectedCornerRadius =
+                    (selectedButton.background as GradientDrawable).cornerRadius
+                if (selectedCornerRadius.toInt() != selectedButtonUiModel.cornerRadius) {
+                    launch {
+                        selectedButton.animateTo(
+                            selectedButtonUiModel,
+                            if (uiModel.currentButtonIndex == count - 1) {
+                                onProgressChanged
+                            } else {
+                                { _, _ -> }
+                            },
+                        )
+                    }
+                }
+                val unselectedCornerRadius =
+                    (unselectedButton.background as GradientDrawable).cornerRadius
+                if (unselectedCornerRadius.toInt() != unselectedButtonUiModel.cornerRadius) {
+                    launch {
+                        unselectedButton.animateTo(
+                            unselectedButtonUiModel,
+                            if (previousIndex == count - 1) {
+                                onProgressChanged
+                            } else {
+                                { _, _ -> }
+                            },
+                        )
+                    }
+                }
                 launch {
                     delay(CLOSE_DRAWER_DELAY)
                     bindButtons(viewModel, uiModel, onAnimationEnd, isAnimated = true)
@@ -383,11 +390,14 @@
         onProgressChanged: (Float, Boolean) -> Unit = { _, _ -> },
     ) {
         val roundnessAnimation =
-            SpringAnimation(FloatValueHolder(0F)).setSpring(roundnessSpringForce)
-        val colorAnimation = SpringAnimation(FloatValueHolder(0F)).setSpring(colorSpringForce)
+            SpringAnimation(FloatValueHolder(0F), 1F).setSpring(roundnessSpringForce)
+        val colorAnimation = SpringAnimation(FloatValueHolder(0F), 1F).setSpring(colorSpringForce)
         val radius = (background as GradientDrawable).cornerRadius
         val cornerRadiusDiff =
             ringerButtonUiModel.cornerRadius - (background as GradientDrawable).cornerRadius
+
+        roundnessAnimation.minimumVisibleChange = BUTTON_MIN_VISIBLE_CHANGE
+        colorAnimation.minimumVisibleChange = BUTTON_MIN_VISIBLE_CHANGE
         coroutineScope {
             launch {
                 colorAnimation.suspendAnimate { value ->
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt
index 88af210..940c79c 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt
@@ -19,7 +19,6 @@
 import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType
 import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogOverscrollViewBinder
 import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderHapticsViewBinder
-import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderTouchesViewBinder
 import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderViewBinder
 import dagger.BindsInstance
 import dagger.Subcomponent
@@ -34,8 +33,6 @@
 
     fun sliderViewBinder(): VolumeDialogSliderViewBinder
 
-    fun sliderTouchesViewBinder(): VolumeDialogSliderTouchesViewBinder
-
     fun sliderHapticsViewBinder(): VolumeDialogSliderHapticsViewBinder
 
     fun overscrollViewBinder(): VolumeDialogOverscrollViewBinder
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt
index 04dc80c..3988acb 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt
@@ -16,7 +16,9 @@
 
 package com.android.systemui.volume.dialog.sliders.domain.interactor
 
+import com.android.settingslib.volume.shared.model.AudioStream
 import com.android.systemui.plugins.VolumeDialogController
+import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
 import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
 import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogStateInteractor
 import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel
@@ -27,6 +29,8 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.mapNotNull
 import kotlinx.coroutines.flow.stateIn
 
@@ -39,8 +43,17 @@
     @VolumeDialog private val coroutineScope: CoroutineScope,
     volumeDialogStateInteractor: VolumeDialogStateInteractor,
     private val volumeDialogController: VolumeDialogController,
+    zenModeInteractor: ZenModeInteractor,
 ) {
 
+    val isDisabledByZenMode: Flow<Boolean> =
+        if (sliderType is VolumeDialogSliderType.Stream) {
+            zenModeInteractor.activeModesBlockingStream(AudioStream(sliderType.audioStream)).map {
+                it.mainMode != null
+            }
+        } else {
+            flowOf(false)
+        }
     val slider: Flow<VolumeDialogStreamModel> =
         volumeDialogStateInteractor.volumeDialogState
             .mapNotNull {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderTouchesViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderTouchesViewBinder.kt
deleted file mode 100644
index 4ecac7a..0000000
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderTouchesViewBinder.kt
+++ /dev/null
@@ -1,41 +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.volume.dialog.sliders.ui
-
-import android.annotation.SuppressLint
-import android.view.View
-import com.android.systemui.res.R
-import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
-import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderInputEventsViewModel
-import com.google.android.material.slider.Slider
-import javax.inject.Inject
-
-@VolumeDialogSliderScope
-class VolumeDialogSliderTouchesViewBinder
-@Inject
-constructor(private val viewModel: VolumeDialogSliderInputEventsViewModel) {
-
-    @SuppressLint("ClickableViewAccessibility")
-    fun bind(view: View) {
-        with(view.requireViewById<Slider>(R.id.volume_dialog_slider)) {
-            setOnTouchListener { _, event ->
-                viewModel.onTouchEvent(event)
-                false
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
index 67ffb06..3b964fd 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
@@ -23,6 +23,7 @@
 import androidx.dynamicanimation.animation.SpringForce
 import com.android.systemui.res.R
 import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderInputEventsViewModel
 import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderStateModel
 import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel
 import com.google.android.material.slider.Slider
@@ -35,7 +36,10 @@
 @VolumeDialogSliderScope
 class VolumeDialogSliderViewBinder
 @Inject
-constructor(private val viewModel: VolumeDialogSliderViewModel) {
+constructor(
+    private val viewModel: VolumeDialogSliderViewModel,
+    private val inputViewModel: VolumeDialogSliderInputEventsViewModel,
+) {
 
     private val sliderValueProperty =
         object : FloatPropertyCompat<Slider>("value") {
@@ -51,16 +55,21 @@
             dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
         }
 
+    @SuppressLint("ClickableViewAccessibility")
     fun CoroutineScope.bind(view: View) {
         var isInitialUpdate = true
         val sliderView: Slider = view.requireViewById(R.id.volume_dialog_slider)
         val animation = SpringAnimation(sliderView, sliderValueProperty)
         animation.spring = springForce
-
+        sliderView.setOnTouchListener { _, event ->
+            inputViewModel.onTouchEvent(event)
+            false
+        }
         sliderView.addOnChangeListener { _, value, fromUser ->
             viewModel.setStreamVolume(value.roundToInt(), fromUser)
         }
 
+        viewModel.isDisabledByZenMode.onEach { sliderView.isEnabled = !it }.launchIn(this)
         viewModel.state
             .onEach {
                 sliderView.setModel(it, animation, isInitialUpdate)
@@ -82,7 +91,7 @@
         // coerce the current value to the new value range before animating it. This prevents
         // animating from the value that is outside of current [valueFrom, valueTo].
         value = value.coerceIn(valueFrom, valueTo)
-        setTrackIconActiveStart(model.iconRes)
+        trackIconActiveStart = model.icon
         if (isInitialUpdate) {
             value = model.value
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt
index f066b56..75d427a 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt
@@ -71,7 +71,6 @@
         viewsToAnimate: Array<View>,
     ) {
         with(component.sliderViewBinder()) { bind(sliderContainer) }
-        with(component.sliderTouchesViewBinder()) { bind(sliderContainer) }
         with(component.sliderHapticsViewBinder()) { bind(sliderContainer) }
         with(component.overscrollViewBinder()) { bind(sliderContainer, viewsToAnimate) }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt
index 5c39b6f..daf4c82 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt
@@ -16,13 +16,16 @@
 
 package com.android.systemui.volume.dialog.sliders.ui.viewmodel
 
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.drawable.Drawable
 import android.media.AudioManager
 import androidx.annotation.DrawableRes
-import com.android.settingslib.notification.domain.interactor.NotificationsSoundPolicyInteractor
 import com.android.settingslib.volume.domain.interactor.AudioVolumeInteractor
 import com.android.settingslib.volume.shared.model.AudioStream
 import com.android.settingslib.volume.shared.model.RingerMode
 import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
@@ -31,11 +34,12 @@
 class VolumeDialogSliderIconProvider
 @Inject
 constructor(
-    private val notificationsSoundPolicyInteractor: NotificationsSoundPolicyInteractor,
+    private val context: Context,
+    private val zenModeInteractor: ZenModeInteractor,
     private val audioVolumeInteractor: AudioVolumeInteractor,
 ) {
 
-    @DrawableRes
+    @SuppressLint("UseCompatLoadingForDrawables")
     fun getStreamIcon(
         stream: Int,
         level: Int,
@@ -43,54 +47,71 @@
         levelMax: Int,
         isMuted: Boolean,
         isRoutedToBluetooth: Boolean,
-    ): Flow<Int> {
+    ): Flow<Drawable> {
         return combine(
-            notificationsSoundPolicyInteractor.isZenMuted(AudioStream(stream)),
+            zenModeInteractor.activeModesBlockingStream(AudioStream(stream)),
             ringerModeForStream(stream),
-        ) { isZenMuted, ringerMode ->
-            val isStreamOffline = level == 0 || isMuted
-            if (isZenMuted) {
-                // TODO(b/372466264) use icon for the corresponding zenmode
-                return@combine com.android.internal.R.drawable.ic_qs_dnd
-            }
-            when (ringerMode?.value) {
-                AudioManager.RINGER_MODE_VIBRATE ->
-                    return@combine R.drawable.ic_volume_ringer_vibrate
-                AudioManager.RINGER_MODE_SILENT -> return@combine R.drawable.ic_ring_volume_off
-            }
-            if (isRoutedToBluetooth) {
-                return@combine if (stream == AudioManager.STREAM_VOICE_CALL) {
-                    R.drawable.ic_volume_bt_sco
-                } else {
-                    if (isStreamOffline) {
-                        R.drawable.ic_volume_media_bt_mute
-                    } else {
-                        R.drawable.ic_volume_media_bt
-                    }
-                }
-            }
-
-            return@combine if (isStreamOffline) {
-                getMutedIconForStream(stream) ?: getIconForStream(stream)
+        ) { activeModesBlockingStream, ringerMode ->
+            if (activeModesBlockingStream.mainMode?.icon != null) {
+                return@combine activeModesBlockingStream.mainMode.icon.drawable
             } else {
-                if (level < (levelMax + levelMin) / 2) {
-                    // This icon is different on TV
-                    R.drawable.ic_volume_media_low
-                } else {
-                    getIconForStream(stream)
-                }
+                context.getDrawable(
+                    getIconRes(
+                        stream,
+                        level,
+                        levelMin,
+                        levelMax,
+                        isMuted,
+                        isRoutedToBluetooth,
+                        ringerMode,
+                    )
+                )!!
             }
         }
     }
 
     @DrawableRes
-    private fun getMutedIconForStream(stream: Int): Int? {
-        return when (stream) {
-            AudioManager.STREAM_MUSIC -> R.drawable.ic_volume_media_mute
-            AudioManager.STREAM_NOTIFICATION -> R.drawable.ic_volume_ringer_mute
-            AudioManager.STREAM_ALARM -> R.drawable.ic_volume_alarm_mute
-            AudioManager.STREAM_SYSTEM -> R.drawable.ic_volume_system_mute
-            else -> null
+    private fun getIconRes(
+        stream: Int,
+        level: Int,
+        levelMin: Int,
+        levelMax: Int,
+        isMuted: Boolean,
+        isRoutedToBluetooth: Boolean,
+        ringerMode: RingerMode?,
+    ): Int {
+        val isStreamOffline = level == 0 || isMuted
+        when (ringerMode?.value) {
+            AudioManager.RINGER_MODE_VIBRATE -> return R.drawable.ic_volume_ringer_vibrate
+            AudioManager.RINGER_MODE_SILENT -> return R.drawable.ic_ring_volume_off
+        }
+        if (isRoutedToBluetooth) {
+            return if (stream == AudioManager.STREAM_VOICE_CALL) {
+                R.drawable.ic_volume_bt_sco
+            } else {
+                if (isStreamOffline) {
+                    R.drawable.ic_volume_media_bt_mute
+                } else {
+                    R.drawable.ic_volume_media_bt
+                }
+            }
+        }
+
+        return if (isStreamOffline) {
+            when (stream) {
+                AudioManager.STREAM_MUSIC -> R.drawable.ic_volume_media_mute
+                AudioManager.STREAM_NOTIFICATION -> R.drawable.ic_volume_ringer_mute
+                AudioManager.STREAM_ALARM -> R.drawable.ic_volume_alarm_mute
+                AudioManager.STREAM_SYSTEM -> R.drawable.ic_volume_system_mute
+                else -> null
+            } ?: getIconForStream(stream)
+        } else {
+            if (level < (levelMax + levelMin) / 2) {
+                // This icon is different on TV
+                R.drawable.ic_volume_media_low
+            } else {
+                getIconForStream(stream)
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt
index 5750c04..8df9e78 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt
@@ -16,21 +16,21 @@
 
 package com.android.systemui.volume.dialog.sliders.ui.viewmodel
 
-import androidx.annotation.DrawableRes
+import android.graphics.drawable.Drawable
 import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel
 
 data class VolumeDialogSliderStateModel(
     val minValue: Float,
     val maxValue: Float,
     val value: Float,
-    @DrawableRes val iconRes: Int,
+    val icon: Drawable,
 )
 
-fun VolumeDialogStreamModel.toStateModel(@DrawableRes iconRes: Int): VolumeDialogSliderStateModel {
+fun VolumeDialogStreamModel.toStateModel(icon: Drawable): VolumeDialogSliderStateModel {
     return VolumeDialogSliderStateModel(
         minValue = levelMin.toFloat(),
         value = level.toFloat(),
         maxValue = levelMax.toFloat(),
-        iconRes = iconRes,
+        icon = icon,
     )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt
index 6d8457b..d999910 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt
@@ -66,12 +66,14 @@
     private val model: Flow<VolumeDialogStreamModel> =
         interactor.slider
             .filter {
-                val lastVolumeUpdateTime = userVolumeUpdates.value?.timestampMillis ?: 0
+                val currentVolumeUpdate = userVolumeUpdates.value ?: return@filter true
+                val lastVolumeUpdateTime = currentVolumeUpdate.timestampMillis
                 getTimestampMillis() - lastVolumeUpdateTime > VOLUME_UPDATE_GRACE_PERIOD
             }
             .stateIn(coroutineScope, SharingStarted.Eagerly, null)
             .filterNotNull()
 
+    val isDisabledByZenMode: Flow<Boolean> = interactor.isDisabledByZenMode
     val state: Flow<VolumeDialogSliderStateModel> =
         model
             .flatMapLatest { streamModel ->
@@ -81,7 +83,7 @@
                             level = level,
                             levelMin = levelMin,
                             levelMax = levelMax,
-                            isMuted = muted,
+                            isMuted = muteSupported && muted,
                             isRoutedToBluetooth = routedToBluetooth,
                         )
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/VolumeDialogResources.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/VolumeDialogResources.kt
deleted file mode 100644
index e5cf62b..0000000
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/VolumeDialogResources.kt
+++ /dev/null
@@ -1,68 +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.volume.dialog.ui
-
-import android.content.Context
-import android.content.res.Resources
-import com.android.systemui.dagger.qualifiers.UiBackground
-import com.android.systemui.res.R
-import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.statusbar.policy.onConfigChanged
-import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
-import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
-import javax.inject.Inject
-import kotlin.coroutines.CoroutineContext
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.stateIn
-
-/**
- * Provides cached resources [Flow]s that update when the configuration changes.
- *
- * Consume or use [kotlinx.coroutines.flow.first] to get the value.
- */
-@VolumeDialogScope
-class VolumeDialogResources
-@Inject
-constructor(
-    @VolumeDialog private val coroutineScope: CoroutineScope,
-    @UiBackground private val uiBackgroundContext: CoroutineContext,
-    private val context: Context,
-    private val configurationController: ConfigurationController,
-) {
-
-    val dialogShowDurationMillis: Flow<Long> = configurationResource {
-        getInteger(R.integer.config_dialogShowAnimationDurationMs).toLong()
-    }
-
-    val dialogHideDurationMillis: Flow<Long> = configurationResource {
-        getInteger(R.integer.config_dialogHideAnimationDurationMs).toLong()
-    }
-
-    private fun <T> configurationResource(get: Resources.() -> T): Flow<T> =
-        configurationController.onConfigChanged
-            .map { context.resources.get() }
-            .onStart { emit(context.resources.get()) }
-            .flowOn(uiBackgroundContext)
-            .stateIn(coroutineScope, SharingStarted.Eagerly, null)
-            .filterNotNull()
-}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
index a3166a9..428dc6e 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
@@ -17,23 +17,28 @@
 package com.android.systemui.volume.dialog.ui.binder
 
 import android.app.Dialog
+import android.content.res.Resources
 import android.graphics.Rect
 import android.graphics.Region
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewTreeObserver
 import android.view.ViewTreeObserver.InternalInsetsInfo
+import android.view.WindowInsets
 import androidx.constraintlayout.motion.widget.MotionLayout
+import androidx.core.view.updatePadding
 import com.android.internal.view.RotationPolicy
+import com.android.systemui.common.ui.view.onApplyWindowInsets
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.res.R
 import com.android.systemui.util.children
+import com.android.systemui.util.kotlin.awaitCancellationThenDispose
 import com.android.systemui.volume.SystemUIInterpolators
 import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
 import com.android.systemui.volume.dialog.ringer.ui.binder.VolumeDialogRingerViewBinder
 import com.android.systemui.volume.dialog.settings.ui.binder.VolumeDialogSettingsButtonViewBinder
 import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
 import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSlidersViewBinder
-import com.android.systemui.volume.dialog.ui.VolumeDialogResources
 import com.android.systemui.volume.dialog.ui.utils.JankListenerFactory
 import com.android.systemui.volume.dialog.ui.utils.suspendAnimate
 import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogViewModel
@@ -42,7 +47,7 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.onEach
@@ -56,7 +61,7 @@
 class VolumeDialogViewBinder
 @Inject
 constructor(
-    private val volumeResources: VolumeDialogResources,
+    @Main resources: Resources,
     private val viewModel: VolumeDialogViewModel,
     private val jankListenerFactory: JankListenerFactory,
     private val tracer: VolumeTracer,
@@ -65,10 +70,16 @@
     private val settingsButtonViewBinder: VolumeDialogSettingsButtonViewBinder,
 ) {
 
+    private val dialogShowAnimationDurationMs =
+        resources.getInteger(R.integer.config_dialogShowAnimationDurationMs).toLong()
+    private val dialogHideAnimationDurationMs =
+        resources.getInteger(R.integer.config_dialogHideAnimationDurationMs).toLong()
+
     fun CoroutineScope.bind(dialog: Dialog) {
+        val insets: MutableStateFlow<WindowInsets> =
+            MutableStateFlow(WindowInsets.Builder().build())
         // Root view of the Volume Dialog.
         val root: MotionLayout = dialog.requireViewById(R.id.volume_dialog_root)
-        root.alpha = 0f
 
         animateVisibility(root, dialog, viewModel.dialogVisibilityModel)
 
@@ -83,6 +94,22 @@
 
         launch { root.viewTreeObserver.computeInternalInsetsListener(root) }
 
+        launch {
+            root
+                .onApplyWindowInsets { v, newInsets ->
+                    val insetsValues = newInsets.getInsets(WindowInsets.Type.displayCutout())
+                    v.updatePadding(
+                        left = insetsValues.left,
+                        top = insetsValues.top,
+                        right = insetsValues.right,
+                        bottom = insetsValues.bottom,
+                    )
+                    insets.value = newInsets
+                    WindowInsets.CONSUMED
+                }
+                .awaitCancellationThenDispose()
+        }
+
         with(volumeDialogRingerViewBinder) { bind(root) }
         with(slidersViewBinder) { bind(root) }
         with(settingsButtonViewBinder) { bind(root) }
@@ -98,13 +125,15 @@
                 when (it) {
                     is VolumeDialogVisibilityModel.Visible -> {
                         tracer.traceVisibilityEnd(it)
-                        calculateTranslationX(view)?.let(view::setTranslationX)
-                        view.animateShow(volumeResources.dialogShowDurationMillis.first())
+                        view.animateShow(
+                            duration = dialogShowAnimationDurationMs,
+                            translationX = calculateTranslationX(view),
+                        )
                     }
                     is VolumeDialogVisibilityModel.Dismissed -> {
                         tracer.traceVisibilityEnd(it)
                         view.animateHide(
-                            duration = volumeResources.dialogHideDurationMillis.first(),
+                            duration = dialogHideAnimationDurationMs,
                             translationX = calculateTranslationX(view),
                         )
                         dialog.dismiss()
@@ -129,24 +158,15 @@
         }
     }
 
-    private suspend fun View.animateShow(duration: Long) {
+    private suspend fun View.animateShow(duration: Long, translationX: Float?) {
+        translationX?.let { setTranslationX(translationX) }
+        alpha = 0f
         animate()
             .alpha(1f)
             .translationX(0f)
             .setDuration(duration)
             .setInterpolator(SystemUIInterpolators.LogDecelerateInterpolator())
             .suspendAnimate(jankListenerFactory.show(this, duration))
-        /* TODO(b/369993851)
-        .withEndAction(Runnable {
-            if (!Prefs.getBoolean(mContext, Prefs.Key.TOUCHED_RINGER_TOGGLE, false)) {
-                if (mRingerIcon != null) {
-                    mRingerIcon.postOnAnimationDelayed(
-                        getSinglePressFor(mRingerIcon), 1500
-                    )
-                }
-            }
-        })
-         */
     }
 
     private suspend fun View.animateHide(duration: Long, translationX: Float?) {
@@ -155,22 +175,7 @@
                 .alpha(0f)
                 .setDuration(duration)
                 .setInterpolator(SystemUIInterpolators.LogAccelerateInterpolator())
-        /*  TODO(b/369993851)
-        .withEndAction(
-            Runnable {
-                mHandler.postDelayed(
-                    Runnable {
-                        hideRingerDrawer()
-
-                    },
-                    50
-                )
-            }
-        )
-         */
-        if (translationX != null) {
-            animator.translationX(translationX)
-        }
+        translationX?.let { animator.translationX(it) }
         animator.suspendAnimate(jankListenerFactory.dismiss(this, duration))
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
index b20dffb..7a6ede4 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
@@ -44,9 +44,9 @@
 @Inject
 constructor(
     private val context: Context,
-    private val dialogVisibilityInteractor: VolumeDialogVisibilityInteractor,
+    dialogVisibilityInteractor: VolumeDialogVisibilityInteractor,
     volumeDialogSlidersInteractor: VolumeDialogSlidersInteractor,
-    private val volumeDialogStateInteractor: VolumeDialogStateInteractor,
+    volumeDialogStateInteractor: VolumeDialogStateInteractor,
     devicePostureController: DevicePostureController,
     configurationController: ConfigurationController,
 ) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
index cec3d1e..5b8d9b0 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
@@ -18,9 +18,6 @@
 
 import android.content.Context
 import android.media.AudioManager
-import android.media.AudioManager.STREAM_ALARM
-import android.media.AudioManager.STREAM_MUSIC
-import android.media.AudioManager.STREAM_NOTIFICATION
 import android.util.Log
 import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.internal.logging.UiEventLogger
@@ -34,8 +31,6 @@
 import com.android.systemui.modes.shared.ModesUiIcons
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
-import com.android.systemui.statusbar.policy.domain.model.ActiveZenModes
-import com.android.systemui.util.kotlin.combine
 import com.android.systemui.volume.panel.shared.VolumePanelLogger
 import com.android.systemui.volume.panel.ui.VolumePanelUiEvent
 import dagger.assisted.Assisted
@@ -43,12 +38,15 @@
 import dagger.assisted.AssistedInject
 import kotlin.math.roundToInt
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.stateIn
 
@@ -101,48 +99,16 @@
         )
 
     override val slider: StateFlow<SliderState> =
-        if (ModesUiIcons.isEnabled) {
-            combine(
-                    audioVolumeInteractor.getAudioStream(audioStream),
-                    audioVolumeInteractor.canChangeVolume(audioStream),
-                    audioVolumeInteractor.ringerMode,
-                    zenModeInteractor.activeModesBlockingEverything,
-                    zenModeInteractor.activeModesBlockingAlarms,
-                    zenModeInteractor.activeModesBlockingMedia,
-                ) {
-                    model,
-                    isEnabled,
-                    ringerMode,
-                    modesBlockingEverything,
-                    modesBlockingAlarms,
-                    modesBlockingMedia ->
-                    volumePanelLogger.onVolumeUpdateReceived(audioStream, model.volume)
-                    model.toState(
-                        isEnabled,
-                        ringerMode,
-                        getStreamDisabledMessage(
-                            modesBlockingEverything,
-                            modesBlockingAlarms,
-                            modesBlockingMedia,
-                        ),
-                    )
-                }
-                .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty)
-        } else {
-            combine(
-                    audioVolumeInteractor.getAudioStream(audioStream),
-                    audioVolumeInteractor.canChangeVolume(audioStream),
-                    audioVolumeInteractor.ringerMode,
-                ) { model, isEnabled, ringerMode ->
-                    volumePanelLogger.onVolumeUpdateReceived(audioStream, model.volume)
-                    model.toState(
-                        isEnabled,
-                        ringerMode,
-                        getStreamDisabledMessageWithoutModes(audioStream),
-                    )
-                }
-                .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty)
-        }
+        combine(
+                audioVolumeInteractor.getAudioStream(audioStream),
+                audioVolumeInteractor.canChangeVolume(audioStream),
+                audioVolumeInteractor.ringerMode,
+                streamDisabledMessage(),
+            ) { model, isEnabled, ringerMode, streamDisabledMessage ->
+                volumePanelLogger.onVolumeUpdateReceived(audioStream, model.volume)
+                model.toState(isEnabled, ringerMode, streamDisabledMessage)
+            }
+            .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty)
 
     init {
         volumeChanges
@@ -229,40 +195,32 @@
         )
     }
 
-    private fun getStreamDisabledMessage(
-        blockingEverything: ActiveZenModes,
-        blockingAlarms: ActiveZenModes,
-        blockingMedia: ActiveZenModes,
-    ): String {
-        // TODO: b/372213356 - Figure out the correct messages for VOICE_CALL and RING.
-        //  In fact, VOICE_CALL should not be affected by interruption filtering at all.
-        return if (audioStream.value == STREAM_NOTIFICATION) {
-            context.getString(R.string.stream_notification_unavailable)
-        } else {
-            val blockingModeName =
-                when {
-                    blockingEverything.mainMode != null -> blockingEverything.mainMode.name
-                    audioStream.value == STREAM_ALARM -> blockingAlarms.mainMode?.name
-                    audioStream.value == STREAM_MUSIC -> blockingMedia.mainMode?.name
-                    else -> null
-                }
-
-            if (blockingModeName != null) {
-                context.getString(R.string.stream_unavailable_by_modes, blockingModeName)
+    // TODO: b/372213356 - Figure out the correct messages for VOICE_CALL and RING.
+    //  In fact, VOICE_CALL should not be affected by interruption filtering at all.
+    private fun streamDisabledMessage(): Flow<String> {
+        return if (ModesUiIcons.isEnabled) {
+            if (audioStream.value == AudioManager.STREAM_NOTIFICATION) {
+                flowOf(context.getString(R.string.stream_notification_unavailable))
             } else {
-                // Should not actually be visible, but as a catch-all.
-                context.getString(R.string.stream_unavailable_by_unknown)
+                if (zenModeInteractor.canBeBlockedByZenMode(audioStream)) {
+                    zenModeInteractor.activeModesBlockingStream(audioStream).map { blockingZenModes
+                        ->
+                        blockingZenModes.mainMode?.name?.let {
+                            context.getString(R.string.stream_unavailable_by_modes, it)
+                        } ?: context.getString(R.string.stream_unavailable_by_unknown)
+                    }
+                } else {
+                    flowOf(context.getString(R.string.stream_unavailable_by_unknown))
+                }
             }
-        }
-    }
-
-    private fun getStreamDisabledMessageWithoutModes(audioStream: AudioStream): String {
-        // TODO: b/372213356 - Figure out the correct messages for VOICE_CALL and RING.
-        //  In fact, VOICE_CALL should not be affected by interruption filtering at all.
-        return if (audioStream.value == STREAM_NOTIFICATION) {
-            context.getString(R.string.stream_notification_unavailable)
         } else {
-            context.getString(R.string.stream_alarm_unavailable)
+            flowOf(
+                if (audioStream.value == AudioManager.STREAM_NOTIFICATION) {
+                    context.getString(R.string.stream_notification_unavailable)
+                } else {
+                    context.getString(R.string.stream_alarm_unavailable)
+                }
+            )
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index 4abbbac..bac2c47 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -24,13 +24,15 @@
 import android.widget.FrameLayout
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND_INACTIVE
+import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.BroadcastDispatcher
 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.domain.interactor.KeyguardInteractorFactory
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.shared.model.Edge
 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
 import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
@@ -97,8 +99,9 @@
 class ClockEventControllerTest : SysuiTestCase() {
 
     private val kosmos = testKosmos()
-    private val zenModeRepository = kosmos.fakeZenModeRepository
     private val testScope = kosmos.testScope
+    private val zenModeRepository by lazy { kosmos.fakeZenModeRepository }
+    private val zenModeInteractor by lazy { kosmos.zenModeInteractor }
 
     @JvmField @Rule val mockito = MockitoJUnit.rule()
 
@@ -106,7 +109,6 @@
     private lateinit var repository: FakeKeyguardRepository
     private val clockBuffers = ClockMessageBuffers(LogcatOnlyMessageBuffer(LogLevel.DEBUG))
     private lateinit var underTest: ClockEventController
-    private lateinit var dndModeId: String
 
     @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
     @Mock private lateinit var batteryController: BatteryController
@@ -156,17 +158,12 @@
         whenever(largeClockController.theme).thenReturn(ThemeConfig(true, null))
         whenever(userTracker.userId).thenReturn(1)
 
-        dndModeId = MANUAL_DND_INACTIVE.id
-        zenModeRepository.addMode(MANUAL_DND_INACTIVE)
+        repository = kosmos.fakeKeyguardRepository
 
-        repository = FakeKeyguardRepository()
-
-        val withDeps = KeyguardInteractorFactory.create(repository = repository)
-
-        withDeps.featureFlags.apply { set(Flags.REGION_SAMPLING, false) }
+        kosmos.fakeFeatureFlagsClassic.set(Flags.REGION_SAMPLING, false)
         underTest =
             ClockEventController(
-                withDeps.keyguardInteractor,
+                kosmos.keyguardInteractor,
                 keyguardTransitionInteractor,
                 broadcastDispatcher,
                 batteryController,
@@ -177,9 +174,9 @@
                 mainExecutor,
                 bgExecutor,
                 clockBuffers,
-                withDeps.featureFlags,
+                kosmos.fakeFeatureFlagsClassic,
                 zenModeController,
-                kosmos.zenModeInteractor,
+                zenModeInteractor,
                 userTracker,
             )
         underTest.clock = clock
@@ -504,7 +501,7 @@
             runCurrent()
             clearInvocations(events)
 
-            zenModeRepository.activateMode(dndModeId)
+            zenModeRepository.activateMode(MANUAL_DND)
             runCurrent()
 
             verify(events)
@@ -512,7 +509,7 @@
                     eq(ZenData(ZenMode.IMPORTANT_INTERRUPTIONS, R.string::dnd_is_on.name))
                 )
 
-            zenModeRepository.deactivateMode(dndModeId)
+            zenModeRepository.deactivateMode(MANUAL_DND)
             runCurrent()
 
             verify(events).onZenDataChanged(eq(ZenData(ZenMode.OFF, R.string::dnd_is_off.name)))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java
index 9d9fb9c..6ad2128 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java
@@ -30,9 +30,6 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
@@ -51,11 +48,8 @@
 import android.view.View;
 import android.view.WindowManager;
 import android.view.accessibility.AccessibilityManager;
-import android.view.animation.DecelerateInterpolator;
-import android.view.animation.Interpolator;
 import android.window.InputTransferToken;
 
-import androidx.annotation.NonNull;
 import androidx.test.filters.FlakyTest;
 import androidx.test.filters.SmallTest;
 
@@ -80,19 +74,17 @@
 @RunWith(AndroidTestingRunner.class)
 @FlakyTest(bugId = 385115361)
 public class FullscreenMagnificationControllerTest extends SysuiTestCase {
-    private static final long ANIMATION_DURATION_MS = 100L;
     private static final long WAIT_TIMEOUT_S = 5L * HW_TIMEOUT_MULTIPLIER;
-    private static final long ANIMATION_TIMEOUT_MS =
-            5L * ANIMATION_DURATION_MS * HW_TIMEOUT_MULTIPLIER;
 
     private static final String UNIQUE_DISPLAY_ID_PRIMARY = "000";
     private static final String UNIQUE_DISPLAY_ID_SECONDARY = "111";
     private static final int CORNER_RADIUS_PRIMARY = 10;
     private static final int CORNER_RADIUS_SECONDARY = 20;
+    private static final int DISABLED = 0;
+    private static final int ENABLED = 3;
 
     private FullscreenMagnificationController mFullscreenMagnificationController;
     private SurfaceControlViewHost mSurfaceControlViewHost;
-    private ValueAnimator mShowHideBorderAnimator;
     private SurfaceControl.Transaction mTransaction;
     private TestableWindowManager mWindowManager;
     @Mock
@@ -136,7 +128,6 @@
         mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
 
         mTransaction = new SurfaceControl.Transaction();
-        mShowHideBorderAnimator = spy(newNullTargetObjectAnimator());
         mFullscreenMagnificationController = new FullscreenMagnificationController(
                 mContext,
                 mContext.getMainThreadHandler(),
@@ -146,141 +137,68 @@
                 mContext.getSystemService(WindowManager.class),
                 mIWindowManager,
                 scvhSupplier,
-                mTransaction,
-                mShowHideBorderAnimator);
+                mTransaction);
     }
 
     @After
     public void tearDown() {
-        getInstrumentation().runOnMainSync(
-                () -> mFullscreenMagnificationController
-                        .onFullscreenMagnificationActivationChanged(false));
+        getInstrumentation().runOnMainSync(() ->
+                mFullscreenMagnificationController.cleanUpBorder());
     }
 
     @Test
-    public void enableFullscreenMagnification_visibleBorder()
+    public void createShowTargetAnimator_runAnimator_alphaIsEqualToOne() {
+        View view = new View(mContext);
+        view.setAlpha(0f);
+        ValueAnimator animator = mFullscreenMagnificationController.createShowTargetAnimator(view);
+        animator.end();
+        assertThat(view.getAlpha()).isEqualTo(1f);
+    }
+
+    @Test
+    public void createHideTargetAnimator_runAnimator_alphaIsEqualToZero() {
+        View view = new View(mContext);
+        view.setAlpha(1f);
+        ValueAnimator animator = mFullscreenMagnificationController.createHideTargetAnimator(view);
+        animator.end();
+        assertThat(view.getAlpha()).isEqualTo(0f);
+    }
+
+    @Test
+    public void enableFullscreenMagnification_stateEnabled()
             throws InterruptedException, RemoteException {
-        CountDownLatch transactionCommittedLatch = new CountDownLatch(1);
-        CountDownLatch animationEndLatch = new CountDownLatch(1);
-        mTransaction.addTransactionCommittedListener(
-                Runnable::run, transactionCommittedLatch::countDown);
-        mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                animationEndLatch.countDown();
-            }
-        });
-        getInstrumentation().runOnMainSync(() ->
-                //Enable fullscreen magnification
-                mFullscreenMagnificationController
-                        .onFullscreenMagnificationActivationChanged(true));
-        assertWithMessage("Failed to wait for transaction committed")
-                .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
-                .isTrue();
-        assertWithMessage("Failed to wait for animation to be finished")
-                .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
-                .isTrue();
-        verify(mShowHideBorderAnimator).start();
+        enableFullscreenMagnificationAndWaitForTransactionAndAnimation();
+
+        assertThat(mFullscreenMagnificationController.getState()).isEqualTo(ENABLED);
         verify(mIWindowManager)
                 .watchRotation(any(IRotationWatcher.class), eq(Display.DEFAULT_DISPLAY));
-        assertThat(mSurfaceControlViewHost.getView().isVisibleToUser()).isTrue();
     }
 
     @Test
-    public void disableFullscreenMagnification_reverseAnimationAndReleaseScvh()
+    public void disableFullscreenMagnification_stateDisabled()
             throws InterruptedException, RemoteException {
-        CountDownLatch transactionCommittedLatch = new CountDownLatch(1);
-        CountDownLatch enableAnimationEndLatch = new CountDownLatch(1);
-        CountDownLatch disableAnimationEndLatch = new CountDownLatch(1);
-        mTransaction.addTransactionCommittedListener(
-                Runnable::run, transactionCommittedLatch::countDown);
-        mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) {
-                if (isReverse) {
-                    disableAnimationEndLatch.countDown();
-                } else {
-                    enableAnimationEndLatch.countDown();
-                }
-            }
+        enableFullscreenMagnificationAndWaitForTransactionAndAnimation();
+
+        getInstrumentation().runOnMainSync(() -> {
+            // Disable fullscreen magnification
+            mFullscreenMagnificationController
+                    .onFullscreenMagnificationActivationChanged(false);
         });
-        getInstrumentation().runOnMainSync(() ->
-                //Enable fullscreen magnification
-                mFullscreenMagnificationController
-                        .onFullscreenMagnificationActivationChanged(true));
-        assertWithMessage("Failed to wait for transaction committed")
-                .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
-                .isTrue();
-        assertWithMessage("Failed to wait for enabling animation to be finished")
-                .that(enableAnimationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
-                .isTrue();
-        verify(mShowHideBorderAnimator).start();
+        waitForIdleSync();
+        assertThat(mFullscreenMagnificationController.mShowHideBorderAnimator).isNotNull();
+        mFullscreenMagnificationController.mShowHideBorderAnimator.end();
+        waitForIdleSync();
 
-        getInstrumentation().runOnMainSync(() ->
-                // Disable fullscreen magnification
-                mFullscreenMagnificationController
-                        .onFullscreenMagnificationActivationChanged(false));
-
-        assertWithMessage("Failed to wait for disabling animation to be finished")
-                .that(disableAnimationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
-                .isTrue();
-        verify(mShowHideBorderAnimator).reverse();
+        assertThat(mFullscreenMagnificationController.getState()).isEqualTo(DISABLED);
         verify(mSurfaceControlViewHost).release();
         verify(mIWindowManager).removeRotationWatcher(any(IRotationWatcher.class));
     }
 
     @Test
-    public void onFullscreenMagnificationActivationChangeTrue_deactivating_reverseAnimator()
-            throws InterruptedException {
-        // Simulate the hiding border animation is running
-        when(mShowHideBorderAnimator.isRunning()).thenReturn(true);
-        CountDownLatch transactionCommittedLatch = new CountDownLatch(1);
-        CountDownLatch animationEndLatch = new CountDownLatch(1);
-        mTransaction.addTransactionCommittedListener(
-                Runnable::run, transactionCommittedLatch::countDown);
-        mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                animationEndLatch.countDown();
-            }
-        });
-
-        getInstrumentation().runOnMainSync(
-                () -> mFullscreenMagnificationController
-                            .onFullscreenMagnificationActivationChanged(true));
-
-        assertWithMessage("Failed to wait for transaction committed")
-                .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
-                .isTrue();
-        assertWithMessage("Failed to wait for animation to be finished")
-                .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
-                        .isTrue();
-        verify(mShowHideBorderAnimator).reverse();
-    }
-
-    @Test
     public void onScreenSizeChanged_activated_borderChangedToExpectedSize()
             throws InterruptedException {
-        CountDownLatch transactionCommittedLatch = new CountDownLatch(1);
-        CountDownLatch animationEndLatch = new CountDownLatch(1);
-        mTransaction.addTransactionCommittedListener(
-                Runnable::run, transactionCommittedLatch::countDown);
-        mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                animationEndLatch.countDown();
-            }
-        });
-        getInstrumentation().runOnMainSync(() ->
-                //Enable fullscreen magnification
-                mFullscreenMagnificationController
-                        .onFullscreenMagnificationActivationChanged(true));
-        assertWithMessage("Failed to wait for transaction committed")
-                .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
-                .isTrue();
-        assertWithMessage("Failed to wait for animation to be finished")
-                .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
-                .isTrue();
+        enableFullscreenMagnificationAndWaitForTransactionAndAnimation();
+
         final Rect testWindowBounds = new Rect(
                 mWindowManager.getCurrentWindowMetrics().getBounds());
         testWindowBounds.set(testWindowBounds.left, testWindowBounds.top,
@@ -304,29 +222,8 @@
     @Test
     public void enableFullscreenMagnification_applyPrimaryCornerRadius()
             throws InterruptedException {
-        CountDownLatch transactionCommittedLatch = new CountDownLatch(1);
-        CountDownLatch animationEndLatch = new CountDownLatch(1);
-        mTransaction.addTransactionCommittedListener(
-                Runnable::run, transactionCommittedLatch::countDown);
-        mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                animationEndLatch.countDown();
-            }
-        });
+        enableFullscreenMagnificationAndWaitForTransactionAndAnimation();
 
-        getInstrumentation().runOnMainSync(() ->
-                //Enable fullscreen magnification
-                mFullscreenMagnificationController
-                        .onFullscreenMagnificationActivationChanged(true));
-        assertWithMessage("Failed to wait for transaction committed")
-                .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
-                .isTrue();
-        assertWithMessage("Failed to wait for animation to be finished")
-                .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
-                .isTrue();
-
-        // Verify the initial corner radius is applied
         GradientDrawable backgroundDrawable =
                 (GradientDrawable) mSurfaceControlViewHost.getView().getBackground();
         assertThat(backgroundDrawable.getCornerRadius()).isEqualTo(CORNER_RADIUS_PRIMARY);
@@ -334,28 +231,8 @@
 
     @EnableFlags(Flags.FLAG_UPDATE_CORNER_RADIUS_ON_DISPLAY_CHANGED)
     @Test
-    public void onDisplayChanged_updateCornerRadiusToSecondary() throws InterruptedException {
-        CountDownLatch transactionCommittedLatch = new CountDownLatch(1);
-        CountDownLatch animationEndLatch = new CountDownLatch(1);
-        mTransaction.addTransactionCommittedListener(
-                Runnable::run, transactionCommittedLatch::countDown);
-        mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                animationEndLatch.countDown();
-            }
-        });
-
-        getInstrumentation().runOnMainSync(() ->
-                //Enable fullscreen magnification
-                mFullscreenMagnificationController
-                        .onFullscreenMagnificationActivationChanged(true));
-        assertWithMessage("Failed to wait for transaction committed")
-                .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
-                .isTrue();
-        assertWithMessage("Failed to wait for animation to be finished")
-                .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
-                .isTrue();
+    public void onDisplayChanged_applyCornerRadiusToBorder() throws InterruptedException {
+        enableFullscreenMagnificationAndWaitForTransactionAndAnimation();
 
         ArgumentCaptor<DisplayManager.DisplayListener> displayListenerCaptor =
                 ArgumentCaptor.forClass(DisplayManager.DisplayListener.class);
@@ -372,22 +249,34 @@
                 .addOverride(
                         com.android.internal.R.dimen.rounded_corner_radius,
                         CORNER_RADIUS_SECONDARY);
+
         getInstrumentation().runOnMainSync(() ->
                 displayListenerCaptor.getValue().onDisplayChanged(Display.DEFAULT_DISPLAY));
         waitForIdleSync();
+
         // Verify the corner radius is updated
         GradientDrawable backgroundDrawable2 =
                 (GradientDrawable) mSurfaceControlViewHost.getView().getBackground();
         assertThat(backgroundDrawable2.getCornerRadius()).isEqualTo(CORNER_RADIUS_SECONDARY);
     }
 
+    private void enableFullscreenMagnificationAndWaitForTransactionAndAnimation()
+            throws InterruptedException {
+        CountDownLatch transactionCommittedLatch = new CountDownLatch(1);
+        mTransaction.addTransactionCommittedListener(
+                Runnable::run, transactionCommittedLatch::countDown);
 
-    private ValueAnimator newNullTargetObjectAnimator() {
-        final ValueAnimator animator =
-                ObjectAnimator.ofFloat(/* target= */ null, View.ALPHA, 0f, 1f);
-        Interpolator interpolator = new DecelerateInterpolator(2.5f);
-        animator.setInterpolator(interpolator);
-        animator.setDuration(ANIMATION_DURATION_MS);
-        return animator;
+        getInstrumentation().runOnMainSync(() ->
+                //Enable fullscreen magnification
+                mFullscreenMagnificationController
+                        .onFullscreenMagnificationActivationChanged(true));
+
+        assertWithMessage("Failed to wait for transaction committed")
+                .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
+                .isTrue();
+        waitForIdleSync();
+        assertThat(mFullscreenMagnificationController.mShowHideBorderAnimator).isNotNull();
+        mFullscreenMagnificationController.mShowHideBorderAnimator.end();
+        waitForIdleSync();
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
index 856c379..9f6ad56 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
@@ -82,7 +82,7 @@
         final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
         final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext,
                 stubWindowManager);
-        final SecureSettings secureSettings = TestUtils.mockSecureSettings();
+        final SecureSettings secureSettings = TestUtils.mockSecureSettings(mContext);
         final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager,
                 secureSettings, mHearingAidDeviceManager);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
index 33cfb38..1500340 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
@@ -144,7 +144,7 @@
     private HearingAidDeviceManager mHearingAidDeviceManager;
     @Mock
     private PackageManager mMockPackageManager;
-    private final SecureSettings mSecureSettings = TestUtils.mockSecureSettings();
+    private final SecureSettings mSecureSettings = TestUtils.mockSecureSettings(mContext);
 
     private final NotificationManager mMockNotificationManager = mock(NotificationManager.class);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt
index 5d622ea..e61acc4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.plugins.activityStarter
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
@@ -224,6 +225,30 @@
         }
     }
 
+    @Test
+    fun testOnActionIconClick_audioSharingMediaDevice_stopBroadcast() {
+        with(kosmos) {
+            testScope.runTest {
+                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+                actionInteractorImpl.onActionIconClick(inAudioSharingMediaDeviceItem) {}
+                assertThat(bluetoothTileDialogAudioSharingRepository.audioSharingStarted)
+                    .isEqualTo(false)
+            }
+        }
+    }
+
+    @Test
+    fun testOnActionIconClick_availableAudioSharingMediaDevice_startBroadcast() {
+        with(kosmos) {
+            testScope.runTest {
+                bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+                actionInteractorImpl.onActionIconClick(connectedAudioSharingMediaDeviceItem) {}
+                assertThat(bluetoothTileDialogAudioSharingRepository.audioSharingStarted)
+                    .isEqualTo(true)
+            }
+        }
+    }
+
     private companion object {
         const val DEVICE_NAME = "device"
         const val DEVICE_CONNECTION_SUMMARY = "active"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt
index 6bfd080..4396b0a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt
@@ -32,6 +32,9 @@
 import com.android.settingslib.bluetooth.CachedBluetoothDevice
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.model.SysUiState
 import com.android.systemui.res.R
 import com.android.systemui.shade.data.repository.shadeDialogContextInteractor
@@ -43,9 +46,8 @@
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.test.TestCoroutineScheduler
 import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Rule
@@ -93,7 +95,6 @@
 
     private val fakeSystemClock = FakeSystemClock()
 
-    private lateinit var scheduler: TestCoroutineScheduler
     private lateinit var dispatcher: CoroutineDispatcher
     private lateinit var testScope: TestScope
     private lateinit var icon: Pair<Drawable, String>
@@ -104,9 +105,8 @@
 
     @Before
     fun setUp() {
-        scheduler = TestCoroutineScheduler()
-        dispatcher = UnconfinedTestDispatcher(scheduler)
-        testScope = TestScope(dispatcher)
+        dispatcher = kosmos.testDispatcher
+        testScope = kosmos.testScope
 
         whenever(sysuiState.setFlag(anyLong(), anyBoolean())).thenReturn(sysuiState)
 
@@ -124,23 +124,19 @@
                 kosmos.shadeDialogContextInteractor,
             )
 
-        whenever(
-            sysuiDialogFactory.create(
-                any(SystemUIDialog.Delegate::class.java),
-                any()
-            )
-        ).thenAnswer {
-            SystemUIDialog(
-                mContext,
-                0,
-                SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK,
-                dialogManager,
-                sysuiState,
-                fakeBroadcastDispatcher,
-                dialogTransitionAnimator,
-                it.getArgument(0),
-            )
-        }
+        whenever(sysuiDialogFactory.create(any(SystemUIDialog.Delegate::class.java), any()))
+            .thenAnswer {
+                SystemUIDialog(
+                    mContext,
+                    0,
+                    SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK,
+                    dialogManager,
+                    sysuiState,
+                    fakeBroadcastDispatcher,
+                    dialogTransitionAnimator,
+                    it.getArgument(0),
+                )
+            }
 
         icon = Pair(drawable, DEVICE_NAME)
         deviceItem =
@@ -194,20 +190,29 @@
 
     @Test
     fun testDeviceItemViewHolder_cachedDeviceNotBusy() {
-        deviceItem.isEnabled = true
+        testScope.runTest {
+            deviceItem.isEnabled = true
 
-        val view =
-            LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
-        val viewHolder =
-            mBluetoothTileDialogDelegate
-                .Adapter(bluetoothTileDialogCallback)
-                .DeviceItemViewHolder(view)
-        viewHolder.bind(deviceItem, bluetoothTileDialogCallback)
-        val container = view.requireViewById<View>(R.id.bluetooth_device_row)
+            val view =
+                LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
+            val viewHolder = mBluetoothTileDialogDelegate.Adapter().DeviceItemViewHolder(view)
+            viewHolder.bind(deviceItem)
+            val container = view.requireViewById<View>(R.id.bluetooth_device_row)
 
-        assertThat(container).isNotNull()
-        assertThat(container.isEnabled).isTrue()
-        assertThat(container.hasOnClickListeners()).isTrue()
+            assertThat(container).isNotNull()
+            assertThat(container.isEnabled).isTrue()
+            assertThat(container.hasOnClickListeners()).isTrue()
+            val value by collectLastValue(mBluetoothTileDialogDelegate.deviceItemClick)
+            runCurrent()
+            container.performClick()
+            runCurrent()
+            assertThat(value).isNotNull()
+            value?.let {
+                assertThat(it.target).isEqualTo(DeviceItemClick.Target.ENTIRE_ROW)
+                assertThat(it.clickedView).isEqualTo(container)
+                assertThat(it.deviceItem).isEqualTo(deviceItem)
+            }
+        }
     }
 
     @Test
@@ -229,9 +234,9 @@
                     sysuiDialogFactory,
                     kosmos.shadeDialogContextInteractor,
                 )
-                .Adapter(bluetoothTileDialogCallback)
+                .Adapter()
                 .DeviceItemViewHolder(view)
-        viewHolder.bind(deviceItem, bluetoothTileDialogCallback)
+        viewHolder.bind(deviceItem)
         val container = view.requireViewById<View>(R.id.bluetooth_device_row)
 
         assertThat(container).isNotNull()
@@ -240,6 +245,32 @@
     }
 
     @Test
+    fun testDeviceItemViewHolder_clickActionIcon() {
+        testScope.runTest {
+            deviceItem.isEnabled = true
+
+            val view =
+                LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
+            val viewHolder = mBluetoothTileDialogDelegate.Adapter().DeviceItemViewHolder(view)
+            viewHolder.bind(deviceItem)
+            val actionIconView = view.requireViewById<View>(R.id.gear_icon)
+
+            assertThat(actionIconView).isNotNull()
+            assertThat(actionIconView.hasOnClickListeners()).isTrue()
+            val value by collectLastValue(mBluetoothTileDialogDelegate.deviceItemClick)
+            runCurrent()
+            actionIconView.performClick()
+            runCurrent()
+            assertThat(value).isNotNull()
+            value?.let {
+                assertThat(it.target).isEqualTo(DeviceItemClick.Target.ACTION_ICON)
+                assertThat(it.clickedView).isEqualTo(actionIconView)
+                assertThat(it.deviceItem).isEqualTo(deviceItem)
+            }
+        }
+    }
+
+    @Test
     fun testOnDeviceUpdated_hideSeeAll_showPairNew() {
         testScope.runTest {
             val dialog = mBluetoothTileDialogDelegate.createDialog()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
index 5bf1513..0aa5199 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
@@ -118,6 +118,7 @@
             .isEqualTo(DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE)
         assertThat(deviceItem.cachedBluetoothDevice).isEqualTo(cachedDevice)
         assertThat(deviceItem.deviceName).isEqualTo(DEVICE_NAME)
+        assertThat(deviceItem.actionIconRes).isEqualTo(R.drawable.ic_add)
         assertThat(deviceItem.isActive).isFalse()
         assertThat(deviceItem.connectionSummary)
             .isEqualTo(
@@ -292,6 +293,7 @@
         assertThat(deviceItem.cachedBluetoothDevice).isEqualTo(cachedDevice)
         assertThat(deviceItem.deviceName).isEqualTo(DEVICE_NAME)
         assertThat(deviceItem.connectionSummary).isEqualTo(CONNECTION_SUMMARY)
+        assertThat(deviceItem.actionIconRes).isEqualTo(R.drawable.ic_settings_24dp)
     }
 
     companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt
index 387cc08..1320223 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt
@@ -33,7 +33,6 @@
 import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.ui.BouncerDialogFactory
-import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout
 import com.android.systemui.bouncer.ui.viewmodel.bouncerSceneContentViewModelFactory
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
similarity index 94%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
index 7a3089f..77c40a1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.data.repository
 
+import android.animation.AnimationHandler
 import android.animation.Animator
 import android.animation.ValueAnimator
 import android.platform.test.annotations.EnableFlags
@@ -36,6 +37,7 @@
 import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.util.FrameCallbackProvider
 import com.android.systemui.keyguard.util.KeyguardTransitionRunner
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
@@ -52,9 +54,12 @@
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.withContext
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -70,13 +75,29 @@
 
     private lateinit var underTest: KeyguardTransitionRepository
     private lateinit var runner: KeyguardTransitionRunner
+    private lateinit var callbackProvider: FrameCallbackProvider
 
     private val animatorListener = mock<Animator.AnimatorListener>()
 
     @Before
     fun setUp() {
         underTest = KeyguardTransitionRepositoryImpl(Dispatchers.Main)
-        runner = KeyguardTransitionRunner(underTest)
+        runBlocking {
+            callbackProvider = FrameCallbackProvider(testScope.backgroundScope)
+            withContext(Dispatchers.Main) {
+                // AnimationHandler uses ThreadLocal storage, and ValueAnimators MUST start from
+                // main thread
+                AnimationHandler.getInstance().setProvider(callbackProvider)
+            }
+            runner = KeyguardTransitionRunner(callbackProvider.frames, underTest)
+        }
+    }
+
+    @After
+    fun tearDown() {
+        runBlocking {
+            withContext(Dispatchers.Main) { AnimationHandler.getInstance().setProvider(null) }
+        }
     }
 
     @Test
@@ -84,13 +105,11 @@
         testScope.runTest {
             val steps = mutableListOf<TransitionStep>()
             val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this)
-
             runner.startTransition(
                 this,
                 TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, getAnimator()),
                 maxFrames = 100,
             )
-
             assertSteps(steps, listWithStep(BigDecimal(.1)), AOD, LOCKSCREEN)
             job.cancel()
         }
@@ -119,12 +138,12 @@
                 ),
             )
 
-            val firstTransitionSteps = listWithStep(step = BigDecimal(.1), stop = BigDecimal(.1))
-            assertSteps(steps.subList(0, 4), firstTransitionSteps, AOD, LOCKSCREEN)
+            val firstTransitionSteps = listWithStep(step = BigDecimal(.1), stop = BigDecimal(.2))
+            assertSteps(steps.subList(0, 5), firstTransitionSteps, AOD, LOCKSCREEN)
 
-            // Second transition starts from .1 (LAST_VALUE)
-            val secondTransitionSteps = listWithStep(step = BigDecimal(.1), start = BigDecimal(.1))
-            assertSteps(steps.subList(4, steps.size), secondTransitionSteps, LOCKSCREEN, AOD)
+            // Second transition starts from .2 (LAST_VALUE)
+            val secondTransitionSteps = listWithStep(step = BigDecimal(.1), start = BigDecimal(.2))
+            assertSteps(steps.subList(5, steps.size), secondTransitionSteps, LOCKSCREEN, AOD)
 
             job.cancel()
             job2.cancel()
@@ -154,12 +173,12 @@
                 ),
             )
 
-            val firstTransitionSteps = listWithStep(step = BigDecimal(.1), stop = BigDecimal(.1))
-            assertSteps(steps.subList(0, 4), firstTransitionSteps, AOD, LOCKSCREEN)
+            val firstTransitionSteps = listWithStep(step = BigDecimal(.1), stop = BigDecimal(.2))
+            assertSteps(steps.subList(0, 5), firstTransitionSteps, AOD, LOCKSCREEN)
 
             // Second transition starts from 0 (RESET)
             val secondTransitionSteps = listWithStep(start = BigDecimal(0), step = BigDecimal(.1))
-            assertSteps(steps.subList(4, steps.size), secondTransitionSteps, LOCKSCREEN, AOD)
+            assertSteps(steps.subList(5, steps.size), secondTransitionSteps, LOCKSCREEN, AOD)
 
             job.cancel()
             job2.cancel()
@@ -173,7 +192,7 @@
             runner.startTransition(
                 this,
                 TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, getAnimator()),
-                maxFrames = 3,
+                maxFrames = 2,
             )
 
             // Now start 2nd transition, which will interrupt the first
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
index 7ba797c..86063ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
@@ -39,7 +39,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
-import android.graphics.PorterDuffColorFilter;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
 import android.media.AudioDeviceAttributes;
@@ -1286,13 +1285,6 @@
     }
 
     @Test
-    public void setColorFilter_setColorFilterToDrawable() {
-        mMediaSwitchingController.setColorFilter(mDrawable, true);
-
-        verify(mDrawable).setColorFilter(any(PorterDuffColorFilter.class));
-    }
-
-    @Test
     public void resetGroupMediaDevices_clearGroupDevices() {
         final MediaDevice selectedMediaDevice1 = mock(MediaDevice.class);
         final MediaDevice selectedMediaDevice2 = mock(MediaDevice.class);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/power/PowerNotificationWarningsTest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt
new file mode 100644
index 0000000..a192446
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt
@@ -0,0 +1,819 @@
+/*
+ * 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.dialog
+
+import android.content.Intent
+import android.os.Handler
+import android.os.fakeExecutorHandler
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyManager
+import android.telephony.telephonyManager
+import android.testing.TestableLooper.RunWithLooper
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.Button
+import android.widget.LinearLayout
+import android.widget.Switch
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.annotation.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.internal.logging.UiEventLogger
+import com.android.settingslib.wifi.WifiEnterpriseRestrictionUtils
+import com.android.systemui.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.flags.setFlagValue
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import com.android.wifitrackerlib.WifiEntry
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.MockitoSession
+import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@RunWithLooper(setAsMainLooper = true)
+@UiThreadTest
+class InternetDetailsContentManagerTest : SysuiTestCase() {
+    private val kosmos = Kosmos()
+    private val handler: Handler = kosmos.fakeExecutorHandler
+    private val scope: CoroutineScope = mock<CoroutineScope>()
+    private val telephonyManager: TelephonyManager = kosmos.telephonyManager
+    private val internetWifiEntry: WifiEntry = mock<WifiEntry>()
+    private val wifiEntries: List<WifiEntry> = mock<List<WifiEntry>>()
+    private val internetAdapter = mock<InternetAdapter>()
+    private val internetDetailsContentController: InternetDetailsContentController =
+        mock<InternetDetailsContentController>()
+    private val keyguard: KeyguardStateController = mock<KeyguardStateController>()
+    private val dialogTransitionAnimator: DialogTransitionAnimator =
+        mock<DialogTransitionAnimator>()
+    private val bgExecutor = FakeExecutor(FakeSystemClock())
+    private lateinit var internetDetailsContentManager: InternetDetailsContentManager
+    private var subTitle: View? = null
+    private var ethernet: LinearLayout? = null
+    private var mobileDataLayout: LinearLayout? = null
+    private var mobileToggleSwitch: Switch? = null
+    private var wifiToggle: LinearLayout? = null
+    private var wifiToggleSwitch: Switch? = null
+    private var wifiToggleSummary: TextView? = null
+    private var connectedWifi: LinearLayout? = null
+    private var wifiList: RecyclerView? = null
+    private var seeAll: LinearLayout? = null
+    private var wifiScanNotify: LinearLayout? = null
+    private var airplaneModeSummaryText: TextView? = null
+    private var mockitoSession: MockitoSession? = null
+    private var sharedWifiButton: Button? = null
+    private lateinit var contentView: View
+
+    @Before
+    fun setUp() {
+        // TODO: b/377388104 enable this flag after integrating with details view.
+        mSetFlagsRule.setFlagValue(Flags.FLAG_QS_TILE_DETAILED_VIEW, false)
+        whenever(telephonyManager.createForSubscriptionId(ArgumentMatchers.anyInt()))
+            .thenReturn(telephonyManager)
+        whenever(internetWifiEntry.title).thenReturn(WIFI_TITLE)
+        whenever(internetWifiEntry.getSummary(false)).thenReturn(WIFI_SUMMARY)
+        whenever(internetWifiEntry.isDefaultNetwork).thenReturn(true)
+        whenever(internetWifiEntry.hasInternetAccess()).thenReturn(true)
+        whenever(wifiEntries.size).thenReturn(1)
+        whenever(internetDetailsContentController.getDialogTitleText()).thenReturn(TITLE)
+        whenever(internetDetailsContentController.getMobileNetworkTitle(ArgumentMatchers.anyInt()))
+            .thenReturn(MOBILE_NETWORK_TITLE)
+        whenever(
+                internetDetailsContentController.getMobileNetworkSummary(ArgumentMatchers.anyInt())
+            )
+            .thenReturn(MOBILE_NETWORK_SUMMARY)
+        whenever(internetDetailsContentController.isWifiEnabled).thenReturn(true)
+        whenever(internetDetailsContentController.activeAutoSwitchNonDdsSubId)
+            .thenReturn(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
+        mockitoSession =
+            ExtendedMockito.mockitoSession()
+                .spyStatic(WifiEnterpriseRestrictionUtils::class.java)
+                .startMocking()
+        whenever(WifiEnterpriseRestrictionUtils.isChangeWifiStateAllowed(mContext)).thenReturn(true)
+        createView()
+    }
+
+    private fun createView() {
+        contentView =
+            LayoutInflater.from(mContext).inflate(R.layout.internet_connectivity_dialog, null)
+        internetDetailsContentManager =
+            InternetDetailsContentManager(
+                internetDetailsContentController,
+                canConfigMobileData = true,
+                canConfigWifi = true,
+                coroutineScope = scope,
+                context = mContext,
+                internetDialog = null,
+                uiEventLogger = mock<UiEventLogger>(),
+                dialogTransitionAnimator = dialogTransitionAnimator,
+                handler = handler,
+                backgroundExecutor = bgExecutor,
+                keyguard = keyguard,
+            )
+
+        internetDetailsContentManager.bind(contentView)
+        internetDetailsContentManager.adapter = internetAdapter
+        internetDetailsContentManager.connectedWifiEntry = internetWifiEntry
+        internetDetailsContentManager.wifiEntriesCount = wifiEntries.size
+
+        subTitle = contentView.requireViewById(R.id.internet_dialog_subtitle)
+        ethernet = contentView.requireViewById(R.id.ethernet_layout)
+        mobileDataLayout = contentView.requireViewById(R.id.mobile_network_layout)
+        mobileToggleSwitch = contentView.requireViewById(R.id.mobile_toggle)
+        wifiToggle = contentView.requireViewById(R.id.turn_on_wifi_layout)
+        wifiToggleSwitch = contentView.requireViewById(R.id.wifi_toggle)
+        wifiToggleSummary = contentView.requireViewById(R.id.wifi_toggle_summary)
+        connectedWifi = contentView.requireViewById(R.id.wifi_connected_layout)
+        wifiList = contentView.requireViewById(R.id.wifi_list_layout)
+        seeAll = contentView.requireViewById(R.id.see_all_layout)
+        wifiScanNotify = contentView.requireViewById(R.id.wifi_scan_notify_layout)
+        airplaneModeSummaryText = contentView.requireViewById(R.id.airplane_mode_summary)
+        sharedWifiButton = contentView.requireViewById(R.id.share_wifi_button)
+    }
+
+    @After
+    fun tearDown() {
+        internetDetailsContentManager.unBind()
+        mockitoSession!!.finishMocking()
+    }
+
+    @Test
+    fun createView_setAccessibilityPaneTitleToQuickSettings() {
+        assertThat(contentView.accessibilityPaneTitle)
+            .isEqualTo(mContext.getText(R.string.accessibility_desc_quick_settings))
+    }
+
+    @Test
+    fun hideWifiViews_WifiViewsGone() {
+        internetDetailsContentManager.hideWifiViews()
+
+        assertThat(internetDetailsContentManager.isProgressBarVisible).isFalse()
+        assertThat(wifiToggle!!.visibility).isEqualTo(View.GONE)
+        assertThat(connectedWifi!!.visibility).isEqualTo(View.GONE)
+        assertThat(wifiList!!.visibility).isEqualTo(View.GONE)
+        assertThat(seeAll!!.visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun updateContent_withApmOn_internetDialogSubTitleGone() {
+        whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(true)
+        internetDetailsContentManager.updateContent(true)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(subTitle!!.visibility).isEqualTo(View.VISIBLE)
+        }
+    }
+
+    @Test
+    fun updateContent_withApmOff_internetDialogSubTitleVisible() {
+        whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(false)
+        internetDetailsContentManager.updateContent(true)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(subTitle!!.visibility).isEqualTo(View.VISIBLE)
+        }
+    }
+
+    @Test
+    fun updateContent_apmOffAndHasEthernet_showEthernet() {
+        whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(false)
+        whenever(internetDetailsContentController.hasEthernet()).thenReturn(true)
+        internetDetailsContentManager.updateContent(true)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(ethernet!!.visibility).isEqualTo(View.VISIBLE)
+        }
+    }
+
+    @Test
+    fun updateContent_apmOffAndNoEthernet_hideEthernet() {
+        whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(false)
+        whenever(internetDetailsContentController.hasEthernet()).thenReturn(false)
+        internetDetailsContentManager.updateContent(true)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(ethernet!!.visibility).isEqualTo(View.GONE)
+        }
+    }
+
+    @Test
+    fun updateContent_apmOnAndHasEthernet_showEthernet() {
+        whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(true)
+        whenever(internetDetailsContentController.hasEthernet()).thenReturn(true)
+        internetDetailsContentManager.updateContent(true)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(ethernet!!.visibility).isEqualTo(View.VISIBLE)
+        }
+    }
+
+    @Test
+    fun updateContent_apmOnAndNoEthernet_hideEthernet() {
+        whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(true)
+        whenever(internetDetailsContentController.hasEthernet()).thenReturn(false)
+        internetDetailsContentManager.updateContent(true)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(ethernet!!.visibility).isEqualTo(View.GONE)
+        }
+    }
+
+    @Test
+    fun updateContent_apmOffAndNotCarrierNetwork_mobileDataLayoutGone() {
+        // Mobile network should be gone if the list of active subscriptionId is null.
+        whenever(internetDetailsContentController.isCarrierNetworkActive).thenReturn(false)
+        whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(false)
+        whenever(internetDetailsContentController.hasActiveSubIdOnDds()).thenReturn(false)
+        internetDetailsContentManager.updateContent(true)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(mobileDataLayout!!.visibility).isEqualTo(View.GONE)
+        }
+    }
+
+    @Test
+    fun updateContent_apmOnWithCarrierNetworkAndWifiStatus_mobileDataLayoutVisible() {
+        // Carrier network should be visible if airplane mode ON and Wi-Fi is ON.
+        whenever(internetDetailsContentController.isCarrierNetworkActive).thenReturn(true)
+        whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(true)
+        whenever(internetDetailsContentController.isWifiEnabled).thenReturn(true)
+        internetDetailsContentManager.updateContent(true)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(mobileDataLayout!!.visibility).isEqualTo(View.VISIBLE)
+        }
+    }
+
+    @Test
+    fun updateContent_apmOnWithCarrierNetworkAndWifiStatus_mobileDataLayoutGone() {
+        // Carrier network should be gone if airplane mode ON and Wi-Fi is off.
+        whenever(internetDetailsContentController.isCarrierNetworkActive).thenReturn(true)
+        whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(true)
+        whenever(internetDetailsContentController.isWifiEnabled).thenReturn(false)
+        internetDetailsContentManager.updateContent(true)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(mobileDataLayout!!.visibility).isEqualTo(View.GONE)
+        }
+    }
+
+    @Test
+    fun updateContent_apmOnAndNoCarrierNetwork_mobileDataLayoutGone() {
+        whenever(internetDetailsContentController.isCarrierNetworkActive).thenReturn(false)
+        whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(true)
+        internetDetailsContentManager.updateContent(true)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(mobileDataLayout!!.visibility).isEqualTo(View.GONE)
+        }
+    }
+
+    @Test
+    fun updateContent_apmOnAndWifiOnHasCarrierNetwork_showAirplaneSummary() {
+        whenever(internetDetailsContentController.isCarrierNetworkActive).thenReturn(true)
+        whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(true)
+        internetDetailsContentManager.connectedWifiEntry = null
+        whenever(internetDetailsContentController.activeNetworkIsCellular()).thenReturn(false)
+        internetDetailsContentManager.updateContent(true)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(mobileDataLayout!!.visibility).isEqualTo(View.VISIBLE)
+            assertThat(airplaneModeSummaryText!!.visibility).isEqualTo(View.VISIBLE)
+        }
+    }
+
+    @Test
+    fun updateContent_apmOffAndWifiOnHasCarrierNetwork_notShowApmSummary() {
+        whenever(internetDetailsContentController.isCarrierNetworkActive).thenReturn(true)
+        whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(false)
+        internetDetailsContentManager.connectedWifiEntry = null
+        whenever(internetDetailsContentController.activeNetworkIsCellular()).thenReturn(false)
+        internetDetailsContentManager.updateContent(true)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(airplaneModeSummaryText!!.visibility).isEqualTo(View.GONE)
+        }
+    }
+
+    @Test
+    fun updateContent_apmOffAndHasCarrierNetwork_notShowApmSummary() {
+        whenever(internetDetailsContentController.isCarrierNetworkActive).thenReturn(true)
+        whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(false)
+        internetDetailsContentManager.updateContent(true)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(airplaneModeSummaryText!!.visibility).isEqualTo(View.GONE)
+        }
+    }
+
+    @Test
+    fun updateContent_apmOnAndNoCarrierNetwork_notShowApmSummary() {
+        whenever(internetDetailsContentController.isCarrierNetworkActive).thenReturn(false)
+        whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(true)
+        internetDetailsContentManager.updateContent(true)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(airplaneModeSummaryText!!.visibility).isEqualTo(View.GONE)
+        }
+    }
+
+    @Test
+    fun updateContent_mobileDataIsEnabled_checkMobileDataSwitch() {
+        whenever(internetDetailsContentController.hasActiveSubIdOnDds()).thenReturn(true)
+        whenever(internetDetailsContentController.isCarrierNetworkActive).thenReturn(true)
+        whenever(internetDetailsContentController.isMobileDataEnabled).thenReturn(true)
+        mobileToggleSwitch!!.isChecked = false
+        internetDetailsContentManager.updateContent(true)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(mobileToggleSwitch!!.isChecked).isTrue()
+        }
+    }
+
+    @Test
+    fun updateContent_mobileDataIsNotChanged_checkMobileDataSwitch() {
+        whenever(internetDetailsContentController.hasActiveSubIdOnDds()).thenReturn(true)
+        whenever(internetDetailsContentController.isCarrierNetworkActive).thenReturn(true)
+        whenever(internetDetailsContentController.isMobileDataEnabled).thenReturn(false)
+        mobileToggleSwitch!!.isChecked = false
+        internetDetailsContentManager.updateContent(true)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(mobileToggleSwitch!!.isChecked).isFalse()
+        }
+    }
+
+    @Test
+    fun updateContent_wifiOnAndHasInternetWifi_showConnectedWifi() {
+        whenever(internetDetailsContentController.activeAutoSwitchNonDdsSubId).thenReturn(1)
+        whenever(internetDetailsContentController.hasActiveSubIdOnDds()).thenReturn(true)
+
+        // The preconditions WiFi ON and Internet WiFi are already in setUp()
+        whenever(internetDetailsContentController.activeNetworkIsCellular()).thenReturn(false)
+
+        internetDetailsContentManager.updateContent(true)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(connectedWifi!!.visibility).isEqualTo(View.VISIBLE)
+            val secondaryLayout =
+                contentView.requireViewById<LinearLayout>(R.id.secondary_mobile_network_layout)
+            assertThat(secondaryLayout.visibility).isEqualTo(View.GONE)
+        }
+    }
+
+    @Test
+    fun updateContent_wifiOnAndNoConnectedWifi_hideConnectedWifi() {
+        // The precondition WiFi ON is already in setUp()
+        internetDetailsContentManager.connectedWifiEntry = null
+        whenever(internetDetailsContentController.activeNetworkIsCellular()).thenReturn(false)
+
+        internetDetailsContentManager.updateContent(false)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(connectedWifi!!.visibility).isEqualTo(View.GONE)
+        }
+    }
+
+    @Test
+    fun updateContent_wifiOnAndNoWifiEntry_showWifiListAndSeeAllArea() {
+        // The precondition WiFi ON is already in setUp()
+        internetDetailsContentManager.connectedWifiEntry = null
+        internetDetailsContentManager.wifiEntriesCount = 0
+        internetDetailsContentManager.updateContent(false)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(connectedWifi!!.visibility).isEqualTo(View.GONE)
+            // Show a blank block to fix the details content height even if there is no WiFi list
+            assertThat(wifiList!!.visibility).isEqualTo(View.VISIBLE)
+            verify(internetAdapter).setMaxEntriesCount(3)
+            assertThat(seeAll!!.visibility).isEqualTo(View.INVISIBLE)
+        }
+    }
+
+    @Test
+    fun updateContent_wifiOnAndOneWifiEntry_showWifiListAndSeeAllArea() {
+        // The precondition WiFi ON is already in setUp()
+        internetDetailsContentManager.connectedWifiEntry = null
+        internetDetailsContentManager.wifiEntriesCount = 1
+        internetDetailsContentManager.updateContent(false)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(connectedWifi!!.visibility).isEqualTo(View.GONE)
+            // Show a blank block to fix the details content height even if there is no WiFi list
+            assertThat(wifiList!!.visibility).isEqualTo(View.VISIBLE)
+            verify(internetAdapter).setMaxEntriesCount(3)
+            assertThat(seeAll!!.visibility).isEqualTo(View.INVISIBLE)
+        }
+    }
+
+    @Test
+    fun updateContent_wifiOnAndHasConnectedWifi_showAllWifiAndSeeAllArea() {
+        // The preconditions WiFi ON and WiFi entries are already in setUp()
+        internetDetailsContentManager.wifiEntriesCount = 0
+        internetDetailsContentManager.updateContent(false)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(connectedWifi!!.visibility).isEqualTo(View.VISIBLE)
+            // Show a blank block to fix the details content height even if there is no WiFi list
+            assertThat(wifiList!!.visibility).isEqualTo(View.VISIBLE)
+            verify(internetAdapter).setMaxEntriesCount(2)
+            assertThat(seeAll!!.visibility).isEqualTo(View.INVISIBLE)
+        }
+    }
+
+    @Test
+    fun updateContent_wifiOnAndHasMaxWifiList_showWifiListAndSeeAll() {
+        // The preconditions WiFi ON and WiFi entries are already in setUp()
+        internetDetailsContentManager.connectedWifiEntry = null
+        internetDetailsContentManager.wifiEntriesCount =
+            InternetDetailsContentController.MAX_WIFI_ENTRY_COUNT
+        internetDetailsContentManager.hasMoreWifiEntries = true
+        internetDetailsContentManager.updateContent(false)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(connectedWifi!!.visibility).isEqualTo(View.GONE)
+            assertThat(wifiList!!.visibility).isEqualTo(View.VISIBLE)
+            verify(internetAdapter).setMaxEntriesCount(3)
+            assertThat(seeAll!!.visibility).isEqualTo(View.VISIBLE)
+        }
+    }
+
+    @Test
+    fun updateContent_wifiOnAndHasBothWifiEntry_showBothWifiEntryAndSeeAll() {
+        // The preconditions WiFi ON and WiFi entries are already in setUp()
+        internetDetailsContentManager.wifiEntriesCount =
+            InternetDetailsContentController.MAX_WIFI_ENTRY_COUNT - 1
+        internetDetailsContentManager.hasMoreWifiEntries = true
+        internetDetailsContentManager.updateContent(false)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(connectedWifi!!.visibility).isEqualTo(View.VISIBLE)
+            assertThat(wifiList!!.visibility).isEqualTo(View.VISIBLE)
+            verify(internetAdapter).setMaxEntriesCount(2)
+            assertThat(seeAll!!.visibility).isEqualTo(View.VISIBLE)
+        }
+    }
+
+    @Test
+    fun updateContent_deviceLockedAndNoConnectedWifi_showWifiToggle() {
+        // The preconditions WiFi entries are already in setUp()
+        whenever(internetDetailsContentController.isDeviceLocked).thenReturn(true)
+        internetDetailsContentManager.connectedWifiEntry = null
+        internetDetailsContentManager.updateContent(false)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            // Show WiFi Toggle without background
+            assertThat(wifiToggle!!.visibility).isEqualTo(View.VISIBLE)
+            assertThat(wifiToggle!!.background).isNull()
+            // Hide Wi-Fi networks and See all
+            assertThat(connectedWifi!!.visibility).isEqualTo(View.GONE)
+            assertThat(wifiList!!.visibility).isEqualTo(View.GONE)
+            assertThat(seeAll!!.visibility).isEqualTo(View.GONE)
+        }
+    }
+
+    @Test
+    fun updateContent_deviceLockedAndHasConnectedWifi_showWifiToggleWithBackground() {
+        // The preconditions WiFi ON and WiFi entries are already in setUp()
+        whenever(internetDetailsContentController.isDeviceLocked).thenReturn(true)
+        internetDetailsContentManager.updateContent(false)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            // Show WiFi Toggle with highlight background
+            assertThat(wifiToggle!!.visibility).isEqualTo(View.VISIBLE)
+            assertThat(wifiToggle!!.background).isNotNull()
+            // Hide Wi-Fi networks and See all
+            assertThat(connectedWifi!!.visibility).isEqualTo(View.GONE)
+            assertThat(wifiList!!.visibility).isEqualTo(View.GONE)
+            assertThat(seeAll!!.visibility).isEqualTo(View.GONE)
+        }
+    }
+
+    @Test
+    fun updateContent_disallowChangeWifiState_disableWifiSwitch() {
+        whenever(WifiEnterpriseRestrictionUtils.isChangeWifiStateAllowed(mContext))
+            .thenReturn(false)
+        createView()
+        internetDetailsContentManager.updateContent(false)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            // Disable Wi-Fi switch and show restriction message in summary.
+            assertThat(wifiToggleSwitch!!.isEnabled).isFalse()
+            assertThat(wifiToggleSummary!!.visibility).isEqualTo(View.VISIBLE)
+            assertThat(wifiToggleSummary!!.text.length).isNotEqualTo(0)
+        }
+    }
+
+    @Test
+    fun updateContent_allowChangeWifiState_enableWifiSwitch() {
+        whenever(WifiEnterpriseRestrictionUtils.isChangeWifiStateAllowed(mContext)).thenReturn(true)
+        createView()
+        internetDetailsContentManager.updateContent(false)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            // Enable Wi-Fi switch and hide restriction message in summary.
+            assertThat(wifiToggleSwitch!!.isEnabled).isTrue()
+            assertThat(wifiToggleSummary!!.visibility).isEqualTo(View.GONE)
+        }
+    }
+
+    @Test
+    fun updateContent_showSecondaryDataSub() {
+        whenever(internetDetailsContentController.activeAutoSwitchNonDdsSubId).thenReturn(1)
+        whenever(internetDetailsContentController.hasActiveSubIdOnDds()).thenReturn(true)
+        whenever(internetDetailsContentController.isAirplaneModeEnabled).thenReturn(false)
+
+        clearInvocations(internetDetailsContentController)
+        internetDetailsContentManager.updateContent(true)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            val primaryLayout =
+                contentView.requireViewById<LinearLayout>(R.id.mobile_network_layout)
+            val secondaryLayout =
+                contentView.requireViewById<LinearLayout>(R.id.secondary_mobile_network_layout)
+
+            verify(internetDetailsContentController).getMobileNetworkSummary(1)
+            assertThat(primaryLayout.background).isNotEqualTo(secondaryLayout.background)
+        }
+    }
+
+    @Test
+    fun updateContent_wifiOn_hideWifiScanNotify() {
+        // The preconditions WiFi ON and WiFi entries are already in setUp()
+        internetDetailsContentManager.updateContent(false)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(wifiScanNotify!!.visibility).isEqualTo(View.GONE)
+        }
+
+        assertThat(wifiScanNotify!!.visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun updateContent_wifiOffAndWifiScanOff_hideWifiScanNotify() {
+        whenever(internetDetailsContentController.isWifiEnabled).thenReturn(false)
+        whenever(internetDetailsContentController.isWifiScanEnabled).thenReturn(false)
+        internetDetailsContentManager.updateContent(false)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(wifiScanNotify!!.visibility).isEqualTo(View.GONE)
+        }
+
+        assertThat(wifiScanNotify!!.visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun updateContent_wifiOffAndWifiScanOnAndDeviceLocked_hideWifiScanNotify() {
+        whenever(internetDetailsContentController.isWifiEnabled).thenReturn(false)
+        whenever(internetDetailsContentController.isWifiScanEnabled).thenReturn(true)
+        whenever(internetDetailsContentController.isDeviceLocked).thenReturn(true)
+        internetDetailsContentManager.updateContent(false)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(wifiScanNotify!!.visibility).isEqualTo(View.GONE)
+        }
+
+        assertThat(wifiScanNotify!!.visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun updateContent_wifiOffAndWifiScanOnAndDeviceUnlocked_showWifiScanNotify() {
+        whenever(internetDetailsContentController.isWifiEnabled).thenReturn(false)
+        whenever(internetDetailsContentController.isWifiScanEnabled).thenReturn(true)
+        whenever(internetDetailsContentController.isDeviceLocked).thenReturn(false)
+        internetDetailsContentManager.updateContent(false)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(wifiScanNotify!!.visibility).isEqualTo(View.VISIBLE)
+            val wifiScanNotifyText =
+                contentView.requireViewById<TextView>(R.id.wifi_scan_notify_text)
+            assertThat(wifiScanNotifyText.text.length).isNotEqualTo(0)
+            assertThat(wifiScanNotifyText.movementMethod).isNotNull()
+        }
+    }
+
+    @Test
+    fun updateContent_wifiIsDisabled_uncheckWifiSwitch() {
+        whenever(internetDetailsContentController.isWifiEnabled).thenReturn(false)
+        wifiToggleSwitch!!.isChecked = true
+        internetDetailsContentManager.updateContent(false)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(wifiToggleSwitch!!.isChecked).isFalse()
+        }
+    }
+
+    @Test
+    @Throws(Exception::class)
+    fun updateContent_wifiIsEnabled_checkWifiSwitch() {
+        whenever(internetDetailsContentController.isWifiEnabled).thenReturn(true)
+        wifiToggleSwitch!!.isChecked = false
+        internetDetailsContentManager.updateContent(false)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(wifiToggleSwitch!!.isChecked).isTrue()
+        }
+    }
+
+    @Test
+    fun onClickSeeMoreButton_clickSeeAll_verifyLaunchNetworkSetting() {
+        seeAll!!.performClick()
+
+        verify(internetDetailsContentController)
+            .launchNetworkSetting(contentView.requireViewById(R.id.see_all_layout))
+    }
+
+    @Test
+    fun onWifiScan_isScanTrue_setProgressBarVisibleTrue() {
+        internetDetailsContentManager.isProgressBarVisible = false
+
+        internetDetailsContentManager.internetDetailsCallback.onWifiScan(true)
+
+        assertThat(internetDetailsContentManager.isProgressBarVisible).isTrue()
+    }
+
+    @Test
+    fun onWifiScan_isScanFalse_setProgressBarVisibleFalse() {
+        internetDetailsContentManager.isProgressBarVisible = true
+
+        internetDetailsContentManager.internetDetailsCallback.onWifiScan(false)
+
+        assertThat(internetDetailsContentManager.isProgressBarVisible).isFalse()
+    }
+
+    @Test
+    fun updateContent_shareWifiIntentNull_hideButton() {
+        whenever(
+                internetDetailsContentController.getConfiguratorQrCodeGeneratorIntentOrNull(
+                    ArgumentMatchers.any()
+                )
+            )
+            .thenReturn(null)
+        internetDetailsContentManager.updateContent(false)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(sharedWifiButton?.visibility).isEqualTo(View.GONE)
+        }
+    }
+
+    @Test
+    fun updateContent_shareWifiShareable_showButton() {
+        whenever(
+                internetDetailsContentController.getConfiguratorQrCodeGeneratorIntentOrNull(
+                    ArgumentMatchers.any()
+                )
+            )
+            .thenReturn(Intent())
+        internetDetailsContentManager.updateContent(false)
+        bgExecutor.runAllReady()
+
+        internetDetailsContentManager.internetContentData.observe(
+            internetDetailsContentManager.lifecycleOwner!!
+        ) {
+            assertThat(sharedWifiButton?.visibility).isEqualTo(View.VISIBLE)
+        }
+    }
+
+    companion object {
+        private const val TITLE = "Internet"
+        private const val MOBILE_NETWORK_TITLE = "Mobile Title"
+        private const val MOBILE_NETWORK_SUMMARY = "Mobile Summary"
+        private const val WIFI_TITLE = "Connected Wi-Fi Title"
+        private const val WIFI_SUMMARY = "Connected Wi-Fi Summary"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
index a0ecb80..f695c13 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
@@ -76,6 +76,8 @@
 
     @Mock private lateinit var iActivityManager: IActivityManager
 
+    @Mock private lateinit var beforeUserSwitchingReply: IRemoteCallback
+
     @Mock private lateinit var userSwitchingReply: IRemoteCallback
 
     @Mock(stubOnly = true) private lateinit var dumpManager: DumpManager
@@ -199,9 +201,10 @@
 
             val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
             verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
-            captor.value.onBeforeUserSwitching(newID)
+            captor.value.onBeforeUserSwitching(newID, beforeUserSwitchingReply)
             captor.value.onUserSwitching(newID, userSwitchingReply)
             runCurrent()
+            verify(beforeUserSwitchingReply).sendResult(any())
             verify(userSwitchingReply).sendResult(any())
 
             verify(userManager).getProfiles(newID)
@@ -341,10 +344,11 @@
 
             val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
             verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
-            captor.value.onBeforeUserSwitching(newID)
+            captor.value.onBeforeUserSwitching(newID, beforeUserSwitchingReply)
             captor.value.onUserSwitching(newID, userSwitchingReply)
             runCurrent()
 
+            verify(beforeUserSwitchingReply).sendResult(any())
             verify(userSwitchingReply).sendResult(any())
             assertThat(callback.calledOnUserChanging).isEqualTo(1)
             assertThat(callback.lastUser).isEqualTo(newID)
@@ -395,7 +399,7 @@
 
             val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
             verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
-            captor.value.onBeforeUserSwitching(newID)
+            captor.value.onBeforeUserSwitching(newID, any())
             captor.value.onUserSwitchComplete(newID)
             runCurrent()
 
@@ -453,8 +457,10 @@
 
             val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
             verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
+            captor.value.onBeforeUserSwitching(newID, beforeUserSwitchingReply)
             captor.value.onUserSwitching(newID, userSwitchingReply)
             runCurrent()
+            verify(beforeUserSwitchingReply).sendResult(any())
             verify(userSwitchingReply).sendResult(any())
             captor.value.onUserSwitchComplete(newID)
 
@@ -488,6 +494,7 @@
         }
 
     private class TestCallback : UserTracker.Callback {
+        var calledOnBeforeUserChanging = 0
         var calledOnUserChanging = 0
         var calledOnUserChanged = 0
         var calledOnProfilesChanged = 0
@@ -495,6 +502,11 @@
         var lastUserContext: Context? = null
         var lastUserProfiles = emptyList<UserInfo>()
 
+        override fun onBeforeUserSwitching(newUser: Int) {
+            calledOnBeforeUserChanging++
+            lastUser = newUser
+        }
+
         override fun onUserChanging(newUser: Int, userContext: Context) {
             calledOnUserChanging++
             lastUser = newUser
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
index 4cad5f7..77ca51c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
@@ -36,7 +36,7 @@
 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle
 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.RunningChipAnim
 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.ShowingPersistentDot
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
 import com.android.systemui.statusbar.window.StatusBarWindowController
 import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
 import com.android.systemui.util.time.FakeSystemClock
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinatorTest.kt
index a3c5181..f31d490 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinatorTest.kt
@@ -26,7 +26,6 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
 import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderListListener
-import com.android.systemui.statusbar.notification.collection.render.NotifStackController
 import com.android.systemui.util.mockito.withArgCaptor
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -48,7 +47,6 @@
 
     private val pipeline: NotifPipeline = mock()
     private val notifLiveDataStoreImpl: NotifLiveDataStoreImpl = mock()
-    private val stackController: NotifStackController = mock()
     private val section: NotifSection = mock()
 
     @Before
@@ -63,7 +61,7 @@
 
     @Test
     fun testUpdateDataStore_withOneEntry() {
-        afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
+        afterRenderListListener.onAfterRenderList(listOf(entry))
         verify(notifLiveDataStoreImpl).setActiveNotifList(eq(listOf(entry)))
         verifyNoMoreInteractions(notifLiveDataStoreImpl)
     }
@@ -86,8 +84,7 @@
                     .setSection(section)
                     .build(),
                 notificationEntry("baz", 1),
-            ),
-            stackController,
+            )
         )
         val list: List<NotificationEntry> = withArgCaptor {
             verify(notifLiveDataStoreImpl).setActiveNotifList(capture())
@@ -111,7 +108,7 @@
 
     @Test
     fun testUpdateDataStore_withZeroEntries_whenNewPipelineEnabled() {
-        afterRenderListListener.onAfterRenderList(listOf(), stackController)
+        afterRenderListListener.onAfterRenderList(listOf())
         verify(notifLiveDataStoreImpl).setActiveNotifList(eq(listOf()))
         verifyNoMoreInteractions(notifLiveDataStoreImpl)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
index 2c37f51..97e99b9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
@@ -15,7 +15,6 @@
  */
 package com.android.systemui.statusbar.notification.collection.coordinator
 
-import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.testing.TestableLooper.RunWithLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -29,23 +28,20 @@
 import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderListListener
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl
-import com.android.systemui.statusbar.notification.collection.render.NotifStackController
-import com.android.systemui.statusbar.notification.collection.render.NotifStats
+import com.android.systemui.statusbar.notification.data.model.NotifStats
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor
-import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING
 import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT
 import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController
+import com.android.systemui.util.mockito.withArgCaptor
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.verify
-import org.mockito.kotlin.verifyNoMoreInteractions
 import org.mockito.kotlin.whenever
 
 @SmallTest
@@ -63,7 +59,6 @@
     private val sensitiveNotificationProtectionController:
         SensitiveNotificationProtectionController =
         mock()
-    private val stackController: NotifStackController = mock()
     private val section: NotifSection = mock()
     private val row: ExpandableNotificationRow = mock()
 
@@ -82,198 +77,94 @@
                 sensitiveNotificationProtectionController,
             )
         coordinator.attach(pipeline)
-        val captor = argumentCaptor<OnAfterRenderListListener>()
-        verify(pipeline).addOnAfterRenderListListener(captor.capture())
-        afterRenderListListener = captor.lastValue
+        afterRenderListListener = withArgCaptor {
+            verify(pipeline).addOnAfterRenderListListener(capture())
+        }
     }
 
     @Test
     fun testSetRenderedListOnInteractor() {
-        afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
+        afterRenderListListener.onAfterRenderList(listOf(entry))
         verify(renderListInteractor).setRenderedList(eq(listOf(entry)))
     }
 
     @Test
-    @EnableFlags(FooterViewRefactor.FLAG_NAME)
-    fun testSetRenderedListOnInteractor_footerFlagOn() {
-        afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
-        verify(renderListInteractor).setRenderedList(eq(listOf(entry)))
-    }
-
-    @Test
-    @DisableFlags(FooterViewRefactor.FLAG_NAME)
     fun testSetNotificationStats_clearableAlerting() {
         whenever(section.bucket).thenReturn(BUCKET_ALERTING)
-        afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
-        verify(stackController)
+        afterRenderListListener.onAfterRenderList(listOf(entry))
+        verify(activeNotificationsInteractor)
             .setNotifStats(
                 NotifStats(
-                    1,
                     hasNonClearableAlertingNotifs = false,
                     hasClearableAlertingNotifs = true,
                     hasNonClearableSilentNotifs = false,
                     hasClearableSilentNotifs = false,
                 )
             )
-        verifyNoMoreInteractions(activeNotificationsInteractor)
     }
 
     @Test
-    @DisableFlags(FooterViewRefactor.FLAG_NAME)
     @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING, FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX)
     fun testSetNotificationStats_isSensitiveStateActive_nonClearableAlerting() {
         whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true)
         whenever(section.bucket).thenReturn(BUCKET_ALERTING)
-        afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
-        verify(stackController)
+        afterRenderListListener.onAfterRenderList(listOf(entry))
+        verify(activeNotificationsInteractor)
             .setNotifStats(
                 NotifStats(
-                    1,
                     hasNonClearableAlertingNotifs = true,
                     hasClearableAlertingNotifs = false,
                     hasNonClearableSilentNotifs = false,
                     hasClearableSilentNotifs = false,
                 )
             )
-        verifyNoMoreInteractions(activeNotificationsInteractor)
     }
 
     @Test
-    @DisableFlags(FooterViewRefactor.FLAG_NAME)
     fun testSetNotificationStats_clearableSilent() {
         whenever(section.bucket).thenReturn(BUCKET_SILENT)
-        afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
-        verify(stackController)
+        afterRenderListListener.onAfterRenderList(listOf(entry))
+        verify(activeNotificationsInteractor)
             .setNotifStats(
                 NotifStats(
-                    1,
                     hasNonClearableAlertingNotifs = false,
                     hasClearableAlertingNotifs = false,
                     hasNonClearableSilentNotifs = false,
                     hasClearableSilentNotifs = true,
                 )
             )
-        verifyNoMoreInteractions(activeNotificationsInteractor)
     }
 
     @Test
-    @DisableFlags(FooterViewRefactor.FLAG_NAME)
     @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING, FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX)
     fun testSetNotificationStats_isSensitiveStateActive_nonClearableSilent() {
         whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true)
         whenever(section.bucket).thenReturn(BUCKET_SILENT)
-        afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
-        verify(stackController)
+        afterRenderListListener.onAfterRenderList(listOf(entry))
+        verify(activeNotificationsInteractor)
             .setNotifStats(
                 NotifStats(
-                    1,
                     hasNonClearableAlertingNotifs = false,
                     hasClearableAlertingNotifs = false,
                     hasNonClearableSilentNotifs = true,
                     hasClearableSilentNotifs = false,
                 )
             )
-        verifyNoMoreInteractions(activeNotificationsInteractor)
     }
 
     @Test
-    @EnableFlags(FooterViewRefactor.FLAG_NAME)
-    fun testSetNotificationStats_footerFlagOn_clearableAlerting() {
-        whenever(section.bucket).thenReturn(BUCKET_ALERTING)
-        afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
-        verify(activeNotificationsInteractor)
-            .setNotifStats(
-                NotifStats(
-                    1,
-                    hasNonClearableAlertingNotifs = false,
-                    hasClearableAlertingNotifs = true,
-                    hasNonClearableSilentNotifs = false,
-                    hasClearableSilentNotifs = false,
-                )
-            )
-        verifyNoMoreInteractions(stackController)
-    }
-
-    @Test
-    @EnableFlags(
-        FooterViewRefactor.FLAG_NAME,
-        FLAG_SCREENSHARE_NOTIFICATION_HIDING,
-        FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX,
-    )
-    fun testSetNotificationStats_footerFlagOn_isSensitiveStateActive_nonClearableAlerting() {
-        whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true)
-        whenever(section.bucket).thenReturn(BUCKET_ALERTING)
-        afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
-        verify(activeNotificationsInteractor)
-            .setNotifStats(
-                NotifStats(
-                    1,
-                    hasNonClearableAlertingNotifs = true,
-                    hasClearableAlertingNotifs = false,
-                    hasNonClearableSilentNotifs = false,
-                    hasClearableSilentNotifs = false,
-                )
-            )
-        verifyNoMoreInteractions(stackController)
-    }
-
-    @Test
-    @EnableFlags(FooterViewRefactor.FLAG_NAME)
-    fun testSetNotificationStats_footerFlagOn_clearableSilent() {
-        whenever(section.bucket).thenReturn(BUCKET_SILENT)
-        afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
-        verify(activeNotificationsInteractor)
-            .setNotifStats(
-                NotifStats(
-                    1,
-                    hasNonClearableAlertingNotifs = false,
-                    hasClearableAlertingNotifs = false,
-                    hasNonClearableSilentNotifs = false,
-                    hasClearableSilentNotifs = true,
-                )
-            )
-        verifyNoMoreInteractions(stackController)
-    }
-
-    @Test
-    @EnableFlags(
-        FooterViewRefactor.FLAG_NAME,
-        FLAG_SCREENSHARE_NOTIFICATION_HIDING,
-        FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX,
-    )
-    fun testSetNotificationStats_footerFlagOn_isSensitiveStateActive_nonClearableSilent() {
-        whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true)
-        whenever(section.bucket).thenReturn(BUCKET_SILENT)
-        afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
-        verify(activeNotificationsInteractor)
-            .setNotifStats(
-                NotifStats(
-                    1,
-                    hasNonClearableAlertingNotifs = false,
-                    hasClearableAlertingNotifs = false,
-                    hasNonClearableSilentNotifs = true,
-                    hasClearableSilentNotifs = false,
-                )
-            )
-        verifyNoMoreInteractions(stackController)
-    }
-
-    @Test
-    @EnableFlags(FooterViewRefactor.FLAG_NAME)
-    fun testSetNotificationStats_footerFlagOn_nonClearableRedacted() {
+    fun testSetNotificationStats_nonClearableRedacted() {
         entry.setSensitive(true, true)
         whenever(section.bucket).thenReturn(BUCKET_ALERTING)
-        afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
+        afterRenderListListener.onAfterRenderList(listOf(entry))
         verify(activeNotificationsInteractor)
             .setNotifStats(
                 NotifStats(
-                    1,
                     hasNonClearableAlertingNotifs = true,
                     hasClearableAlertingNotifs = false,
                     hasNonClearableSilentNotifs = false,
                     hasClearableSilentNotifs = false,
                 )
             )
-        verifyNoMoreInteractions(stackController)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt
index 25138fd..c4ef4f9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt
@@ -35,9 +35,9 @@
 import androidx.test.InstrumentationRegistry
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapperTest.Companion.any
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
@@ -111,19 +111,8 @@
     }
 
     @Test
-    @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
-    fun testCreateIcons_chipNotifIconFlagDisabled_statusBarChipIconIsNull() {
-        val entry =
-            notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true)
-        entry?.let { iconManager.createIcons(it) }
-        testScope.runCurrent()
-
-        assertThat(entry?.icons?.statusBarChipIcon).isNull()
-    }
-
-    @Test
-    @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
-    fun testCreateIcons_chipNotifIconFlagEnabled_statusBarChipIconIsNull() {
+    @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+    fun testCreateIcons_cdFlagDisabled_statusBarChipIconIsNotNull() {
         val entry =
             notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true)
         entry?.let { iconManager.createIcons(it) }
@@ -133,6 +122,17 @@
     }
 
     @Test
+    @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+    fun testCreateIcons_cdFlagEnabled_statusBarChipIconIsNull() {
+        val entry =
+            notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true)
+        entry?.let { iconManager.createIcons(it) }
+        testScope.runCurrent()
+
+        assertThat(entry?.icons?.statusBarChipIcon).isNull()
+    }
+
+    @Test
     fun testCreateIcons_importantConversation_shortcutIcon() {
         val entry =
             notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true)
@@ -158,7 +158,7 @@
             notificationEntry(
                 hasShortcut = false,
                 hasMessageSenderIcon = false,
-                hasLargeIcon = true
+                hasLargeIcon = true,
             )
         entry?.channel?.isImportantConversation = true
         entry?.let { iconManager.createIcons(it) }
@@ -172,7 +172,7 @@
             notificationEntry(
                 hasShortcut = false,
                 hasMessageSenderIcon = false,
-                hasLargeIcon = false
+                hasLargeIcon = false,
             )
         entry?.channel?.isImportantConversation = true
         entry?.let { iconManager.createIcons(it) }
@@ -187,7 +187,7 @@
                 hasShortcut = true,
                 hasMessageSenderIcon = true,
                 useMessagingStyle = false,
-                hasLargeIcon = true
+                hasLargeIcon = true,
             )
         entry?.channel?.isImportantConversation = true
         entry?.let { iconManager.createIcons(it) }
@@ -204,8 +204,8 @@
     }
 
     @Test
-    @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
-    fun testCreateIcons_sensitiveImportantConversation() {
+    @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+    fun testCreateIcons_cdFlagDisabled_sensitiveImportantConversation() {
         val entry =
             notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false)
         entry?.setSensitive(true, true)
@@ -219,8 +219,23 @@
     }
 
     @Test
-    @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
-    fun testUpdateIcons_sensitiveImportantConversation() {
+    @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+    fun testCreateIcons_cdFlagEnabled_sensitiveImportantConversation() {
+        val entry =
+            notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false)
+        entry?.setSensitive(true, true)
+        entry?.channel?.isImportantConversation = true
+        entry?.let { iconManager.createIcons(it) }
+        testScope.runCurrent()
+        assertThat(entry?.icons?.statusBarIcon?.sourceIcon).isEqualTo(shortcutIc)
+        assertThat(entry?.icons?.statusBarChipIcon).isNull()
+        assertThat(entry?.icons?.shelfIcon?.sourceIcon).isEqualTo(smallIc)
+        assertThat(entry?.icons?.aodIcon?.sourceIcon).isEqualTo(smallIc)
+    }
+
+    @Test
+    @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+    fun testUpdateIcons_cdFlagDisabled_sensitiveImportantConversation() {
         val entry =
             notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false)
         entry?.setSensitive(true, true)
@@ -236,6 +251,23 @@
     }
 
     @Test
+    @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+    fun testUpdateIcons_cdFlagEnabled_sensitiveImportantConversation() {
+        val entry =
+            notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false)
+        entry?.setSensitive(true, true)
+        entry?.channel?.isImportantConversation = true
+        entry?.let { iconManager.createIcons(it) }
+        // Updating the icons after creation shouldn't break anything
+        entry?.let { iconManager.updateIcons(it) }
+        testScope.runCurrent()
+        assertThat(entry?.icons?.statusBarIcon?.sourceIcon).isEqualTo(shortcutIc)
+        assertThat(entry?.icons?.statusBarChipIcon).isNull()
+        assertThat(entry?.icons?.shelfIcon?.sourceIcon).isEqualTo(smallIc)
+        assertThat(entry?.icons?.aodIcon?.sourceIcon).isEqualTo(smallIc)
+    }
+
+    @Test
     fun testUpdateIcons_sensitivityChange() {
         val entry =
             notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false)
@@ -254,7 +286,7 @@
         hasShortcut: Boolean,
         hasMessageSenderIcon: Boolean,
         useMessagingStyle: Boolean = true,
-        hasLargeIcon: Boolean
+        hasLargeIcon: Boolean,
     ): NotificationEntry? {
         val n =
             Notification.Builder(mContext, "id")
@@ -270,7 +302,7 @@
                         SystemClock.currentThreadTimeMillis(),
                         Person.Builder()
                             .setIcon(if (hasMessageSenderIcon) messageIc else null)
-                            .build()
+                            .build(),
                     )
                 )
         if (useMessagingStyle) {
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 e1a8916..3763282 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
@@ -16,7 +16,6 @@
 
 package com.android.systemui.statusbar.notification.stack;
 
-import static android.view.View.GONE;
 import static android.view.WindowInsets.Type.ime;
 
 import static com.android.systemui.flags.SceneContainerFlagParameterizationKt.parameterizeSceneContainerFlag;
@@ -28,17 +27,14 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertNotNull;
 import static junit.framework.Assert.assertTrue;
 
 import static org.junit.Assert.assertFalse;
-import static org.mockito.AdditionalMatchers.not;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
@@ -64,7 +60,6 @@
 import android.view.ViewGroup;
 import android.view.WindowInsets;
 import android.view.WindowInsetsAnimation;
-import android.widget.TextView;
 
 import androidx.test.filters.SmallTest;
 
@@ -92,8 +87,6 @@
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
 import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix;
 import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView;
-import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
-import com.android.systemui.statusbar.notification.footer.shared.NotifRedesignFooter;
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView;
 import com.android.systemui.statusbar.notification.headsup.AvalancheController;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -603,158 +596,6 @@
     }
 
     @Test
-    @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME})
-    public void manageNotifications_visible() {
-        FooterView view = mock(FooterView.class);
-        mStackScroller.setFooterView(view);
-        when(view.willBeGone()).thenReturn(true);
-
-        mStackScroller.updateFooterView(true, false, true);
-
-        verify(view).setVisible(eq(true), anyBoolean());
-        verify(view).setClearAllButtonVisible(eq(false), anyBoolean());
-    }
-
-    @Test
-    @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME})
-    public void clearAll_visible() {
-        FooterView view = mock(FooterView.class);
-        mStackScroller.setFooterView(view);
-        when(view.willBeGone()).thenReturn(true);
-
-        mStackScroller.updateFooterView(true, true, true);
-
-        verify(view).setVisible(eq(true), anyBoolean());
-        verify(view).setClearAllButtonVisible(eq(true), anyBoolean());
-    }
-
-    @Test
-    @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME})
-    public void testInflateFooterView() {
-        mStackScroller.inflateFooterView();
-        ArgumentCaptor<FooterView> captor = ArgumentCaptor.forClass(FooterView.class);
-        verify(mStackScroller).setFooterView(captor.capture());
-
-        assertNotNull(captor.getValue().findViewById(R.id.manage_text));
-        assertNotNull(captor.getValue().findViewById(R.id.dismiss_text));
-    }
-
-    @Test
-    @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME})
-    public void testUpdateFooter_noNotifications() {
-        setBarStateForTest(StatusBarState.SHADE);
-        mStackScroller.setCurrentUserSetup(true);
-
-        FooterView view = mock(FooterView.class);
-        mStackScroller.setFooterView(view);
-        mStackScroller.updateFooter();
-        verify(mStackScroller, atLeastOnce()).updateFooterView(false, false, true);
-    }
-
-    @Test
-    @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME})
-    @DisableSceneContainer
-    public void testUpdateFooter_remoteInput() {
-        setBarStateForTest(StatusBarState.SHADE);
-        mStackScroller.setCurrentUserSetup(true);
-
-        mStackScroller.setIsRemoteInputActive(true);
-        when(mStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1);
-        when(mStackScrollLayoutController.hasActiveClearableNotifications(eq(ROWS_ALL)))
-                .thenReturn(true);
-
-        FooterView view = mock(FooterView.class);
-        mStackScroller.setFooterView(view);
-        mStackScroller.updateFooter();
-        verify(mStackScroller, atLeastOnce()).updateFooterView(false, true, true);
-    }
-
-    @Test
-    @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME})
-    public void testUpdateFooter_withoutNotifications() {
-        setBarStateForTest(StatusBarState.SHADE);
-        mStackScroller.setCurrentUserSetup(true);
-
-        when(mStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0);
-        when(mStackScrollLayoutController.hasActiveClearableNotifications(eq(ROWS_ALL)))
-                .thenReturn(false);
-
-        FooterView view = mock(FooterView.class);
-        mStackScroller.setFooterView(view);
-        mStackScroller.updateFooter();
-        verify(mStackScroller, atLeastOnce()).updateFooterView(false, false, true);
-    }
-
-    @Test
-    @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME})
-    @DisableSceneContainer
-    public void testUpdateFooter_oneClearableNotification() {
-        setBarStateForTest(StatusBarState.SHADE);
-        mStackScroller.setCurrentUserSetup(true);
-
-        when(mStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1);
-        when(mStackScrollLayoutController.hasActiveClearableNotifications(eq(ROWS_ALL)))
-                .thenReturn(true);
-
-        FooterView view = mock(FooterView.class);
-        mStackScroller.setFooterView(view);
-        mStackScroller.updateFooter();
-        verify(mStackScroller, atLeastOnce()).updateFooterView(true, true, true);
-    }
-
-    @Test
-    @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME})
-    @DisableSceneContainer
-    public void testUpdateFooter_withoutHistory() {
-        setBarStateForTest(StatusBarState.SHADE);
-        mStackScroller.setCurrentUserSetup(true);
-
-        when(mStackScrollLayoutController.isHistoryEnabled()).thenReturn(false);
-        when(mStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1);
-        when(mStackScrollLayoutController.hasActiveClearableNotifications(eq(ROWS_ALL)))
-                .thenReturn(true);
-
-        FooterView view = mock(FooterView.class);
-        mStackScroller.setFooterView(view);
-        mStackScroller.updateFooter();
-        verify(mStackScroller, atLeastOnce()).updateFooterView(true, true, false);
-    }
-
-    @Test
-    @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME})
-    public void testUpdateFooter_oneClearableNotification_beforeUserSetup() {
-        setBarStateForTest(StatusBarState.SHADE);
-        mStackScroller.setCurrentUserSetup(false);
-
-        when(mStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1);
-        when(mStackScrollLayoutController.hasActiveClearableNotifications(eq(ROWS_ALL)))
-                .thenReturn(true);
-
-        FooterView view = mock(FooterView.class);
-        mStackScroller.setFooterView(view);
-        mStackScroller.updateFooter();
-        verify(mStackScroller, atLeastOnce()).updateFooterView(false, true, true);
-    }
-
-    @Test
-    @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME})
-    @DisableSceneContainer
-    public void testUpdateFooter_oneNonClearableNotification() {
-        setBarStateForTest(StatusBarState.SHADE);
-        mStackScroller.setCurrentUserSetup(true);
-
-        when(mStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1);
-        when(mStackScrollLayoutController.hasActiveClearableNotifications(eq(ROWS_ALL)))
-                .thenReturn(false);
-        when(mEmptyShadeView.getVisibility()).thenReturn(GONE);
-
-        FooterView view = mock(FooterView.class);
-        mStackScroller.setFooterView(view);
-        mStackScroller.updateFooter();
-        verify(mStackScroller, atLeastOnce()).updateFooterView(true, false, true);
-    }
-
-    @Test
     public void testFooterPosition_atEnd() {
         // add footer
         FooterView view = mock(FooterView.class);
@@ -772,19 +613,6 @@
     }
 
     @Test
-    @DisableFlags({FooterViewRefactor.FLAG_NAME,
-        ModesEmptyShadeFix.FLAG_NAME,
-        NotifRedesignFooter.FLAG_NAME})
-    public void testReInflatesFooterViews() {
-        when(mEmptyShadeView.getTextResource()).thenReturn(R.string.empty_shade_text);
-        clearInvocations(mStackScroller);
-        mStackScroller.reinflateViews();
-        verify(mStackScroller).setFooterView(any());
-        verify(mStackScroller).setEmptyShadeView(any());
-    }
-
-    @Test
-    @EnableFlags(FooterViewRefactor.FLAG_NAME)
     @DisableFlags(ModesEmptyShadeFix.FLAG_NAME)
     public void testReInflatesEmptyShadeView() {
         when(mEmptyShadeView.getTextResource()).thenReturn(R.string.empty_shade_text);
@@ -1231,31 +1059,6 @@
     }
 
     @Test
-    @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME})
-    public void hasFilteredOutSeenNotifs_updateFooter() {
-        mStackScroller.setCurrentUserSetup(true);
-
-        // add footer
-        mStackScroller.inflateFooterView();
-        TextView footerLabel =
-                mStackScroller.mFooterView.requireViewById(R.id.unlock_prompt_footer);
-
-        mStackScroller.setHasFilteredOutSeenNotifications(true);
-        mStackScroller.updateFooter();
-
-        assertThat(footerLabel.getVisibility()).isEqualTo(View.VISIBLE);
-    }
-
-    @Test
-    @DisableFlags({FooterViewRefactor.FLAG_NAME, ModesEmptyShadeFix.FLAG_NAME})
-    public void hasFilteredOutSeenNotifs_updateEmptyShadeView() {
-        mStackScroller.setHasFilteredOutSeenNotifications(true);
-        mStackScroller.updateEmptyShadeView(true, false);
-
-        verify(mEmptyShadeView).setFooterText(not(eq(0)));
-    }
-
-    @Test
     @DisableSceneContainer
     public void testWindowInsetAnimationProgress_updatesBottomInset() {
         int imeInset = 100;
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 3a99328..30ab416 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
@@ -42,6 +42,7 @@
 import android.testing.TestableLooper.RunWithLooper;
 import android.view.View;
 
+import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
 
 import com.android.keyguard.KeyguardUpdateMonitor;
@@ -77,6 +78,8 @@
 import com.android.systemui.statusbar.phone.ui.StatusBarIconController;
 import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.FakeHomeStatusBarViewBinder;
 import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.FakeHomeStatusBarViewModel;
+import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel;
+import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.HomeStatusBarViewModelFactory;
 import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.StatusBarOperatorNameViewModel;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.window.StatusBarWindowController;
@@ -1268,6 +1271,15 @@
                 mock(StatusBarOperatorNameViewModel.class));
         mCollapsedStatusBarViewBinder = new FakeHomeStatusBarViewBinder();
 
+        HomeStatusBarViewModelFactory homeStatusBarViewModelFactory =
+                new HomeStatusBarViewModelFactory() {
+            @NonNull
+            @Override
+            public HomeStatusBarViewModel create(int displayId) {
+                return mCollapsedStatusBarViewModel;
+            }
+        };
+
         return new CollapsedStatusBarFragment(
                 mStatusBarFragmentComponentFactory,
                 mOngoingCallController,
@@ -1275,7 +1287,7 @@
                 mShadeExpansionStateManager,
                 mStatusBarIconController,
                 mIconManagerFactory,
-                mCollapsedStatusBarViewModel,
+                homeStatusBarViewModelFactory,
                 mCollapsedStatusBarViewBinder,
                 mStatusBarHideIconsForBouncerManager,
                 mKeyguardStateController,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt
diff --git a/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt b/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt
index 25d1c37..7ed7361 100644
--- a/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt
+++ b/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt
@@ -435,6 +435,8 @@
 
     override fun unbundleNotification(key: String) {}
 
+    override fun rebundleNotification(key: String) {}
+
     companion object {
         const val DEFAULT_DISPLAY_ID = Display.DEFAULT_DISPLAY
         const val SECONDARY_DISPLAY_ID = 2
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt
index 5ac41ec..f380789 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt
@@ -23,11 +23,11 @@
 val Kosmos.mockActivityTransitionAnimatorController by
     Kosmos.Fixture { mock<ActivityTransitionAnimator.Controller>() }
 
-val Kosmos.activityTransitionAnimator by
+var Kosmos.activityTransitionAnimator by
     Kosmos.Fixture {
         ActivityTransitionAnimator(
             // The main thread is checked in a bunch of places inside the different transitions
             // animators, so we have to pass the real main executor here.
-            mainExecutor = testCase.context.mainExecutor,
+            mainExecutor = testCase.context.mainExecutor
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt
index 1709329..2a1877a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt
@@ -24,7 +24,6 @@
     @Main mainExecutor: Executor,
     isUnlocked: Boolean = true,
     isShowingAlternateAuthOnUnlock: Boolean = false,
-    isPredictiveBackQsDialogAnim: Boolean = false,
     interactionJankMonitor: InteractionJankMonitor,
 ): DialogTransitionAnimator {
     return DialogTransitionAnimator(
@@ -35,10 +34,6 @@
                 isShowingAlternateAuthOnUnlock = isShowingAlternateAuthOnUnlock,
             ),
         interactionJankMonitor = interactionJankMonitor,
-        featureFlags =
-            object : AnimationFeatureFlags {
-                override val isPredictiveBackQsDialogAnim = isPredictiveBackQsDialogAnim
-            },
         transitionAnimator = fakeTransitionAnimator(mainExecutor),
         isForTesting = true,
     )
@@ -50,6 +45,8 @@
     private val isShowingAlternateAuthOnUnlock: Boolean = false,
 ) : DialogTransitionAnimator.Callback {
     override fun isDreaming(): Boolean = isDreaming
+
     override fun isUnlocked(): Boolean = isUnlocked
+
     override fun isShowingAlternateAuthOnUnlock() = isShowingAlternateAuthOnUnlock
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt
index a839f17..c744eac 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt
@@ -33,6 +33,9 @@
     var sourceAdded: Boolean = false
         private set
 
+    var audioSharingStarted: Boolean = false
+        private set
+
     private var profile: LocalBluetoothLeBroadcast? = null
 
     override val leAudioBroadcastProfile: LocalBluetoothLeBroadcast?
@@ -50,7 +53,13 @@
 
     override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) {}
 
-    override suspend fun startAudioSharing() {}
+    override suspend fun startAudioSharing() {
+        audioSharingStarted = true
+    }
+
+    override suspend fun stopAudioSharing() {
+        audioSharingStarted = false
+    }
 
     fun setAudioSharingAvailable(available: Boolean) {
         mutableAvailable = available
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/compose/Snapshot.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/compose/Snapshot.kt
new file mode 100644
index 0000000..fb6699c
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/compose/Snapshot.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2025 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.compose
+
+import androidx.compose.runtime.snapshots.Snapshot
+import com.android.systemui.kosmos.runCurrent
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+
+/**
+ * Runs the given test [block] in a [TestScope] that's set up such that the Compose snapshot state
+ * is settled eagerly. This is the Compose equivalent to using an [UnconfinedTestDispatcher] or
+ * using [runCurrent] a lot.
+ *
+ * Note that this shouldn't be needed or used in a Compose test environment.
+ */
+fun TestScope.runTestWithSnapshots(block: suspend TestScope.() -> Unit) {
+    val handle = Snapshot.registerGlobalWriteObserver { Snapshot.sendApplyNotifications() }
+
+    try {
+        runTest { block() }
+    } finally {
+        handle.dispose()
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt
index 2a7e3e9..490b89b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.dump.dumpManager
 import com.android.systemui.keyevent.domain.interactor.keyEventInteractor
 import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardBypassInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.util.time.systemClock
@@ -34,6 +35,8 @@
         DeviceEntryHapticsInteractor(
             biometricSettingsRepository = biometricSettingsRepository,
             deviceEntryBiometricAuthInteractor = deviceEntryBiometricAuthInteractor,
+            deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor,
+            keyguardBypassInteractor = keyguardBypassInteractor,
             deviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor,
             deviceEntrySourceInteractor = deviceEntrySourceInteractor,
             fingerprintPropertyRepository = fingerprintPropertyRepository,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt
index 3fc60e3..a64fc24 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt
@@ -115,3 +115,6 @@
 interface FakeDisplayRepositoryModule {
     @Binds fun bindFake(fake: FakeDisplayRepository): DisplayRepository
 }
+
+val DisplayRepository.fake
+    get() = this as FakeDisplayRepository
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt
index dad8569..9815da9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt
@@ -19,7 +19,6 @@
 import android.platform.test.annotations.EnableFlags
 import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR
 import com.android.systemui.Flags.FLAG_NOTIFICATION_AVALANCHE_THROTTLE_HUN
-import com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_SYSUI
 import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
 
 /**
@@ -29,7 +28,6 @@
 @EnableFlags(
     FLAG_KEYGUARD_WM_STATE_REFACTOR,
     FLAG_NOTIFICATION_AVALANCHE_THROTTLE_HUN,
-    FLAG_PREDICTIVE_BACK_SYSUI,
     FLAG_SCENE_CONTAINER,
 )
 @Retention(AnnotationRetention.RUNTIME)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
index 47991b3..3df3ee9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
@@ -154,6 +154,7 @@
             shortcutHelperStateRepository,
             activityStarter,
             testScope,
+            customInputGesturesRepository
         )
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 1288d31..8489d83 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -46,9 +46,6 @@
         MutableSharedFlow(extraBufferCapacity = 1)
     override val keyguardDoneAnimationsFinished: Flow<Unit> = _keyguardDoneAnimationsFinished
 
-    private val _clockShouldBeCentered = MutableStateFlow<Boolean>(true)
-    override val clockShouldBeCentered: Flow<Boolean> = _clockShouldBeCentered
-
     private val _dismissAction = MutableStateFlow<DismissAction>(DismissAction.None)
     override val dismissAction: StateFlow<DismissAction> = _dismissAction
 
@@ -192,10 +189,6 @@
         _keyguardDoneAnimationsFinished.tryEmit(Unit)
     }
 
-    override fun setClockShouldBeCentered(shouldBeCentered: Boolean) {
-        _clockShouldBeCentered.value = shouldBeCentered
-    }
-
     override fun setKeyguardEnabled(enabled: Boolean) {
         _isKeyguardEnabled.value = enabled
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index 8209ee1..f479100 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -402,6 +402,12 @@
             )
         )
     }
+
+    suspend fun transitionTo(from: KeyguardState, to: KeyguardState) {
+        sendTransitionStep(TransitionStep(from, to, 0f, TransitionState.STARTED))
+        sendTransitionStep(TransitionStep(from, to, 0.5f, TransitionState.RUNNING))
+        sendTransitionStep(TransitionStep(from, to, 1f, TransitionState.FINISHED))
+    }
 }
 
 @Module
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt
index 3de8093..ee21bdc 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt
@@ -24,8 +24,6 @@
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.power.domain.interactor.PowerInteractor
-import com.android.systemui.power.domain.interactor.PowerInteractorFactory
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.shade.data.repository.FakeShadeRepository
 import com.android.systemui.util.mockito.mock
@@ -55,7 +53,6 @@
         fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor = mock(),
         fromOccludedTransitionInteractor: FromOccludedTransitionInteractor = mock(),
         fromAlternateBouncerTransitionInteractor: FromAlternateBouncerTransitionInteractor = mock(),
-        powerInteractor: PowerInteractor = PowerInteractorFactory.create().powerInteractor,
         testScope: CoroutineScope = TestScope(),
     ): WithDependencies {
         // Mock these until they are replaced by kosmos
@@ -73,10 +70,8 @@
             bouncerRepository = bouncerRepository,
             configurationRepository = configurationRepository,
             shadeRepository = shadeRepository,
-            powerInteractor = powerInteractor,
             KeyguardInteractor(
                 repository = repository,
-                powerInteractor = powerInteractor,
                 bouncerRepository = bouncerRepository,
                 configurationInteractor = ConfigurationInteractorImpl(configurationRepository),
                 shadeRepository = shadeRepository,
@@ -99,7 +94,6 @@
         val bouncerRepository: FakeKeyguardBouncerRepository,
         val configurationRepository: FakeConfigurationRepository,
         val shadeRepository: FakeShadeRepository,
-        val powerInteractor: PowerInteractor,
         val keyguardInteractor: KeyguardInteractor,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt
index f5f8ef7..869bae2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt
@@ -21,7 +21,6 @@
 import com.android.systemui.keyguard.data.repository.keyguardRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.shade.data.repository.shadeRepository
 
@@ -29,7 +28,6 @@
     Kosmos.Fixture {
         KeyguardInteractor(
             repository = keyguardRepository,
-            powerInteractor = powerInteractor,
             bouncerRepository = keyguardBouncerRepository,
             configurationInteractor = configurationInteractor,
             shadeRepository = shadeRepository,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt
index 15d00d9..edc1cce 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt
@@ -20,4 +20,5 @@
 
 class FakeBouncerTransition : PrimaryBouncerTransition {
     override val windowBlurRadius: MutableStateFlow<Float> = MutableStateFlow(0.0f)
+    override val notificationBlurRadius: MutableStateFlow<Float> = MutableStateFlow(0.0f)
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
index 4383560..439df54 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
@@ -1,6 +1,7 @@
 package com.android.systemui.kosmos
 
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.compose.runTestWithSnapshots
 import com.android.systemui.coroutines.FlowValue
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
@@ -16,7 +17,6 @@
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
 import org.mockito.kotlin.verify
 
 var Kosmos.testDispatcher by Fixture { StandardTestDispatcher() }
@@ -52,10 +52,11 @@
 
 /**
  * Run this test body with a [Kosmos] as receiver, and using the [testScope] currently installed in
- * that kosmos instance
+ * that Kosmos instance
  */
-fun Kosmos.runTest(testBody: suspend Kosmos.() -> Unit) =
-    testScope.runTest testBody@{ this@runTest.testBody() }
+fun Kosmos.runTest(testBody: suspend Kosmos.() -> Unit) = let { kosmos ->
+    testScope.runTestWithSnapshots { kosmos.testBody() }
+}
 
 fun Kosmos.runCurrent() = testScope.runCurrent()
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
index 82b5f63..72c7500 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.scene.domain.startable
 
 import com.android.internal.logging.uiEventLogger
+import com.android.systemui.animation.activityTransitionAnimator
 import com.android.systemui.authentication.domain.interactor.authenticationInteractor
 import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
 import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
@@ -85,5 +86,6 @@
         vibratorHelper = vibratorHelper,
         msdlPlayer = msdlPlayer,
         disabledContentInteractor = disabledContentInteractor,
+        activityTransitionAnimator = activityTransitionAnimator,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/view/SceneJankMonitorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/view/SceneJankMonitorKosmos.kt
new file mode 100644
index 0000000..bcba5ee
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/view/SceneJankMonitorKosmos.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2025 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.scene.ui.view
+
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
+import com.android.systemui.jank.interactionJankMonitor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+
+val Kosmos.sceneJankMonitorFactory: SceneJankMonitor.Factory by Fixture {
+    object : SceneJankMonitor.Factory {
+        override fun create(): SceneJankMonitor {
+            return SceneJankMonitor(
+                authenticationInteractor = authenticationInteractor,
+                deviceUnlockedInteractor = deviceUnlockedInteractor,
+                interactionJankMonitor = interactionJankMonitor,
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
index ab193d2..b3d89db 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
@@ -203,6 +203,7 @@
     val isUserInputOngoing = MutableStateFlow(true)
 
     override fun setShadeAndQsExpansion(shadeExpansion: Float, qsExpansion: Float) {
+        shadeRepository.setLegacyIsQsExpanded(qsExpansion > 0f)
         if (shadeExpansion == 1f) {
             setIdleScene(Scenes.Shade)
         } else if (qsExpansion == 1f) {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
index 4a86fd5..74deaab 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
@@ -21,6 +21,8 @@
 import dagger.Binds
 import dagger.Module
 import javax.inject.Inject
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
@@ -37,8 +39,8 @@
     override val udfpsTransitionToFullShadeProgress =
         _udfpsTransitionToFullShadeProgress.asStateFlow()
 
-    private val _currentFling: MutableStateFlow<FlingInfo?> = MutableStateFlow(null)
-    override val currentFling: StateFlow<FlingInfo?> = _currentFling.asStateFlow()
+    override val currentFling: MutableSharedFlow<FlingInfo?> =
+        MutableSharedFlow(replay = 2, onBufferOverflow = BufferOverflow.DROP_OLDEST)
 
     private val _lockscreenShadeExpansion = MutableStateFlow(0f)
     override val lockscreenShadeExpansion = _lockscreenShadeExpansion.asStateFlow()
@@ -139,7 +141,7 @@
     }
 
     override fun setCurrentFling(info: FlingInfo?) {
-        _currentFling.value = info
+        currentFling.tryEmit(info)
     }
 
     override fun setLockscreenShadeExpansion(lockscreenShadeExpansion: Float) {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt
index 4af5e7d..6e44df8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt
@@ -23,11 +23,21 @@
 import com.android.systemui.shade.ShadeDisplayChangeLatencyTracker
 import com.android.systemui.shade.ShadeWindowLayoutParams
 import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository
+import java.util.Optional
+import org.mockito.kotlin.any
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
 
 val Kosmos.shadeLayoutParams by Kosmos.Fixture { ShadeWindowLayoutParams.create(mockedContext) }
 
-val Kosmos.mockedWindowContext by Kosmos.Fixture { mock<WindowContext>() }
+val Kosmos.mockedWindowContext by
+    Kosmos.Fixture {
+        mock<WindowContext>().apply {
+            whenever(reparentToDisplay(any())).thenAnswer { displayIdParam ->
+                whenever(displayId).thenReturn(displayIdParam.arguments[0] as Int)
+            }
+        }
+    }
 val Kosmos.mockedShadeDisplayChangeLatencyTracker by
     Kosmos.Fixture { mock<ShadeDisplayChangeLatencyTracker>() }
 val Kosmos.shadeDisplaysInteractor by
@@ -38,5 +48,6 @@
             testScope.backgroundScope,
             testScope.backgroundScope.coroutineContext,
             mockedShadeDisplayChangeLatencyTracker,
+            Optional.of(shadeExpandedStateInteractor),
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
index af6d624..1dc7229 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.shade.ShadeModule
@@ -30,6 +31,7 @@
 import com.android.systemui.statusbar.policy.data.repository.userSetupRepository
 import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor
 import com.android.systemui.user.domain.interactor.userSwitcherInteractor
+import org.mockito.kotlin.mock
 
 var Kosmos.baseShadeInteractor: BaseShadeInteractor by
     Kosmos.Fixture {
@@ -71,3 +73,7 @@
             shadeModeInteractor = shadeModeInteractor,
         )
     }
+var Kosmos.mockShadeInteractor: ShadeInteractor by Kosmos.Fixture { mock() }
+val Kosmos.shadeExpandedStateInteractor by
+    Kosmos.Fixture { ShadeExpandedStateInteractorImpl(shadeInteractor, testScope.backgroundScope) }
+val Kosmos.fakeShadeExpandedStateInteractor by Kosmos.Fixture { FakeShadeExpandedStateInteractor() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeStatusBarContentInsetsProviderStore.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeStatusBarContentInsetsProviderStore.kt
index 642c2ff..67f8572 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeStatusBarContentInsetsProviderStore.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeStatusBarContentInsetsProviderStore.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.statusbar.data.repository
 
 import android.view.Display
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
 import org.mockito.kotlin.mock
 
 class FakeStatusBarContentInsetsProviderStore() : StatusBarContentInsetsProviderStore {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStoreKosmos.kt
index a34fb09..af7a463 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStoreKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStoreKosmos.kt
@@ -21,7 +21,7 @@
 import com.android.systemui.display.data.repository.displayWindowPropertiesRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.statusbar.phone.statusBarContentInsetsProviderFactory
+import com.android.systemui.statusbar.layout.statusBarContentInsetsProviderFactory
 import com.android.systemui.sysUICutoutProviderFactory
 
 val Kosmos.fakeStatusBarContentInsetsProviderStore by
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/FakeStatusBarContentInsetsProviderFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/FakeStatusBarContentInsetsProviderFactory.kt
similarity index 95%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/FakeStatusBarContentInsetsProviderFactory.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/FakeStatusBarContentInsetsProviderFactory.kt
index 4fb8cf4..ad742c8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/FakeStatusBarContentInsetsProviderFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/FakeStatusBarContentInsetsProviderFactory.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
 
 import android.content.Context
 import com.android.systemui.SysUICutoutProvider
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt
new file mode 100644
index 0000000..90897fa
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2025 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.layout
+
+import android.content.applicationContext
+import com.android.systemui.SysUICutoutProvider
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.commandline.commandRegistry
+import com.android.systemui.statusbar.policy.configurationController
+import com.android.systemui.statusbar.policy.fake
+import org.mockito.kotlin.mock
+
+val Kosmos.mockStatusBarContentInsetsProvider by
+    Kosmos.Fixture { mock<StatusBarContentInsetsProvider>() }
+
+val Kosmos.statusBarContentInsetsProvider by
+    Kosmos.Fixture {
+        StatusBarContentInsetsProviderImpl(
+            applicationContext,
+            configurationController.fake,
+            dumpManager,
+            commandRegistry,
+            mock<SysUICutoutProvider>(),
+        )
+    }
+
+val Kosmos.fakeStatusBarContentInsetsProviderFactory by
+    Kosmos.Fixture { FakeStatusBarContentInsetsProviderFactory() }
+
+var Kosmos.statusBarContentInsetsProviderFactory: StatusBarContentInsetsProviderImpl.Factory by
+    Kosmos.Fixture { fakeStatusBarContentInsetsProviderFactory }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelKosmos.kt
new file mode 100644
index 0000000..889d469
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelKosmos.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2025 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.layout.ui.viewmodel
+
+import com.android.systemui.display.data.repository.displayRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.statusbar.data.repository.multiDisplayStatusBarContentInsetsProviderStore
+import com.android.systemui.statusbar.layout.statusBarContentInsetsProvider
+
+val Kosmos.statusBarContentInsetsViewModel by
+    Kosmos.Fixture { StatusBarContentInsetsViewModel(statusBarContentInsetsProvider) }
+
+val Kosmos.multiDisplayStatusBarContentInsetsViewModelStore by
+    Kosmos.Fixture {
+        MultiDisplayStatusBarContentInsetsViewModelStore(
+            applicationCoroutineScope,
+            displayRepository,
+            multiDisplayStatusBarContentInsetsProviderStore,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
index d1619b7..60e092c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
@@ -57,6 +57,7 @@
 import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
 import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor
+import com.android.systemui.window.ui.viewmodel.fakeBouncerTransitions
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -99,6 +100,7 @@
         primaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel,
         primaryBouncerToLockscreenTransitionViewModel =
             primaryBouncerToLockscreenTransitionViewModel,
+        primaryBouncerTransitions = fakeBouncerTransitions,
         aodBurnInViewModel = aodBurnInViewModel,
         communalSceneInteractor = communalSceneInteractor,
         headsUpNotificationInteractor = { headsUpNotificationInteractor },
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderKosmos.kt
deleted file mode 100644
index 705df3c..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderKosmos.kt
+++ /dev/null
@@ -1,31 +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.phone
-
-import com.android.systemui.kosmos.Kosmos
-import org.mockito.kotlin.mock
-
-val Kosmos.mockStatusBarContentInsetsProvider by
-    Kosmos.Fixture { mock<StatusBarContentInsetsProvider>() }
-
-var Kosmos.statusBarContentInsetsProvider by Kosmos.Fixture { mockStatusBarContentInsetsProvider }
-
-val Kosmos.fakeStatusBarContentInsetsProviderFactory by
-    Kosmos.Fixture { FakeStatusBarContentInsetsProviderFactory() }
-
-var Kosmos.statusBarContentInsetsProviderFactory: StatusBarContentInsetsProviderImpl.Factory by
-    Kosmos.Fixture { fakeStatusBarContentInsetsProviderFactory }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt
index b38a723..db7e31b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel
 
+import android.content.testableContext
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.kosmos.Kosmos
@@ -26,6 +27,7 @@
 import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel
 import com.android.systemui.statusbar.events.domain.interactor.systemStatusEventAnimationInteractor
 import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.statusBarPopupChipsViewModel
+import com.android.systemui.statusbar.layout.ui.viewmodel.multiDisplayStatusBarContentInsetsViewModelStore
 import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
 import com.android.systemui.statusbar.phone.domain.interactor.darkIconInteractor
@@ -36,6 +38,7 @@
 var Kosmos.homeStatusBarViewModel: HomeStatusBarViewModel by
     Kosmos.Fixture {
         HomeStatusBarViewModelImpl(
+            testableContext.displayId,
             homeStatusBarInteractor,
             homeStatusBarIconBlockListInteractor,
             lightsOutInteractor,
@@ -51,6 +54,7 @@
             ongoingActivityChipsViewModel,
             statusBarPopupChipsViewModel,
             systemStatusEventAnimationInteractor,
+            multiDisplayStatusBarContentInsetsViewModelStore,
             applicationCoroutineScope,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt
index 282f594..1e47013 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt
@@ -25,3 +25,6 @@
     Kosmos.Fixture { FakeConfigurationController() }
 val Kosmos.statusBarConfigurationController: StatusBarConfigurationController by
     Kosmos.Fixture { fakeConfigurationController }
+
+val ConfigurationController.fake
+    get() = this as FakeConfigurationController
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt
index 1ba5ddb..fc0c92e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt
@@ -20,5 +20,5 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 
-val Kosmos.zenModeRepository by Fixture { fakeZenModeRepository }
+var Kosmos.zenModeRepository by Fixture { fakeZenModeRepository }
 val Kosmos.fakeZenModeRepository by Fixture { FakeZenModeRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt
index 7eaecb1..3a19547 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt
@@ -19,7 +19,7 @@
 import android.content.Context
 import com.android.app.viewcapture.ViewCaptureAwareWindowManager
 import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
 
 class FakeStatusBarWindowControllerFactory : StatusBarWindowController.Factory {
     override fun create(
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt
index 23f2b42..f595aef 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt
@@ -22,7 +22,7 @@
 import com.android.systemui.concurrency.fakeExecutor
 import com.android.systemui.fragments.fragmentService
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.statusbar.phone.statusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.statusBarContentInsetsProvider
 import com.android.systemui.statusbar.policy.statusBarConfigurationController
 import java.util.Optional
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
index ed5322e..db19d6e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
@@ -39,7 +39,7 @@
 
 val Kosmos.mediaOutputActionsInteractor by
     Kosmos.Fixture { MediaOutputActionsInteractor(mediaOutputDialogManager) }
-val Kosmos.mediaControllerRepository by Kosmos.Fixture { FakeMediaControllerRepository() }
+var Kosmos.mediaControllerRepository by Kosmos.Fixture { FakeMediaControllerRepository() }
 val Kosmos.mediaOutputInteractor by
     Kosmos.Fixture {
         MediaOutputInteractor(
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt
index 712ec41..3f2b479 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt
@@ -19,4 +19,4 @@
 import com.android.systemui.kosmos.Kosmos
 
 val Kosmos.fakeAudioRepository by Kosmos.Fixture { FakeAudioRepository() }
-val Kosmos.audioRepository by Kosmos.Fixture { fakeAudioRepository }
+var Kosmos.audioRepository by Kosmos.Fixture { fakeAudioRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/VolumeDialogKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/VolumeDialogKosmos.kt
new file mode 100644
index 0000000..e243193
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/VolumeDialogKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.dagger.volumeDialogComponentFactory
+import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor
+
+val Kosmos.volumeDialog by
+    Kosmos.Fixture {
+        VolumeDialog(
+            context = applicationContext,
+            visibilityInteractor = volumeDialogVisibilityInteractor,
+            componentFactory = volumeDialogComponentFactory,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponentKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponentKosmos.kt
new file mode 100644
index 0000000..73e5d8d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponentKosmos.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.dagger
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderComponent
+import com.android.systemui.volume.dialog.sliders.dagger.volumeDialogSliderComponentFactory
+import com.android.systemui.volume.dialog.ui.binder.VolumeDialogViewBinder
+import com.android.systemui.volume.dialog.ui.binder.volumeDialogViewBinder
+import kotlinx.coroutines.CoroutineScope
+
+val Kosmos.volumeDialogComponentFactory by
+    Kosmos.Fixture {
+        object : VolumeDialogComponent.Factory {
+            override fun create(scope: CoroutineScope): VolumeDialogComponent =
+                volumeDialogComponent
+        }
+    }
+val Kosmos.volumeDialogComponent by
+    Kosmos.Fixture {
+        object : VolumeDialogComponent {
+            override fun volumeDialogViewBinder(): VolumeDialogViewBinder = volumeDialogViewBinder
+
+            override fun sliderComponentFactory(): VolumeDialogSliderComponent.Factory =
+                volumeDialogSliderComponentFactory
+        }
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt
index 291dfc0..3d5698b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt
@@ -19,4 +19,4 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.volume.dialog.data.VolumeDialogVisibilityRepository
 
-val Kosmos.volumeDialogVisibilityRepository by Kosmos.Fixture { VolumeDialogVisibilityRepository() }
+var Kosmos.volumeDialogVisibilityRepository by Kosmos.Fixture { VolumeDialogVisibilityRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt
index db9c48d..8f122b5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt
@@ -16,8 +16,6 @@
 
 package com.android.systemui.volume.dialog.domain.interactor
 
-import android.os.Handler
-import android.os.looper
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.plugins.volumeDialogController
@@ -27,6 +25,6 @@
         VolumeDialogCallbacksInteractor(
             volumeDialogController = volumeDialogController,
             coroutineScope = applicationCoroutineScope,
-            bgHandler = Handler(looper),
+            bgHandler = null,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/VolumeDialogRingerViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/VolumeDialogRingerViewBinderKosmos.kt
new file mode 100644
index 0000000..7cbdc3d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/VolumeDialogRingerViewBinderKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.ringer
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.ringer.ui.binder.VolumeDialogRingerViewBinder
+import com.android.systemui.volume.dialog.ringer.ui.viewmodel.volumeDialogRingerDrawerViewModel
+
+val Kosmos.volumeDialogRingerViewBinder by
+    Kosmos.Fixture { VolumeDialogRingerViewBinder(volumeDialogRingerDrawerViewModel) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt
index 44371b4..cf357b4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt
@@ -20,5 +20,5 @@
 
 val Kosmos.fakeVolumeDialogRingerFeedbackRepository by
     Kosmos.Fixture { FakeVolumeDialogRingerFeedbackRepository() }
-val Kosmos.volumeDialogRingerFeedbackRepository by
+var Kosmos.volumeDialogRingerFeedbackRepository: VolumeDialogRingerFeedbackRepository by
     Kosmos.Fixture { fakeVolumeDialogRingerFeedbackRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt
index a494d04..4bebf89 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt
@@ -21,7 +21,7 @@
 import com.android.systemui.plugins.volumeDialogController
 import com.android.systemui.volume.data.repository.audioSystemRepository
 import com.android.systemui.volume.dialog.domain.interactor.volumeDialogStateInteractor
-import com.android.systemui.volume.dialog.ringer.data.repository.fakeVolumeDialogRingerFeedbackRepository
+import com.android.systemui.volume.dialog.ringer.data.repository.volumeDialogRingerFeedbackRepository
 
 val Kosmos.volumeDialogRingerInteractor by
     Kosmos.Fixture {
@@ -30,6 +30,6 @@
             volumeDialogStateInteractor = volumeDialogStateInteractor,
             controller = volumeDialogController,
             audioSystemRepository = audioSystemRepository,
-            ringerFeedbackRepository = fakeVolumeDialogRingerFeedbackRepository,
+            ringerFeedbackRepository = volumeDialogRingerFeedbackRepository,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractorKosmos.kt
new file mode 100644
index 0000000..26b8bca
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractorKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.settings.domain
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.statusbar.policy.deviceProvisionedController
+import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor
+import com.android.systemui.volume.panel.domain.interactor.volumePanelGlobalStateInteractor
+
+val Kosmos.volumeDialogSettingsButtonInteractor by
+    Kosmos.Fixture {
+        VolumeDialogSettingsButtonInteractor(
+            applicationCoroutineScope,
+            deviceProvisionedController,
+            volumePanelGlobalStateInteractor,
+            volumeDialogVisibilityInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinderKosmos.kt
new file mode 100644
index 0000000..f9e128d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinderKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.settings.ui.binder
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.settings.ui.viewmodel.volumeDialogSettingsButtonViewModel
+
+val Kosmos.volumeDialogSettingsButtonViewBinder by
+    Kosmos.Fixture { VolumeDialogSettingsButtonViewBinder(volumeDialogSettingsButtonViewModel) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/viewmodel/VolumeDialogSettingsButtonViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/viewmodel/VolumeDialogSettingsButtonViewModelKosmos.kt
new file mode 100644
index 0000000..0ae3b03
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/viewmodel/VolumeDialogSettingsButtonViewModelKosmos.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.volume.dialog.settings.ui.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.volume.dialog.settings.domain.volumeDialogSettingsButtonInteractor
+import com.android.systemui.volume.mediaDeviceSessionInteractor
+import com.android.systemui.volume.mediaOutputInteractor
+
+val Kosmos.volumeDialogSettingsButtonViewModel by
+    Kosmos.Fixture {
+        VolumeDialogSettingsButtonViewModel(
+            applicationContext,
+            testScope.testScheduler,
+            applicationCoroutineScope,
+            mediaOutputInteractor,
+            mediaDeviceSessionInteractor,
+            volumeDialogSettingsButtonInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt
new file mode 100644
index 0000000..4f79f7b4
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.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.volume.dialog.sliders.dagger
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.volumeDialogController
+import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
+import com.android.systemui.volume.data.repository.audioRepository
+import com.android.systemui.volume.dialog.data.repository.volumeDialogVisibilityRepository
+import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType
+import com.android.systemui.volume.dialog.sliders.domain.model.volumeDialogSliderType
+import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogOverscrollViewBinder
+import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderHapticsViewBinder
+import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderViewBinder
+import com.android.systemui.volume.dialog.sliders.ui.volumeDialogOverscrollViewBinder
+import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSliderHapticsViewBinder
+import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSliderViewBinder
+import com.android.systemui.volume.mediaControllerRepository
+import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.mediaControllerInteractor
+
+private val Kosmos.mutableSliderComponentKosmoses: MutableMap<VolumeDialogSliderType, Kosmos> by
+    Kosmos.Fixture { mutableMapOf() }
+
+val Kosmos.volumeDialogSliderComponentFactory by
+    Kosmos.Fixture {
+        object : VolumeDialogSliderComponent.Factory {
+            override fun create(sliderType: VolumeDialogSliderType): VolumeDialogSliderComponent =
+                volumeDialogSliderComponent(sliderType)
+        }
+    }
+
+fun Kosmos.volumeDialogSliderComponent(type: VolumeDialogSliderType): VolumeDialogSliderComponent {
+    return object : VolumeDialogSliderComponent {
+
+        private val localKosmos
+            get() =
+                mutableSliderComponentKosmoses.getOrPut(type) {
+                    Kosmos().also {
+                        it.setupVolumeDialogSliderComponent(this@volumeDialogSliderComponent, type)
+                    }
+                }
+
+        override fun sliderViewBinder(): VolumeDialogSliderViewBinder =
+            localKosmos.volumeDialogSliderViewBinder
+
+        override fun sliderHapticsViewBinder(): VolumeDialogSliderHapticsViewBinder =
+            localKosmos.volumeDialogSliderHapticsViewBinder
+
+        override fun overscrollViewBinder(): VolumeDialogOverscrollViewBinder =
+            localKosmos.volumeDialogOverscrollViewBinder
+    }
+}
+
+private fun Kosmos.setupVolumeDialogSliderComponent(
+    parentKosmos: Kosmos,
+    type: VolumeDialogSliderType,
+) {
+    volumeDialogSliderType = type
+    applicationContext = parentKosmos.applicationContext
+    testScope = parentKosmos.testScope
+
+    volumeDialogController = parentKosmos.volumeDialogController
+    mediaControllerInteractor = parentKosmos.mediaControllerInteractor
+    mediaControllerRepository = parentKosmos.mediaControllerRepository
+    zenModeRepository = parentKosmos.zenModeRepository
+    volumeDialogVisibilityRepository = parentKosmos.volumeDialogVisibilityRepository
+    audioRepository = parentKosmos.audioRepository
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractorKosmos.kt
index 44917dd..198d72a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractorKosmos.kt
@@ -19,6 +19,7 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.plugins.volumeDialogController
+import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
 import com.android.systemui.volume.dialog.domain.interactor.volumeDialogStateInteractor
 import com.android.systemui.volume.dialog.sliders.domain.model.volumeDialogSliderType
 
@@ -29,5 +30,6 @@
             applicationCoroutineScope,
             volumeDialogStateInteractor,
             volumeDialogController,
+            zenModeInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinderKosmos.kt
new file mode 100644
index 0000000..13d6ca9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinderKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogOverscrollViewModel
+
+val Kosmos.volumeDialogOverscrollViewBinder by
+    Kosmos.Fixture { VolumeDialogOverscrollViewBinder(volumeDialogOverscrollViewModel) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt
new file mode 100644
index 0000000..d6845b1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui
+
+import com.android.systemui.haptics.msdl.msdlPlayer
+import com.android.systemui.haptics.vibratorHelper
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.time.systemClock
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderInputEventsViewModel
+
+val Kosmos.volumeDialogSliderHapticsViewBinder by
+    Kosmos.Fixture {
+        VolumeDialogSliderHapticsViewBinder(
+            volumeDialogSliderInputEventsViewModel,
+            vibratorHelper,
+            msdlPlayer,
+            systemClock,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt
new file mode 100644
index 0000000..c6db717
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderInputEventsViewModel
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderViewModel
+
+val Kosmos.volumeDialogSliderViewBinder by
+    Kosmos.Fixture {
+        VolumeDialogSliderViewBinder(
+            volumeDialogSliderViewModel,
+            volumeDialogSliderInputEventsViewModel,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinderKosmos.kt
new file mode 100644
index 0000000..83527d9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinderKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSlidersViewModel
+
+val Kosmos.volumeDialogSlidersViewBinder by
+    Kosmos.Fixture { VolumeDialogSlidersViewBinder(volumeDialogSlidersViewModel) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModelKosmos.kt
new file mode 100644
index 0000000..fe2f3d8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModelKosmos.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSliderInputEventsInteractor
+
+val Kosmos.volumeDialogOverscrollViewModel by
+    Kosmos.Fixture {
+        VolumeDialogOverscrollViewModel(applicationContext, volumeDialogSliderInputEventsInteractor)
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProviderKosmos.kt
new file mode 100644
index 0000000..09f9f1c
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProviderKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
+import com.android.systemui.volume.domain.interactor.audioVolumeInteractor
+
+val Kosmos.volumeDialogSliderIconProvider by
+    Kosmos.Fixture {
+        VolumeDialogSliderIconProvider(
+            context = applicationContext,
+            audioVolumeInteractor = audioVolumeInteractor,
+            zenModeInteractor = zenModeInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModelKosmos.kt
new file mode 100644
index 0000000..2de0e8f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModelKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSliderInputEventsInteractor
+
+val Kosmos.volumeDialogSliderInputEventsViewModel by
+    Kosmos.Fixture {
+        VolumeDialogSliderInputEventsViewModel(
+            applicationCoroutineScope,
+            volumeDialogSliderInputEventsInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt
new file mode 100644
index 0000000..63cd440
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.util.time.systemClock
+import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor
+import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSliderInteractor
+
+val Kosmos.volumeDialogSliderViewModel by
+    Kosmos.Fixture {
+        VolumeDialogSliderViewModel(
+            interactor = volumeDialogSliderInteractor,
+            visibilityInteractor = volumeDialogVisibilityInteractor,
+            coroutineScope = applicationCoroutineScope,
+            volumeDialogSliderIconProvider = volumeDialogSliderIconProvider,
+            systemClock = systemClock,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModelKosmos.kt
new file mode 100644
index 0000000..5531f76
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModelKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.volume.dialog.sliders.dagger.volumeDialogSliderComponentFactory
+import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSlidersInteractor
+
+val Kosmos.volumeDialogSlidersViewModel by
+    Kosmos.Fixture {
+        VolumeDialogSlidersViewModel(
+            applicationCoroutineScope,
+            volumeDialogSlidersInteractor,
+            volumeDialogSliderComponentFactory,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt
new file mode 100644
index 0000000..dc09e32
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.ui.binder
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.ringer.volumeDialogRingerViewBinder
+import com.android.systemui.volume.dialog.settings.ui.binder.volumeDialogSettingsButtonViewBinder
+import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSlidersViewBinder
+import com.android.systemui.volume.dialog.ui.utils.jankListenerFactory
+import com.android.systemui.volume.dialog.ui.viewmodel.volumeDialogViewModel
+import com.android.systemui.volume.dialog.utils.volumeTracer
+
+val Kosmos.volumeDialogViewBinder by
+    Kosmos.Fixture {
+        VolumeDialogViewBinder(
+            applicationContext.resources,
+            volumeDialogViewModel,
+            jankListenerFactory,
+            volumeTracer,
+            volumeDialogRingerViewBinder,
+            volumeDialogSlidersViewBinder,
+            volumeDialogSettingsButtonViewBinder,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactoryKosmos.kt
new file mode 100644
index 0000000..35ec5d3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactoryKosmos.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.volume.dialog.ui.utils
+
+import com.android.systemui.jank.interactionJankMonitor
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.jankListenerFactory by Kosmos.Fixture { JankListenerFactory(interactionJankMonitor) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModelKosmos.kt
new file mode 100644
index 0000000..05ef462
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModelKosmos.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.volume.dialog.ui.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.policy.configurationController
+import com.android.systemui.statusbar.policy.devicePostureController
+import com.android.systemui.volume.dialog.domain.interactor.volumeDialogStateInteractor
+import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor
+import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSlidersInteractor
+
+val Kosmos.volumeDialogViewModel by
+    Kosmos.Fixture {
+        VolumeDialogViewModel(
+            applicationContext,
+            volumeDialogVisibilityInteractor,
+            volumeDialogSlidersInteractor,
+            volumeDialogStateInteractor,
+            devicePostureController,
+            configurationController,
+        )
+    }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Booleans.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Booleans.kt
new file mode 100644
index 0000000..ca02576
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Booleans.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2025 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.kairos
+
+/** Returns a [State] that is `true` only when all of [states] are `true`. */
+@ExperimentalKairosApi
+fun allOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.allTrue() }
+
+/** Returns a [State] that is `true` when any of [states] are `true`. */
+@ExperimentalKairosApi
+fun anyOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.anyTrue() }
+
+/** Returns a [State] containing the inverse of the Boolean held by the original [State]. */
+@ExperimentalKairosApi fun not(state: State<Boolean>): State<Boolean> = state.mapCheapUnsafe { !it }
+
+private fun Iterable<Boolean>.allTrue() = all { it }
+
+private fun Iterable<Boolean>.anyTrue() = any { it }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt
index b691870..bd2173c 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt
@@ -17,17 +17,14 @@
 package com.android.systemui.kairos
 
 import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.just
 import com.android.systemui.kairos.util.map
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.EmptyCoroutineContext
 import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.awaitCancellation
-import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.FlowCollector
 import kotlinx.coroutines.flow.MutableSharedFlow
@@ -36,9 +33,8 @@
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.dropWhile
 import kotlinx.coroutines.flow.scan
-import kotlinx.coroutines.launch
 
-/** A function that modifies the KairosNetwork. */
+/** A computation that can modify the Kairos network. */
 typealias BuildSpec<A> = BuildScope.() -> A
 
 /**
@@ -56,17 +52,7 @@
 
 /** Operations that add inputs and outputs to a Kairos network. */
 @ExperimentalKairosApi
-interface BuildScope : StateScope {
-
-    /**
-     * A [KairosNetwork] handle that is bound to this [BuildScope].
-     *
-     * It supports all of the standard functionality by which external code can interact with this
-     * Kairos network, but all [activated][KairosNetwork.activateSpec] [BuildSpec]s are bound as
-     * children to this [BuildScope], such that when this [BuildScope] is destroyed, all children
-     * are also destroyed.
-     */
-    val kairosNetwork: KairosNetwork
+interface BuildScope : HasNetwork, StateScope {
 
     /**
      * Defers invoking [block] until after the current [BuildScope] code-path completes, returning a
@@ -110,11 +96,21 @@
      * executed if this [BuildScope] is still active by that time. It can be deactivated due to a
      * -Latest combinator, for example.
      *
-     * Shorthand for:
-     * ```kotlin
-     *   events.observe { effect { ... } }
+     * [Disposing][DisposableHandle.dispose] of the returned [DisposableHandle] will stop the
+     * observation of new emissions. It will however *not* cancel any running effects from previous
+     * emissions. To achieve this behavior, use [launchScope] or [asyncScope] to create a child
+     * build scope:
+     * ``` kotlin
+     *   val job = launchScope {
+     *       events.observe { x ->
+     *           launchEffect { longRunningEffect(x) }
+     *       }
+     *   }
+     *   // cancels observer and any running effects:
+     *   job.cancel()
      * ```
      */
+    // TODO: remove disposable handle return? might add more confusion than convenience
     fun <A> Events<A>.observe(
         coroutineContext: CoroutineContext = EmptyCoroutineContext,
         block: EffectScope.(A) -> Unit = {},
@@ -129,7 +125,7 @@
      * same key are undone (any registered [observers][observe] are unregistered, and any pending
      * [side-effects][effect] are cancelled).
      *
-     * If the [Maybe] contained within the value for an associated key is [none], then the
+     * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
      * previously-active [BuildSpec] will be undone with no replacement.
      */
     fun <K, A, B> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey(
@@ -138,32 +134,32 @@
     ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>>
 
     /**
-     * Creates an instance of an [Events] with elements that are from [builder].
+     * Creates an instance of an [Events] with elements that are emitted from [builder].
      *
      * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the
-     * provided [MutableState].
+     * provided [EventProducerScope].
      *
      * By default, [builder] is only running while the returned [Events] is being
      * [observed][observe]. If you want it to run at all times, simply add a no-op observer:
-     * ```kotlin
-     * events { ... }.apply { observe() }
+     * ``` kotlin
+     *   events { ... }.apply { observe() }
      * ```
      */
-    fun <T> events(
-        name: String? = null,
-        builder: suspend EventProducerScope<T>.() -> Unit,
-    ): Events<T>
+    // TODO: eventually this should be defined on KairosNetwork + an extension on HasNetwork
+    //  - will require modifying InputNode so that it can be manually killed, as opposed to using
+    //    takeUntil (which requires a StateScope).
+    fun <T> events(builder: suspend EventProducerScope<T>.() -> Unit): Events<T>
 
     /**
      * Creates an instance of an [Events] with elements that are emitted from [builder].
      *
      * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the
-     * provided [MutableState].
+     * provided [CoalescingEventProducerScope].
      *
      * By default, [builder] is only running while the returned [Events] is being
      * [observed][observe]. If you want it to run at all times, simply add a no-op observer:
-     * ```kotlin
-     * events { ... }.apply { observe() }
+     * ``` kotlin
+     *   events { ... }.apply { observe() }
      * ```
      *
      * In the event of backpressure, emissions are *coalesced* into batches. When a value is
@@ -171,6 +167,7 @@
      * [coalesce]. Once the batch is consumed by the kairos network in the next transaction, the
      * batch is reset back to [getInitialValue].
      */
+    // TODO: see TODO for [events]
     fun <In, Out> coalescingEvents(
         getInitialValue: () -> Out,
         coalesce: (old: Out, new: In) -> Out,
@@ -186,6 +183,7 @@
      *
      * The return value from [block] can be accessed via the returned [DeferredValue].
      */
+    // TODO: return a DisposableHandle instead of Job?
     fun <A> asyncScope(block: BuildSpec<A>): Pair<DeferredValue<A>, Job>
 
     // TODO: once we have context params, these can all become extensions:
@@ -198,9 +196,9 @@
      * outside of the current Kairos transaction; when [transform] returns, the returned value is
      * emitted from the result [Events] in a new transaction.
      *
-     * Shorthand for:
-     * ```kotlin
-     * events.mapLatestBuild { a -> asyncEvent { transform(a) } }.flatten()
+     * ``` kotlin
+     *     fun <A, B> Events<A>.mapAsyncLatest(transform: suspend (A) -> B): Events<B> =
+     *         mapLatestBuild { a -> asyncEvent { transform(a) } }.flatten()
      * ```
      */
     fun <A, B> Events<A>.mapAsyncLatest(transform: suspend (A) -> B): Events<B> =
@@ -219,42 +217,19 @@
     /**
      * Returns a [StateFlow] whose [value][StateFlow.value] tracks the current
      * [value of this State][State.sample], and will emit at the same rate as [State.changes].
-     *
-     * Note that the [value][StateFlow.value] is not available until the *end* of the current
-     * transaction. If you need the current value before this time, then use [State.sample].
      */
     fun <A> State<A>.toStateFlow(): StateFlow<A> {
-        val uninitialized = Any()
-        var initialValue: Any? = uninitialized
-        val innerStateFlow = MutableStateFlow<Any?>(uninitialized)
-        deferredBuildScope {
-            initialValue = sample()
-            changes.observe {
-                innerStateFlow.value = it
-                initialValue = null
-            }
-        }
-
-        @Suppress("UNCHECKED_CAST")
-        fun getValue(innerValue: Any?): A =
-            when {
-                innerValue !== uninitialized -> innerValue as A
-                initialValue !== uninitialized -> initialValue as A
-                else ->
-                    error(
-                        "Attempted to access StateFlow.value before Kairos transaction has completed."
-                    )
-            }
-
+        val innerStateFlow = MutableStateFlow(sampleDeferred())
+        changes.observe { innerStateFlow.value = deferredOf(it) }
         return object : StateFlow<A> {
             override val replayCache: List<A>
-                get() = innerStateFlow.replayCache.map(::getValue)
+                get() = innerStateFlow.replayCache.map { it.value }
 
             override val value: A
-                get() = getValue(innerStateFlow.value)
+                get() = innerStateFlow.value.value
 
             override suspend fun collect(collector: FlowCollector<A>): Nothing {
-                innerStateFlow.collect { collector.emit(getValue(it)) }
+                innerStateFlow.collect { collector.emit(it.value) }
             }
         }
     }
@@ -365,14 +340,14 @@
         initialSpec: BuildSpec<A>
     ): Pair<Events<B>, DeferredValue<A>> {
         val (events, result) =
-            mapCheap { spec -> mapOf(Unit to just(spec)) }
+            mapCheap { spec -> mapOf(Unit to Maybe.present(spec)) }
                 .applyLatestSpecForKey(initialSpecs = mapOf(Unit to initialSpec), numKeys = 1)
         val outEvents: Events<B> =
             events.mapMaybe {
                 checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" }
             }
         val outInit: DeferredValue<A> = deferredBuildScope {
-            val initResult: Map<Unit, A> = result.get()
+            val initResult: Map<Unit, A> = result.value
             check(Unit in initResult) {
                 "applyLatest: expected initial result, but none present in: $initResult"
             }
@@ -425,7 +400,7 @@
         transform: BuildScope.(A) -> B,
     ): Pair<Events<B>, DeferredValue<B>> =
         mapCheap { buildSpec { transform(it) } }
-            .applyLatestSpec(initialSpec = buildSpec { transform(initialValue.get()) })
+            .applyLatestSpec(initialSpec = buildSpec { transform(initialValue.value) })
 
     /**
      * Returns an [Events] containing the results of applying each [BuildSpec] emitted from the
@@ -436,7 +411,7 @@
      * same key are undone (any registered [observers][observe] are unregistered, and any pending
      * [side-effects][effect] are cancelled).
      *
-     * If the [Maybe] contained within the value for an associated key is [none], then the
+     * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
      * previously-active [BuildSpec] will be undone with no replacement.
      */
     fun <K, A, B> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey(
@@ -445,6 +420,17 @@
     ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> =
         applyLatestSpecForKey(deferredOf(initialSpecs), numKeys)
 
+    /**
+     * Returns an [Incremental] containing the results of applying each [BuildSpec] emitted from the
+     * original [Incremental].
+     *
+     * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] with the
+     * same key are undone (any registered [observers][observe] are unregistered, and any pending
+     * [side-effects][effect] are cancelled).
+     *
+     * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
+     * previously-active [BuildSpec] will be undone with no replacement.
+     */
     fun <K, V> Incremental<K, BuildSpec<V>>.applyLatestSpecForKey(
         numKeys: Int? = null
     ): Incremental<K, V> {
@@ -460,7 +446,7 @@
      * same key are undone (any registered [observers][observe] are unregistered, and any pending
      * [side-effects][effect] are cancelled).
      *
-     * If the [Maybe] contained within the value for an associated key is [none], then the
+     * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
      * previously-active [BuildSpec] will be undone with no replacement.
      */
     fun <K, V> Events<Map<K, Maybe<BuildSpec<V>>>>.applyLatestSpecForKey(
@@ -476,7 +462,7 @@
      * same key are undone (any registered [observers][observe] are unregistered, and any pending
      * [side-effects][effect] are cancelled).
      *
-     * If the [Maybe] contained within the value for an associated key is [none], then the
+     * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
      * previously-active [BuildSpec] will be undone with no replacement.
      */
     fun <K, V> Events<Map<K, Maybe<BuildSpec<V>>>>.holdLatestSpecForKey(
@@ -495,7 +481,7 @@
      * same key are undone (any registered [observers][observe] are unregistered, and any pending
      * [side-effects][effect] are cancelled).
      *
-     * If the [Maybe] contained within the value for an associated key is [none], then the
+     * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
      * previously-active [BuildSpec] will be undone with no replacement.
      */
     fun <K, V> Events<Map<K, Maybe<BuildSpec<V>>>>.holdLatestSpecForKey(
@@ -513,7 +499,7 @@
      * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
      * cancelled).
      *
-     * If the [Maybe] contained within the value for an associated key is [none], then the
+     * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
      * previously-active [BuildScope] will be undone with no replacement.
      */
     fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey(
@@ -524,7 +510,7 @@
         map { patch -> patch.mapValues { (k, v) -> v.map { buildSpec { transform(k, it) } } } }
             .applyLatestSpecForKey(
                 deferredBuildScope {
-                    initialValues.get().mapValues { (k, v) -> buildSpec { transform(k, v) } }
+                    initialValues.value.mapValues { (k, v) -> buildSpec { transform(k, v) } }
                 },
                 numKeys = numKeys,
             )
@@ -539,7 +525,7 @@
      * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
      * cancelled).
      *
-     * If the [Maybe] contained within the value for an associated key is [none], then the
+     * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
      * previously-active [BuildScope] will be undone with no replacement.
      */
     fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey(
@@ -558,7 +544,7 @@
      * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
      * cancelled).
      *
-     * If the [Maybe] contained within the value for an associated key is [none], then the
+     * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
      * previously-active [BuildScope] will be undone with no replacement.
      */
     fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey(
@@ -570,7 +556,7 @@
     fun <R> Events<R>.nextDeferred(): Deferred<R> {
         lateinit var next: CompletableDeferred<R>
         val job = launchScope { nextOnly().observe { next.complete(it) } }
-        next = CompletableDeferred<R>(parent = job)
+        next = CompletableDeferred(parent = job)
         return next
     }
 
@@ -581,12 +567,11 @@
     }
 
     /** Returns an [Events] that emits whenever this [Flow] emits. */
-    fun <A> Flow<A>.toEvents(name: String? = null): Events<A> =
-        events(name) { collect { emit(it) } }
+    fun <A> Flow<A>.toEvents(): Events<A> = events { collect { emit(it) } }
 
     /**
      * Shorthand for:
-     * ```kotlin
+     * ``` kotlin
      * flow.toEvents().holdState(initialValue)
      * ```
      */
@@ -594,7 +579,7 @@
 
     /**
      * Shorthand for:
-     * ```kotlin
+     * ``` kotlin
      * flow.scan(initialValue, operation).toEvents().holdState(initialValue)
      * ```
      */
@@ -603,7 +588,7 @@
 
     /**
      * Shorthand for:
-     * ```kotlin
+     * ``` kotlin
      * flow.scan(initialValue) { a, f -> f(a) }.toEvents().holdState(initialValue)
      * ```
      */
@@ -679,6 +664,13 @@
      * Invokes [block] on the value held in this [State]. [block] receives an [BuildScope] that can
      * be used to make further modifications to the Kairos network, and/or perform side-effects via
      * [effect].
+     *
+     * ``` kotlin
+     *     fun <A> State<A>.observeBuild(block: BuildScope.(A) -> Unit = {}): Job = launchScope {
+     *         block(sample())
+     *         changes.observeBuild(block)
+     *     }
+     * ```
      */
     fun <A> State<A>.observeBuild(block: BuildScope.(A) -> Unit = {}): Job = launchScope {
         block(sample())
@@ -706,12 +698,9 @@
  * outside of the current Kairos transaction; when it completes, the returned [Events] emits in a
  * new transaction.
  *
- * Shorthand for:
- * ```
- * events { emitter: MutableEvents<A> ->
- *     val a = block()
- *     emitter.emit(a)
- * }
+ * ``` kotlin
+ *   fun <A> BuildScope.asyncEvent(block: suspend () -> A): Events<A> =
+ *       events { emit(block()) }.apply { observe() }
  * ```
  */
 @ExperimentalKairosApi
@@ -730,9 +719,12 @@
  * executed if this [BuildScope] is still active by that time. It can be deactivated due to a
  * -Latest combinator, for example.
  *
- * Shorthand for:
- * ```kotlin
- *   launchScope { now.observe { block() } }
+ * ``` kotlin
+ *   fun BuildScope.effect(
+ *       context: CoroutineContext = EmptyCoroutineContext,
+ *       block: EffectScope.() -> Unit,
+ *   ): Job =
+ *       launchScope { now.observe(context) { block() } }
  * ```
  */
 @ExperimentalKairosApi
@@ -748,13 +740,14 @@
  * done because the current [BuildScope] might be deactivated within this transaction, perhaps due
  * to a -Latest combinator. If this happens, then the coroutine will never actually be started.
  *
- * Shorthand for:
- * ```kotlin
- *   effect { effectCoroutineScope.launch { block() } }
+ * ``` kotlin
+ *   fun BuildScope.launchEffect(block: suspend KairosScope.() -> Unit): Job =
+ *       effect { effectCoroutineScope.launch { block() } }
  * ```
  */
 @ExperimentalKairosApi
-fun BuildScope.launchEffect(block: suspend CoroutineScope.() -> Unit): Job = asyncEffect(block)
+fun BuildScope.launchEffect(block: suspend KairosCoroutineScope.() -> Unit): Job =
+    asyncEffect(block)
 
 /**
  * Launches [block] in a new coroutine, returning the result as a [Deferred].
@@ -764,17 +757,18 @@
  * to a -Latest combinator. If this happens, then the coroutine will never actually be started.
  *
  * Shorthand for:
- * ```kotlin
- *   CompletableDeferred<R>.apply {
- *       effect { effectCoroutineScope.launch { complete(coroutineScope { block() }) } }
- *     }
- *     .await()
+ * ``` kotlin
+ *   fun <R> BuildScope.asyncEffect(block: suspend KairosScope.() -> R): Deferred<R> =
+ *       CompletableDeferred<R>.apply {
+ *               effect { effectCoroutineScope.launch { complete(block()) } }
+ *           }
+ *           .await()
  * ```
  */
 @ExperimentalKairosApi
-fun <R> BuildScope.asyncEffect(block: suspend CoroutineScope.() -> R): Deferred<R> {
+fun <R> BuildScope.asyncEffect(block: suspend KairosCoroutineScope.() -> R): Deferred<R> {
     val result = CompletableDeferred<R>()
-    val job = effect { effectCoroutineScope.launch { result.complete(coroutineScope(block)) } }
+    val job = effect { launch { result.complete(block()) } }
     val handle = job.invokeOnCompletion { result.cancel() }
     result.invokeOnCompletion {
         handle.dispose()
@@ -795,7 +789,7 @@
  *
  * By default, [builder] is only running while the returned [Events] is being
  * [observed][BuildScope.observe]. If you want it to run at all times, simply add a no-op observer:
- * ```kotlin
+ * ``` kotlin
  * events { ... }.apply { observe() }
  * ```
  *
@@ -819,7 +813,7 @@
  *
  * By default, [builder] is only running while the returned [Events] is being
  * [observed][BuildScope.observe]. If you want it to run at all times, simply add a no-op observer:
- * ```kotlin
+ * ``` kotlin
  * events { ... }.apply { observe() }
  * ```
  *
@@ -837,7 +831,7 @@
         }
 
 /** Scope for emitting to a [BuildScope.coalescingEvents]. */
-interface CoalescingEventProducerScope<in T> {
+fun interface CoalescingEventProducerScope<in T> {
     /**
      * Inserts [value] into the current batch, enqueueing it for emission from this [Events] if not
      * already pending.
@@ -850,7 +844,7 @@
 }
 
 /** Scope for emitting to a [BuildScope.events]. */
-interface EventProducerScope<in T> {
+fun interface EventProducerScope<in T> {
     /**
      * Emits a [value] to this [Events], suspending the caller until the Kairos transaction
      * containing the emission has completed.
@@ -868,3 +862,11 @@
     } finally {
         block()
     }
+
+/**
+ * Runs [spec] in this [BuildScope], and then re-runs it whenever [rebuildSignal] emits. Returns a
+ * [State] that holds the result of the currently-active [BuildSpec].
+ */
+@ExperimentalKairosApi
+fun <A> BuildScope.rebuildOn(rebuildSignal: Events<*>, spec: BuildSpec<A>): State<A> =
+    rebuildSignal.map { spec }.holdLatestSpec(spec)
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt
deleted file mode 100644
index c208646..0000000
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt
+++ /dev/null
@@ -1,278 +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.kairos
-
-import com.android.systemui.kairos.util.These
-import com.android.systemui.kairos.util.WithPrev
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.none
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.channelFlow
-import kotlinx.coroutines.flow.conflate
-
-/**
- * Returns an [Events] that emits the value sampled from the [Transactional] produced by each
- * emission of the original [Events], within the same transaction of the original emission.
- */
-@ExperimentalKairosApi
-fun <A> Events<Transactional<A>>.sampleTransactionals(): Events<A> = map { it.sample() }
-
-/** @see TransactionScope.sample */
-@ExperimentalKairosApi
-fun <A, B, C> Events<A>.sample(
-    state: State<B>,
-    transform: TransactionScope.(A, B) -> C,
-): Events<C> = map { transform(it, state.sample()) }
-
-/** @see TransactionScope.sample */
-@ExperimentalKairosApi
-fun <A, B, C> Events<A>.sample(
-    sampleable: Transactional<B>,
-    transform: TransactionScope.(A, B) -> C,
-): Events<C> = map { transform(it, sampleable.sample()) }
-
-/**
- * Like [sample], but if [state] is changing at the time it is sampled ([changes] is emitting), then
- * the new value is passed to [transform].
- *
- * Note that [sample] is both more performant, and safer to use with recursive definitions. You will
- * generally want to use it rather than this.
- *
- * @see sample
- */
-@ExperimentalKairosApi
-fun <A, B, C> Events<A>.samplePromptly(
-    state: State<B>,
-    transform: TransactionScope.(A, B) -> C,
-): Events<C> =
-    sample(state) { a, b -> These.thiz(a to b) }
-        .mergeWith(state.changes.map { These.that(it) }) { thiz, that ->
-            These.both((thiz as These.This).thiz, (that as These.That).that)
-        }
-        .mapMaybe { these ->
-            when (these) {
-                // both present, transform the upstream value and the new value
-                is These.Both -> just(transform(these.thiz.first, these.that))
-                // no upstream present, so don't perform the sample
-                is These.That -> none()
-                // just the upstream, so transform the upstream and the old value
-                is These.This -> just(transform(these.thiz.first, these.thiz.second))
-            }
-        }
-
-/**
- * Returns a cold [Flow] that, when collected, emits from this [Events]. [network] is needed to
- * transactionally connect to / disconnect from the [Events] when collection starts/stops.
- */
-@ExperimentalKairosApi
-fun <A> Events<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
-    channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, emits from this [State]. [network] is needed to
- * transactionally connect to / disconnect from the [State] when collection starts/stops.
- */
-@ExperimentalKairosApi
-fun <A> State<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
-    channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this
- * [network], and then emits from the returned [Events].
- *
- * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up.
- */
-@ExperimentalKairosApi
-@JvmName("eventsSpecToColdConflatedFlow")
-fun <A> BuildSpec<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
-    channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this
- * [network], and then emits from the returned [State].
- *
- * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up.
- */
-@ExperimentalKairosApi
-@JvmName("stateSpecToColdConflatedFlow")
-fun <A> BuildSpec<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
-    channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in
- * this [network], and then emits from the returned [Events].
- */
-@ExperimentalKairosApi
-@JvmName("transactionalFlowToColdConflatedFlow")
-fun <A> Transactional<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
-    channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in
- * this [network], and then emits from the returned [State].
- */
-@ExperimentalKairosApi
-@JvmName("transactionalStateToColdConflatedFlow")
-fun <A> Transactional<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
-    channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, applies this [Stateful] in a new transaction in this
- * [network], and then emits from the returned [Events].
- *
- * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up.
- */
-@ExperimentalKairosApi
-@JvmName("statefulFlowToColdConflatedFlow")
-fun <A> Stateful<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
-    channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in
- * this [network], and then emits from the returned [State].
- *
- * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up.
- */
-@ExperimentalKairosApi
-@JvmName("statefulStateToColdConflatedFlow")
-fun <A> Stateful<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
-    channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate()
-
-/** Return an [Events] that emits from the original [Events] only when [state] is `true`. */
-@ExperimentalKairosApi
-fun <A> Events<A>.filter(state: State<Boolean>): Events<A> = filter { state.sample() }
-
-private fun Iterable<Boolean>.allTrue() = all { it }
-
-private fun Iterable<Boolean>.anyTrue() = any { it }
-
-/** Returns a [State] that is `true` only when all of [states] are `true`. */
-@ExperimentalKairosApi
-fun allOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.allTrue() }
-
-/** Returns a [State] that is `true` when any of [states] are `true`. */
-@ExperimentalKairosApi
-fun anyOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.anyTrue() }
-
-/** Returns a [State] containing the inverse of the Boolean held by the original [State]. */
-@ExperimentalKairosApi fun not(state: State<Boolean>): State<Boolean> = state.mapCheapUnsafe { !it }
-
-/**
- * Represents a modal Kairos sub-network.
- *
- * When [enabled][enableMode], all network modifications are applied immediately to the Kairos
- * network. When the returned [Events] emits a [BuildMode], that mode is enabled and replaces this
- * mode, undoing all modifications in the process (any registered [observers][BuildScope.observe]
- * are unregistered, and any pending [side-effects][BuildScope.effect] are cancelled).
- *
- * Use [compiledBuildSpec] to compile and stand-up a mode graph.
- *
- * @see StatefulMode
- */
-@ExperimentalKairosApi
-fun interface BuildMode<out A> {
-    /**
-     * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a
-     * new mode.
-     */
-    fun BuildScope.enableMode(): Pair<A, Events<BuildMode<A>>>
-}
-
-/**
- * Returns an [BuildSpec] that, when [applied][BuildScope.applySpec], stands up a modal-transition
- * graph starting with this [BuildMode], automatically switching to new modes as they are produced.
- *
- * @see BuildMode
- */
-@ExperimentalKairosApi
-val <A> BuildMode<A>.compiledBuildSpec: BuildSpec<State<A>>
-    get() = buildSpec {
-        var modeChangeEvents by EventsLoop<BuildMode<A>>()
-        val activeMode: State<Pair<A, Events<BuildMode<A>>>> =
-            modeChangeEvents
-                .map { it.run { buildSpec { enableMode() } } }
-                .holdLatestSpec(buildSpec { enableMode() })
-        modeChangeEvents =
-            activeMode
-                .map { statefully { it.second.nextOnly() } }
-                .applyLatestStateful()
-                .switchEvents()
-        activeMode.map { it.first }
-    }
-
-/**
- * Represents a modal Kairos sub-network.
- *
- * When [enabled][enableMode], all state accumulation is immediately started. When the returned
- * [Events] emits a [BuildMode], that mode is enabled and replaces this mode, stopping all state
- * accumulation in the process.
- *
- * Use [compiledStateful] to compile and stand-up a mode graph.
- *
- * @see BuildMode
- */
-@ExperimentalKairosApi
-fun interface StatefulMode<out A> {
-    /**
-     * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a
-     * new mode.
-     */
-    fun StateScope.enableMode(): Pair<A, Events<StatefulMode<A>>>
-}
-
-/**
- * Returns an [Stateful] that, when [applied][StateScope.applyStateful], stands up a
- * modal-transition graph starting with this [StatefulMode], automatically switching to new modes as
- * they are produced.
- *
- * @see BuildMode
- */
-@ExperimentalKairosApi
-val <A> StatefulMode<A>.compiledStateful: Stateful<State<A>>
-    get() = statefully {
-        var modeChangeEvents by EventsLoop<StatefulMode<A>>()
-        val activeMode: State<Pair<A, Events<StatefulMode<A>>>> =
-            modeChangeEvents
-                .map { it.run { statefully { enableMode() } } }
-                .holdLatestStateful(statefully { enableMode() })
-        modeChangeEvents =
-            activeMode
-                .map { statefully { it.second.nextOnly() } }
-                .applyLatestStateful()
-                .switchEvents()
-        activeMode.map { it.first }
-    }
-
-/**
- * Runs [spec] in this [BuildScope], and then re-runs it whenever [rebuildSignal] emits. Returns a
- * [State] that holds the result of the currently-active [BuildSpec].
- */
-@ExperimentalKairosApi
-fun <A> BuildScope.rebuildOn(rebuildSignal: Events<*>, spec: BuildSpec<A>): State<A> =
-    rebuildSignal.map { spec }.holdLatestSpec(spec)
-
-/**
- * Like [changes] but also includes the old value of this [State].
- *
- * Shorthand for:
- * ``` kotlin
- *     stateChanges.map { WithPrev(previousValue = sample(), newValue = it) }
- * ```
- */
-@ExperimentalKairosApi
-val <A> State<A>.transitions: Events<WithPrev<A, A>>
-    get() = changes.map { WithPrev(previousValue = sample(), newValue = it) }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combine.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combine.kt
new file mode 100644
index 0000000..b3d89c3
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combine.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2025 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.kairos
+
+import com.android.systemui.kairos.internal.NoScope
+import com.android.systemui.kairos.internal.init
+import com.android.systemui.kairos.internal.zipStates
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.combineState
+ */
+@ExperimentalKairosApi
+@JvmName(name = "stateCombine")
+fun <A, B, C> State<A>.combine(other: State<B>, transform: KairosScope.(A, B) -> C): State<C> =
+    combine(this, other, transform)
+
+/**
+ * Returns a [State] by combining the values held inside the given [States][State] into a [List].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A> Iterable<State<A>>.combine(): State<List<A>> {
+    val operatorName = "combine"
+    val name = operatorName
+    return StateInit(
+        init(name) {
+            val states = map { it.init }
+            zipStates(
+                name,
+                operatorName,
+                states.size,
+                states = init(null) { states.map { it.connect(this) } },
+            )
+        }
+    )
+}
+
+/**
+ * Returns a [State] by combining the values held inside the given [States][State] into a [Map].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <K, A> Map<K, State<A>>.combine(): State<Map<K, A>> =
+    asIterable().map { (k, state) -> state.map { v -> k to v } }.combine().map { it.toMap() }
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A, B> Iterable<State<A>>.combine(transform: KairosScope.(List<A>) -> B): State<B> =
+    combine().map(transform)
+
+/**
+ * Returns a [State] by combining the values held inside the given [State]s into a [List].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A> combine(vararg states: State<A>): State<List<A>> = states.asIterable().combine()
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A, B> combine(vararg states: State<A>, transform: KairosScope.(List<A>) -> B): State<B> =
+    states.asIterable().combine(transform)
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A, B, Z> combine(
+    stateA: State<A>,
+    stateB: State<B>,
+    transform: KairosScope.(A, B) -> Z,
+): State<Z> {
+    val operatorName = "combine"
+    val name = operatorName
+    return StateInit(
+        init(name) {
+            zipStates(name, operatorName, stateA.init, stateB.init) { a, b ->
+                NoScope.transform(a, b)
+            }
+        }
+    )
+}
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A, B, C, Z> combine(
+    stateA: State<A>,
+    stateB: State<B>,
+    stateC: State<C>,
+    transform: KairosScope.(A, B, C) -> Z,
+): State<Z> {
+    val operatorName = "combine"
+    val name = operatorName
+    return StateInit(
+        init(name) {
+            zipStates(name, operatorName, stateA.init, stateB.init, stateC.init) { a, b, c ->
+                NoScope.transform(a, b, c)
+            }
+        }
+    )
+}
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A, B, C, D, Z> combine(
+    stateA: State<A>,
+    stateB: State<B>,
+    stateC: State<C>,
+    stateD: State<D>,
+    transform: KairosScope.(A, B, C, D) -> Z,
+): State<Z> {
+    val operatorName = "combine"
+    val name = operatorName
+    return StateInit(
+        init(name) {
+            zipStates(name, operatorName, stateA.init, stateB.init, stateC.init, stateD.init) {
+                a,
+                b,
+                c,
+                d ->
+                NoScope.transform(a, b, c, d)
+            }
+        }
+    )
+}
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A, B, C, D, E, Z> combine(
+    stateA: State<A>,
+    stateB: State<B>,
+    stateC: State<C>,
+    stateD: State<D>,
+    stateE: State<E>,
+    transform: KairosScope.(A, B, C, D, E) -> Z,
+): State<Z> {
+    val operatorName = "combine"
+    val name = operatorName
+    return StateInit(
+        init(name) {
+            zipStates(
+                name,
+                operatorName,
+                stateA.init,
+                stateB.init,
+                stateC.init,
+                stateD.init,
+                stateE.init,
+            ) { a, b, c, d, e ->
+                NoScope.transform(a, b, c, d, e)
+            }
+        }
+    )
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/DeferredValue.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/DeferredValue.kt
new file mode 100644
index 0000000..4b9bb0e
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/DeferredValue.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2025 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.kairos
+
+import com.android.systemui.kairos.internal.CompletableLazy
+
+/**
+ * A value that may not be immediately (synchronously) available, but is guaranteed to be available
+ * before this transaction is completed.
+ */
+@ExperimentalKairosApi
+class DeferredValue<out A> internal constructor(internal val unwrapped: Lazy<A>) {
+    /**
+     * Returns the value held by this [DeferredValue], or throws [IllegalStateException] if it is
+     * not yet available.
+     */
+    val value: A
+        get() = unwrapped.value
+}
+
+/** Returns an already-available [DeferredValue] containing [value]. */
+@ExperimentalKairosApi
+fun <A> deferredOf(value: A): DeferredValue<A> = DeferredValue(CompletableLazy(value))
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt
index 7e257f2..14d45d4 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt
@@ -16,33 +16,46 @@
 
 package com.android.systemui.kairos
 
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Job
 
 /**
- * Scope for external side-effects triggered by the Kairos network. This still occurs within the
- * context of a transaction, so general suspending calls are disallowed to prevent blocking the
- * transaction. You can use [effectCoroutineScope] to [launch][kotlinx.coroutines.launch] new
- * coroutines to perform long-running asynchronous work. This scope is alive for the duration of the
- * containing [BuildScope] that this side-effect scope is running in.
+ * Scope for external side-effects triggered by the Kairos network.
+ *
+ * This still occurs within the context of a transaction, so general suspending calls are disallowed
+ * to prevent blocking the transaction. You can [launch] new coroutines to perform long-running
+ * asynchronous work. These coroutines are kept alive for the duration of the containing
+ * [BuildScope] that this side-effect scope is running in.
  */
 @ExperimentalKairosApi
-interface EffectScope : TransactionScope {
+interface EffectScope : HasNetwork, TransactionScope {
     /**
-     * A [CoroutineScope] whose lifecycle lives for as long as this [EffectScope] is alive. This is
-     * generally until the [Job][kotlinx.coroutines.Job] returned by [BuildScope.effect] is
-     * cancelled.
+     * Creates a coroutine that is a child of this [EffectScope], and returns its future result as a
+     * [Deferred].
+     *
+     * @see kotlinx.coroutines.async
      */
-    @ExperimentalKairosApi val effectCoroutineScope: CoroutineScope
+    fun <R> async(
+        context: CoroutineContext = EmptyCoroutineContext,
+        start: CoroutineStart = CoroutineStart.DEFAULT,
+        block: suspend KairosCoroutineScope.() -> R,
+    ): Deferred<R>
 
     /**
-     * A [KairosNetwork] instance that can be used to transactionally query / modify the Kairos
-     * network.
+     * Launches a new coroutine that is a child of this [EffectScope] without blocking the current
+     * thread and returns a reference to the coroutine as a [Job].
      *
-     * The lambda passed to [KairosNetwork.transact] on this instance will receive an [BuildScope]
-     * that is lifetime-bound to this [EffectScope]. Once this [EffectScope] is no longer alive, any
-     * modifications to the Kairos network performed via this [KairosNetwork] instance will be
-     * undone (any registered [observers][BuildScope.observe] are unregistered, and any pending
-     * [side-effects][BuildScope.effect] are cancelled).
+     * @see kotlinx.coroutines.launch
      */
-    @ExperimentalKairosApi val kairosNetwork: KairosNetwork
+    fun launch(
+        context: CoroutineContext = EmptyCoroutineContext,
+        start: CoroutineStart = CoroutineStart.DEFAULT,
+        block: suspend KairosCoroutineScope.() -> Unit,
+    ): Job = async(context, start, block)
 }
+
+@ExperimentalKairosApi interface KairosCoroutineScope : HasNetwork, CoroutineScope
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt
index e7d0096..8f468c1 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.kairos
 
 import com.android.systemui.kairos.internal.CompletableLazy
-import com.android.systemui.kairos.internal.DemuxImpl
 import com.android.systemui.kairos.internal.EventsImpl
 import com.android.systemui.kairos.internal.Init
 import com.android.systemui.kairos.internal.InitScope
@@ -27,22 +26,11 @@
 import com.android.systemui.kairos.internal.activated
 import com.android.systemui.kairos.internal.cached
 import com.android.systemui.kairos.internal.constInit
-import com.android.systemui.kairos.internal.demuxMap
-import com.android.systemui.kairos.internal.filterImpl
-import com.android.systemui.kairos.internal.filterJustImpl
 import com.android.systemui.kairos.internal.init
 import com.android.systemui.kairos.internal.mapImpl
-import com.android.systemui.kairos.internal.mergeNodes
-import com.android.systemui.kairos.internal.mergeNodesLeft
 import com.android.systemui.kairos.internal.neverImpl
-import com.android.systemui.kairos.internal.switchDeferredImplSingle
-import com.android.systemui.kairos.internal.switchPromptImplSingle
 import com.android.systemui.kairos.internal.util.hashString
-import com.android.systemui.kairos.util.Either
-import com.android.systemui.kairos.util.Either.Left
-import com.android.systemui.kairos.util.Either.Right
 import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.just
 import com.android.systemui.kairos.util.toMaybe
 import java.util.concurrent.atomic.AtomicReference
 import kotlin.reflect.KProperty
@@ -51,7 +39,16 @@
 import kotlinx.coroutines.async
 import kotlinx.coroutines.coroutineScope
 
-/** A series of values of type [A] available at discrete points in time. */
+/**
+ * A series of values of type [A] available at discrete points in time.
+ *
+ * [Events] follow these rules:
+ * 1. Within a single Kairos network transaction, an [Events] instance will only emit *once*.
+ * 2. The order that different [Events] instances emit values within a transaction is undefined, and
+ *    are conceptually *simultaneous*.
+ * 3. [Events] emissions are *ephemeral* and do not last beyond the transaction they are emitted,
+ *    unless explicitly [observed][BuildScope.observe] or [held][StateScope.holdState] as a [State].
+ */
 @ExperimentalKairosApi
 sealed class Events<out A> {
     companion object {
@@ -67,7 +64,9 @@
  * A forward-reference to an [Events]. Useful for recursive definitions.
  *
  * This reference can be used like a standard [Events], but will throw an error if its [loopback] is
- * unset before the end of the first transaction which accesses it.
+ * unset before it is [observed][BuildScope.observe].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.eventsLoop
  */
 @ExperimentalKairosApi
 class EventsLoop<A> : Events<A>() {
@@ -76,7 +75,10 @@
     internal val init: Init<EventsImpl<A>> =
         init(name = null) { deferred.value.init.connect(evalScope = this) }
 
-    /** The [Events] this reference is referring to. */
+    /**
+     * The [Events] this reference is referring to. Must be set before this [EventsLoop] is
+     * [observed][BuildScope.observe].
+     */
     var loopback: Events<A>? = null
         set(value) {
             value?.let {
@@ -102,6 +104,12 @@
  * will be queried and used.
  *
  * Useful for recursive definitions.
+ *
+ * ``` kotlin
+ *   fun <A> Lazy<Events<A>>.defer() = deferredEvents { value }
+ * ```
+ *
+ * @see deferredEvents
  */
 @ExperimentalKairosApi fun <A> Lazy<Events<A>>.defer(): Events<A> = deferInline { value }
 
@@ -113,6 +121,12 @@
  * and used.
  *
  * Useful for recursive definitions.
+ *
+ * ``` kotlin
+ *   fun <A> DeferredValue<Events<A>>.defer() = deferredEvents { get() }
+ * ```
+ *
+ * @see deferredEvents
  */
 @ExperimentalKairosApi
 fun <A> DeferredValue<Events<A>>.defer(): Events<A> = deferInline { unwrapped.value }
@@ -130,25 +144,27 @@
     NoScope.block()
 }
 
-/** Returns an [Events] that emits the new value of this [State] when it changes. */
-@ExperimentalKairosApi
-val <A> State<A>.changes: Events<A>
-    get() = EventsInit(init(name = null) { init.connect(evalScope = this).changes })
-
 /**
- * Returns an [Events] that contains only the [just] results of applying [transform] to each value
- * of the original [Events].
+ * Returns an [Events] that contains only the
+ * [present][com.android.systemui.kairos.util.Maybe.present] results of applying [transform] to each
+ * value of the original [Events].
  *
+ * @sample com.android.systemui.kairos.KairosSamples.mapMaybe
  * @see mapNotNull
  */
 @ExperimentalKairosApi
 fun <A, B> Events<A>.mapMaybe(transform: TransactionScope.(A) -> Maybe<B>): Events<B> =
-    map(transform).filterJust()
+    map(transform).filterPresent()
 
 /**
  * Returns an [Events] that contains only the non-null results of applying [transform] to each value
  * of the original [Events].
  *
+ * ``` kotlin
+ *  fun <A> Events<A>.mapNotNull(transform: TransactionScope.(A) -> B?): Events<B> =
+ *      mapMaybe { if (it == null) absent else present(it) }
+ * ```
+ *
  * @see mapMaybe
  */
 @ExperimentalKairosApi
@@ -156,23 +172,11 @@
     transform(it).toMaybe()
 }
 
-/** Returns an [Events] containing only values of the original [Events] that are not null. */
-@ExperimentalKairosApi
-fun <A> Events<A?>.filterNotNull(): Events<A> = mapCheap { it.toMaybe() }.filterJust()
-
-/** Shorthand for `mapNotNull { it as? A }`. */
-@ExperimentalKairosApi
-inline fun <reified A> Events<*>.filterIsInstance(): Events<A> =
-    mapCheap { it as? A }.filterNotNull()
-
-/** Shorthand for `mapMaybe { it }`. */
-@ExperimentalKairosApi
-fun <A> Events<Maybe<A>>.filterJust(): Events<A> =
-    EventsInit(constInit(name = null, filterJustImpl { init.connect(evalScope = this) }))
-
 /**
  * Returns an [Events] containing the results of applying [transform] to each value of the original
  * [Events].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.mapEvents
  */
 @ExperimentalKairosApi
 fun <A, B> Events<A>.map(transform: TransactionScope.(A) -> B): Events<B> {
@@ -184,6 +188,7 @@
  * Like [map], but the emission is not cached during the transaction. Use only if [transform] is
  * fast and pure.
  *
+ * @sample com.android.systemui.kairos.KairosSamples.mapCheap
  * @see map
  */
 @ExperimentalKairosApi
@@ -196,8 +201,9 @@
  * Returns an [Events] that invokes [action] before each value of the original [Events] is emitted.
  * Useful for logging and debugging.
  *
- * ```
- *   pulse.onEach { foo(it) } == pulse.map { foo(it); it }
+ * ``` kotlin
+ *   fun <A> Events<A>.onEach(action: TransactionScope.(A) -> Unit): Events<A> =
+ *       map { it.also { action(it) } }
  * ```
  *
  * Note that the side effects performed in [onEach] are only performed while the resulting [Events]
@@ -207,29 +213,19 @@
  */
 @ExperimentalKairosApi
 fun <A> Events<A>.onEach(action: TransactionScope.(A) -> Unit): Events<A> = map {
-    action(it)
-    it
-}
-
-/**
- * Returns an [Events] containing only values of the original [Events] that satisfy the given
- * [predicate].
- */
-@ExperimentalKairosApi
-fun <A> Events<A>.filter(predicate: TransactionScope.(A) -> Boolean): Events<A> {
-    val pulse = filterImpl({ init.connect(evalScope = this) }) { predicate(it) }
-    return EventsInit(constInit(name = null, pulse))
+    it.also { action(it) }
 }
 
 /**
  * Splits an [Events] of pairs into a pair of [Events], where each returned [Events] emits half of
  * the original.
  *
- * Shorthand for:
- * ```kotlin
- * val lefts = map { it.first }
- * val rights = map { it.second }
- * return Pair(lefts, rights)
+ * ``` kotlin
+ *   fun <A, B> Events<Pair<A, B>>.unzip(): Pair<Events<A>, Events<B>> {
+ *       val lefts = map { it.first }
+ *       val rights = map { it.second }
+ *       return lefts to rights
+ *   }
  * ```
  */
 @ExperimentalKairosApi
@@ -240,246 +236,6 @@
 }
 
 /**
- * Merges the given [Events] into a single [Events] that emits events from both.
- *
- * Because [Events] can only emit one value per transaction, the provided [transformCoincidence]
- * function is used to combine coincident emissions to produce the result value to be emitted by the
- * merged [Events].
- */
-@ExperimentalKairosApi
-fun <A> Events<A>.mergeWith(
-    other: Events<A>,
-    name: String? = null,
-    transformCoincidence: TransactionScope.(A, A) -> A = { a, _ -> a },
-): Events<A> {
-    val node =
-        mergeNodes(
-            name = name,
-            getPulse = { init.connect(evalScope = this) },
-            getOther = { other.init.connect(evalScope = this) },
-        ) { a, b ->
-            transformCoincidence(a, b)
-        }
-    return EventsInit(constInit(name = null, node))
-}
-
-/**
- * Merges the given [Events] into a single [Events] that emits events from all. All coincident
- * emissions are collected into the emitted [List], preserving the input ordering.
- *
- * @see mergeWith
- * @see mergeLeft
- */
-@ExperimentalKairosApi
-fun <A> merge(vararg events: Events<A>): Events<List<A>> = events.asIterable().merge()
-
-/**
- * Merges the given [Events] into a single [Events] that emits events from all. In the case of
- * coincident emissions, the emission from the left-most [Events] is emitted.
- *
- * @see merge
- */
-@ExperimentalKairosApi
-fun <A> mergeLeft(vararg events: Events<A>): Events<A> = events.asIterable().mergeLeft()
-
-/**
- * Merges the given [Events] into a single [Events] that emits events from all.
- *
- * Because [Events] can only emit one value per transaction, the provided [transformCoincidence]
- * function is used to combine coincident emissions to produce the result value to be emitted by the
- * merged [Events].
- */
-// TODO: can be optimized to avoid creating the intermediate list
-fun <A> merge(vararg events: Events<A>, transformCoincidence: (A, A) -> A): Events<A> =
-    merge(*events).map { l -> l.reduce(transformCoincidence) }
-
-/**
- * Merges the given [Events] into a single [Events] that emits events from all. All coincident
- * emissions are collected into the emitted [List], preserving the input ordering.
- *
- * @see mergeWith
- * @see mergeLeft
- */
-@ExperimentalKairosApi
-fun <A> Iterable<Events<A>>.merge(): Events<List<A>> =
-    EventsInit(constInit(name = null, mergeNodes { map { it.init.connect(evalScope = this) } }))
-
-/**
- * Merges the given [Events] into a single [Events] that emits events from all. In the case of
- * coincident emissions, the emission from the left-most [Events] is emitted.
- *
- * @see merge
- */
-@ExperimentalKairosApi
-fun <A> Iterable<Events<A>>.mergeLeft(): Events<A> =
-    EventsInit(constInit(name = null, mergeNodesLeft { map { it.init.connect(evalScope = this) } }))
-
-/**
- * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are
- * collected into the emitted [List], preserving the input ordering.
- *
- * @see mergeWith
- */
-@ExperimentalKairosApi fun <A> Sequence<Events<A>>.merge(): Events<List<A>> = asIterable().merge()
-
-/**
- * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are
- * collected into the emitted [Map], and are given the same key of the associated [Events] in the
- * input [Map].
- *
- * @see mergeWith
- */
-@ExperimentalKairosApi
-fun <K, A> Map<K, Events<A>>.merge(): Events<Map<K, A>> =
-    asSequence()
-        .map { (k, events) -> events.map { a -> k to a } }
-        .toList()
-        .merge()
-        .map { it.toMap() }
-
-/**
- * Returns a [GroupedEvents] that can be used to efficiently split a single [Events] into multiple
- * downstream [Events].
- *
- * The input [Events] emits [Map] instances that specify which downstream [Events] the associated
- * value will be emitted from. These downstream [Events] can be obtained via
- * [GroupedEvents.eventsForKey].
- *
- * An example:
- * ```
- *   val fooEvents: Events<Map<String, Foo>> = ...
- *   val fooById: GroupedEvents<String, Foo> = fooEvents.groupByKey()
- *   val fooBar: Events<Foo> = fooById["bar"]
- * ```
- *
- * This is semantically equivalent to `val fooBar = fooEvents.mapNotNull { map -> map["bar"] }` but
- * is significantly more efficient; specifically, using [mapNotNull] in this way incurs a `O(n)`
- * performance hit, where `n` is the number of different [mapNotNull] operations used to filter on a
- * specific key's presence in the emitted [Map]. [groupByKey] internally uses a [HashMap] to lookup
- * the appropriate downstream [Events], and so operates in `O(1)`.
- *
- * Note that the returned [GroupedEvents] should be cached and re-used to gain the performance
- * benefit.
- *
- * @see selector
- */
-@ExperimentalKairosApi
-fun <K, A> Events<Map<K, A>>.groupByKey(numKeys: Int? = null): GroupedEvents<K, A> =
-    GroupedEvents(demuxMap({ init.connect(this) }, numKeys))
-
-/**
- * Shorthand for `map { mapOf(extractKey(it) to it) }.groupByKey()`
- *
- * @see groupByKey
- */
-@ExperimentalKairosApi
-fun <K, A> Events<A>.groupBy(
-    numKeys: Int? = null,
-    extractKey: TransactionScope.(A) -> K,
-): GroupedEvents<K, A> = map { mapOf(extractKey(it) to it) }.groupByKey(numKeys)
-
-/**
- * Returns two new [Events] that contain elements from this [Events] that satisfy or don't satisfy
- * [predicate].
- *
- * Using this is equivalent to `upstream.filter(predicate) to upstream.filter { !predicate(it) }`
- * but is more efficient; specifically, [partition] will only invoke [predicate] once per element.
- */
-@ExperimentalKairosApi
-fun <A> Events<A>.partition(
-    predicate: TransactionScope.(A) -> Boolean
-): Pair<Events<A>, Events<A>> {
-    val grouped: GroupedEvents<Boolean, A> = groupBy(numKeys = 2, extractKey = predicate)
-    return Pair(grouped.eventsForKey(true), grouped.eventsForKey(false))
-}
-
-/**
- * Returns two new [Events] that contain elements from this [Events]; [Pair.first] will contain
- * [Left] values, and [Pair.second] will contain [Right] values.
- *
- * Using this is equivalent to using [filterIsInstance] in conjunction with [map] twice, once for
- * [Left]s and once for [Right]s, but is slightly more efficient; specifically, the
- * [filterIsInstance] check is only performed once per element.
- */
-@ExperimentalKairosApi
-fun <A, B> Events<Either<A, B>>.partitionEither(): Pair<Events<A>, Events<B>> {
-    val (left, right) = partition { it is Left }
-    return Pair(left.mapCheap { (it as Left).value }, right.mapCheap { (it as Right).value })
-}
-
-/**
- * A mapping from keys of type [K] to [Events] emitting values of type [A].
- *
- * @see groupByKey
- */
-@ExperimentalKairosApi
-class GroupedEvents<in K, out A> internal constructor(internal val impl: DemuxImpl<K, A>) {
-    /**
-     * Returns an [Events] that emits values of type [A] that correspond to the given [key].
-     *
-     * @see groupByKey
-     */
-    fun eventsForKey(key: K): Events<A> = EventsInit(constInit(name = null, impl.eventsForKey(key)))
-
-    /**
-     * Returns an [Events] that emits values of type [A] that correspond to the given [key].
-     *
-     * @see groupByKey
-     */
-    operator fun get(key: K): Events<A> = eventsForKey(key)
-}
-
-/**
- * Returns an [Events] that switches to the [Events] contained within this [State] whenever it
- * changes.
- *
- * This switch does take effect until the *next* transaction after [State] changes. For a switch
- * that takes effect immediately, see [switchEventsPromptly].
- */
-@ExperimentalKairosApi
-fun <A> State<Events<A>>.switchEvents(name: String? = null): Events<A> {
-    val patches =
-        mapImpl({ init.connect(this).changes }) { newEvents, _ -> newEvents.init.connect(this) }
-    return EventsInit(
-        constInit(
-            name = null,
-            switchDeferredImplSingle(
-                name = name,
-                getStorage = {
-                    init.connect(this).getCurrentWithEpoch(this).first.init.connect(this)
-                },
-                getPatches = { patches },
-            ),
-        )
-    )
-}
-
-/**
- * Returns an [Events] that switches to the [Events] contained within this [State] whenever it
- * changes.
- *
- * This switch takes effect immediately within the same transaction that [State] changes. In
- * general, you should prefer [switchEvents] over this method. It is both safer and more performant.
- */
-// TODO: parameter to handle coincidental emission from both old and new
-@ExperimentalKairosApi
-fun <A> State<Events<A>>.switchEventsPromptly(): Events<A> {
-    val patches =
-        mapImpl({ init.connect(this).changes }) { newEvents, _ -> newEvents.init.connect(this) }
-    return EventsInit(
-        constInit(
-            name = null,
-            switchPromptImplSingle(
-                getStorage = {
-                    init.connect(this).getCurrentWithEpoch(this).first.init.connect(this)
-                },
-                getPatches = { patches },
-            ),
-        )
-    )
-}
-
-/**
  * A mutable [Events] that provides the ability to [emit] values to the network, handling
  * backpressure by coalescing all emissions into batches.
  *
@@ -494,7 +250,7 @@
     private val getInitialValue: () -> Out,
     internal val impl: InputNode<Out> = InputNode(),
 ) : Events<Out>() {
-    internal val storage = AtomicReference(false to lazy { getInitialValue() })
+    private val storage = AtomicReference(false to lazy { getInitialValue() })
 
     override fun toString(): String = "${this::class.simpleName}@$hashString"
 
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Filter.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Filter.kt
new file mode 100644
index 0000000..8ca5ac8
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Filter.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2025 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.kairos
+
+import com.android.systemui.kairos.internal.constInit
+import com.android.systemui.kairos.internal.filterImpl
+import com.android.systemui.kairos.internal.filterPresentImpl
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.toMaybe
+
+/** Return an [Events] that emits from the original [Events] only when [state] is `true`. */
+@ExperimentalKairosApi
+fun <A> Events<A>.filter(state: State<Boolean>): Events<A> = filter { state.sample() }
+
+/**
+ * Returns an [Events] containing only values of the original [Events] that are not null.
+ *
+ * ``` kotlin
+ *  fun <A> Events<A?>.filterNotNull(): Events<A> = mapNotNull { it }
+ * ```
+ *
+ * @see mapNotNull
+ */
+@ExperimentalKairosApi
+fun <A> Events<A?>.filterNotNull(): Events<A> = mapCheap { it.toMaybe() }.filterPresent()
+
+/**
+ * Returns an [Events] containing only values of the original [Events] that are instances of [A].
+ *
+ * ``` kotlin
+ *   inline fun <reified A> Events<*>.filterIsInstance(): Events<A> =
+ *       mapNotNull { it as? A }
+ * ```
+ *
+ * @see mapNotNull
+ */
+@ExperimentalKairosApi
+inline fun <reified A> Events<*>.filterIsInstance(): Events<A> =
+    mapCheap { it as? A }.filterNotNull()
+
+/**
+ * Returns an [Events] containing only values of the original [Events] that are present.
+ *
+ * ``` kotlin
+ *  fun <A> Events<Maybe<A>>.filterPresent(): Events<A> = mapMaybe { it }
+ * ```
+ *
+ * @see mapMaybe
+ */
+@ExperimentalKairosApi
+fun <A> Events<Maybe<A>>.filterPresent(): Events<A> =
+    EventsInit(constInit(name = null, filterPresentImpl { init.connect(evalScope = this) }))
+
+/**
+ * Returns an [Events] containing only values of the original [Events] that satisfy the given
+ * [predicate].
+ *
+ * ``` kotlin
+ *   fun <A> Events<A>.filter(predicate: TransactionScope.(A) -> Boolean): Events<A> =
+ *       mapMaybe { if (predicate(it)) present(it) else absent }
+ * ```
+ *
+ * @see mapMaybe
+ */
+@ExperimentalKairosApi
+fun <A> Events<A>.filter(predicate: TransactionScope.(A) -> Boolean): Events<A> {
+    val pulse = filterImpl({ init.connect(evalScope = this) }) { predicate(it) }
+    return EventsInit(constInit(name = null, pulse))
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/GroupBy.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/GroupBy.kt
new file mode 100644
index 0000000..45da34a
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/GroupBy.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2025 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.kairos
+
+import com.android.systemui.kairos.internal.DemuxImpl
+import com.android.systemui.kairos.internal.constInit
+import com.android.systemui.kairos.internal.demuxMap
+import com.android.systemui.kairos.util.Either
+import com.android.systemui.kairos.util.These
+import com.android.systemui.kairos.util.maybeFirst
+import com.android.systemui.kairos.util.maybeSecond
+import com.android.systemui.kairos.util.orError
+
+/**
+ * Returns a [GroupedEvents] that can be used to efficiently split a single [Events] into multiple
+ * downstream [Events].
+ *
+ * The input [Events] emits [Map] instances that specify which downstream [Events] the associated
+ * value will be emitted from. These downstream [Events] can be obtained via
+ * [GroupedEvents.eventsForKey].
+ *
+ * An example:
+ * ```
+ *   val fooEvents: Events<Map<String, Foo>> = ...
+ *   val fooById: GroupedEvents<String, Foo> = fooEvents.groupByKey()
+ *   val fooBar: Events<Foo> = fooById["bar"]
+ * ```
+ *
+ * This is semantically equivalent to `val fooBar = fooEvents.mapNotNull { map -> map["bar"] }` but
+ * is significantly more efficient; specifically, using [mapNotNull] in this way incurs a `O(n)`
+ * performance hit, where `n` is the number of different [mapNotNull] operations used to filter on a
+ * specific key's presence in the emitted [Map]. [groupByKey] internally uses a [HashMap] to lookup
+ * the appropriate downstream [Events], and so operates in `O(1)`.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal [HashMap].
+ *
+ * Note that the returned [GroupedEvents] should be cached and re-used to gain the performance
+ * benefit.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.groupByKey
+ * @see selector
+ */
+@ExperimentalKairosApi
+fun <K, A> Events<Map<K, A>>.groupByKey(numKeys: Int? = null): GroupedEvents<K, A> =
+    GroupedEvents(demuxMap({ init.connect(this) }, numKeys))
+
+/**
+ * Returns a [GroupedEvents] that can be used to efficiently split a single [Events] into multiple
+ * downstream [Events]. The downstream [Events] are associated with a [key][K], which is derived
+ * from each emission of the original [Events] via [extractKey].
+ *
+ * ``` kotlin
+ *   fun <K, A> Events<A>.groupBy(
+ *       numKeys: Int? = null,
+ *       extractKey: TransactionScope.(A) -> K,
+ *   ): GroupedEvents<K, A> =
+ *       map { mapOf(extractKey(it) to it) }.groupByKey(numKeys)
+ * ```
+ *
+ * @see groupByKey
+ */
+@ExperimentalKairosApi
+fun <K, A> Events<A>.groupBy(
+    numKeys: Int? = null,
+    extractKey: TransactionScope.(A) -> K,
+): GroupedEvents<K, A> = map { mapOf(extractKey(it) to it) }.groupByKey(numKeys)
+
+/**
+ * A mapping from keys of type [K] to [Events] emitting values of type [A].
+ *
+ * @see groupByKey
+ */
+@ExperimentalKairosApi
+class GroupedEvents<in K, out A> internal constructor(internal val impl: DemuxImpl<K, A>) {
+    /**
+     * Returns an [Events] that emits values of type [A] that correspond to the given [key].
+     *
+     * @see groupByKey
+     */
+    fun eventsForKey(key: K): Events<A> = EventsInit(constInit(name = null, impl.eventsForKey(key)))
+
+    /**
+     * Returns an [Events] that emits values of type [A] that correspond to the given [key].
+     *
+     * @see groupByKey
+     */
+    operator fun get(key: K): Events<A> = eventsForKey(key)
+}
+
+/**
+ * Returns two new [Events] that contain elements from this [Events] that satisfy or don't satisfy
+ * [predicate].
+ *
+ * Using this is equivalent to `upstream.filter(predicate) to upstream.filter { !predicate(it) }`
+ * but is more efficient; specifically, [partition] will only invoke [predicate] once per element.
+ *
+ * ``` kotlin
+ *   fun <A> Events<A>.partition(
+ *       predicate: TransactionScope.(A) -> Boolean
+ *   ): Pair<Events<A>, Events<A>> =
+ *       map { if (predicate(it)) left(it) else right(it) }.partitionEither()
+ * ```
+ *
+ * @see partitionEither
+ */
+@ExperimentalKairosApi
+fun <A> Events<A>.partition(
+    predicate: TransactionScope.(A) -> Boolean
+): Pair<Events<A>, Events<A>> {
+    val grouped: GroupedEvents<Boolean, A> = groupBy(numKeys = 2, extractKey = predicate)
+    return Pair(grouped.eventsForKey(true), grouped.eventsForKey(false))
+}
+
+/**
+ * Returns two new [Events] that contain elements from this [Events]; [Pair.first] will contain
+ * [First] values, and [Pair.second] will contain [Second] values.
+ *
+ * Using this is equivalent to using [filterIsInstance] in conjunction with [map] twice, once for
+ * [First]s and once for [Second]s, but is slightly more efficient; specifically, the
+ * [filterIsInstance] check is only performed once per element.
+ *
+ * ``` kotlin
+ *   fun <A, B> Events<Either<A, B>>.partitionEither(): Pair<Events<A>, Events<B>> =
+ *     map { it.asThese() }.partitionThese()
+ * ```
+ *
+ * @see partitionThese
+ */
+@ExperimentalKairosApi
+fun <A, B> Events<Either<A, B>>.partitionEither(): Pair<Events<A>, Events<B>> {
+    val (left, right) = partition { it is Either.First }
+    return Pair(
+        left.mapCheap { (it as Either.First).value },
+        right.mapCheap { (it as Either.Second).value },
+    )
+}
+
+/**
+ * Returns two new [Events] that contain elements from this [Events]; [Pair.first] will contain
+ * [These.first] values, and [Pair.second] will contain [These.second] values. If the original
+ * emission was a [These.both], then both result [Events] will emit a value simultaneously.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.partitionThese
+ */
+@ExperimentalKairosApi
+fun <A, B> Events<These<A, B>>.partitionThese(): Pair<Events<A>, Events<B>> {
+    val grouped =
+        mapCheap {
+                when (it) {
+                    is These.Both -> mapOf(true to it, false to it)
+                    is These.Second -> mapOf(false to it)
+                    is These.First -> mapOf(true to it)
+                }
+            }
+            .groupByKey(numKeys = 2)
+    return Pair(
+        grouped.eventsForKey(true).mapCheap {
+            it.maybeFirst().orError { "unexpected missing value" }
+        },
+        grouped.eventsForKey(false).mapCheap {
+            it.maybeSecond().orError { "unexpected missing value" }
+        },
+    )
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt
index c95b9e8..d88ae3b8 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt
@@ -21,27 +21,31 @@
 import com.android.systemui.kairos.internal.Init
 import com.android.systemui.kairos.internal.InitScope
 import com.android.systemui.kairos.internal.NoScope
-import com.android.systemui.kairos.internal.awaitValues
 import com.android.systemui.kairos.internal.constIncremental
 import com.android.systemui.kairos.internal.constInit
 import com.android.systemui.kairos.internal.init
-import com.android.systemui.kairos.internal.mapImpl
 import com.android.systemui.kairos.internal.mapValuesImpl
-import com.android.systemui.kairos.internal.store.ConcurrentHashMapK
-import com.android.systemui.kairos.internal.switchDeferredImpl
-import com.android.systemui.kairos.internal.switchPromptImpl
 import com.android.systemui.kairos.internal.util.hashString
 import com.android.systemui.kairos.util.MapPatch
-import com.android.systemui.kairos.util.map
 import com.android.systemui.kairos.util.mapPatchFromFullDiff
 import kotlin.reflect.KProperty
 
-/** A [State] tracking a [Map] that receives incremental updates. */
+/**
+ * A [State] tracking a [Map] that receives incremental updates.
+ *
+ * [Incremental] allows one to react to the [subset of changes][updates] to the held map, without
+ * having to perform a manual diff of the map to determine what changed.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.incrementals
+ */
 sealed class Incremental<K, out V> : State<Map<K, V>>() {
     abstract override val init: Init<IncrementalImpl<K, V>>
 }
 
-/** An [Incremental] that never changes. */
+/**
+ * Returns a constant [Incremental] that never changes. [changes] and [updates] are both equivalent
+ * to [emptyEvents], and [TransactionScope.sample] will always produce [value].
+ */
 @ExperimentalKairosApi
 fun <K, V> incrementalOf(value: Map<K, V>): Incremental<K, V> {
     val operatorName = "stateOf"
@@ -57,6 +61,10 @@
  * [value][Lazy.value] will be queried and used.
  *
  * Useful for recursive definitions.
+ *
+ * ``` kotlin
+ *   fun <A> Lazy<Incremental<K, V>>.defer() = deferredIncremental { value }
+ * ```
  */
 @ExperimentalKairosApi
 fun <K, V> Lazy<Incremental<K, V>>.defer(): Incremental<K, V> = deferInline { value }
@@ -69,6 +77,10 @@
  * queried and used.
  *
  * Useful for recursive definitions.
+ *
+ * ``` kotlin
+ *   fun <A> DeferredValue<Incremental<K, V>>.defer() = deferredIncremental { get() }
+ * ```
  */
 @ExperimentalKairosApi
 fun <K, V> DeferredValue<Incremental<K, V>>.defer(): Incremental<K, V> = deferInline {
@@ -119,94 +131,14 @@
 }
 
 /**
- * Returns an [Events] that emits from a merged, incrementally-accumulated collection of [Events]
- * emitted from this, following the same "patch" rules as outlined in
- * [StateScope.foldStateMapIncrementally].
+ * A forward-reference to an [Incremental]. Useful for recursive definitions.
  *
- * Conceptually this is equivalent to:
- * ```kotlin
- *   fun <K, V> State<Map<K, V>>.mergeEventsIncrementally(): Events<Map<K, V>> =
- *     map { it.merge() }.switchEvents()
- * ```
- *
- * While the behavior is equivalent to the conceptual definition above, the implementation is
- * significantly more efficient.
- *
- * @see merge
+ * This reference can be used like a standard [Incremental], but will throw an error if its
+ * [loopback] is unset before it is [observed][BuildScope.observe] or
+ * [sampled][TransactionScope.sample]. Note that it is safe to invoke
+ * [TransactionScope.sampleDeferred] before [loopback] is set, provided the [DeferredValue] is not
+ * [queried][KairosScope.get].
  */
-fun <K, V> Incremental<K, Events<V>>.mergeEventsIncrementally(): Events<Map<K, V>> {
-    val operatorName = "mergeEventsIncrementally"
-    val name = operatorName
-    val patches =
-        mapImpl({ init.connect(this).patches }) { patch, _ ->
-            patch.mapValues { (_, m) -> m.map { events -> events.init.connect(this) } }.asIterable()
-        }
-    return EventsInit(
-        constInit(
-            name,
-            switchDeferredImpl(
-                    name = name,
-                    getStorage = {
-                        init
-                            .connect(this)
-                            .getCurrentWithEpoch(this)
-                            .first
-                            .mapValues { (_, events) -> events.init.connect(this) }
-                            .asIterable()
-                    },
-                    getPatches = { patches },
-                    storeFactory = ConcurrentHashMapK.Factory(),
-                )
-                .awaitValues(),
-        )
-    )
-}
-
-/**
- * Returns an [Events] that emits from a merged, incrementally-accumulated collection of [Events]
- * emitted from this, following the same "patch" rules as outlined in
- * [StateScope.foldStateMapIncrementally].
- *
- * Conceptually this is equivalent to:
- * ```kotlin
- *   fun <K, V> State<Map<K, V>>.mergeEventsIncrementallyPromptly(): Events<Map<K, V>> =
- *     map { it.merge() }.switchEventsPromptly()
- * ```
- *
- * While the behavior is equivalent to the conceptual definition above, the implementation is
- * significantly more efficient.
- *
- * @see merge
- */
-fun <K, V> Incremental<K, Events<V>>.mergeEventsIncrementallyPromptly(): Events<Map<K, V>> {
-    val operatorName = "mergeEventsIncrementally"
-    val name = operatorName
-    val patches =
-        mapImpl({ init.connect(this).patches }) { patch, _ ->
-            patch.mapValues { (_, m) -> m.map { events -> events.init.connect(this) } }.asIterable()
-        }
-    return EventsInit(
-        constInit(
-            name,
-            switchPromptImpl(
-                    name = name,
-                    getStorage = {
-                        init
-                            .connect(this)
-                            .getCurrentWithEpoch(this)
-                            .first
-                            .mapValues { (_, events) -> events.init.connect(this) }
-                            .asIterable()
-                    },
-                    getPatches = { patches },
-                    storeFactory = ConcurrentHashMapK.Factory(),
-                )
-                .awaitValues(),
-        )
-    )
-}
-
-/** A forward-reference to an [Incremental], allowing for recursive definitions. */
 @ExperimentalKairosApi
 class IncrementalLoop<K, V>(private val name: String? = null) : Incremental<K, V>() {
 
@@ -215,7 +147,10 @@
     override val init: Init<IncrementalImpl<K, V>> =
         init(name) { deferred.value.init.connect(evalScope = this) }
 
-    /** The [Incremental] this [IncrementalLoop] will forward to. */
+    /**
+     * The [Incremental] this reference is referring to. Must be set before this [IncrementalLoop]
+     * is [observed][BuildScope.observe].
+     */
     var loopback: Incremental<K, V>? = null
         set(value) {
             value?.let {
@@ -237,8 +172,8 @@
 }
 
 /**
- * Returns an [Incremental] whose [updates] are calculated by diffing the given [State]'s
- * [transitions].
+ * Returns an [Incremental] whose [updates] are calculated by [diffing][mapPatchFromFullDiff] the
+ * given [State]'s [transitions].
  */
 fun <K, V> State<Map<K, V>>.asIncremental(): Incremental<K, V> {
     if (this is Incremental<K, V>) return this
@@ -264,34 +199,6 @@
     )
 }
 
-/** Returns an [Incremental] that acts like the current value of the given [State]. */
-fun <K, V> State<Incremental<K, V>>.switchIncremental(): Incremental<K, V> {
-    val stateChangePatches =
-        transitions.mapNotNull { (old, new) ->
-            mapPatchFromFullDiff(old.sample(), new.sample()).takeIf { it.isNotEmpty() }
-        }
-    val innerChanges =
-        map { inner ->
-                merge(stateChangePatches, inner.updates) { switchPatch, upcomingPatch ->
-                    switchPatch + upcomingPatch
-                }
-            }
-            .switchEventsPromptly()
-    val flattened = flatten()
-    return IncrementalInit(
-        init("switchIncremental") {
-            val upstream = flattened.init.connect(this)
-            IncrementalImpl(
-                "switchIncremental",
-                "switchIncremental",
-                upstream.changes,
-                innerChanges.init.connect(this),
-                upstream.store,
-            )
-        }
-    )
-}
-
 private inline fun <K, V> deferInline(
     crossinline block: InitScope.() -> Incremental<K, V>
 ): Incremental<K, V> = IncrementalInit(init(name = null) { block().init.connect(evalScope = this) })
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt
index 77598b3..19e3fcd 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt
@@ -23,16 +23,15 @@
 import com.android.systemui.kairos.internal.util.childScope
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineName
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.job
 import kotlinx.coroutines.launch
 
-/**
- * Marks declarations that are still **experimental** and shouldn't be used in general production
- * code.
- */
+/** Marks APIs that are still **experimental** and shouldn't be used in general production code. */
 @RequiresOptIn(
     message = "This API is experimental and should not be used in general production code."
 )
@@ -139,37 +138,46 @@
     private val endSignal: Events<Any>,
 ) : KairosNetwork {
     override suspend fun <R> transact(block: TransactionScope.() -> R): R =
-        network.transaction("KairosNetwork.transact") { block() }.await()
+        network.transaction("KairosNetwork.transact") { block() }.awaitOrCancel()
 
     override suspend fun activateSpec(spec: BuildSpec<*>) {
-        val stopEmitter =
-            CoalescingMutableEvents(
-                name = "activateSpec",
-                coalesce = { _, _: Unit -> },
-                network = network,
-                getInitialValue = {},
-            )
-        val job =
-            network
-                .transaction("KairosNetwork.activateSpec") {
-                    val buildScope =
-                        BuildScopeImpl(
-                            stateScope =
-                                StateScopeImpl(
-                                    evalScope = this,
-                                    endSignal = mergeLeft(stopEmitter, endSignal),
-                                ),
-                            coroutineScope = scope,
-                        )
-                    buildScope.launchScope(spec)
+        val stopEmitter = conflatedMutableEvents<Unit>()
+        network
+            .transaction("KairosNetwork.activateSpec") {
+                val buildScope =
+                    BuildScopeImpl(
+                        stateScope =
+                            StateScopeImpl(
+                                evalScope = this,
+                                endSignalLazy = lazy { mergeLeft(stopEmitter, endSignal) },
+                            ),
+                        coroutineScope = scope,
+                    )
+                buildScope.launchScope {
+                    spec.applySpec()
+                    launchEffect { awaitCancellationAndThen { stopEmitter.emit(Unit) } }
                 }
-                .await()
-        awaitCancellationAndThen {
-            stopEmitter.emit(Unit)
-            job.cancel()
-        }
+            }
+            .awaitOrCancel()
+            .joinOrCancel()
     }
 
+    private suspend fun <T> Deferred<T>.awaitOrCancel(): T =
+        try {
+            await()
+        } catch (ex: CancellationException) {
+            cancel(ex)
+            throw ex
+        }
+
+    private suspend fun Job.joinOrCancel(): Unit =
+        try {
+            join()
+        } catch (ex: CancellationException) {
+            cancel(ex)
+            throw ex
+        }
+
     override fun <In, Out> coalescingMutableEvents(
         coalesce: (old: Out, new: In) -> Out,
         getInitialValue: () -> Out,
@@ -214,3 +222,45 @@
     scope.launch(CoroutineName("launchKairosNetwork scheduler")) { network.runInputScheduler() }
     return RootKairosNetwork(network, scope, scope.coroutineContext.job)
 }
+
+@ExperimentalKairosApi
+interface HasNetwork : KairosScope {
+    /**
+     * A [KairosNetwork] handle that is bound to the lifetime of a [BuildScope].
+     *
+     * It supports all of the standard functionality by which external code can interact with this
+     * Kairos network, but all [activated][KairosNetwork.activateSpec] [BuildSpec]s are bound as
+     * children to the [BuildScope], such that when the [BuildScope] is destroyed, all children are
+     * also destroyed.
+     */
+    val kairosNetwork: KairosNetwork
+}
+
+/** Returns a [MutableEvents] that can emit values into this [KairosNetwork]. */
+@ExperimentalKairosApi
+fun <T> HasNetwork.MutableEvents(): MutableEvents<T> = MutableEvents(kairosNetwork)
+
+/** Returns a [MutableState] with initial state [initialValue]. */
+@ExperimentalKairosApi
+fun <T> HasNetwork.MutableState(initialValue: T): MutableState<T> =
+    MutableState(kairosNetwork, initialValue)
+
+/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
+@ExperimentalKairosApi
+fun <In, Out> HasNetwork.CoalescingMutableEvents(
+    coalesce: (old: Out, new: In) -> Out,
+    initialValue: Out,
+): CoalescingMutableEvents<In, Out> = CoalescingMutableEvents(kairosNetwork, coalesce, initialValue)
+
+/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
+@ExperimentalKairosApi
+fun <In, Out> HasNetwork.CoalescingMutableEvents(
+    coalesce: (old: Out, new: In) -> Out,
+    getInitialValue: () -> Out,
+): CoalescingMutableEvents<In, Out> =
+    CoalescingMutableEvents(kairosNetwork, coalesce, getInitialValue)
+
+/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
+@ExperimentalKairosApi
+fun <T> HasNetwork.ConflatedMutableEvents(): CoalescingMutableEvents<T, T> =
+    ConflatedMutableEvents(kairosNetwork)
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt
index ce3e923..e526f45 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt
@@ -16,42 +16,11 @@
 
 package com.android.systemui.kairos
 
-import com.android.systemui.kairos.internal.CompletableLazy
-
 /** Denotes [KairosScope] interfaces as [DSL markers][DslMarker]. */
 @DslMarker annotation class KairosScopeMarker
 
 /**
- * Base scope for all Kairos scopes. Used to prevent implicitly capturing other scopes from in
+ * Base scope for all Kairos scopes. Used to prevent implicitly capturing other scopes from inner
  * lambdas.
  */
-@KairosScopeMarker
-@ExperimentalKairosApi
-interface KairosScope {
-    /** Returns the value held by the [DeferredValue], suspending until available if necessary. */
-    fun <A> DeferredValue<A>.get(): A = unwrapped.value
-}
-
-/**
- * A value that may not be immediately (synchronously) available, but is guaranteed to be available
- * before this transaction is completed.
- *
- * @see KairosScope.get
- */
-@ExperimentalKairosApi
-class DeferredValue<out A> internal constructor(internal val unwrapped: Lazy<A>)
-
-/**
- * Returns the value held by this [DeferredValue], or throws [IllegalStateException] if it is not
- * yet available.
- *
- * This API is not meant for general usage within the Kairos network. It is made available mainly
- * for debugging and logging. You should always prefer [get][KairosScope.get] if possible.
- *
- * @see KairosScope.get
- */
-@ExperimentalKairosApi fun <A> DeferredValue<A>.getUnsafe(): A = unwrapped.value
-
-/** Returns an already-available [DeferredValue] containing [value]. */
-@ExperimentalKairosApi
-fun <A> deferredOf(value: A): DeferredValue<A> = DeferredValue(CompletableLazy(value))
+@KairosScopeMarker @ExperimentalKairosApi interface KairosScope
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Merge.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Merge.kt
new file mode 100644
index 0000000..de9dca4
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Merge.kt
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2025 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.kairos
+
+import com.android.systemui.kairos.internal.awaitValues
+import com.android.systemui.kairos.internal.constInit
+import com.android.systemui.kairos.internal.mapImpl
+import com.android.systemui.kairos.internal.mergeNodes
+import com.android.systemui.kairos.internal.mergeNodesLeft
+import com.android.systemui.kairos.internal.store.ConcurrentHashMapK
+import com.android.systemui.kairos.internal.switchDeferredImpl
+import com.android.systemui.kairos.internal.switchPromptImpl
+import com.android.systemui.kairos.util.map
+
+/**
+ * Merges the given [Events] into a single [Events] that emits events from both.
+ *
+ * Because [Events] can only emit one value per transaction, the provided [transformCoincidence]
+ * function is used to combine coincident emissions to produce the result value to be emitted by the
+ * merged [Events].
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.mergeWith(
+ *     other: Events<A>,
+ *     transformCoincidence: TransactionScope.(A, A) -> A = { a, _ -> a },
+ * ): Events<A> =
+ *     listOf(this, other).merge().map { it.reduce(transformCoincidence) }
+ * ```
+ *
+ * @see merge
+ */
+@ExperimentalKairosApi
+fun <A> Events<A>.mergeWith(
+    other: Events<A>,
+    transformCoincidence: TransactionScope.(A, A) -> A = { a, _ -> a },
+): Events<A> {
+    val node =
+        mergeNodes(
+            getPulse = { init.connect(evalScope = this) },
+            getOther = { other.init.connect(evalScope = this) },
+        ) { a, b ->
+            transformCoincidence(a, b)
+        }
+    return EventsInit(constInit(name = null, node))
+}
+
+/**
+ * Merges the given [Events] into a single [Events] that emits events from all. All coincident
+ * emissions are collected into the emitted [List], preserving the input ordering.
+ *
+ * ``` kotlin
+ *   fun <A> merge(vararg events: Events<A>): Events<List<A>> = events.asIterable().merge()
+ * ```
+ *
+ * @see mergeWith
+ * @see mergeLeft
+ */
+@ExperimentalKairosApi
+fun <A> merge(vararg events: Events<A>): Events<List<A>> = events.asIterable().merge()
+
+/**
+ * Merges the given [Events] into a single [Events] that emits events from all. In the case of
+ * coincident emissions, the emission from the left-most [Events] is emitted.
+ *
+ * ``` kotlin
+ *   fun <A> mergeLeft(vararg events: Events<A>): Events<A> = events.asIterable().mergeLeft()
+ * ```
+ *
+ * @see merge
+ */
+@ExperimentalKairosApi
+fun <A> mergeLeft(vararg events: Events<A>): Events<A> = events.asIterable().mergeLeft()
+
+/**
+ * Merges the given [Events] into a single [Events] that emits events from all.
+ *
+ * Because [Events] can only emit one value per transaction, the provided [transformCoincidence]
+ * function is used to combine coincident emissions to produce the result value to be emitted by the
+ * merged [Events].
+ *
+ * ``` kotlin
+ *   fun <A> merge(vararg events: Events<A>, transformCoincidence: (A, A) -> A): Events<A> =
+ *       merge(*events).map { l -> l.reduce(transformCoincidence) }
+ * ```
+ */
+fun <A> merge(vararg events: Events<A>, transformCoincidence: (A, A) -> A): Events<A> =
+    merge(*events).map { l -> l.reduce(transformCoincidence) }
+
+/**
+ * Merges the given [Events] into a single [Events] that emits events from all. All coincident
+ * emissions are collected into the emitted [List], preserving the input ordering.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.merge
+ * @see mergeWith
+ * @see mergeLeft
+ */
+@ExperimentalKairosApi
+fun <A> Iterable<Events<A>>.merge(): Events<List<A>> =
+    EventsInit(constInit(name = null, mergeNodes { map { it.init.connect(evalScope = this) } }))
+
+/**
+ * Merges the given [Events] into a single [Events] that emits events from all. In the case of
+ * coincident emissions, the emission from the left-most [Events] is emitted.
+ *
+ * Semantically equivalent to the following definition:
+ * ``` kotlin
+ *   fun <A> Iterable<Events<A>>.mergeLeft(): Events<A> =
+ *       merge().mapCheap { it.first() }
+ * ```
+ *
+ * In reality, the implementation avoids allocating the intermediate list of all coincident
+ * emissions.
+ *
+ * @see merge
+ */
+@ExperimentalKairosApi
+fun <A> Iterable<Events<A>>.mergeLeft(): Events<A> =
+    EventsInit(constInit(name = null, mergeNodesLeft { map { it.init.connect(evalScope = this) } }))
+
+/**
+ * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are
+ * collected into the emitted [List], preserving the input ordering.
+ *
+ * ``` kotlin
+ *   fun <A> Sequence<Events<A>>.merge(): Events<List<A>> = asIterable().merge()
+ * ```
+ *
+ * @see mergeWith
+ */
+@ExperimentalKairosApi fun <A> Sequence<Events<A>>.merge(): Events<List<A>> = asIterable().merge()
+
+/**
+ * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are
+ * collected into the emitted [Map], and are given the same key of the associated [Events] in the
+ * input [Map].
+ *
+ * ``` kotlin
+ *   fun <K, A> Map<K, Events<A>>.merge(): Events<Map<K, A>> =
+ *       asSequence()
+ *           .map { (k, events) -> events.map { a -> k to a } }
+ *           .toList()
+ *           .merge()
+ *           .map { it.toMap() }
+ * ```
+ *
+ * @see merge
+ */
+@ExperimentalKairosApi
+fun <K, A> Map<K, Events<A>>.merge(): Events<Map<K, A>> =
+    asSequence()
+        .map { (k, events) -> events.map { a -> k to a } }
+        .toList()
+        .merge()
+        .map { it.toMap() }
+
+/**
+ * Returns an [Events] that emits from a merged, incrementally-accumulated collection of [Events]
+ * emitted from this, following the patch rules outlined in
+ * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch].
+ *
+ * Conceptually this is equivalent to:
+ * ``` kotlin
+ *   fun <K, V> State<Map<K, V>>.mergeEventsIncrementally(): Events<Map<K, V>> =
+ *       map { it.merge() }.switchEvents()
+ * ```
+ *
+ * While the behavior is equivalent to the conceptual definition above, the implementation is
+ * significantly more efficient.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.mergeEventsIncrementally
+ * @see merge
+ */
+fun <K, V> Incremental<K, Events<V>>.mergeEventsIncrementally(): Events<Map<K, V>> {
+    val operatorName = "mergeEventsIncrementally"
+    val name = operatorName
+    val patches =
+        mapImpl({ init.connect(this).patches }) { patch, _ ->
+            patch.mapValues { (_, m) -> m.map { events -> events.init.connect(this) } }.asIterable()
+        }
+    return EventsInit(
+        constInit(
+            name,
+            switchDeferredImpl(
+                    name = name,
+                    getStorage = {
+                        init
+                            .connect(this)
+                            .getCurrentWithEpoch(this)
+                            .first
+                            .mapValues { (_, events) -> events.init.connect(this) }
+                            .asIterable()
+                    },
+                    getPatches = { patches },
+                    storeFactory = ConcurrentHashMapK.Factory(),
+                )
+                .awaitValues(),
+        )
+    )
+}
+
+/**
+ * Returns an [Events] that emits from a merged, incrementally-accumulated collection of [Events]
+ * emitted from this, following the patch rules outlined in
+ * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch].
+ *
+ * Conceptually this is equivalent to:
+ * ``` kotlin
+ *   fun <K, V> State<Map<K, V>>.mergeEventsIncrementallyPromptly(): Events<Map<K, V>> =
+ *       map { it.merge() }.switchEventsPromptly()
+ * ```
+ *
+ * While the behavior is equivalent to the conceptual definition above, the implementation is
+ * significantly more efficient.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.mergeEventsIncrementallyPromptly
+ * @see merge
+ */
+fun <K, V> Incremental<K, Events<V>>.mergeEventsIncrementallyPromptly(): Events<Map<K, V>> {
+    val operatorName = "mergeEventsIncrementallyPromptly"
+    val name = operatorName
+    val patches =
+        mapImpl({ init.connect(this).patches }) { patch, _ ->
+            patch.mapValues { (_, m) -> m.map { events -> events.init.connect(this) } }.asIterable()
+        }
+    return EventsInit(
+        constInit(
+            name,
+            switchPromptImpl(
+                    name = name,
+                    getStorage = {
+                        init
+                            .connect(this)
+                            .getCurrentWithEpoch(this)
+                            .first
+                            .mapValues { (_, events) -> events.init.connect(this) }
+                            .asIterable()
+                    },
+                    getPatches = { patches },
+                    storeFactory = ConcurrentHashMapK.Factory(),
+                )
+                .awaitValues(),
+        )
+    )
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Modes.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Modes.kt
new file mode 100644
index 0000000..6c070a6
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Modes.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2025 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.kairos
+
+/**
+ * A modal Kairos sub-network.
+ *
+ * When [enabled][enableMode], all network modifications are applied immediately to the Kairos
+ * network. When the returned [Events] emits a [BuildMode], that mode is enabled and replaces this
+ * mode, undoing all modifications in the process (any registered [observers][BuildScope.observe]
+ * are unregistered, and any pending [side-effects][BuildScope.effect] are cancelled).
+ *
+ * Use [compiledBuildSpec] to compile and stand-up a mode graph.
+ *
+ * @see StatefulMode
+ */
+@ExperimentalKairosApi
+fun interface BuildMode<out A> {
+    /**
+     * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a
+     * new mode.
+     */
+    fun BuildScope.enableMode(): Pair<A, Events<BuildMode<A>>>
+}
+
+/**
+ * Returns a [BuildSpec] that, when [applied][BuildScope.applySpec], stands up a modal-transition
+ * graph starting with this [BuildMode], automatically switching to new modes as they are produced.
+ *
+ * @see BuildMode
+ */
+@ExperimentalKairosApi
+val <A> BuildMode<A>.compiledBuildSpec: BuildSpec<State<A>>
+    get() = buildSpec {
+        var modeChangeEvents by EventsLoop<BuildMode<A>>()
+        val activeMode: State<Pair<A, Events<BuildMode<A>>>> =
+            modeChangeEvents
+                .map { it.run { buildSpec { enableMode() } } }
+                .holdLatestSpec(buildSpec { enableMode() })
+        modeChangeEvents =
+            activeMode
+                .map { statefully { it.second.nextOnly() } }
+                .applyLatestStateful()
+                .switchEvents()
+        activeMode.map { it.first }
+    }
+
+/**
+ * A modal Kairos sub-network.
+ *
+ * When [enabled][enableMode], all state accumulation is immediately started. When the returned
+ * [Events] emits a [BuildMode], that mode is enabled and replaces this mode, stopping all state
+ * accumulation in the process.
+ *
+ * Use [compiledStateful] to compile and stand-up a mode graph.
+ *
+ * @see BuildMode
+ */
+@ExperimentalKairosApi
+fun interface StatefulMode<out A> {
+    /**
+     * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a
+     * new mode.
+     */
+    fun StateScope.enableMode(): Pair<A, Events<StatefulMode<A>>>
+}
+
+/**
+ * Returns a [Stateful] that, when [applied][StateScope.applyStateful], stands up a modal-transition
+ * graph starting with this [StatefulMode], automatically switching to new modes as they are
+ * produced.
+ *
+ * @see StatefulMode
+ */
+@ExperimentalKairosApi
+val <A> StatefulMode<A>.compiledStateful: Stateful<State<A>>
+    get() = statefully {
+        var modeChangeEvents by EventsLoop<StatefulMode<A>>()
+        val activeMode: State<Pair<A, Events<StatefulMode<A>>>> =
+            modeChangeEvents
+                .map { it.run { statefully { enableMode() } } }
+                .holdLatestStateful(statefully { enableMode() })
+        modeChangeEvents =
+            activeMode
+                .map { statefully { it.second.nextOnly() } }
+                .applyLatestStateful()
+                .switchEvents()
+        activeMode.map { it.first }
+    }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Selector.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Selector.kt
new file mode 100644
index 0000000..f7decbb
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Selector.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2025 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.kairos
+
+import com.android.systemui.kairos.internal.DerivedMapCheap
+import com.android.systemui.kairos.internal.StateImpl
+import com.android.systemui.kairos.internal.init
+
+/**
+ * Returns a [StateSelector] that can be used to efficiently check if the input [State] is currently
+ * holding a specific value.
+ *
+ * An example:
+ * ```
+ *   val intState: State<Int> = ...
+ *   val intSelector: StateSelector<Int> = intState.selector()
+ *   // Tracks if intState is holding 1
+ *   val isOne: State<Boolean> = intSelector.whenSelected(1)
+ * ```
+ *
+ * This is semantically equivalent to `val isOne = intState.map { i -> i == 1 }`, but is
+ * significantly more efficient; specifically, using [State.map] in this way incurs a `O(n)`
+ * performance hit, where `n` is the number of different [State.map] operations used to track a
+ * specific value. [selector] internally uses a [HashMap] to lookup the appropriate downstream
+ * [State] to update, and so operates in `O(1)`.
+ *
+ * Note that the returned [StateSelector] should be cached and re-used to gain the performance
+ * benefit.
+ *
+ * @see groupByKey
+ */
+@ExperimentalKairosApi
+fun <A> State<A>.selector(numDistinctValues: Int? = null): StateSelector<A> =
+    StateSelector(
+        this,
+        changes
+            .map { new -> mapOf(new to true, sampleDeferred().value to false) }
+            .groupByKey(numDistinctValues),
+    )
+
+/**
+ * Tracks the currently selected value of type [A] from an upstream [State].
+ *
+ * @see selector
+ */
+@ExperimentalKairosApi
+class StateSelector<in A>
+internal constructor(
+    private val upstream: State<A>,
+    private val groupedChanges: GroupedEvents<A, Boolean>,
+) {
+    /**
+     * Returns a [State] that tracks whether the upstream [State] is currently holding the given
+     * [value].
+     *
+     * @see selector
+     */
+    fun whenSelected(value: A): State<Boolean> {
+        val operatorName = "StateSelector#whenSelected"
+        val name = "$operatorName[$value]"
+        return StateInit(
+            init(name) {
+                StateImpl(
+                    name,
+                    operatorName,
+                    groupedChanges.impl.eventsForKey(value),
+                    DerivedMapCheap(upstream.init) { it == value },
+                )
+            }
+        )
+    }
+
+    operator fun get(value: A): State<Boolean> = whenSelected(value)
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt
index 1f0a19d..22ca83c 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.kairos
 
 import com.android.systemui.kairos.internal.CompletableLazy
-import com.android.systemui.kairos.internal.DerivedMapCheap
 import com.android.systemui.kairos.internal.EventsImpl
 import com.android.systemui.kairos.internal.Init
 import com.android.systemui.kairos.internal.InitScope
@@ -37,20 +36,31 @@
 import com.android.systemui.kairos.internal.mapStateImpl
 import com.android.systemui.kairos.internal.mapStateImplCheap
 import com.android.systemui.kairos.internal.util.hashString
-import com.android.systemui.kairos.internal.zipStateMap
-import com.android.systemui.kairos.internal.zipStates
+import com.android.systemui.kairos.util.WithPrev
 import kotlin.reflect.KProperty
 
 /**
- * A time-varying value with discrete changes. Essentially, a combination of a [Transactional] that
- * holds a value, and an [Events] that emits when the value changes.
+ * A time-varying value with discrete changes. Conceptually, a combination of a [Transactional] that
+ * holds a value, and an [Events] that emits when the value [changes].
+ *
+ * [States][State] follow these rules:
+ * 1. In the same transaction that [changes] emits a new value, [sample] will continue to return the
+ *    previous value.
+ * 2. Unless it is [constant][stateOf], [States][State] can only be created via [StateScope]
+ *    operations, or derived from other existing [States][State] via [State.map], [combine], etc.
+ * 3. [States][State] can only be [sampled][TransactionScope.sample] within a [TransactionScope].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.states
  */
 @ExperimentalKairosApi
 sealed class State<out A> {
     internal abstract val init: Init<StateImpl<A>>
 }
 
-/** A [State] that never changes. */
+/**
+ * Returns a constant [State] that never changes. [changes] is equivalent to [emptyEvents] and
+ * [TransactionScope.sample] will always produce [value].
+ */
 @ExperimentalKairosApi
 fun <A> stateOf(value: A): State<A> {
     val operatorName = "stateOf"
@@ -65,6 +75,10 @@
  * will be queried and used.
  *
  * Useful for recursive definitions.
+ *
+ * ``` kotlin
+ *   fun <A> Lazy<State<A>>.defer() = deferredState { value }
+ * ```
  */
 @ExperimentalKairosApi fun <A> Lazy<State<A>>.defer(): State<A> = deferInline { value }
 
@@ -76,6 +90,10 @@
  * and used.
  *
  * Useful for recursive definitions.
+ *
+ * ``` kotlin
+ *   fun <A> DeferredValue<State<A>>.defer() = deferredState { get() }
+ * ```
  */
 @ExperimentalKairosApi
 fun <A> DeferredValue<State<A>>.defer(): State<A> = deferInline { unwrapped.value }
@@ -94,6 +112,8 @@
 /**
  * Returns a [State] containing the results of applying [transform] to the value held by the
  * original [State].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.mapState
  */
 @ExperimentalKairosApi
 fun <A, B> State<A>.map(transform: KairosScope.(A) -> B): State<B> {
@@ -110,10 +130,12 @@
  * Returns a [State] that transforms the value held inside this [State] by applying it to the
  * [transform].
  *
- * Note that unlike [map], the result is not cached. This means that not only should [transform] be
- * fast and pure, it should be *monomorphic* (1-to-1). Failure to do this means that [changes] for
- * the returned [State] will operate unexpectedly, emitting at rates that do not reflect an
- * observable change to the returned [State].
+ * Note that unlike [State.map], the result is not cached. This means that not only should
+ * [transform] be fast and pure, it should be *monomorphic* (1-to-1). Failure to do this means that
+ * [changes] for the returned [State] will operate unexpectedly, emitting at rates that do not
+ * reflect an observable change to the returned [State].
+ *
+ * @see State.map
  */
 @ExperimentalKairosApi
 fun <A, B> State<A>.mapCheapUnsafe(transform: KairosScope.(A) -> B): State<B> {
@@ -125,214 +147,30 @@
 }
 
 /**
- * Returns a [State] by combining the values held inside the given [State]s by applying them to the
- * given function [transform].
- */
-@ExperimentalKairosApi
-fun <A, B, C> State<A>.combineWith(other: State<B>, transform: KairosScope.(A, B) -> C): State<C> =
-    combine(this, other, transform)
-
-/**
  * Splits a [State] of pairs into a pair of [Events][State], where each returned [State] holds half
  * of the original.
  *
- * Shorthand for:
- * ```kotlin
- * val lefts = map { it.first }
- * val rights = map { it.second }
- * return Pair(lefts, rights)
+ * ``` kotlin
+ *   fun <A, B> State<Pair<A, B>>.unzip(): Pair<State<A>, State<B>> {
+ *       val first = map { it.first }
+ *       val second = map { it.second }
+ *       return first to second
+ *   }
  * ```
  */
 @ExperimentalKairosApi
 fun <A, B> State<Pair<A, B>>.unzip(): Pair<State<A>, State<B>> {
-    val left = map { it.first }
-    val right = map { it.second }
-    return left to right
+    val first = map { it.first }
+    val second = map { it.second }
+    return first to second
 }
 
 /**
- * Returns a [State] by combining the values held inside the given [States][State] into a [List].
+ * Returns a [State] by applying [transform] to the value held by the original [State].
  *
- * @see State.combineWith
+ * @sample com.android.systemui.kairos.KairosSamples.flatMap
  */
 @ExperimentalKairosApi
-fun <A> Iterable<State<A>>.combine(): State<List<A>> {
-    val operatorName = "combine"
-    val name = operatorName
-    return StateInit(
-        init(name) {
-            val states = map { it.init }
-            zipStates(
-                name,
-                operatorName,
-                states.size,
-                states = init(null) { states.map { it.connect(this) } },
-            )
-        }
-    )
-}
-
-/**
- * Returns a [State] by combining the values held inside the given [States][State] into a [Map].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <K, A> Map<K, State<A>>.combine(): State<Map<K, A>> {
-    val operatorName = "combine"
-    val name = operatorName
-    return StateInit(
-        init(name) {
-            zipStateMap(
-                name,
-                operatorName,
-                size,
-                states = init(null) { mapValues { it.value.init.connect(evalScope = this) } },
-            )
-        }
-    )
-}
-
-/**
- * Returns a [State] whose value is generated with [transform] by combining the current values of
- * each given [State].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A, B> Iterable<State<A>>.combine(transform: KairosScope.(List<A>) -> B): State<B> =
-    combine().map(transform)
-
-/**
- * Returns a [State] by combining the values held inside the given [State]s into a [List].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A> combine(vararg states: State<A>): State<List<A>> = states.asIterable().combine()
-
-/**
- * Returns a [State] whose value is generated with [transform] by combining the current values of
- * each given [State].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A, B> combine(vararg states: State<A>, transform: KairosScope.(List<A>) -> B): State<B> =
-    states.asIterable().combine(transform)
-
-/**
- * Returns a [State] whose value is generated with [transform] by combining the current values of
- * each given [State].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A, B, Z> combine(
-    stateA: State<A>,
-    stateB: State<B>,
-    transform: KairosScope.(A, B) -> Z,
-): State<Z> {
-    val operatorName = "combine"
-    val name = operatorName
-    return StateInit(
-        init(name) {
-            zipStates(name, operatorName, stateA.init, stateB.init) { a, b ->
-                NoScope.transform(a, b)
-            }
-        }
-    )
-}
-
-/**
- * Returns a [State] whose value is generated with [transform] by combining the current values of
- * each given [State].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A, B, C, Z> combine(
-    stateA: State<A>,
-    stateB: State<B>,
-    stateC: State<C>,
-    transform: KairosScope.(A, B, C) -> Z,
-): State<Z> {
-    val operatorName = "combine"
-    val name = operatorName
-    return StateInit(
-        init(name) {
-            zipStates(name, operatorName, stateA.init, stateB.init, stateC.init) { a, b, c ->
-                NoScope.transform(a, b, c)
-            }
-        }
-    )
-}
-
-/**
- * Returns a [State] whose value is generated with [transform] by combining the current values of
- * each given [State].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A, B, C, D, Z> combine(
-    stateA: State<A>,
-    stateB: State<B>,
-    stateC: State<C>,
-    stateD: State<D>,
-    transform: KairosScope.(A, B, C, D) -> Z,
-): State<Z> {
-    val operatorName = "combine"
-    val name = operatorName
-    return StateInit(
-        init(name) {
-            zipStates(name, operatorName, stateA.init, stateB.init, stateC.init, stateD.init) {
-                a,
-                b,
-                c,
-                d ->
-                NoScope.transform(a, b, c, d)
-            }
-        }
-    )
-}
-
-/**
- * Returns a [State] whose value is generated with [transform] by combining the current values of
- * each given [State].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A, B, C, D, E, Z> combine(
-    stateA: State<A>,
-    stateB: State<B>,
-    stateC: State<C>,
-    stateD: State<D>,
-    stateE: State<E>,
-    transform: KairosScope.(A, B, C, D, E) -> Z,
-): State<Z> {
-    val operatorName = "combine"
-    val name = operatorName
-    return StateInit(
-        init(name) {
-            zipStates(
-                name,
-                operatorName,
-                stateA.init,
-                stateB.init,
-                stateC.init,
-                stateD.init,
-                stateE.init,
-            ) { a, b, c, d, e ->
-                NoScope.transform(a, b, c, d, e)
-            }
-        }
-    )
-}
-
-/** Returns a [State] by applying [transform] to the value held by the original [State]. */
-@ExperimentalKairosApi
 fun <A, B> State<A>.flatMap(transform: KairosScope.(A) -> State<B>): State<B> {
     val operatorName = "flatMap"
     val name = operatorName
@@ -345,77 +183,18 @@
     )
 }
 
-/** Shorthand for `flatMap { it }` */
+/**
+ * Returns a [State] that behaves like the current value of the original [State].
+ *
+ * ``` kotlin
+ *   fun <A> State<State<A>>.flatten() = flatMap { it }
+ * ```
+ *
+ * @see flatMap
+ */
 @ExperimentalKairosApi fun <A> State<State<A>>.flatten() = flatMap { it }
 
 /**
- * Returns a [StateSelector] that can be used to efficiently check if the input [State] is currently
- * holding a specific value.
- *
- * An example:
- * ```
- *   val intState: State<Int> = ...
- *   val intSelector: StateSelector<Int> = intState.selector()
- *   // Tracks if lInt is holding 1
- *   val isOne: State<Boolean> = intSelector.whenSelected(1)
- * ```
- *
- * This is semantically equivalent to `val isOne = intState.map { i -> i == 1 }`, but is
- * significantly more efficient; specifically, using [State.map] in this way incurs a `O(n)`
- * performance hit, where `n` is the number of different [State.map] operations used to track a
- * specific value. [selector] internally uses a [HashMap] to lookup the appropriate downstream
- * [State] to update, and so operates in `O(1)`.
- *
- * Note that the returned [StateSelector] should be cached and re-used to gain the performance
- * benefit.
- *
- * @see groupByKey
- */
-@ExperimentalKairosApi
-fun <A> State<A>.selector(numDistinctValues: Int? = null): StateSelector<A> =
-    StateSelector(
-        this,
-        changes
-            .map { new -> mapOf(new to true, sampleDeferred().get() to false) }
-            .groupByKey(numDistinctValues),
-    )
-
-/**
- * Tracks the currently selected value of type [A] from an upstream [State].
- *
- * @see selector
- */
-@ExperimentalKairosApi
-class StateSelector<in A>
-internal constructor(
-    private val upstream: State<A>,
-    private val groupedChanges: GroupedEvents<A, Boolean>,
-) {
-    /**
-     * Returns a [State] that tracks whether the upstream [State] is currently holding the given
-     * [value].
-     *
-     * @see selector
-     */
-    fun whenSelected(value: A): State<Boolean> {
-        val operatorName = "StateSelector#whenSelected"
-        val name = "$operatorName[$value]"
-        return StateInit(
-            init(name) {
-                StateImpl(
-                    name,
-                    operatorName,
-                    groupedChanges.impl.eventsForKey(value),
-                    DerivedMapCheap(upstream.init) { it == value },
-                )
-            }
-        )
-    }
-
-    operator fun get(value: A): State<Boolean> = whenSelected(value)
-}
-
-/**
  * A mutable [State] that provides the ability to manually [set its value][setValue].
  *
  * Multiple invocations of [setValue] that occur before a transaction are conflated; only the most
@@ -441,6 +220,9 @@
     override val init: Init<StateImpl<T>>
         get() = state.init
 
+    // TODO: not convinced this is totally safe
+    //  - at least for the BuildScope smart-constructor, we can avoid the network.transaction { }
+    //    call since we're already in a transaction
     internal val state = run {
         val changes = input.impl
         val name = null
@@ -491,7 +273,17 @@
     fun setValueDeferred(value: DeferredValue<T>) = input.emit(value.unwrapped)
 }
 
-/** A forward-reference to a [State], allowing for recursive definitions. */
+/**
+ * A forward-reference to a [State]. Useful for recursive definitions.
+ *
+ * This reference can be used like a standard [State], but will throw an error if its [loopback] is
+ * unset before it is [observed][BuildScope.observe] or [sampled][TransactionScope.sample].
+ *
+ * Note that it is safe to invoke [TransactionScope.sampleDeferred] before [loopback] is set,
+ * provided the returned [DeferredValue] is not [queried][KairosScope.get].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.stateLoop
+ */
 @ExperimentalKairosApi
 class StateLoop<A> : State<A>() {
 
@@ -502,7 +294,10 @@
     override val init: Init<StateImpl<A>> =
         init(name) { deferred.value.init.connect(evalScope = this) }
 
-    /** The [State] this [StateLoop] will forward to. */
+    /**
+     * The [State] this reference is referring to. Must be set before this [StateLoop] is
+     * [observed][BuildScope.observe] or [sampled][TransactionScope.sample].
+     */
     var loopback: State<A>? = null
         set(value) {
             value?.let {
@@ -528,3 +323,24 @@
 
 private inline fun <A> deferInline(crossinline block: InitScope.() -> State<A>): State<A> =
     StateInit(init(name = null) { block().init.connect(evalScope = this) })
+
+/**
+ * Like [changes] but also includes the old value of this [State].
+ *
+ * Shorthand for:
+ * ``` kotlin
+ *     stateChanges.map { WithPrev(previousValue = sample(), newValue = it) }
+ * ```
+ */
+@ExperimentalKairosApi
+val <A> State<A>.transitions: Events<WithPrev<A, A>>
+    get() = changes.map { WithPrev(previousValue = sample(), newValue = it) }
+
+/**
+ * Returns an [Events] that emits the new value of this [State] when it changes.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.changes
+ */
+@ExperimentalKairosApi
+val <A> State<A>.changes: Events<A>
+    get() = EventsInit(init(name = null) { init.connect(evalScope = this).changes })
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt
index 933ff1a..faeffe8 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt
@@ -16,13 +16,11 @@
 
 package com.android.systemui.kairos
 
+import com.android.systemui.kairos.util.MapPatch
 import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.Just
 import com.android.systemui.kairos.util.WithPrev
-import com.android.systemui.kairos.util.just
 import com.android.systemui.kairos.util.map
 import com.android.systemui.kairos.util.mapMaybeValues
-import com.android.systemui.kairos.util.none
 import com.android.systemui.kairos.util.zipWith
 
 // TODO: caching story? should each Scope have a cache of applied Stateful instances?
@@ -64,6 +62,8 @@
      * Note that the value contained within the [State] is not updated until *after* all [Events]
      * have been processed; this keeps the value of the [State] consistent during the entire Kairos
      * transaction.
+     *
+     * @see holdState
      */
     fun <A> Events<A>.holdStateDeferred(initialValue: DeferredValue<A>): State<A>
 
@@ -71,113 +71,128 @@
      * Returns a [State] holding a [Map] that is updated incrementally whenever this emits a value.
      *
      * The value emitted is used as a "patch" for the tracked [Map]; for each key [K] in the emitted
-     * map, an associated value of [Just] will insert or replace the value in the tracked [Map], and
-     * an associated value of [none] will remove the key from the tracked [Map].
+     * map, an associated value of [present][Maybe.present] will insert or replace the value in the
+     * tracked [Map], and an associated value of [absent][Maybe.absent] will remove the key from the
+     * tracked [Map].
+     *
+     * @sample com.android.systemui.kairos.KairosSamples.incrementals
+     * @see MapPatch
      */
-    fun <K, V> Events<Map<K, Maybe<V>>>.foldStateMapIncrementally(
+    fun <K, V> Events<MapPatch<K, V>>.foldStateMapIncrementally(
         initialValues: DeferredValue<Map<K, V>>
     ): Incremental<K, V>
 
+    /**
+     * Returns an [Events] the emits the result of applying [Statefuls][Stateful] emitted from the
+     * original [Events].
+     *
+     * Unlike [applyLatestStateful], state accumulation is not stopped with each subsequent emission
+     * of the original [Events].
+     */
+    fun <A> Events<Stateful<A>>.applyStatefuls(): Events<A>
+
+    /**
+     * Returns an [Events] containing the results of applying each [Stateful] emitted from the
+     * original [Events], and a [DeferredValue] containing the result of applying [init]
+     * immediately.
+     *
+     * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+     * then the previously-active [Stateful] will be stopped with no replacement.
+     *
+     * When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
+     * with the same key is stopped.
+     *
+     * The optional [numKeys] argument is an optimization used to initialize the internal storage.
+     */
+    fun <K, A, B> Events<MapPatch<K, Stateful<A>>>.applyLatestStatefulForKey(
+        init: DeferredValue<Map<K, Stateful<B>>>,
+        numKeys: Int? = null,
+    ): Pair<Events<MapPatch<K, A>>, DeferredValue<Map<K, B>>>
+
     // TODO: everything below this comment can be made into extensions once we have context params
 
     /**
      * Returns an [Events] that emits from a merged, incrementally-accumulated collection of
-     * [Events] emitted from this, following the same "patch" rules as outlined in
-     * [foldStateMapIncrementally].
+     * [Events] emitted from this, following the patch rules outlined in
+     * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch].
      *
-     * Conceptually this is equivalent to:
-     * ```kotlin
-     *   fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally(
-     *     initialEvents: Map<K, Events<V>>,
+     * ``` kotlin
+     *   fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementally(
+     *     initialEvents: DeferredValue<Map<K, Events<V>>>,
      *   ): Events<Map<K, V>> =
-     *     foldMapIncrementally(initialEvents).map { it.merge() }.switchEvents()
+     *     foldMapIncrementally(initialEvents).mergeEventsIncrementally(initialEvents)
      * ```
      *
-     * While the behavior is equivalent to the conceptual definition above, the implementation is
-     * significantly more efficient.
-     *
+     * @see Incremental.mergeEventsIncrementally
      * @see merge
      */
-    fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally(
-        name: String? = null,
-        initialEvents: DeferredValue<Map<K, Events<V>>>,
+    fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementally(
+        initialEvents: DeferredValue<Map<K, Events<V>>>
     ): Events<Map<K, V>> = foldStateMapIncrementally(initialEvents).mergeEventsIncrementally()
 
     /**
      * Returns an [Events] that emits from a merged, incrementally-accumulated collection of
-     * [Events] emitted from this, following the same "patch" rules as outlined in
-     * [foldStateMapIncrementally].
+     * [Events] emitted from this, following the patch rules outlined in
+     * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch].
      *
-     * Conceptually this is equivalent to:
-     * ```kotlin
-     *   fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly(
-     *     initialEvents: Map<K, Events<V>>,
+     * ``` kotlin
+     *   fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementallyPromptly(
+     *     initialEvents: DeferredValue<Map<K, Events<V>>>,
      *   ): Events<Map<K, V>> =
-     *     foldMapIncrementally(initialEvents).map { it.merge() }.switchEventsPromptly()
+     *     foldMapIncrementally(initialEvents).mergeEventsIncrementallyPromptly(initialEvents)
      * ```
      *
-     * While the behavior is equivalent to the conceptual definition above, the implementation is
-     * significantly more efficient.
-     *
+     * @see Incremental.mergeEventsIncrementallyPromptly
      * @see merge
      */
-    fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly(
-        initialEvents: DeferredValue<Map<K, Events<V>>>,
-        name: String? = null,
+    fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementallyPromptly(
+        initialEvents: DeferredValue<Map<K, Events<V>>>
     ): Events<Map<K, V>> =
         foldStateMapIncrementally(initialEvents).mergeEventsIncrementallyPromptly()
 
     /**
      * Returns an [Events] that emits from a merged, incrementally-accumulated collection of
-     * [Events] emitted from this, following the same "patch" rules as outlined in
-     * [foldStateMapIncrementally].
+     * [Events] emitted from this, following the patch rules outlined in
+     * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch].
      *
-     * Conceptually this is equivalent to:
-     * ```kotlin
-     *   fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally(
+     * ``` kotlin
+     *   fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementally(
      *     initialEvents: Map<K, Events<V>>,
      *   ): Events<Map<K, V>> =
-     *     foldMapIncrementally(initialEvents).map { it.merge() }.switchEvents()
+     *     foldMapIncrementally(initialEvents).mergeEventsIncrementally(initialEvents)
      * ```
      *
-     * While the behavior is equivalent to the conceptual definition above, the implementation is
-     * significantly more efficient.
-     *
+     * @see Incremental.mergeEventsIncrementally
      * @see merge
      */
-    fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally(
-        name: String? = null,
-        initialEvents: Map<K, Events<V>> = emptyMap(),
-    ): Events<Map<K, V>> = mergeIncrementally(name, deferredOf(initialEvents))
+    fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementally(
+        initialEvents: Map<K, Events<V>> = emptyMap()
+    ): Events<Map<K, V>> = mergeEventsIncrementally(deferredOf(initialEvents))
 
     /**
      * Returns an [Events] that emits from a merged, incrementally-accumulated collection of
-     * [Events] emitted from this, following the same "patch" rules as outlined in
-     * [foldStateMapIncrementally].
+     * [Events] emitted from this, following the patch rules outlined in
+     * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch].
      *
-     * Conceptually this is equivalent to:
-     * ```kotlin
-     *   fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly(
+     * ``` kotlin
+     *   fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementallyPromptly(
      *     initialEvents: Map<K, Events<V>>,
      *   ): Events<Map<K, V>> =
-     *     foldMapIncrementally(initialEvents).map { it.merge() }.switchEventsPromptly()
+     *     foldMapIncrementally(initialEvents).mergeEventsIncrementallyPromptly(initialEvents)
      * ```
      *
-     * While the behavior is equivalent to the conceptual definition above, the implementation is
-     * significantly more efficient.
-     *
+     * @see Incremental.mergeEventsIncrementallyPromptly
      * @see merge
      */
-    fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly(
-        initialEvents: Map<K, Events<V>> = emptyMap(),
-        name: String? = null,
-    ): Events<Map<K, V>> = mergeIncrementallyPromptly(deferredOf(initialEvents), name)
+    fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementallyPromptly(
+        initialEvents: Map<K, Events<V>> = emptyMap()
+    ): Events<Map<K, V>> = mergeEventsIncrementallyPromptly(deferredOf(initialEvents))
 
     /** Applies the [Stateful] within this [StateScope]. */
     fun <A> Stateful<A>.applyStateful(): A = this()
 
     /**
-     * Applies the [Stateful] within this [StateScope], returning the result as an [DeferredValue].
+     * Applies the [Stateful] within this [StateScope], returning the result as a [DeferredValue].
      */
     fun <A> Stateful<A>.applyStatefulDeferred(): DeferredValue<A> = deferredStateScope {
         applyStateful()
@@ -190,42 +205,59 @@
      * Note that the value contained within the [State] is not updated until *after* all [Events]
      * have been processed; this keeps the value of the [State] consistent during the entire Kairos
      * transaction.
+     *
+     * @sample com.android.systemui.kairos.KairosSamples.holdState
+     * @see holdStateDeferred
      */
     fun <A> Events<A>.holdState(initialValue: A): State<A> =
         holdStateDeferred(deferredOf(initialValue))
 
     /**
-     * Returns an [Events] the emits the result of applying [Statefuls][Stateful] emitted from the
-     * original [Events].
-     *
-     * Unlike [applyLatestStateful], state accumulation is not stopped with each subsequent emission
-     * of the original [Events].
-     */
-    fun <A> Events<Stateful<A>>.applyStatefuls(): Events<A>
-
-    /**
      * Returns an [Events] containing the results of applying [transform] to each value of the
      * original [Events].
      *
      * [transform] can perform state accumulation via its [StateScope] receiver. Unlike
      * [mapLatestStateful], accumulation is not stopped with each subsequent emission of the
      * original [Events].
+     *
+     * ``` kotlin
+     *   fun <A, B> Events<A>.mapStateful(transform: StateScope.(A) -> B): Events<B> =
+     *       map { statefully { transform(it) } }.applyStatefuls()
+     * ```
      */
     fun <A, B> Events<A>.mapStateful(transform: StateScope.(A) -> B): Events<B> =
-        map { statefully { transform(it) } }.applyStatefuls()
+        mapCheap { statefully { transform(it) } }.applyStatefuls()
 
     /**
      * Returns a [State] the holds the result of applying the [Stateful] held by the original
      * [State].
      *
      * Unlike [applyLatestStateful], state accumulation is not stopped with each state change.
+     *
+     * ``` kotlin
+     *   fun <A> State<Stateful<A>>.applyStatefuls(): State<A> =
+     *       changes
+     *           .applyStatefuls()
+     *           .holdState(initialValue = sample().applyStateful())
+     * ```
      */
     fun <A> State<Stateful<A>>.applyStatefuls(): State<A> =
         changes
             .applyStatefuls()
-            .holdStateDeferred(initialValue = deferredStateScope { sampleDeferred().get()() })
+            .holdStateDeferred(
+                initialValue = deferredStateScope { sampleDeferred().value.applyStateful() }
+            )
 
-    /** Returns an [Events] that switches to the [Events] emitted by the original [Events]. */
+    /**
+     * Returns an [Events] that acts like the most recent [Events] to be emitted from the original
+     * [Events].
+     *
+     * ``` kotlin
+     *   fun <A> Events<Events<A>>.flatten() = holdState(emptyEvents).switchEvents()
+     * ```
+     *
+     * @see switchEvents
+     */
     fun <A> Events<Events<A>>.flatten() = holdState(emptyEvents).switchEvents()
 
     /**
@@ -234,9 +266,14 @@
      *
      * [transform] can perform state accumulation via its [StateScope] receiver. With each
      * invocation of [transform], state accumulation from previous invocation is stopped.
+     *
+     * ``` kotlin
+     *   fun <A, B> Events<A>.mapLatestStateful(transform: StateScope.(A) -> B): Events<B> =
+     *       map { statefully { transform(it) } }.applyLatestStateful()
+     * ```
      */
     fun <A, B> Events<A>.mapLatestStateful(transform: StateScope.(A) -> B): Events<B> =
-        map { statefully { transform(it) } }.applyLatestStateful()
+        mapCheap { statefully { transform(it) } }.applyLatestStateful()
 
     /**
      * Returns an [Events] that switches to a new [Events] produced by [transform] every time the
@@ -244,6 +281,13 @@
      *
      * [transform] can perform state accumulation via its [StateScope] receiver. With each
      * invocation of [transform], state accumulation from previous invocation is stopped.
+     *
+     * ``` kotlin
+     *   fun <A, B> Events<A>.flatMapLatestStateful(
+     *       transform: StateScope.(A) -> Events<B>
+     *   ): Events<B> =
+     *       mapLatestStateful(transform).flatten()
+     * ```
      */
     fun <A, B> Events<A>.flatMapLatestStateful(transform: StateScope.(A) -> Events<B>): Events<B> =
         mapLatestStateful(transform).flatten()
@@ -254,6 +298,8 @@
      *
      * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] is
      * stopped.
+     *
+     * @sample com.android.systemui.kairos.KairosSamples.applyLatestStateful
      */
     fun <A> Events<Stateful<A>>.applyLatestStateful(): Events<A> = applyLatestStateful {}.first
 
@@ -281,19 +327,18 @@
         init: Stateful<A>
     ): Pair<Events<B>, DeferredValue<A>> {
         val (events, result) =
-            mapCheap { spec -> mapOf(Unit to just(spec)) }
+            mapCheap { spec -> mapOf(Unit to Maybe.present(spec)) }
                 .applyLatestStatefulForKey(init = mapOf(Unit to init), numKeys = 1)
         val outEvents: Events<B> =
             events.mapMaybe {
                 checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" }
             }
         val outInit: DeferredValue<A> = deferredTransactionScope {
-            val initResult: Map<Unit, A> = result.get()
+            val initResult: Map<Unit, A> = result.value
             check(Unit in initResult) {
                 "applyLatest: expected initial result, but none present in: $initResult"
             }
-            @Suppress("UNCHECKED_CAST")
-            initResult.getOrDefault(Unit) { null } as A
+            initResult.getValue(Unit)
         }
         return Pair(outEvents, outInit)
     }
@@ -303,34 +348,32 @@
      * original [Events], and a [DeferredValue] containing the result of applying [init]
      * immediately.
      *
-     * If the [Maybe] contained within the value for an associated key is [none], then the
-     * previously-active [Stateful] will be stopped with no replacement.
-     *
-     * When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
-     * with the same key is stopped.
-     */
-    fun <K, A, B> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey(
-        init: DeferredValue<Map<K, Stateful<B>>>,
-        numKeys: Int? = null,
-    ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>>
-
-    /**
-     * Returns an [Events] containing the results of applying each [Stateful] emitted from the
-     * original [Events], and a [DeferredValue] containing the result of applying [init]
-     * immediately.
-     *
      * When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
      * with the same key is stopped.
      *
-     * If the [Maybe] contained within the value for an associated key is [none], then the
-     * previously-active [Stateful] will be stopped with no replacement.
+     * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+     * then the previously-active [Stateful] will be stopped with no replacement.
+     *
+     * The optional [numKeys] argument is an optimization used to initialize the internal storage.
      */
-    fun <K, A, B> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey(
+    fun <K, A, B> Events<MapPatch<K, Stateful<A>>>.applyLatestStatefulForKey(
         init: Map<K, Stateful<B>>,
         numKeys: Int? = null,
-    ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> =
+    ): Pair<Events<MapPatch<K, A>>, DeferredValue<Map<K, B>>> =
         applyLatestStatefulForKey(deferredOf(init), numKeys)
 
+    /**
+     * Returns an [Incremental] containing the latest results of applying each [Stateful] emitted
+     * from the original [Incremental]'s [updates].
+     *
+     * When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
+     * with the same key is stopped.
+     *
+     * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+     * then the previously-active [Stateful] will be stopped with no replacement.
+     *
+     * The optional [numKeys] argument is an optimization used to initialize the internal storage.
+     */
     fun <K, V> Incremental<K, Stateful<V>>.applyLatestStatefulForKey(
         numKeys: Int? = null
     ): Incremental<K, V> {
@@ -345,10 +388,12 @@
      * When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
      * with the same key is stopped.
      *
-     * If the [Maybe] contained within the value for an associated key is [none], then the
-     * previously-active [Stateful] will be stopped with no replacement.
+     * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+     * then the previously-active [Stateful] will be stopped with no replacement.
+     *
+     * The optional [numKeys] argument is an optimization used to initialize the internal storage.
      */
-    fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.holdLatestStatefulForKey(
+    fun <K, A> Events<MapPatch<K, Stateful<A>>>.holdLatestStatefulForKey(
         init: DeferredValue<Map<K, Stateful<A>>>,
         numKeys: Int? = null,
     ): Incremental<K, A> {
@@ -363,28 +408,33 @@
      * When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
      * with the same key is stopped.
      *
-     * If the [Maybe] contained within the value for an associated key is [none], then the
-     * previously-active [Stateful] will be stopped with no replacement.
+     * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+     * then the previously-active [Stateful] will be stopped with no replacement.
+     *
+     * The optional [numKeys] argument is an optimization used to initialize the internal storage.
      */
-    fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.holdLatestStatefulForKey(
+    fun <K, A> Events<MapPatch<K, Stateful<A>>>.holdLatestStatefulForKey(
         init: Map<K, Stateful<A>> = emptyMap(),
         numKeys: Int? = null,
     ): Incremental<K, A> = holdLatestStatefulForKey(deferredOf(init), numKeys)
 
     /**
      * Returns an [Events] containing the results of applying each [Stateful] emitted from the
-     * original [Events], and a [DeferredValue] containing the result of applying [stateInit]
-     * immediately.
+     * original [Events].
      *
      * When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
      * with the same key is stopped.
      *
-     * If the [Maybe] contained within the value for an associated key is [none], then the
-     * previously-active [Stateful] will be stopped with no replacement.
+     * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+     * then the previously-active [Stateful] will be stopped with no replacement.
+     *
+     * The optional [numKeys] argument is an optimization used to initialize the internal storage.
+     *
+     * @sample com.android.systemui.kairos.KairosSamples.applyLatestStatefulForKey
      */
-    fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey(
+    fun <K, A> Events<MapPatch<K, Stateful<A>>>.applyLatestStatefulForKey(
         numKeys: Int? = null
-    ): Events<Map<K, Maybe<A>>> =
+    ): Events<MapPatch<K, A>> =
         applyLatestStatefulForKey(init = emptyMap<K, Stateful<*>>(), numKeys = numKeys).first
 
     /**
@@ -395,18 +445,20 @@
      * [transform] can perform state accumulation via its [StateScope] receiver. With each
      * invocation of [transform], state accumulation from previous invocation is stopped.
      *
-     * If the [Maybe] contained within the value for an associated key is [none], then the
-     * previously-active [StateScope] will be stopped with no replacement.
+     * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+     * then the previously-active [StateScope] will be stopped with no replacement.
+     *
+     * The optional [numKeys] argument is an optimization used to initialize the internal storage.
      */
-    fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey(
+    fun <K, A, B> Events<MapPatch<K, A>>.mapLatestStatefulForKey(
         initialValues: DeferredValue<Map<K, A>>,
         numKeys: Int? = null,
         transform: StateScope.(A) -> B,
-    ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> =
+    ): Pair<Events<MapPatch<K, B>>, DeferredValue<Map<K, B>>> =
         map { patch -> patch.mapValues { (_, v) -> v.map { statefully { transform(it) } } } }
             .applyLatestStatefulForKey(
                 deferredStateScope {
-                    initialValues.get().mapValues { (_, v) -> statefully { transform(v) } }
+                    initialValues.value.mapValues { (_, v) -> statefully { transform(v) } }
                 },
                 numKeys = numKeys,
             )
@@ -419,14 +471,16 @@
      * [transform] can perform state accumulation via its [StateScope] receiver. With each
      * invocation of [transform], state accumulation from previous invocation is stopped.
      *
-     * If the [Maybe] contained within the value for an associated key is [none], then the
-     * previously-active [StateScope] will be stopped with no replacement.
+     * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+     * then the previously-active [StateScope] will be stopped with no replacement.
+     *
+     * The optional [numKeys] argument is an optimization used to initialize the internal storage.
      */
-    fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey(
+    fun <K, A, B> Events<MapPatch<K, A>>.mapLatestStatefulForKey(
         initialValues: Map<K, A>,
         numKeys: Int? = null,
         transform: StateScope.(A) -> B,
-    ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> =
+    ): Pair<Events<MapPatch<K, B>>, DeferredValue<Map<K, B>>> =
         mapLatestStatefulForKey(deferredOf(initialValues), numKeys, transform)
 
     /**
@@ -436,13 +490,24 @@
      * [transform] can perform state accumulation via its [StateScope] receiver. With each
      * invocation of [transform], state accumulation from previous invocation is stopped.
      *
-     * If the [Maybe] contained within the value for an associated key is [none], then the
-     * previously-active [StateScope] will be stopped with no replacement.
+     * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+     * then the previously-active [StateScope] will be stopped with no replacement.
+     *
+     * The optional [numKeys] argument is an optimization used to initialize the internal storage.
+     *
+     * ``` kotlin
+     *   fun <K, A, B> Events<MapPatch<K, A>>.mapLatestStatefulForKey(
+     *       numKeys: Int? = null,
+     *       transform: StateScope.(A) -> B,
+     *   ): Pair<Events<MapPatch<K, B>>, DeferredValue<Map<K, B>>> =
+     *       map { patch -> patch.mapValues { (_, mv) -> mv.map { statefully { transform(it) } } } }
+     *           .applyLatestStatefulForKey(numKeys)
+     * ```
      */
-    fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey(
+    fun <K, A, B> Events<MapPatch<K, A>>.mapLatestStatefulForKey(
         numKeys: Int? = null,
         transform: StateScope.(A) -> B,
-    ): Events<Map<K, Maybe<B>>> = mapLatestStatefulForKey(emptyMap(), numKeys, transform).first
+    ): Events<MapPatch<K, B>> = mapLatestStatefulForKey(emptyMap(), numKeys, transform).first
 
     /**
      * Returns an [Events] that will only emit the next event of the original [Events], and then
@@ -450,18 +515,31 @@
      *
      * If the original [Events] is emitting an event at this exact time, then it will be the only
      * even emitted from the result [Events].
+     *
+     * ``` kotlin
+     *   fun <A> Events<A>.nextOnly(): Events<A> =
+     *       EventsLoop<A>().apply {
+     *           loopback = map { emptyEvents }.holdState(this@nextOnly).switchEvents()
+     *       }
+     * ```
      */
-    fun <A> Events<A>.nextOnly(name: String? = null): Events<A> =
+    fun <A> Events<A>.nextOnly(): Events<A> =
         if (this === emptyEvents) {
             this
         } else {
-            EventsLoop<A>().also {
-                it.loopback =
-                    it.mapCheap { emptyEvents }.holdState(this@nextOnly).switchEvents(name)
+            EventsLoop<A>().apply {
+                loopback = mapCheap { emptyEvents }.holdState(this@nextOnly).switchEvents()
             }
         }
 
-    /** Returns an [Events] that skips the next emission of the original [Events]. */
+    /**
+     * Returns an [Events] that skips the next emission of the original [Events].
+     *
+     * ``` kotlin
+     *   fun <A> Events<A>.skipNext(): Events<A> =
+     *       nextOnly().map { this@skipNext }.holdState(emptyEvents).switchEvents()
+     * ```
+     */
     fun <A> Events<A>.skipNext(): Events<A> =
         if (this === emptyEvents) {
             this
@@ -475,6 +553,11 @@
      *
      * If the original [Events] emits at the same time as [stop], then the returned [Events] will
      * emit that value.
+     *
+     * ``` kotlin
+     *   fun <A> Events<A>.takeUntil(stop: Events<*>): Events<A> =
+     *       stop.map { emptyEvents }.nextOnly().holdState(this).switchEvents()
+     * ```
      */
     fun <A> Events<A>.takeUntil(stop: Events<*>): Events<A> =
         if (stop === emptyEvents) {
@@ -494,14 +577,19 @@
         val (_, init: DeferredValue<Map<Unit, A>>) =
             stop
                 .nextOnly()
-                .map { mapOf(Unit to none<Stateful<A>>()) }
+                .map { mapOf(Unit to Maybe.absent<Stateful<A>>()) }
                 .applyLatestStatefulForKey(init = mapOf(Unit to stateful), numKeys = 1)
-        return deferredStateScope { init.get().getValue(Unit) }
+        return deferredStateScope { init.value.getValue(Unit) }
     }
 
     /**
      * Returns an [Events] that emits values from the original [Events] up to and including a value
      * is emitted that satisfies [predicate].
+     *
+     * ``` kotlin
+     *   fun <A> Events<A>.takeUntil(predicate: TransactionScope.(A) -> Boolean): Events<A> =
+     *       takeUntil(filter(predicate))
+     * ```
      */
     fun <A> Events<A>.takeUntil(predicate: TransactionScope.(A) -> Boolean): Events<A> =
         takeUntil(filter(predicate))
@@ -513,6 +601,18 @@
      * Note that the value contained within the [State] is not updated until *after* all [Events]
      * have been processed; this keeps the value of the [State] consistent during the entire Kairos
      * transaction.
+     *
+     * ``` kotlin
+     *   fun <A, B> Events<A>.foldState(
+     *       initialValue: B,
+     *       transform: TransactionScope.(A, B) -> B,
+     *   ): State<B> {
+     *       lateinit var state: State<B>
+     *       return map { a -> transform(a, state.sample()) }
+     *           .holdState(initialValue)
+     *           .also { state = it }
+     *   }
+     * ```
      */
     fun <A, B> Events<A>.foldState(
         initialValue: B,
@@ -529,6 +629,18 @@
      * Note that the value contained within the [State] is not updated until *after* all [Events]
      * have been processed; this keeps the value of the [State] consistent during the entire Kairos
      * transaction.
+     *
+     * ``` kotlin
+     *   fun <A, B> Events<A>.foldStateDeferred(
+     *       initialValue: DeferredValue<B>,
+     *       transform: TransactionScope.(A, B) -> B,
+     *   ): State<B> {
+     *       lateinit var state: State<B>
+     *       return map { a -> transform(a, state.sample()) }
+     *           .holdStateDeferred(initialValue)
+     *           .also { state = it }
+     *   }
+     * ```
      */
     fun <A, B> Events<A>.foldStateDeferred(
         initialValue: DeferredValue<B>,
@@ -551,10 +663,11 @@
      * have been processed; this keeps the value of the [State] consistent during the entire Kairos
      * transaction.
      *
-     * Shorthand for:
-     * ```kotlin
-     * val (changes, initApplied) = applyLatestStateful(init)
-     * return changes.holdStateDeferred(initApplied)
+     * ``` kotlin
+     *   fun <A> Events<Stateful<A>>.holdLatestStateful(init: Stateful<A>): State<A> {
+     *       val (changes, initApplied) = applyLatestStateful(init)
+     *       return changes.holdStateDeferred(initApplied)
+     *   }
      * ```
      */
     fun <A> Events<Stateful<A>>.holdLatestStateful(init: Stateful<A>): State<A> {
@@ -578,8 +691,8 @@
      * that the returned [Events] will not emit until the original [Events] has emitted twice.
      */
     fun <A> Events<A>.pairwise(): Events<WithPrev<A, A>> =
-        mapCheap { just(it) }
-            .pairwise(none)
+        mapCheap { Maybe.present(it) }
+            .pairwise(Maybe.absent)
             .mapMaybe { (prev, next) -> prev.zipWith(next, ::WithPrev) }
 
     /**
@@ -599,10 +712,11 @@
      * Returns a [State] holding a [Map] that is updated incrementally whenever this emits a value.
      *
      * The value emitted is used as a "patch" for the tracked [Map]; for each key [K] in the emitted
-     * map, an associated value of [Just] will insert or replace the value in the tracked [Map], and
-     * an associated value of [none] will remove the key from the tracked [Map].
+     * map, an associated value of [Maybe.present] will insert or replace the value in the tracked
+     * [Map], and an associated value of [absent][Maybe.absent] will remove the key from the tracked
+     * [Map].
      */
-    fun <K, V> Events<Map<K, Maybe<V>>>.foldStateMapIncrementally(
+    fun <K, V> Events<MapPatch<K, V>>.foldStateMapIncrementally(
         initialValues: Map<K, V> = emptyMap()
     ): Incremental<K, V> = foldStateMapIncrementally(deferredOf(initialValues))
 
@@ -610,10 +724,11 @@
      * Returns an [Events] that wraps each emission of the original [Events] into an [IndexedValue],
      * containing the emitted value and its index (starting from zero).
      *
-     * Shorthand for:
-     * ```
-     *   val index = fold(0) { _, oldIdx -> oldIdx + 1 }
-     *   sample(index) { a, idx -> IndexedValue(idx, a) }
+     * ``` kotlin
+     *   fun <A> Events<A>.withIndex(): Events<IndexedValue<A>> {
+     *     val index = fold(0) { _, oldIdx -> oldIdx + 1 }
+     *     return sample(index) { a, idx -> IndexedValue(idx, a) }
+     *   }
      * ```
      */
     fun <A> Events<A>.withIndex(): Events<IndexedValue<A>> {
@@ -625,9 +740,11 @@
      * Returns an [Events] containing the results of applying [transform] to each value of the
      * original [Events] and its index (starting from zero).
      *
-     * Shorthand for:
-     * ```
-     *   withIndex().map { (idx, a) -> transform(idx, a) }
+     * ``` kotlin
+     *   fun <A> Events<A>.mapIndexed(transform: TransactionScope.(Int, A) -> B): Events<B> {
+     *       val index = foldState(0) { _, i -> i + 1 }
+     *       return sample(index) { a, idx -> transform(idx, a) }
+     *   }
      * ```
      */
     fun <A, B> Events<A>.mapIndexed(transform: TransactionScope.(Int, A) -> B): Events<B> {
@@ -635,7 +752,16 @@
         return sample(index) { a, idx -> transform(idx, a) }
     }
 
-    /** Returns an [Events] where all subsequent repetitions of the same value are filtered out. */
+    /**
+     * Returns an [Events] where all subsequent repetitions of the same value are filtered out.
+     *
+     * ``` kotlin
+     *   fun <A> Events<A>.distinctUntilChanged(): Events<A> {
+     *       val state: State<Any?> = holdState(Any())
+     *       return filter { it != state.sample() }
+     *   }
+     * ```
+     */
     fun <A> Events<A>.distinctUntilChanged(): Events<A> {
         val state: State<Any?> = holdState(Any())
         return filter { it != state.sample() }
@@ -647,18 +773,35 @@
      *
      * Note that the returned [Events] will not emit anything until [other] has emitted at least one
      * value.
+     *
+     * ``` kotlin
+     *   fun <A, B, C> Events<A>.sample(
+     *       other: Events<B>,
+     *       transform: TransactionScope.(A, B) -> C,
+     *   ): Events<C> {
+     *       val state = other.mapCheap { Maybe.present(it) }.holdState(Maybe.absent)
+     *       return sample(state) { a, b -> b.map { transform(a, it) } }.filterPresent()
+     *   }
+     * ```
      */
     fun <A, B, C> Events<A>.sample(
         other: Events<B>,
         transform: TransactionScope.(A, B) -> C,
     ): Events<C> {
-        val state = other.mapCheap { just(it) }.holdState(none)
-        return sample(state) { a, b -> b.map { transform(a, it) } }.filterJust()
+        val state = other.mapCheap { Maybe.present(it) }.holdState(Maybe.absent)
+        return sample(state) { a, b -> b.map { transform(a, it) } }.filterPresent()
     }
 
     /**
      * Returns a [State] that samples the [Transactional] held by the given [State] within the same
      * transaction that the state changes.
+     *
+     * ``` kotlin
+     *   fun <A> State<Transactional<A>>.sampleTransactionals(): State<A> =
+     *       changes
+     *           .sampleTransactionals()
+     *           .holdStateDeferred(deferredTransactionScope { sample().sample() })
+     * ```
      */
     fun <A> State<Transactional<A>>.sampleTransactionals(): State<A> =
         changes
@@ -668,6 +811,14 @@
     /**
      * Returns a [State] that transforms the value held inside this [State] by applying it to the
      * given function [transform].
+     *
+     * Note that this is less efficient than [State.map], which should be preferred if [transform]
+     * does not need access to [TransactionScope].
+     *
+     * ``` kotlin
+     *   fun <A, B> State<A>.mapTransactionally(transform: TransactionScope.(A) -> B): State<B> =
+     *       map { transactionally { transform(it) } }.sampleTransactionals()
+     * ```
      */
     fun <A, B> State<A>.mapTransactionally(transform: TransactionScope.(A) -> B): State<B> =
         map { transactionally { transform(it) } }.sampleTransactionals()
@@ -676,7 +827,20 @@
      * Returns a [State] whose value is generated with [transform] by combining the current values
      * of each given [State].
      *
-     * @see State.combineWithTransactionally
+     * Note that this is less efficient than [combine], which should be preferred if [transform]
+     * does not need access to [TransactionScope].
+     *
+     * ``` kotlin
+     *   fun <A, B, Z> combineTransactionally(
+     *       stateA: State<A>,
+     *       stateB: State<B>,
+     *       transform: TransactionScope.(A, B) -> Z,
+     *   ): State<Z> =
+     *       combine(stateA, stateB) { a, b -> transactionally { transform(a, b) } }
+     *           .sampleTransactionals()
+     * ```
+     *
+     * @see State.combineTransactionally
      */
     fun <A, B, Z> combineTransactionally(
         stateA: State<A>,
@@ -690,7 +854,10 @@
      * Returns a [State] whose value is generated with [transform] by combining the current values
      * of each given [State].
      *
-     * @see State.combineWithTransactionally
+     * Note that this is less efficient than [combine], which should be preferred if [transform]
+     * does not need access to [TransactionScope].
+     *
+     * @see State.combineTransactionally
      */
     fun <A, B, C, Z> combineTransactionally(
         stateA: State<A>,
@@ -705,7 +872,10 @@
      * Returns a [State] whose value is generated with [transform] by combining the current values
      * of each given [State].
      *
-     * @see State.combineWithTransactionally
+     * Note that this is less efficient than [combine], which should be preferred if [transform]
+     * does not need access to [TransactionScope].
+     *
+     * @see State.combineTransactionally
      */
     fun <A, B, C, D, Z> combineTransactionally(
         stateA: State<A>,
@@ -719,7 +889,18 @@
             }
             .sampleTransactionals()
 
-    /** Returns a [State] by applying [transform] to the value held by the original [State]. */
+    /**
+     * Returns a [State] by applying [transform] to the value held by the original [State].
+     *
+     * Note that this is less efficient than [flatMap], which should be preferred if [transform]
+     * does not need access to [TransactionScope].
+     *
+     * ``` kotlin
+     *   fun <A, B> State<A>.flatMapTransactionally(
+     *       transform: TransactionScope.(A) -> State<B>
+     *   ): State<B> = map { transactionally { transform(it) } }.sampleTransactionals().flatten()
+     * ```
+     */
     fun <A, B> State<A>.flatMapTransactionally(
         transform: TransactionScope.(A) -> State<B>
     ): State<B> = map { transactionally { transform(it) } }.sampleTransactionals().flatten()
@@ -728,7 +909,10 @@
      * Returns a [State] whose value is generated with [transform] by combining the current values
      * of each given [State].
      *
-     * @see State.combineWithTransactionally
+     * Note that this is less efficient than [combine], which should be preferred if [transform]
+     * does not need access to [TransactionScope].
+     *
+     * @see State.combineTransactionally
      */
     fun <A, Z> combineTransactionally(
         vararg states: State<A>,
@@ -739,7 +923,10 @@
      * Returns a [State] whose value is generated with [transform] by combining the current values
      * of each given [State].
      *
-     * @see State.combineWithTransactionally
+     * Note that this is less efficient than [combine], which should be preferred if [transform]
+     * does not need access to [TransactionScope].
+     *
+     * @see State.combineTransactionally
      */
     fun <A, Z> Iterable<State<A>>.combineTransactionally(
         transform: TransactionScope.(List<A>) -> Z
@@ -748,8 +935,13 @@
     /**
      * Returns a [State] by combining the values held inside the given [State]s by applying them to
      * the given function [transform].
+     *
+     * Note that this is less efficient than [combine], which should be preferred if [transform]
+     * does not need access to [TransactionScope].
      */
-    fun <A, B, C> State<A>.combineWithTransactionally(
+    @Suppress("INAPPLICABLE_JVM_NAME")
+    @JvmName(name = "combineStateTransactionally")
+    fun <A, B, C> State<A>.combineTransactionally(
         other: State<B>,
         transform: TransactionScope.(A, B) -> C,
     ): State<C> = combineTransactionally(this, other, transform)
@@ -757,6 +949,15 @@
     /**
      * Returns an [Incremental] that reflects the state of the original [Incremental], but also adds
      * / removes entries based on the state of the original's values.
+     *
+     * ``` kotlin
+     *   fun <K, V> Incremental<K, State<Maybe<V>>>.applyStateIncrementally(): Incremental<K, V> =
+     *       mapValues { (_, v) -> v.changes }
+     *           .mergeEventsIncrementallyPromptly()
+     *           .foldStateMapIncrementally(
+     *               deferredStateScope { sample().mapMaybeValues { (_, s) -> s.sample() } }
+     *           )
+     * ```
      */
     fun <K, V> Incremental<K, State<Maybe<V>>>.applyStateIncrementally(): Incremental<K, V> =
         mapValues { (_, v) -> v.changes }
@@ -769,6 +970,12 @@
      * Returns an [Incremental] that reflects the state of the original [Incremental], but also adds
      * / removes entries based on the [State] returned from applying [transform] to the original's
      * entries.
+     *
+     * ``` kotlin
+     *   fun <K, V, U> Incremental<K, V>.mapIncrementalState(
+     *       transform: KairosScope.(Map.Entry<K, V>) -> State<Maybe<U>>
+     *   ): Incremental<K, U> = mapValues { transform(it) }.applyStateIncrementally()
+     * ```
      */
     fun <K, V, U> Incremental<K, V>.mapIncrementalState(
         transform: KairosScope.(Map.Entry<K, V>) -> State<Maybe<U>>
@@ -778,16 +985,33 @@
      * Returns an [Incremental] that reflects the state of the original [Incremental], but also adds
      * / removes entries based on the [State] returned from applying [transform] to the original's
      * entries, such that entries are added when that state is `true`, and removed when `false`.
+     *
+     * ``` kotlin
+     *   fun <K, V> Incremental<K, V>.filterIncrementally(
+     *       transform: KairosScope.(Map.Entry<K, V>) -> State<Boolean>
+     *   ): Incremental<K, V> = mapIncrementalState { entry ->
+     *       transform(entry).map { if (it) Maybe.present(entry.value) else Maybe.absent }
+     *   }
+     * ```
      */
     fun <K, V> Incremental<K, V>.filterIncrementally(
         transform: KairosScope.(Map.Entry<K, V>) -> State<Boolean>
     ): Incremental<K, V> = mapIncrementalState { entry ->
-        transform(entry).map { if (it) just(entry.value) else none }
+        transform(entry).map { if (it) Maybe.present(entry.value) else Maybe.absent }
     }
 
     /**
      * Returns an [Incremental] that samples the [Transactionals][Transactional] held by the
      * original within the same transaction that the incremental [updates].
+     *
+     * ``` kotlin
+     *   fun <K, V> Incremental<K, Transactional<V>>.sampleTransactionals(): Incremental<K, V> =
+     *       updates
+     *           .map { patch -> patch.mapValues { (k, mv) -> mv.map { it.sample() } } }
+     *           .foldStateMapIncrementally(
+     *               deferredStateScope { sample().mapValues { (k, v) -> v.sample() } }
+     *           )
+     * ```
      */
     fun <K, V> Incremental<K, Transactional<V>>.sampleTransactionals(): Incremental<K, V> =
         updates
@@ -799,6 +1023,16 @@
     /**
      * Returns an [Incremental] that tracks the entries of the original incremental, but values
      * replaced with those obtained by applying [transform] to each original entry.
+     *
+     * Note that this is less efficient than [mapValues], which should be preferred if [transform]
+     * does not need access to [TransactionScope].
+     *
+     * ``` kotlin
+     *   fun <K, V, U> Incremental<K, V>.mapValuesTransactionally(
+     *       transform: TransactionScope.(Map.Entry<K, V>) -> U
+     *   ): Incremental<K, U> =
+     *       mapValues { transactionally { transform(it) } }.sampleTransactionals()
+     * ```
      */
     fun <K, V, U> Incremental<K, V>.mapValuesTransactionally(
         transform: TransactionScope.(Map.Entry<K, V>) -> U
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Switch.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Switch.kt
new file mode 100644
index 0000000..63e27d0
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Switch.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2025 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.kairos
+
+import com.android.systemui.kairos.internal.IncrementalImpl
+import com.android.systemui.kairos.internal.constInit
+import com.android.systemui.kairos.internal.init
+import com.android.systemui.kairos.internal.mapImpl
+import com.android.systemui.kairos.internal.switchDeferredImplSingle
+import com.android.systemui.kairos.internal.switchPromptImplSingle
+import com.android.systemui.kairos.util.mapPatchFromFullDiff
+
+/**
+ * Returns an [Events] that switches to the [Events] contained within this [State] whenever it
+ * changes.
+ *
+ * This switch does take effect until the *next* transaction after [State] changes. For a switch
+ * that takes effect immediately, see [switchEventsPromptly].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.switchEvents
+ */
+@ExperimentalKairosApi
+fun <A> State<Events<A>>.switchEvents(): Events<A> {
+    val patches =
+        mapImpl({ init.connect(this).changes }) { newEvents, _ -> newEvents.init.connect(this) }
+    return EventsInit(
+        constInit(
+            name = null,
+            switchDeferredImplSingle(
+                getStorage = {
+                    init.connect(this).getCurrentWithEpoch(this).first.init.connect(this)
+                },
+                getPatches = { patches },
+            ),
+        )
+    )
+}
+
+/**
+ * Returns an [Events] that switches to the [Events] contained within this [State] whenever it
+ * changes.
+ *
+ * This switch takes effect immediately within the same transaction that [State] changes. If the
+ * newly-switched-in [Events] is emitting a value within this transaction, then that value will be
+ * emitted from this switch. If not, but the previously-switched-in [Events] *is* emitting, then
+ * that value will be emitted from this switch instead. Otherwise, there will be no emission.
+ *
+ * In general, you should prefer [switchEvents] over this method. It is both safer and more
+ * performant.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.switchEventsPromptly
+ */
+// TODO: parameter to handle coincidental emission from both old and new
+@ExperimentalKairosApi
+fun <A> State<Events<A>>.switchEventsPromptly(): Events<A> {
+    val patches =
+        mapImpl({ init.connect(this).changes }) { newEvents, _ -> newEvents.init.connect(this) }
+    return EventsInit(
+        constInit(
+            name = null,
+            switchPromptImplSingle(
+                getStorage = {
+                    init.connect(this).getCurrentWithEpoch(this).first.init.connect(this)
+                },
+                getPatches = { patches },
+            ),
+        )
+    )
+}
+
+/** Returns an [Incremental] that behaves like current value of this [State]. */
+fun <K, V> State<Incremental<K, V>>.switchIncremental(): Incremental<K, V> {
+    val stateChangePatches =
+        transitions.mapNotNull { (old, new) ->
+            mapPatchFromFullDiff(old.sample(), new.sample()).takeIf { it.isNotEmpty() }
+        }
+    val innerChanges =
+        map { inner ->
+                merge(stateChangePatches, inner.updates) { switchPatch, upcomingPatch ->
+                    switchPatch + upcomingPatch
+                }
+            }
+            .switchEventsPromptly()
+    val flattened = flatten()
+    return IncrementalInit(
+        init("switchIncremental") {
+            val upstream = flattened.init.connect(this)
+            IncrementalImpl(
+                "switchIncremental",
+                "switchIncremental",
+                upstream.changes,
+                innerChanges.init.connect(this),
+                upstream.store,
+            )
+        }
+    )
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/ToColdFlow.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/ToColdFlow.kt
new file mode 100644
index 0000000..3d2768b
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/ToColdFlow.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2025 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.kairos
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.channelFlow
+import kotlinx.coroutines.flow.conflate
+
+/**
+ * Returns a cold [Flow] that, when collected, emits from this [Events]. [network] is needed to
+ * transactionally connect to / disconnect from the [Events] when collection starts/stops.
+ */
+@ExperimentalKairosApi
+fun <A> Events<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+    channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, emits from this [State]. [network] is needed to
+ * transactionally connect to / disconnect from the [State] when collection starts/stops.
+ */
+@ExperimentalKairosApi
+fun <A> State<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+    channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this
+ * [network], and then emits from the returned [Events].
+ *
+ * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up.
+ */
+@ExperimentalKairosApi
+@JvmName("eventsSpecToColdConflatedFlow")
+fun <A> BuildSpec<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+    channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this
+ * [network], and then emits from the returned [State].
+ *
+ * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up.
+ */
+@ExperimentalKairosApi
+@JvmName("stateSpecToColdConflatedFlow")
+fun <A> BuildSpec<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+    channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in
+ * this [network], and then emits from the returned [Events].
+ */
+@ExperimentalKairosApi
+@JvmName("transactionalFlowToColdConflatedFlow")
+fun <A> Transactional<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+    channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in
+ * this [network], and then emits from the returned [State].
+ */
+@ExperimentalKairosApi
+@JvmName("transactionalStateToColdConflatedFlow")
+fun <A> Transactional<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+    channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [Stateful] in a new transaction in this
+ * [network], and then emits from the returned [Events].
+ *
+ * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up.
+ */
+@ExperimentalKairosApi
+@JvmName("statefulFlowToColdConflatedFlow")
+fun <A> Stateful<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+    channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in
+ * this [network], and then emits from the returned [State].
+ *
+ * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up.
+ */
+@ExperimentalKairosApi
+@JvmName("statefulStateToColdConflatedFlow")
+fun <A> Stateful<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+    channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate()
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt
index 2254169..a5ac909 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt
@@ -16,11 +16,14 @@
 
 package com.android.systemui.kairos
 
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.These
+
 /**
  * Kairos operations that are available while a transaction is active.
  *
  * These operations do not accumulate state, which makes [TransactionScope] weaker than
- * [StateScope], but allows them to be used in more places.
+ * [StateScope], but allows it to be used in more places.
  */
 @ExperimentalKairosApi
 interface TransactionScope : KairosScope {
@@ -57,7 +60,7 @@
      */
     fun <A> deferredTransactionScope(block: TransactionScope.() -> A): DeferredValue<A>
 
-    /** An [Events] that emits once, within this transaction, and then never again. */
+    /** An [Events] that emits once, within the current transaction, and then never again. */
     val now: Events<Unit>
 
     /**
@@ -66,7 +69,7 @@
      *
      * @see sampleDeferred
      */
-    fun <A> State<A>.sample(): A = sampleDeferred().get()
+    fun <A> State<A>.sample(): A = sampleDeferred().value
 
     /**
      * Returns the current value held by this [Transactional]. Guaranteed to be consistent within
@@ -74,5 +77,55 @@
      *
      * @see sampleDeferred
      */
-    fun <A> Transactional<A>.sample(): A = sampleDeferred().get()
+    fun <A> Transactional<A>.sample(): A = sampleDeferred().value
 }
+
+/**
+ * Returns an [Events] that emits the value sampled from the [Transactional] produced by each
+ * emission of the original [Events], within the same transaction of the original emission.
+ */
+@ExperimentalKairosApi
+fun <A> Events<Transactional<A>>.sampleTransactionals(): Events<A> = map { it.sample() }
+
+/** @see TransactionScope.sample */
+@ExperimentalKairosApi
+fun <A, B, C> Events<A>.sample(
+    state: State<B>,
+    transform: TransactionScope.(A, B) -> C,
+): Events<C> = map { transform(it, state.sample()) }
+
+/** @see TransactionScope.sample */
+@ExperimentalKairosApi
+fun <A, B, C> Events<A>.sample(
+    sampleable: Transactional<B>,
+    transform: TransactionScope.(A, B) -> C,
+): Events<C> = map { transform(it, sampleable.sample()) }
+
+/**
+ * Like [sample], but if [state] is changing at the time it is sampled ([changes] is emitting), then
+ * the new value is passed to [transform].
+ *
+ * Note that [sample] is both more performant and safer to use with recursive definitions. You will
+ * generally want to use it rather than this.
+ *
+ * @see sample
+ */
+@ExperimentalKairosApi
+fun <A, B, C> Events<A>.samplePromptly(
+    state: State<B>,
+    transform: TransactionScope.(A, B) -> C,
+): Events<C> =
+    sample(state) { a, b -> These.first(a to b) }
+        .mergeWith(state.changes.map { These.second(it) }) { thiz, that ->
+            These.both((thiz as These.First).value, (that as These.Second).value)
+        }
+        .mapMaybe { these ->
+            when (these) {
+                // both present, transform the upstream value and the new value
+                is These.Both -> Maybe.present(transform(these.first.first, these.second))
+                // no upstream present, so don't perform the sample
+                is These.Second -> Maybe.absent()
+                // just the upstream, so transform the upstream and the old value
+                is These.First -> Maybe.present(transform(these.value.first, these.value.second))
+            }
+        }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt
index 9485cd21..cf98821 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt
@@ -29,8 +29,8 @@
  * it is "sampled", a new result may be produced.
  *
  * Because Kairos operates over an "idealized" model of Time that can be passed around as a data
- * type, [Transactional]s are guaranteed to produce the same result if queried multiple times at the
- * same (conceptual) time, in order to preserve _referential transparency_.
+ * type, [Transactionals][Transactional] are guaranteed to produce the same result if queried
+ * multiple times at the same (conceptual) time, in order to preserve _referential transparency_.
  */
 @ExperimentalKairosApi
 class Transactional<out A> internal constructor(internal val impl: State<TransactionalImpl<A>>) {
@@ -50,6 +50,10 @@
  * queried and used.
  *
  * Useful for recursive definitions.
+ *
+ * ``` kotlin
+ *   fun <A> DeferredValue<Transactional<A>>.defer() = deferredTransactional { get() }
+ * ```
  */
 @ExperimentalKairosApi
 fun <A> DeferredValue<Transactional<A>>.defer(): Transactional<A> = deferInline { unwrapped.value }
@@ -62,6 +66,10 @@
  * [value][Lazy.value] will be queried and used.
  *
  * Useful for recursive definitions.
+ *
+ * ``` kotlin
+ *   fun <A> Lazy<Transactional<A>>.defer() = deferredTransactional { value }
+ * ```
  */
 @ExperimentalKairosApi
 fun <A> Lazy<Transactional<A>>.defer(): Transactional<A> = deferInline { value }
@@ -89,7 +97,13 @@
 /**
  * Returns a [Transactional]. The passed [block] will be evaluated on demand at most once per
  * transaction; any subsequent sampling within the same transaction will receive a cached value.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.sampleTransactional
  */
 @ExperimentalKairosApi
 fun <A> transactionally(block: TransactionScope.() -> A): Transactional<A> =
     Transactional(stateOf(transactionalImpl { block() }))
+
+/** Returns a [Transactional] that, when queried, samples this [State]. */
+fun <A> State<A>.asTransactional(): Transactional<A> =
+    Transactional(map { TransactionalImpl.Const(CompletableLazy(it)) })
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt
index b20e77a..2f4c396 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.kairos.Events
 import com.android.systemui.kairos.EventsInit
 import com.android.systemui.kairos.GroupedEvents
+import com.android.systemui.kairos.KairosCoroutineScope
 import com.android.systemui.kairos.KairosNetwork
 import com.android.systemui.kairos.LocalNetwork
 import com.android.systemui.kairos.MutableEvents
@@ -33,20 +34,23 @@
 import com.android.systemui.kairos.groupByKey
 import com.android.systemui.kairos.init
 import com.android.systemui.kairos.internal.util.childScope
+import com.android.systemui.kairos.internal.util.invokeOnCancel
 import com.android.systemui.kairos.internal.util.launchImmediate
 import com.android.systemui.kairos.launchEffect
 import com.android.systemui.kairos.mergeLeft
 import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.Just
-import com.android.systemui.kairos.util.Maybe.None
-import com.android.systemui.kairos.util.just
+import com.android.systemui.kairos.util.Maybe.Absent
+import com.android.systemui.kairos.util.Maybe.Present
 import com.android.systemui.kairos.util.map
 import java.util.concurrent.atomic.AtomicReference
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CompletableJob
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
 import kotlinx.coroutines.cancel
 import kotlinx.coroutines.job
 
@@ -60,12 +64,8 @@
         LocalNetwork(network, coroutineScope, endSignal)
     }
 
-    override fun <T> events(
-        name: String?,
-        builder: suspend EventProducerScope<T>.() -> Unit,
-    ): Events<T> =
+    override fun <T> events(builder: suspend EventProducerScope<T>.() -> Unit): Events<T> =
         buildEvents(
-            name,
             constructEvents = { inputNode ->
                 val events = MutableEvents(network, inputNode)
                 events to
@@ -123,9 +123,9 @@
         val childScope = coroutineScope.childScope()
         lateinit var cancelHandle: DisposableHandle
         val handle = DisposableHandle {
-            subRef.getAndSet(None)?.let { output ->
-                cancelHandle.dispose()
-                if (output is Just) {
+            cancelHandle.dispose()
+            subRef.getAndSet(Absent)?.let { output ->
+                if (output is Present) {
                     @Suppress("DeferredResultUnused")
                     network.transaction("observeEffect cancelled") {
                         scheduleDeactivation(output.value)
@@ -139,14 +139,27 @@
         val outputNode =
             Output<A>(
                 context = coroutineContext,
-                onDeath = { subRef.set(None) },
+                onDeath = { subRef.set(Absent) },
                 onEmit = { output ->
-                    if (subRef.get() is Just) {
+                    if (subRef.get() is Present) {
                         // Not cancelled, safe to emit
                         val scope =
                             object : EffectScope, TransactionScope by this {
-                                override val effectCoroutineScope: CoroutineScope = childScope
-                                override val kairosNetwork: KairosNetwork = localNetwork
+                                override fun <R> async(
+                                    context: CoroutineContext,
+                                    start: CoroutineStart,
+                                    block: suspend KairosCoroutineScope.() -> R,
+                                ): Deferred<R> =
+                                    childScope.async(context, start) {
+                                        object : KairosCoroutineScope, CoroutineScope by this {
+                                                override val kairosNetwork: KairosNetwork
+                                                    get() = localNetwork
+                                            }
+                                            .block()
+                                    }
+
+                                override val kairosNetwork: KairosNetwork
+                                    get() = localNetwork
                             }
                         scope.block(output)
                     }
@@ -162,7 +175,7 @@
                 .activate(evalScope = stateScope.evalScope, outputNode.schedulable)
                 ?.let { (conn, needsEval) ->
                     outputNode.upstream = conn
-                    if (!subRef.compareAndSet(null, just(outputNode))) {
+                    if (!subRef.compareAndSet(null, Maybe.present(outputNode))) {
                         // Job's already been cancelled, schedule deactivation
                         scheduleDeactivation(outputNode)
                     } else if (needsEval) {
@@ -289,21 +302,15 @@
     }
 
     private fun mutableChildBuildScope(): BuildScopeImpl {
-        val stopEmitter = newStopEmitter("mutableChildBuildScope")
         val childScope = coroutineScope.childScope()
-        childScope.coroutineContext.job.invokeOnCompletion { stopEmitter.emit(Unit) }
-        // Ensure that once this transaction is done, the new child scope enters the completing
-        // state (kept alive so long as there are child jobs).
-        // TODO: need to keep the scope alive if it's used to accumulate state.
-        //  Otherwise, stopEmitter will emit early, due to the call to complete().
-        //        scheduleOutput(
-        //            OneShot {
-        //                // TODO: don't like this cast
-        //                (childScope.coroutineContext.job as CompletableJob).complete()
-        //            }
-        //        )
+        val stopEmitter = lazy {
+            newStopEmitter("mutableChildBuildScope").apply {
+                childScope.invokeOnCancel { emit(Unit) }
+            }
+        }
         return BuildScopeImpl(
-            stateScope = StateScopeImpl(evalScope = stateScope.evalScope, endSignal = stopEmitter),
+            stateScope =
+                StateScopeImpl(evalScope = stateScope.evalScope, endSignalLazy = stopEmitter),
             coroutineScope = childScope,
         )
     }
@@ -314,6 +321,7 @@
     coroutineScope: CoroutineScope,
 ) =
     BuildScopeImpl(
-        stateScope = StateScopeImpl(evalScope = this, endSignal = outerScope.endSignal),
+        stateScope =
+            StateScopeImpl(evalScope = this, endSignalLazy = outerScope.stateScope.endSignalLazy),
         coroutineScope,
     )
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt
index 9496b06..f86e761 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt
@@ -19,16 +19,14 @@
 import com.android.systemui.kairos.internal.store.Single
 import com.android.systemui.kairos.internal.store.SingletonMapK
 import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.Just
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.none
+import com.android.systemui.kairos.util.Maybe.Present
 
-internal inline fun <A> filterJustImpl(
+internal inline fun <A> filterPresentImpl(
     crossinline getPulse: EvalScope.() -> EventsImpl<Maybe<A>>
 ): EventsImpl<A> =
     DemuxImpl(
             mapImpl(getPulse) { maybeResult, _ ->
-                if (maybeResult is Just) {
+                if (maybeResult is Present) {
                     Single(maybeResult.value)
                 } else {
                     Single<A>()
@@ -43,6 +41,7 @@
     crossinline getPulse: EvalScope.() -> EventsImpl<A>,
     crossinline f: EvalScope.(A) -> Boolean,
 ): EventsImpl<A> {
-    val mapped = mapImpl(getPulse) { it, _ -> if (f(it)) just(it) else none }.cached()
-    return filterJustImpl { mapped }
+    val mapped =
+        mapImpl(getPulse) { it, _ -> if (f(it)) Maybe.present(it) else Maybe.absent }.cached()
+    return filterPresentImpl { mapped }
 }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt
index 8a3e01a..9b4778a 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt
@@ -17,11 +17,10 @@
 package com.android.systemui.kairos.internal
 
 import com.android.systemui.kairos.internal.store.StoreEntry
+import com.android.systemui.kairos.util.MapPatch
 import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.applyPatch
-import com.android.systemui.kairos.util.just
 import com.android.systemui.kairos.util.map
-import com.android.systemui.kairos.util.none
+import com.android.systemui.kairos.util.toMaybe
 
 internal class IncrementalImpl<K, out V>(
     name: String?,
@@ -48,12 +47,11 @@
     val store = StateSource(init)
     val maybeChanges =
         mapImpl(getPatches) { patch, _ ->
-                val (old, _) = store.getCurrentWithEpoch(evalScope = this)
-                val new = old.applyPatch(patch)
-                if (new != old) just(patch to new) else none
+                val (current, _) = store.getCurrentWithEpoch(evalScope = this)
+                current.applyPatchCalm(patch).toMaybe()
             }
             .cached()
-    val calm = filterJustImpl { maybeChanges }
+    val calm = filterPresentImpl { maybeChanges }
     val changes = mapImpl({ calm }) { (_, change), _ -> change }
     val patches = mapImpl({ calm }) { (patch, _), _ -> patch }
     evalScope.scheduleOutput(
@@ -70,22 +68,44 @@
     return IncrementalImpl(name, operatorName, changes, patches, store)
 }
 
+private fun <K, V> Map<K, V>.applyPatchCalm(
+    patch: MapPatch<K, V>
+): Pair<MapPatch<K, V>, Map<K, V>>? {
+    val current = this
+    val filteredPatch = mutableMapOf<K, Maybe<V>>()
+    val new = current.toMutableMap()
+    for ((key, change) in patch) {
+        when (change) {
+            is Maybe.Present -> {
+                if (key !in current || current.getValue(key) != change.value) {
+                    filteredPatch[key] = change
+                    new[key] = change.value
+                }
+            }
+            Maybe.Absent -> {
+                if (key in current) {
+                    filteredPatch[key] = change
+                    new.remove(key)
+                }
+            }
+        }
+    }
+    return if (filteredPatch.isNotEmpty()) filteredPatch to new else null
+}
+
 internal inline fun <K, V> EventsImpl<Map<K, Maybe<V>>>.calmUpdates(
     state: StateDerived<Map<K, V>>
 ): Pair<EventsImpl<Map<K, Maybe<V>>>, EventsImpl<Map<K, V>>> {
     val maybeUpdate =
         mapImpl({ this@calmUpdates }) { patch, _ ->
                 val (current, _) = state.getCurrentWithEpoch(evalScope = this)
-                val new = current.applyPatch(patch)
-                if (new != current) {
-                    state.setCacheFromPush(new, epoch)
-                    just(patch to new)
-                } else {
-                    none
-                }
+                current
+                    .applyPatchCalm(patch)
+                    ?.also { (_, newMap) -> state.setCacheFromPush(newMap, epoch) }
+                    .toMaybe()
             }
             .cached()
-    val calm = filterJustImpl { maybeUpdate }
+    val calm = filterPresentImpl { maybeUpdate }
     val patches = mapImpl({ calm }) { (p, _), _ -> p }
     val changes = mapImpl({ calm }) { (_, s), _ -> s }
     return patches to changes
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt
index 640c561..4fa1070 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt
@@ -17,8 +17,6 @@
 package com.android.systemui.kairos.internal
 
 import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.none
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 
 /** Performs actions once, when the reactive component is first connected to the network. */
@@ -44,9 +42,9 @@
     @OptIn(ExperimentalCoroutinesApi::class)
     fun getUnsafe(): Maybe<A> =
         if (cache.isInitialized()) {
-            just(cache.value.second)
+            Maybe.present(cache.value.second)
         } else {
-            none
+            Maybe.absent
         }
 }
 
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt
index cf74f75..c11eb12 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt
@@ -28,14 +28,13 @@
 import com.android.systemui.kairos.internal.util.logDuration
 import com.android.systemui.kairos.internal.util.logLn
 import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.Just
-import com.android.systemui.kairos.util.Maybe.None
+import com.android.systemui.kairos.util.Maybe.Absent
+import com.android.systemui.kairos.util.Maybe.Present
 import com.android.systemui.kairos.util.These
 import com.android.systemui.kairos.util.flatMap
 import com.android.systemui.kairos.util.getMaybe
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.maybeThat
-import com.android.systemui.kairos.util.maybeThis
+import com.android.systemui.kairos.util.maybeFirst
+import com.android.systemui.kairos.util.maybeSecond
 import com.android.systemui.kairos.util.merge
 import com.android.systemui.kairos.util.orError
 import com.android.systemui.kairos.util.these
@@ -133,8 +132,8 @@
         val removes = mutableListOf<K>()
         patch.forEach { (k, newUpstream) ->
             when (newUpstream) {
-                is Just -> adds.add(k to newUpstream.value)
-                None -> removes.add(k)
+                is Present -> adds.add(k to newUpstream.value)
+                Absent -> removes.add(k)
             }
         }
 
@@ -282,7 +281,8 @@
     crossinline getStorage: EvalScope.() -> EventsImpl<A>,
     crossinline getPatches: EvalScope.() -> EventsImpl<EventsImpl<A>>,
 ): EventsImpl<A> {
-    val patches = mapImpl(getPatches) { newEvents, _ -> singleOf(just(newEvents)).asIterable() }
+    val patches =
+        mapImpl(getPatches) { newEvents, _ -> singleOf(Maybe.present(newEvents)).asIterable() }
     val switchDeferredImpl =
         switchDeferredImpl(
             name = name,
@@ -402,8 +402,8 @@
 ): EventsImpl<These<A, B>> {
     val storage =
         listOf(
-                mapImpl(getPulse) { it, _ -> These.thiz(it) },
-                mapImpl(getOther) { it, _ -> These.that(it) },
+                mapImpl(getPulse) { it, _ -> These.first(it) },
+                mapImpl(getOther) { it, _ -> These.second(it) },
             )
             .asIterableWithIndex()
     val switchNode =
@@ -417,9 +417,9 @@
         mapImpl({ switchNode }) { it, logIndent ->
             val mergeResults = it.asArrayHolder()
             val first =
-                mergeResults.getMaybe(0).flatMap { it.getPushEvent(logIndent, this).maybeThis() }
+                mergeResults.getMaybe(0).flatMap { it.getPushEvent(logIndent, this).maybeFirst() }
             val second =
-                mergeResults.getMaybe(1).flatMap { it.getPushEvent(logIndent, this).maybeThat() }
+                mergeResults.getMaybe(1).flatMap { it.getPushEvent(logIndent, this).maybeSecond() }
             these(first, second).orError { "unexpected missing merge result" }
         }
     return merged.cached()
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt
index 32aef5c..cb2c6e5 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt
@@ -24,9 +24,8 @@
 import com.android.systemui.kairos.internal.util.hashString
 import com.android.systemui.kairos.internal.util.logDuration
 import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.Just
-import com.android.systemui.kairos.util.Maybe.None
-import com.android.systemui.kairos.util.just
+import com.android.systemui.kairos.util.Maybe.Absent
+import com.android.systemui.kairos.util.Maybe.Present
 
 internal class MuxPromptNode<W, K, V>(
     val name: String?,
@@ -94,8 +93,8 @@
         val removes = mutableListOf<K>()
         patch.forEach { (k, newUpstream) ->
             when (newUpstream) {
-                is Just -> adds.add(k to newUpstream.value)
-                None -> removes.add(k)
+                is Present -> adds.add(k to newUpstream.value)
+                Absent -> removes.add(k)
             }
         }
 
@@ -311,7 +310,9 @@
         switchPromptImpl(
             getStorage = { singleOf(getStorage()).asIterable() },
             getPatches = {
-                mapImpl(getPatches) { newEvents, _ -> singleOf(just(newEvents)).asIterable() }
+                mapImpl(getPatches) { newEvents, _ ->
+                    singleOf(Maybe.present(newEvents)).asIterable()
+                }
             },
             storeFactory = SingletonMapK.Factory(),
         )
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt
index fbc2b364..6e86dd1 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt
@@ -21,9 +21,7 @@
 import com.android.systemui.kairos.internal.util.logDuration
 import com.android.systemui.kairos.internal.util.logLn
 import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.Just
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.none
+import com.android.systemui.kairos.util.Maybe.Present
 import java.util.concurrent.atomic.AtomicLong
 import kotlin.coroutines.ContinuationInterceptor
 import kotlin.time.measureTime
@@ -33,6 +31,7 @@
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.isActive
 import kotlinx.coroutines.job
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.sync.Mutex
@@ -148,6 +147,10 @@
     /** Evaluates [block] inside of a new transaction when the network is ready. */
     fun <R> transaction(reason: String, block: suspend EvalScope.() -> R): Deferred<R> =
         CompletableDeferred<R>(parent = coroutineScope.coroutineContext.job).also { onResult ->
+            if (!coroutineScope.isActive) {
+                onResult.cancel()
+                return@also
+            }
             val job =
                 coroutineScope.launch {
                     inputScheduleChan.send(
@@ -261,25 +264,25 @@
     private val onResult: CompletableDeferred<T>? = null,
     private val onStartTransaction: suspend EvalScope.() -> T,
 ) {
-    private var result: Maybe<T> = none
+    private var result: Maybe<T> = Maybe.absent
 
     suspend fun started(evalScope: EvalScope) {
-        result = just(onStartTransaction(evalScope))
+        result = Maybe.present(onStartTransaction(evalScope))
     }
 
     fun fail(ex: Exception) {
-        result = none
+        result = Maybe.absent
         onResult?.completeExceptionally(ex)
     }
 
     fun completed() {
         if (onResult != null) {
             when (val result = result) {
-                is Just -> onResult.complete(result.value)
+                is Present -> onResult.complete(result.value)
                 else -> {}
             }
         }
-        result = none
+        result = Maybe.absent
     }
 }
 
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt
index 46127cb2..da83258 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt
@@ -22,8 +22,6 @@
 import com.android.systemui.kairos.internal.store.StoreEntry
 import com.android.systemui.kairos.internal.util.hashString
 import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.none
 
 internal open class StateImpl<out A>(
     val name: String?,
@@ -73,7 +71,7 @@
 
     fun getCachedUnsafe(): Maybe<A> {
         @Suppress("UNCHECKED_CAST")
-        return if (cache == EmptyCache) none else just(cache as A)
+        return if (cache == EmptyCache) Maybe.absent else Maybe.present(cache as A)
     }
 
     protected abstract fun recalc(evalScope: EvalScope): Pair<A, Long>?
@@ -117,7 +115,8 @@
 
     override fun toString(): String = "StateImpl(current=$_current, writeEpoch=$writeEpoch)"
 
-    fun getStorageUnsafe(): Maybe<S> = if (_current.isInitialized()) just(_current.value) else none
+    fun getStorageUnsafe(): Maybe<S> =
+        if (_current.isInitialized()) Maybe.present(_current.value) else Maybe.absent
 }
 
 internal fun <A> constState(name: String?, operatorName: String, init: A): StateImpl<A> =
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt
index bd1f94f..53a704a 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt
@@ -36,10 +36,14 @@
 import com.android.systemui.kairos.util.Maybe
 import com.android.systemui.kairos.util.map
 
-internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal: Events<Any>) :
+internal class StateScopeImpl(val evalScope: EvalScope, val endSignalLazy: Lazy<Events<Any>>) :
     InternalStateScope, EvalScope by evalScope {
 
-    override val endSignalOnce: Events<Any> = endSignal.nextOnlyInternal("StateScope.endSignal")
+    override val endSignal: Events<Any> by endSignalLazy
+
+    override val endSignalOnce: Events<Any> by lazy {
+        endSignal.nextOnlyInternal("StateScope.endSignal")
+    }
 
     override fun <A> deferredStateScope(block: StateScope.() -> A): DeferredValue<A> =
         DeferredValue(deferAsync { block() })
@@ -119,7 +123,7 @@
     }
 
     override fun childStateScope(newEnd: Events<Any>) =
-        StateScopeImpl(evalScope, merge(newEnd, endSignal))
+        StateScopeImpl(evalScope, lazy { merge(newEnd, endSignal) })
 
     private fun <A> Events<A>.truncateToScope(operatorName: String): Events<A> =
         if (endSignalOnce === emptyEvents) {
@@ -165,4 +169,4 @@
 }
 
 private fun EvalScope.reenterStateScope(outerScope: StateScopeImpl) =
-    StateScopeImpl(evalScope = this, endSignal = outerScope.endSignal)
+    StateScopeImpl(evalScope = this, endSignalLazy = outerScope.endSignalLazy)
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt
index 9b6940d..c34e67e 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt
@@ -17,8 +17,7 @@
 package com.android.systemui.kairos.internal.util
 
 import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.None
-import com.android.systemui.kairos.util.just
+import com.android.systemui.kairos.util.Maybe.Absent
 import java.util.concurrent.ConcurrentHashMap
 
 private object NULL
@@ -32,7 +31,7 @@
 
     @Suppress("UNCHECKED_CAST")
     operator fun <A> get(key: Key<A>): Maybe<A> =
-        store[key]?.let { just((if (it === NULL) null else it) as A) } ?: None
+        store[key]?.let { Maybe.present((if (it === NULL) null else it) as A) } ?: Absent
 
     operator fun <A> set(key: Key<A>, value: A) {
         store[key] = value ?: NULL
@@ -57,7 +56,7 @@
 
     @Suppress("UNCHECKED_CAST")
     fun <A> remove(key: Key<A>): Maybe<A> =
-        store.remove(key)?.let { just((if (it === NULL) null else it) as A) } ?: None
+        store.remove(key)?.let { Maybe.present((if (it === NULL) null else it) as A) } ?: Absent
 
     @Suppress("UNCHECKED_CAST")
     fun <A> getOrPut(key: Key<A>, defaultValue: () -> A): A =
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt
index 466a9f8..d2a169c 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt
@@ -112,7 +112,7 @@
     }
 }
 
-internal fun CoroutineScope.launchOnCancel(
+internal fun CoroutineScope.invokeOnCancel(
     context: CoroutineContext = EmptyCoroutineContext,
     block: () -> Unit,
 ): Job =
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt
index 957d46f..9f17d56 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt
@@ -18,100 +18,118 @@
 
 package com.android.systemui.kairos.util
 
-import com.android.systemui.kairos.util.Either.Left
-import com.android.systemui.kairos.util.Either.Right
+import com.android.systemui.kairos.util.Either.First
+import com.android.systemui.kairos.util.Either.Second
 
 /**
- * Contains a value of two possibilities: `Left<A>` or `Right<B>`
+ * Contains a value of two possibilities: `First<A>` or `Second<B>`
  *
  * [Either] generalizes sealed classes the same way that [Pair] generalizes data classes; if a
  * [Pair] is effectively an anonymous grouping of two instances, then an [Either] is an anonymous
  * set of two options.
  */
 sealed interface Either<out A, out B> {
-    /** An [Either] that contains a [Left] value. */
-    @JvmInline value class Left<out A>(val value: A) : Either<A, Nothing>
+    /** An [Either] that contains a [First] value. */
+    @JvmInline value class First<out A>(val value: A) : Either<A, Nothing>
 
-    /** An [Either] that contains a [Right] value. */
-    @JvmInline value class Right<out B>(val value: B) : Either<Nothing, B>
+    /** An [Either] that contains a [Second] value. */
+    @JvmInline value class Second<out B>(val value: B) : Either<Nothing, B>
+
+    companion object {
+        /** Constructs an [Either] containing the first possibility. */
+        fun <A> first(value: A): Either<A, Nothing> = First(value)
+
+        /** Constructs a [Either] containing the second possibility. */
+        fun <B> second(value: B): Either<Nothing, B> = Second(value)
+    }
 }
 
 /**
- * Returns an [Either] containing the result of applying [transform] to the [Left] value, or the
- * [Right] value unchanged.
+ * Returns an [Either] containing the result of applying [transform] to the [First] value, or the
+ * [Second] value unchanged.
  */
-inline fun <A, B, C> Either<A, C>.mapLeft(transform: (A) -> B): Either<B, C> =
+inline fun <A, B, C> Either<A, C>.mapFirst(transform: (A) -> B): Either<B, C> =
     when (this) {
-        is Left -> Left(transform(value))
-        is Right -> this
+        is First -> First(transform(value))
+        is Second -> this
     }
 
 /**
- * Returns an [Either] containing the result of applying [transform] to the [Right] value, or the
- * [Left] value unchanged.
+ * Returns an [Either] containing the result of applying [transform] to the [Second] value, or the
+ * [First] value unchanged.
  */
-inline fun <A, B, C> Either<A, B>.mapRight(transform: (B) -> C): Either<A, C> =
+inline fun <A, B, C> Either<A, B>.mapSecond(transform: (B) -> C): Either<A, C> =
     when (this) {
-        is Left -> this
-        is Right -> Right(transform(value))
+        is First -> this
+        is Second -> Second(transform(value))
     }
 
-/** Returns a [Maybe] containing the [Left] value held by this [Either], if present. */
-inline fun <A> Either<A, *>.leftMaybe(): Maybe<A> =
+/** Returns a [Maybe] containing the [First] value held by this [Either], if present. */
+inline fun <A> Either<A, *>.firstMaybe(): Maybe<A> =
     when (this) {
-        is Left -> just(value)
-        else -> none
+        is First -> Maybe.present(value)
+        else -> Maybe.absent
     }
 
-/** Returns the [Left] value held by this [Either], or `null` if this is a [Right] value. */
-inline fun <A> Either<A, *>.leftOrNull(): A? =
+/** Returns the [First] value held by this [Either], or `null` if this is a [Second] value. */
+inline fun <A> Either<A, *>.firstOrNull(): A? =
     when (this) {
-        is Left -> value
+        is First -> value
         else -> null
     }
 
-/** Returns a [Maybe] containing the [Right] value held by this [Either], if present. */
-inline fun <B> Either<*, B>.rightMaybe(): Maybe<B> =
+/** Returns a [Maybe] containing the [Second] value held by this [Either], if present. */
+inline fun <B> Either<*, B>.secondMaybe(): Maybe<B> =
     when (this) {
-        is Right -> just(value)
-        else -> none
+        is Second -> Maybe.present(value)
+        else -> Maybe.absent
     }
 
-/** Returns the [Right] value held by this [Either], or `null` if this is a [Left] value. */
-inline fun <B> Either<*, B>.rightOrNull(): B? =
+/** Returns the [Second] value held by this [Either], or `null` if this is a [First] value. */
+inline fun <B> Either<*, B>.secondOrNull(): B? =
     when (this) {
-        is Right -> value
+        is Second -> value
         else -> null
     }
 
 /**
- * Partitions this sequence of [Either] into two lists; [Pair.first] contains all [Left] values, and
- * [Pair.second] contains all [Right] values.
+ * Returns a [These] containing either the [First] value as [These.first], or the [Second] value as
+ * [These.second]. Will never return a [These.both].
+ */
+fun <A, B> Either<A, B>.asThese(): These<A, B> =
+    when (this) {
+        is Second -> These.second(value)
+        is First -> These.first(value)
+    }
+
+/**
+ * Partitions this sequence of [Either] into two lists; [Pair.first] contains all [First] values,
+ * and [Pair.second] contains all [Second] values.
  */
 fun <A, B> Sequence<Either<A, B>>.partitionEithers(): Pair<List<A>, List<B>> {
-    val lefts = mutableListOf<A>()
-    val rights = mutableListOf<B>()
+    val firsts = mutableListOf<A>()
+    val seconds = mutableListOf<B>()
     for (either in this) {
         when (either) {
-            is Left -> lefts.add(either.value)
-            is Right -> rights.add(either.value)
+            is First -> firsts.add(either.value)
+            is Second -> seconds.add(either.value)
         }
     }
-    return lefts to rights
+    return firsts to seconds
 }
 
 /**
- * Partitions this map of [Either] values into two maps; [Pair.first] contains all [Left] values,
- * and [Pair.second] contains all [Right] values.
+ * Partitions this map of [Either] values into two maps; [Pair.first] contains all [First] values,
+ * and [Pair.second] contains all [Second] values.
  */
 fun <K, A, B> Map<K, Either<A, B>>.partitionEithers(): Pair<Map<K, A>, Map<K, B>> {
-    val lefts = mutableMapOf<K, A>()
-    val rights = mutableMapOf<K, B>()
+    val firsts = mutableMapOf<K, A>()
+    val seconds = mutableMapOf<K, B>()
     for ((k, e) in this) {
         when (e) {
-            is Left -> lefts[k] = e.value
-            is Right -> rights[k] = e.value
+            is First -> firsts[k] = e.value
+            is Second -> seconds[k] = e.value
         }
     }
-    return lefts to rights
+    return firsts to seconds
 }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt
index f368cbf..8fe41bc 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.kairos.util
 
-import com.android.systemui.kairos.util.Either.Left
-import com.android.systemui.kairos.util.Either.Right
-import com.android.systemui.kairos.util.Maybe.Just
+import com.android.systemui.kairos.util.Either.First
+import com.android.systemui.kairos.util.Either.Second
+import com.android.systemui.kairos.util.Maybe.Present
 
 /** A "patch" that can be used to batch-update a [Map], via [applyPatch]. */
 typealias MapPatch<K, V> = Map<K, Maybe<V>>
@@ -27,16 +27,16 @@
  * Returns a new [Map] that has [patch] applied to the original map.
  *
  * For each entry in [patch]:
- * * a [Just] value will be included in the new map, replacing the entry in the original map with
+ * * a [Present] value will be included in the new map, replacing the entry in the original map with
  *   the same key, if present.
- * * a [Maybe.None] value will be omitted from the new map, excluding the entry in the original map
- *   with the same key, if present.
+ * * a [Maybe.Absent] value will be omitted from the new map, excluding the entry in the original
+ *   map with the same key, if present.
  */
 fun <K, V> Map<K, V>.applyPatch(patch: MapPatch<K, V>): Map<K, V> {
     val (adds: List<Pair<K, V>>, removes: List<K>) =
         patch
             .asSequence()
-            .map { (k, v) -> if (v is Just) Left(k to v.value) else Right(k) }
+            .map { (k, v) -> if (v is Present) First(k to v.value) else Second(k) }
             .partitionEithers()
     val removed: Map<K, V> = this - removes.toSet()
     val updated: Map<K, V> = removed + adds
@@ -47,11 +47,11 @@
  * Returns a [MapPatch] that, when applied, includes all of the values from the original [Map].
  *
  * Shorthand for:
- * ```kotlin
- * mapValues { just(it.value) }
+ * ``` kotlin
+ *   mapValues { (key, value) -> Maybe.present(value) }
  * ```
  */
-fun <K, V> Map<K, V>.toMapPatch(): MapPatch<K, V> = mapValues { just(it.value) }
+fun <K, V> Map<K, V>.toMapPatch(): MapPatch<K, V> = mapValues { Maybe.present(it.value) }
 
 /**
  * Returns a [MapPatch] that, when applied, includes all of the entries from [new] whose keys are
@@ -67,10 +67,10 @@
     val adds = new - old.keys
     return buildMap {
         for (removed in removes) {
-            put(removed, none)
+            put(removed, Maybe.absent)
         }
         for ((newKey, newValue) in adds) {
-            put(newKey, just(newValue))
+            put(newKey, Maybe.present(newValue))
         }
     }
 }
@@ -86,13 +86,16 @@
  */
 fun <K, V> mapPatchFromFullDiff(old: Map<K, V>, new: Map<K, V>): MapPatch<K, V> {
     val removes = old.keys - new.keys
-    val adds = new.mapMaybeValues { (k, v) -> if (k in old && v == old[k]) none else just(v) }
+    val adds =
+        new.mapMaybeValues { (k, v) ->
+            if (k in old && v == old[k]) Maybe.absent else Maybe.present(v)
+        }
     return hashMapOf<K, Maybe<V>>().apply {
         for (removed in removes) {
-            put(removed, none)
+            put(removed, Maybe.absent)
         }
         for ((newKey, newValue) in adds) {
-            put(newKey, just(newValue))
+            put(newKey, Maybe.present(newValue))
         }
     }
 }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt
index 681218399..4754bc4 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt
@@ -18,8 +18,8 @@
 
 package com.android.systemui.kairos.util
 
-import com.android.systemui.kairos.util.Maybe.Just
-import com.android.systemui.kairos.util.Maybe.None
+import com.android.systemui.kairos.util.Maybe.Absent
+import com.android.systemui.kairos.util.Maybe.Present
 import kotlin.coroutines.Continuation
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.EmptyCoroutineContext
@@ -31,17 +31,28 @@
 /** Represents a value that may or may not be present. */
 sealed interface Maybe<out A> {
     /** A [Maybe] value that is present. */
-    @JvmInline value class Just<out A> internal constructor(val value: A) : Maybe<A>
+    @JvmInline value class Present<out A> internal constructor(val value: A) : Maybe<A>
 
     /** A [Maybe] value that is not present. */
-    data object None : Maybe<Nothing>
+    data object Absent : Maybe<Nothing>
+
+    companion object {
+        /** Returns a [Maybe] containing [value]. */
+        fun <A> present(value: A): Maybe<A> = Present(value)
+
+        /** A [Maybe] that is not present. */
+        val absent: Maybe<Nothing> = Absent
+
+        /** A [Maybe] that is not present. */
+        inline fun <A> absent(): Maybe<A> = Absent
+    }
 }
 
 /** Utilities to query [Maybe] instances from within a [maybe] block. */
 @RestrictsSuspension
 object MaybeScope {
     suspend operator fun <A> Maybe<A>.not(): A = suspendCoroutine { k ->
-        if (this is Just) k.resume(value)
+        if (this is Present) k.resume(value)
     }
 
     suspend inline fun guard(crossinline block: () -> Boolean): Unit = suspendCoroutine { k ->
@@ -53,7 +64,8 @@
  * Returns a [Maybe] value produced by evaluating [block].
  *
  * [block] can use its [MaybeScope] receiver to query other [Maybe] values, automatically cancelling
- * execution of [block] and producing [None] when attempting to query a [Maybe] that is not present.
+ * execution of [block] and producing [Absent] when attempting to query a [Maybe] that is not
+ * present.
  *
  * This can be used instead of Kotlin's built-in nullability (`?.` and `?:`) operators when dealing
  * with complex combinations of nullables:
@@ -68,33 +80,30 @@
  * ```
  */
 fun <A> maybe(block: suspend MaybeScope.() -> A): Maybe<A> {
-    var maybeResult: Maybe<A> = None
+    var maybeResult: Maybe<A> = Absent
     val k =
         object : Continuation<A> {
             override val context: CoroutineContext = EmptyCoroutineContext
 
             override fun resumeWith(result: Result<A>) {
-                maybeResult = result.getOrNull()?.let { just(it) } ?: None
+                maybeResult = result.getOrNull()?.let { Maybe.present(it) } ?: Absent
             }
         }
     block.startCoroutine(MaybeScope, k)
     return maybeResult
 }
 
-/** Returns a [Just] containing this value, or [None] if `null`. */
+/** Returns a [Maybe] containing this value if it is not `null`. */
 inline fun <A> (A?).toMaybe(): Maybe<A> = maybe(this)
 
-/** Returns a [Just] containing a non-null [value], or [None] if `null`. */
-inline fun <A> maybe(value: A?): Maybe<A> = value?.let(::just) ?: None
+/** Returns a [Maybe] containing [value] if it is not `null`. */
+inline fun <A> maybe(value: A?): Maybe<A> = value?.let { Maybe.present(it) } ?: Absent
 
-/** Returns a [Just] containing [value]. */
-fun <A> just(value: A): Maybe<A> = Just(value)
+/** Returns a [Maybe] that is absent. */
+fun <A> maybeOf(): Maybe<A> = Absent
 
-/** A [Maybe] that is not present. */
-val none: Maybe<Nothing> = None
-
-/** A [Maybe] that is not present. */
-inline fun <A> none(): Maybe<A> = None
+/** Returns a [Maybe] containing [value]. */
+fun <A> maybeOf(value: A): Maybe<A> = Present(value)
 
 /** Returns the value present in this [Maybe], or `null` if not present. */
 inline fun <A> Maybe<A>.orNull(): A? = orElse(null)
@@ -105,22 +114,22 @@
  */
 inline fun <A, B> Maybe<A>.map(transform: (A) -> B): Maybe<B> =
     when (this) {
-        is Just -> just(transform(value))
-        is None -> None
+        is Present -> Maybe.present(transform(value))
+        is Absent -> Absent
     }
 
 /** Returns the result of applying [transform] to the value in the original [Maybe]. */
 inline fun <A, B> Maybe<A>.flatMap(transform: (A) -> Maybe<B>): Maybe<B> =
     when (this) {
-        is Just -> transform(value)
-        is None -> None
+        is Present -> transform(value)
+        is Absent -> Absent
     }
 
 /** Returns the value present in this [Maybe], or the result of [defaultValue] if not present. */
 inline fun <A> Maybe<A>.orElseGet(defaultValue: () -> A): A =
     when (this) {
-        is Just -> value
-        is None -> defaultValue()
+        is Present -> value
+        is Absent -> defaultValue()
     }
 
 /**
@@ -132,8 +141,8 @@
 /** Returns the value present in this [Maybe], or [defaultValue] if not present. */
 inline fun <A> Maybe<A>.orElse(defaultValue: A): A =
     when (this) {
-        is Just -> value
-        is None -> defaultValue
+        is Present -> value
+        is Absent -> defaultValue
     }
 
 /**
@@ -142,15 +151,16 @@
  */
 inline fun <A> Maybe<A>.filter(predicate: (A) -> Boolean): Maybe<A> =
     when (this) {
-        is Just -> if (predicate(value)) this else None
+        is Present -> if (predicate(value)) this else Absent
         else -> this
     }
 
 /** Returns a [List] containing all values that are present in this [Iterable]. */
-fun <A> Iterable<Maybe<A>>.filterJust(): List<A> = asSequence().filterJust().toList()
+fun <A> Iterable<Maybe<A>>.filterPresent(): List<A> = asSequence().filterPresent().toList()
 
 /** Returns a [List] containing all values that are present in this [Sequence]. */
-fun <A> Sequence<Maybe<A>>.filterJust(): Sequence<A> = filterIsInstance<Just<A>>().map { it.value }
+fun <A> Sequence<Maybe<A>>.filterPresent(): Sequence<A> =
+    filterIsInstance<Present<A>>().map { it.value }
 
 // Align
 
@@ -160,23 +170,25 @@
  */
 inline fun <A, B, C> Maybe<A>.alignWith(other: Maybe<B>, transform: (These<A, B>) -> C): Maybe<C> =
     when (this) {
-        is Just -> {
+        is Present -> {
             val a = value
             when (other) {
-                is Just -> {
+                is Present -> {
                     val b = other.value
-                    just(transform(These.both(a, b)))
+                    Maybe.present(transform(These.both(a, b)))
                 }
-                None -> just(transform(These.thiz(a)))
+
+                Absent -> Maybe.present(transform(These.first(a)))
             }
         }
-        None ->
+        Absent ->
             when (other) {
-                is Just -> {
+                is Present -> {
                     val b = other.value
-                    just(transform(These.that(b)))
+                    Maybe.present(transform(These.second(b)))
                 }
-                None -> none
+
+                Absent -> Maybe.absent
             }
     }
 
@@ -190,7 +202,7 @@
  */
 inline fun <A> Maybe<A>.orElseGetMaybe(other: () -> Maybe<A>): Maybe<A> =
     when (this) {
-        is Just -> this
+        is Present -> this
         else -> other()
     }
 
@@ -235,7 +247,7 @@
 inline fun <A, B> Iterable<A>.mapMaybe(transform: (A) -> Maybe<B>): List<B> = buildList {
     for (a in this@mapMaybe) {
         val result = transform(a)
-        if (result is Just) {
+        if (result is Present) {
             add(result.value)
         }
     }
@@ -246,7 +258,7 @@
  * the original sequence.
  */
 fun <A, B> Sequence<A>.mapMaybe(transform: (A) -> Maybe<B>): Sequence<B> =
-    map(transform).filterIsInstance<Just<B>>().map { it.value }
+    map(transform).filterIsInstance<Present<B>>().map { it.value }
 
 /**
  * Returns a map with values of only the present results of applying [transform] to each entry in
@@ -256,14 +268,14 @@
     buildMap {
         for (entry in this@mapMaybeValues) {
             val result = transform(entry)
-            if (result is Just) {
+            if (result is Present) {
                 put(entry.key, result.value)
             }
         }
     }
 
 /** Returns a map with all non-present values filtered out. */
-fun <K, A> Map<K, Maybe<A>>.filterJustValues(): Map<K, A> =
+fun <K, A> Map<K, Maybe<A>>.filterPresentValues(): Map<K, A> =
     asSequence().mapMaybe { (key, mValue) -> mValue.map { key to it } }.toMap()
 
 /**
@@ -277,9 +289,9 @@
 fun <K, V> Map<K, V>.getMaybe(key: K): Maybe<V> {
     val value = get(key)
     if (value == null && !containsKey(key)) {
-        return none
+        return Maybe.absent
     } else {
         @Suppress("UNCHECKED_CAST")
-        return just(value as V)
+        return Maybe.present(value as V)
     }
 }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt
index 092dca4..fc7b1e0 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt
@@ -16,28 +16,28 @@
 
 package com.android.systemui.kairos.util
 
-import com.android.systemui.kairos.util.Maybe.Just
+import com.android.systemui.kairos.util.Maybe.Present
 
 /** Contains at least one of two potential values. */
 sealed class These<out A, out B> {
-    /** Contains a single potential value. */
-    class This<A, B> internal constructor(val thiz: A) : These<A, B>()
+    /** A [These] that contains a [First] value. */
+    class First<A, B> internal constructor(val value: A) : These<A, B>()
 
-    /** Contains a single potential value. */
-    class That<A, B> internal constructor(val that: B) : These<A, B>()
+    /** A [These] that contains a [Second] value. */
+    class Second<A, B> internal constructor(val value: B) : These<A, B>()
 
-    /** Contains both potential values. */
-    class Both<A, B> internal constructor(val thiz: A, val that: B) : These<A, B>()
+    /** A [These] that contains [Both] a [first] and [second] value. */
+    class Both<A, B> internal constructor(val first: A, val second: B) : These<A, B>()
 
     companion object {
-        /** Constructs a [These] containing only [thiz]. */
-        fun <A> thiz(thiz: A): These<A, Nothing> = This(thiz)
+        /** Constructs a [These] containing the first possibility. */
+        fun <A> first(value: A): These<A, Nothing> = First(value)
 
-        /** Constructs a [These] containing only [that]. */
-        fun <B> that(that: B): These<Nothing, B> = That(that)
+        /** Constructs a [These] containing the second possibility. */
+        fun <B> second(value: B): These<Nothing, B> = Second(value)
 
-        /** Constructs a [These] containing both [thiz] and [that]. */
-        fun <A, B> both(thiz: A, that: B): These<A, B> = Both(thiz, that)
+        /** Constructs a [These] containing both possibilities. */
+        fun <A, B> both(first: A, second: B): These<A, B> = Both(first, second)
     }
 }
 
@@ -47,87 +47,88 @@
  */
 inline fun <A> These<A, A>.merge(f: (A, A) -> A): A =
     when (this) {
-        is These.This -> thiz
-        is These.That -> that
-        is These.Both -> f(thiz, that)
+        is These.First -> value
+        is These.Second -> value
+        is These.Both -> f(first, second)
     }
 
-/** Returns the [These.This] [value][These.This.thiz] present in this [These] as a [Maybe]. */
-fun <A> These<A, *>.maybeThis(): Maybe<A> =
+/** Returns the [These.First] [value][These.First.value] present in this [These] as a [Maybe]. */
+fun <A> These<A, *>.maybeFirst(): Maybe<A> =
     when (this) {
-        is These.Both -> just(thiz)
-        is These.That -> none
-        is These.This -> just(thiz)
+        is These.Both -> Maybe.present(first)
+        is These.Second -> Maybe.absent
+        is These.First -> Maybe.present(value)
     }
 
 /**
- * Returns the [These.This] [value][These.This.thiz] present in this [These], or `null` if not
+ * Returns the [These.First] [value][These.First.value] present in this [These], or `null` if not
  * present.
  */
-fun <A : Any> These<A, *>.thisOrNull(): A? =
+fun <A : Any> These<A, *>.firstOrNull(): A? =
     when (this) {
-        is These.Both -> thiz
-        is These.That -> null
-        is These.This -> thiz
+        is These.Both -> first
+        is These.Second -> null
+        is These.First -> value
     }
 
-/** Returns the [These.That] [value][These.That.that] present in this [These] as a [Maybe]. */
-fun <A> These<*, A>.maybeThat(): Maybe<A> =
+/** Returns the [These.Second] [value][These.Second.value] present in this [These] as a [Maybe]. */
+fun <A> These<*, A>.maybeSecond(): Maybe<A> =
     when (this) {
-        is These.Both -> just(that)
-        is These.That -> just(that)
-        is These.This -> none
+        is These.Both -> Maybe.present(second)
+        is These.Second -> Maybe.present(value)
+        is These.First -> Maybe.absent
     }
 
 /**
- * Returns the [These.That] [value][These.That.that] present in this [These], or `null` if not
+ * Returns the [These.Second] [value][These.Second.value] present in this [These], or `null` if not
  * present.
  */
-fun <A : Any> These<*, A>.thatOrNull(): A? =
+fun <A : Any> These<*, A>.secondOrNull(): A? =
     when (this) {
-        is These.Both -> that
-        is These.That -> that
-        is These.This -> null
+        is These.Both -> second
+        is These.Second -> value
+        is These.First -> null
     }
 
 /** Returns [These.Both] values present in this [These] as a [Maybe]. */
 fun <A, B> These<A, B>.maybeBoth(): Maybe<Pair<A, B>> =
     when (this) {
-        is These.Both -> just(thiz to that)
-        else -> none
+        is These.Both -> Maybe.present(first to second)
+        else -> Maybe.absent
     }
 
-/** Returns a [These] containing [thiz] and/or [that] if they are present. */
-fun <A, B> these(thiz: Maybe<A>, that: Maybe<B>): Maybe<These<A, B>> =
-    when (thiz) {
-        is Just ->
-            just(
-                when (that) {
-                    is Just -> These.both(thiz.value, that.value)
-                    else -> These.thiz(thiz.value)
+/** Returns a [These] containing [first] and/or [second] if they are present. */
+fun <A, B> these(first: Maybe<A>, second: Maybe<B>): Maybe<These<A, B>> =
+    when (first) {
+        is Present ->
+            Maybe.present(
+                when (second) {
+                    is Present -> These.both(first.value, second.value)
+                    else -> These.first(first.value)
                 }
             )
+
         else ->
-            when (that) {
-                is Just -> just(These.that(that.value))
-                else -> none
+            when (second) {
+                is Present -> Maybe.present(These.second(second.value))
+                else -> Maybe.absent
             }
     }
 
 /**
- * Returns a [These] containing [thiz] and/or [that] if they are non-null, or `null` if both are
+ * Returns a [These] containing [first] and/or [second] if they are non-null, or `null` if both are
  * `null`.
  */
-fun <A : Any, B : Any> theseNull(thiz: A?, that: B?): These<A, B>? =
-    thiz?.let { that?.let { These.both(thiz, that) } ?: These.thiz(thiz) }
-        ?: that?.let { These.that(that) }
+fun <A : Any, B : Any> theseNotNull(first: A?, second: B?): These<A, B>? =
+    first?.let { second?.let { These.both(first, second) } ?: These.first(first) }
+        ?: second?.let { These.second(second) }
 
 /**
- * Returns two maps, with [Pair.first] containing all [These.This] values and [Pair.second]
- * containing all [These.That] values.
+ * Returns two maps, with [Pair.first] containing all [These.First] values and [Pair.second]
+ * containing all [These.Second] values.
  *
  * If the value is [These.Both], then the associated key with appear in both output maps, bound to
- * [These.Both.thiz] and [These.Both.that] in each respective output.
+ * [These.Both.first] and [These.Both.second] in each respective output.
  */
 fun <K, A, B> Map<K, These<A, B>>.partitionThese(): Pair<Map<K, A>, Map<K, B>> {
     val a = mutableMapOf<K, A>()
@@ -135,14 +136,14 @@
     for ((k, t) in this) {
         when (t) {
             is These.Both -> {
-                a[k] = t.thiz
-                b[k] = t.that
+                a[k] = t.first
+                b[k] = t.second
             }
-            is These.That -> {
-                b[k] = t.that
+            is These.Second -> {
+                b[k] = t.value
             }
-            is These.This -> {
-                a[k] = t.thiz
+            is These.First -> {
+                a[k] = t.value
             }
         }
     }
diff --git a/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosSamples.kt b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosSamples.kt
new file mode 100644
index 0000000..88a5b7a
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosSamples.kt
@@ -0,0 +1,774 @@
+/*
+ * Copyright (C) 2025 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.kairos
+
+import com.android.systemui.kairos.util.MapPatch
+import com.android.systemui.kairos.util.These
+import com.android.systemui.kairos.util.maybeOf
+import com.android.systemui.kairos.util.toMaybe
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class KairosSamples {
+
+    @Test fun test_mapMaybe() = runSample { mapMaybe() }
+
+    fun BuildScope.mapMaybe() {
+        val emitter = MutableEvents<String>()
+        val ints = emitter.mapMaybe { it.toIntOrNull().toMaybe() }
+
+        var observedInput: String? = null
+        emitter.observe { observedInput = it }
+
+        var observedInt: Int? = null
+        ints.observe { observedInt = it }
+
+        launchEffect {
+            // parse succeeds
+            emitter.emit("6")
+            assertEquals(observedInput, "6")
+            assertEquals(observedInt, 6)
+
+            // parse fails
+            emitter.emit("foo")
+            assertEquals(observedInput, "foo")
+            assertEquals(observedInt, 6)
+
+            // parse succeeds
+            emitter.emit("500")
+            assertEquals(observedInput, "500")
+            assertEquals(observedInt, 500)
+        }
+    }
+
+    @Test fun test_mapCheap() = runSample { mapCheap() }
+
+    fun BuildScope.mapCheap() {
+        val emitter = MutableEvents<Int>()
+
+        var invocationCount = 0
+        val squared =
+            emitter.mapCheap {
+                invocationCount++
+                it * it
+            }
+
+        var observedSquare: Int? = null
+        squared.observe { observedSquare = it }
+
+        launchEffect {
+            emitter.emit(10)
+            assertTrue(invocationCount >= 1)
+            assertEquals(observedSquare, 100)
+
+            emitter.emit(2)
+            assertTrue(invocationCount >= 2)
+            assertEquals(observedSquare, 4)
+        }
+    }
+
+    @Test fun test_mapEvents() = runSample { mapEvents() }
+
+    fun BuildScope.mapEvents() {
+        val emitter = MutableEvents<Int>()
+
+        val squared = emitter.map { it * it }
+
+        var observedSquare: Int? = null
+        squared.observe { observedSquare = it }
+
+        launchEffect {
+            emitter.emit(10)
+            assertEquals(observedSquare, 100)
+
+            emitter.emit(2)
+            assertEquals(observedSquare, 4)
+        }
+    }
+
+    @Test fun test_eventsLoop() = runSample { eventsLoop() }
+
+    fun BuildScope.eventsLoop() {
+        val emitter = MutableEvents<Unit>()
+        var newCount: Events<Int> by EventsLoop()
+        val count = newCount.holdState(0)
+        newCount = emitter.map { count.sample() + 1 }
+
+        var observedCount = 0
+        count.observe { observedCount = it }
+
+        launchEffect {
+            emitter.emit(Unit)
+            assertEquals(observedCount, expected = 1)
+
+            emitter.emit(Unit)
+            assertEquals(observedCount, expected = 2)
+        }
+    }
+
+    @Test fun test_stateLoop() = runSample { stateLoop() }
+
+    fun BuildScope.stateLoop() {
+        val emitter = MutableEvents<Unit>()
+        var count: State<Int> by StateLoop()
+        count = emitter.map { count.sample() + 1 }.holdState(0)
+
+        var observedCount = 0
+        count.observe { observedCount = it }
+
+        launchEffect {
+            emitter.emit(Unit)
+            assertEquals(observedCount, expected = 1)
+
+            emitter.emit(Unit)
+            assertEquals(observedCount, expected = 2)
+        }
+    }
+
+    @Test fun test_changes() = runSample { changes() }
+
+    fun BuildScope.changes() {
+        val emitter = MutableEvents<Int>()
+        val state = emitter.holdState(0)
+
+        var numEmissions = 0
+        emitter.observe { numEmissions++ }
+
+        var observedState = 0
+        var numChangeEmissions = 0
+        state.changes.observe {
+            observedState = it
+            numChangeEmissions++
+        }
+
+        launchEffect {
+            emitter.emit(0)
+            assertEquals(numEmissions, expected = 1)
+            assertEquals(numChangeEmissions, expected = 0)
+            assertEquals(observedState, expected = 0)
+
+            emitter.emit(5)
+            assertEquals(numEmissions, expected = 2)
+            assertEquals(numChangeEmissions, expected = 1)
+            assertEquals(observedState, expected = 5)
+
+            emitter.emit(3)
+            assertEquals(numEmissions, expected = 3)
+            assertEquals(numChangeEmissions, expected = 2)
+            assertEquals(observedState, expected = 3)
+
+            emitter.emit(3)
+            assertEquals(numEmissions, expected = 4)
+            assertEquals(numChangeEmissions, expected = 2)
+            assertEquals(observedState, expected = 3)
+
+            emitter.emit(5)
+            assertEquals(numEmissions, expected = 5)
+            assertEquals(numChangeEmissions, expected = 3)
+            assertEquals(observedState, expected = 5)
+        }
+    }
+
+    @Test fun test_partitionThese() = runSample { partitionThese() }
+
+    fun BuildScope.partitionThese() {
+        val emitter = MutableEvents<These<Int, String>>()
+        val (lefts, rights) = emitter.partitionThese()
+
+        var observedLeft: Int? = null
+        lefts.observe { observedLeft = it }
+
+        var observedRight: String? = null
+        rights.observe { observedRight = it }
+
+        launchEffect {
+            emitter.emit(These.first(10))
+            assertEquals(observedLeft, 10)
+            assertEquals(observedRight, null)
+
+            emitter.emit(These.both(2, "foo"))
+            assertEquals(observedLeft, 2)
+            assertEquals(observedRight, "foo")
+
+            emitter.emit(These.second("bar"))
+            assertEquals(observedLeft, 2)
+            assertEquals(observedRight, "bar")
+        }
+    }
+
+    @Test fun test_merge() = runSample { merge() }
+
+    fun BuildScope.merge() {
+        val emitter = MutableEvents<Int>()
+        val fizz = emitter.mapNotNull { if (it % 3 == 0) "Fizz" else null }
+        val buzz = emitter.mapNotNull { if (it % 5 == 0) "Buzz" else null }
+        val fizzbuzz = fizz.mergeWith(buzz) { _, _ -> "Fizz Buzz" }
+        val output = mergeLeft(fizzbuzz, emitter.mapCheap { it.toString() })
+
+        var observedOutput: String? = null
+        output.observe { observedOutput = it }
+
+        launchEffect {
+            emitter.emit(1)
+            assertEquals(observedOutput, "1")
+            emitter.emit(2)
+            assertEquals(observedOutput, "2")
+            emitter.emit(3)
+            assertEquals(observedOutput, "Fizz")
+            emitter.emit(4)
+            assertEquals(observedOutput, "4")
+            emitter.emit(5)
+            assertEquals(observedOutput, "Buzz")
+            emitter.emit(6)
+            assertEquals(observedOutput, "Fizz")
+            emitter.emit(15)
+            assertEquals(observedOutput, "Fizz Buzz")
+        }
+    }
+
+    @Test fun test_groupByKey() = runSample { groupByKey() }
+
+    fun BuildScope.groupByKey() {
+        val emitter = MutableEvents<Map<String, Int>>()
+        val grouped = emitter.groupByKey()
+        val groupA = grouped["A"]
+        val groupB = grouped["B"]
+
+        var numEmissions = 0
+        emitter.observe { numEmissions++ }
+
+        var observedA: Int? = null
+        groupA.observe { observedA = it }
+
+        var observedB: Int? = null
+        groupB.observe { observedB = it }
+
+        launchEffect {
+            // emit to group A
+            emitter.emit(mapOf("A" to 3))
+            assertEquals(numEmissions, 1)
+            assertEquals(observedA, 3)
+            assertEquals(observedB, null)
+
+            // emit to groups B and C, even though there are no observers of C
+            emitter.emit(mapOf("B" to 9, "C" to 100))
+            assertEquals(numEmissions, 2)
+            assertEquals(observedA, 3)
+            assertEquals(observedB, 9)
+
+            // emit to groups A and B
+            emitter.emit(mapOf("B" to 6, "A" to 14))
+            assertEquals(numEmissions, 3)
+            assertEquals(observedA, 14)
+            assertEquals(observedB, 6)
+
+            // emit to group with no listeners
+            emitter.emit(mapOf("Q" to -66))
+            assertEquals(numEmissions, 4)
+            assertEquals(observedA, 14)
+            assertEquals(observedB, 6)
+
+            // no-op emission
+            emitter.emit(emptyMap())
+            assertEquals(numEmissions, 5)
+            assertEquals(observedA, 14)
+            assertEquals(observedB, 6)
+        }
+    }
+
+    @Test fun test_switchEvents() = runSample { switchEvents() }
+
+    fun BuildScope.switchEvents() {
+        val negator = MutableEvents<Unit>()
+        val emitter = MutableEvents<Int>()
+        val negate = negator.foldState(false) { _, negate -> !negate }
+        val output =
+            negate.map { negate -> if (negate) emitter.map { it * -1 } else emitter }.switchEvents()
+
+        var observed: Int? = null
+        output.observe { observed = it }
+
+        launchEffect {
+            // emit like normal
+            emitter.emit(10)
+            assertEquals(observed, 10)
+
+            // enable negation
+            observed = null
+            negator.emit(Unit)
+            assertEquals(observed, null)
+
+            emitter.emit(99)
+            assertEquals(observed, -99)
+
+            // disable negation
+            observed = null
+            negator.emit(Unit)
+            emitter.emit(7)
+            assertEquals(observed, 7)
+        }
+    }
+
+    @Test fun test_switchEventsPromptly() = runSample { switchEventsPromptly() }
+
+    fun BuildScope.switchEventsPromptly() {
+        val emitter = MutableEvents<Int>()
+        val enabled = emitter.map { it > 10 }.holdState(false)
+        val switchedIn = enabled.map { enabled -> if (enabled) emitter else emptyEvents }
+        val deferredSwitch = switchedIn.switchEvents()
+        val promptSwitch = switchedIn.switchEventsPromptly()
+
+        var observedDeferred: Int? = null
+        deferredSwitch.observe { observedDeferred = it }
+
+        var observedPrompt: Int? = null
+        promptSwitch.observe { observedPrompt = it }
+
+        launchEffect {
+            emitter.emit(3)
+            assertEquals(observedDeferred, null)
+            assertEquals(observedPrompt, null)
+
+            emitter.emit(20)
+            assertEquals(observedDeferred, null)
+            assertEquals(observedPrompt, 20)
+
+            emitter.emit(30)
+            assertEquals(observedDeferred, 30)
+            assertEquals(observedPrompt, 30)
+
+            emitter.emit(8)
+            assertEquals(observedDeferred, 8)
+            assertEquals(observedPrompt, 8)
+
+            emitter.emit(1)
+            assertEquals(observedDeferred, 8)
+            assertEquals(observedPrompt, 8)
+        }
+    }
+
+    @Test fun test_sampleTransactional() = runSample { sampleTransactional() }
+
+    fun BuildScope.sampleTransactional() {
+        var store = 0
+        val transactional = transactionally { store++ }
+
+        effect {
+            assertEquals(store, 0)
+            assertEquals(transactional.sample(), 0)
+            assertEquals(store, 1)
+            assertEquals(transactional.sample(), 0)
+            assertEquals(store, 1)
+        }
+    }
+
+    @Test fun test_states() = runSample { states() }
+
+    fun BuildScope.states() {
+        val constantState = stateOf(10)
+        effect { assertEquals(constantState.sample(), 10) }
+
+        val mappedConstantState: State<Int> = constantState.map { it * 2 }
+        effect { assertEquals(mappedConstantState.sample(), 20) }
+
+        val emitter = MutableEvents<Int>()
+        val heldState: State<Int?> = emitter.holdState(null)
+        effect { assertEquals(heldState.sample(), null) }
+
+        var observed: Int? = null
+        var wasObserved = false
+        heldState.observe {
+            observed = it
+            wasObserved = true
+        }
+        launchEffect {
+            assertTrue(wasObserved)
+            emitter.emit(4)
+            assertEquals(observed, 4)
+        }
+
+        val combinedStates: State<Pair<Int, Int?>> =
+            combine(mappedConstantState, heldState) { a, b -> Pair(a, b) }
+
+        effect { assertEquals(combinedStates.sample(), 20 to null) }
+
+        var observedPair: Pair<Int, Int?>? = null
+        combinedStates.observe { observedPair = it }
+        launchEffect {
+            emitter.emit(12)
+            assertEquals(observedPair, 20 to 12)
+        }
+    }
+
+    @Test fun test_holdState() = runSample { holdState() }
+
+    fun BuildScope.holdState() {
+        val emitter = MutableEvents<Int>()
+        val heldState: State<Int?> = emitter.holdState(null)
+        effect { assertEquals(heldState.sample(), null) }
+
+        var observed: Int? = null
+        var wasObserved = false
+        heldState.observe {
+            observed = it
+            wasObserved = true
+        }
+        launchEffect {
+            // observation of the initial state took place immediately
+            assertTrue(wasObserved)
+
+            // state changes are also observed
+            emitter.emit(4)
+            assertEquals(observed, 4)
+
+            emitter.emit(20)
+            assertEquals(observed, 20)
+        }
+    }
+
+    @Test fun test_mapState() = runSample { mapState() }
+
+    fun BuildScope.mapState() {
+        val emitter = MutableEvents<Int>()
+        val held: State<Int> = emitter.holdState(0)
+        val squared: State<Int> = held.map { it * it }
+
+        var observed: Int? = null
+        squared.observe { observed = it }
+
+        launchEffect {
+            assertEquals(observed, 0)
+
+            emitter.emit(10)
+            assertEquals(observed, 100)
+        }
+    }
+
+    @Test fun test_combineState() = runSample { combineState() }
+
+    fun BuildScope.combineState() {
+        val emitter = MutableEvents<Int>()
+        val state = emitter.holdState(0)
+        val squared = state.map { it * it }
+        val negated = state.map { -it }
+        val combined = squared.combine(negated) { a, b -> Pair(a, b) }
+
+        val observed = mutableListOf<Pair<Int, Int>>()
+        combined.observe { observed.add(it) }
+
+        launchEffect {
+            emitter.emit(10)
+            emitter.emit(20)
+            emitter.emit(3)
+
+            assertEquals(observed, listOf(0 to 0, 100 to -10, 400 to -20, 9 to -3))
+        }
+    }
+
+    @Test fun test_flatMap() = runSample { flatMap() }
+
+    fun BuildScope.flatMap() {
+        val toggler = MutableEvents<Unit>()
+        val firstEmitter = MutableEvents<Unit>()
+        val secondEmitter = MutableEvents<Unit>()
+
+        val firstCount: State<Int> = firstEmitter.foldState(0) { _, count -> count + 1 }
+        val secondCount: State<Int> = secondEmitter.foldState(0) { _, count -> count + 1 }
+        val toggleState: State<Boolean> = toggler.foldState(true) { _, state -> !state }
+
+        val activeCount: State<Int> =
+            toggleState.flatMap { b -> if (b) firstCount else secondCount }
+
+        var observed: Int? = null
+        activeCount.observe { observed = it }
+
+        launchEffect {
+            assertEquals(observed, 0)
+
+            firstEmitter.emit(Unit)
+            assertEquals(observed, 1)
+
+            secondEmitter.emit(Unit)
+            assertEquals(observed, 1)
+
+            secondEmitter.emit(Unit)
+            assertEquals(observed, 1)
+
+            toggler.emit(Unit)
+            assertEquals(observed, 2)
+
+            toggler.emit(Unit)
+            assertEquals(observed, 1)
+        }
+    }
+
+    @Test fun test_incrementals() = runSample { incrementals() }
+
+    fun BuildScope.incrementals() {
+        val patchEmitter = MutableEvents<MapPatch<String, Int>>()
+        val incremental: Incremental<String, Int> = patchEmitter.foldStateMapIncrementally()
+        val squared = incremental.mapValues { (key, value) -> value * value }
+
+        var observedUpdate: MapPatch<String, Int>? = null
+        squared.updates.observe { observedUpdate = it }
+
+        var observedState: Map<String, Int>? = null
+        squared.observe { observedState = it }
+
+        launchEffect {
+            assertEquals(observedState, emptyMap())
+            assertEquals(observedUpdate, null)
+
+            // add entry: A => 10
+            patchEmitter.emit(mapOf("A" to maybeOf(10)))
+            assertEquals(observedState, mapOf("A" to 100))
+            assertEquals(observedUpdate, mapOf("A" to maybeOf(100)))
+
+            // update entry: A => 5
+            // add entry: B => 6
+            patchEmitter.emit(mapOf("A" to maybeOf(5), "B" to maybeOf(6)))
+            assertEquals(observedState, mapOf("A" to 25, "B" to 36))
+            assertEquals(observedUpdate, mapOf("A" to maybeOf(25), "B" to maybeOf(36)))
+
+            // remove entry: A
+            // add entry: C => 9
+            // remove non-existent entry: F
+            patchEmitter.emit(mapOf("A" to maybeOf(), "C" to maybeOf(9), "F" to maybeOf()))
+            assertEquals(observedState, mapOf("B" to 36, "C" to 81))
+            // non-existent entry is filtered from the update
+            assertEquals(observedUpdate, mapOf("A" to maybeOf(), "C" to maybeOf(81)))
+        }
+    }
+
+    @Test fun test_mergeEventsIncrementally() = runSample(block = mergeEventsIncrementally())
+
+    fun mergeEventsIncrementally(): BuildSpec<Unit> = buildSpec {
+        val patchEmitter = MutableEvents<MapPatch<String, Events<Int>>>()
+        val incremental: Incremental<String, Events<Int>> = patchEmitter.foldStateMapIncrementally()
+        val merged: Events<Map<String, Int>> = incremental.mergeEventsIncrementally()
+
+        var observed: Map<String, Int>? = null
+        merged.observe { observed = it }
+
+        launchEffect {
+            // add events entry: A
+            val emitterA = MutableEvents<Int>()
+            patchEmitter.emit(mapOf("A" to maybeOf(emitterA)))
+
+            emitterA.emit(100)
+            assertEquals(observed, mapOf("A" to 100))
+
+            // add events entry: B
+            val emitterB = MutableEvents<Int>()
+            patchEmitter.emit(mapOf("B" to maybeOf(emitterB)))
+
+            // merged emits from both A and B
+            emitterB.emit(5)
+            assertEquals(observed, mapOf("B" to 5))
+
+            emitterA.emit(20)
+            assertEquals(observed, mapOf("A" to 20))
+
+            // remove entry: A
+            patchEmitter.emit(mapOf("A" to maybeOf()))
+            emitterA.emit(0)
+            // event is not emitted now that A has been removed
+            assertEquals(observed, mapOf("A" to 20))
+
+            // but B still works
+            emitterB.emit(3)
+            assertEquals(observed, mapOf("B" to 3))
+        }
+    }
+
+    @Test
+    fun test_mergeEventsIncrementallyPromptly() =
+        runSample(block = mergeEventsIncrementallyPromptly())
+
+    fun mergeEventsIncrementallyPromptly(): BuildSpec<Unit> = buildSpec {
+        val patchEmitter = MutableEvents<MapPatch<String, Events<Int>>>()
+        val incremental: Incremental<String, Events<Int>> = patchEmitter.foldStateMapIncrementally()
+        val deferredMerge: Events<Map<String, Int>> = incremental.mergeEventsIncrementally()
+        val promptMerge: Events<Map<String, Int>> = incremental.mergeEventsIncrementallyPromptly()
+
+        var observedDeferred: Map<String, Int>? = null
+        deferredMerge.observe { observedDeferred = it }
+
+        var observedPrompt: Map<String, Int>? = null
+        promptMerge.observe { observedPrompt = it }
+
+        launchEffect {
+            val emitterA = MutableEvents<Int>()
+            patchEmitter.emit(mapOf("A" to maybeOf(emitterA)))
+
+            emitterA.emit(100)
+            assertEquals(observedDeferred, mapOf("A" to 100))
+            assertEquals(observedPrompt, mapOf("A" to 100))
+
+            val emitterB = patchEmitter.map { 5 }
+            patchEmitter.emit(mapOf("B" to maybeOf(emitterB)))
+
+            assertEquals(observedDeferred, mapOf("A" to 100))
+            assertEquals(observedPrompt, mapOf("B" to 5))
+        }
+    }
+
+    @Test fun test_applyLatestStateful() = runSample(block = applyLatestStateful())
+
+    fun applyLatestStateful(): BuildSpec<Unit> = buildSpec {
+        val reset = MutableEvents<Unit>()
+        val emitter = MutableEvents<Unit>()
+        val stateEvents: Events<State<Int>> =
+            reset
+                .map { statefully { emitter.foldState(0) { _, count -> count + 1 } } }
+                .applyLatestStateful()
+        val activeState: State<State<Int>?> = stateEvents.holdState(null)
+
+        launchEffect {
+            // nothing is active yet
+            kairosNetwork.transact { assertEquals(activeState.sample(), null) }
+
+            // activate the counter
+            reset.emit(Unit)
+            val firstState =
+                kairosNetwork.transact {
+                    assertEquals(activeState.sample()?.sample(), 0)
+                    activeState.sample()!!
+                }
+
+            // emit twice
+            emitter.emit(Unit)
+            emitter.emit(Unit)
+            kairosNetwork.transact { assertEquals(firstState.sample(), 2) }
+
+            // start a new counter, disabling the old one
+            reset.emit(Unit)
+            val secondState =
+                kairosNetwork.transact {
+                    assertEquals(activeState.sample()?.sample(), 0)
+                    activeState.sample()!!
+                }
+            kairosNetwork.transact { assertEquals(firstState.sample(), 2) }
+
+            // emit: the new counter updates, but the old one does not
+            emitter.emit(Unit)
+            kairosNetwork.transact { assertEquals(secondState.sample(), 1) }
+            kairosNetwork.transact { assertEquals(firstState.sample(), 2) }
+        }
+    }
+
+    @Test fun test_applyLatestStatefulForKey() = runSample(block = applyLatestStatefulForKey())
+
+    fun applyLatestStatefulForKey(): BuildSpec<Unit> = buildSpec {
+        val reset = MutableEvents<String>()
+        val emitter = MutableEvents<String>()
+        val stateEvents: Events<MapPatch<String, State<Int>>> =
+            reset
+                .map { key ->
+                    mapOf(
+                        key to
+                            maybeOf(
+                                statefully {
+                                    emitter
+                                        .filter { it == key }
+                                        .foldState(0) { _, count -> count + 1 }
+                                }
+                            )
+                    )
+                }
+                .applyLatestStatefulForKey()
+        val activeStatesByKey: Incremental<String, State<Int>> =
+            stateEvents.foldStateMapIncrementally(emptyMap())
+
+        launchEffect {
+            // nothing is active yet
+            kairosNetwork.transact { assertEquals(activeStatesByKey.sample(), emptyMap()) }
+
+            // activate a new entry A
+            reset.emit("A")
+            val firstStateA =
+                kairosNetwork.transact {
+                    val stateMap: Map<String, State<Int>> = activeStatesByKey.sample()
+                    assertEquals(stateMap.keys, setOf("A"))
+                    stateMap.getValue("A").also { assertEquals(it.sample(), 0) }
+                }
+
+            // emit twice to A
+            emitter.emit("A")
+            emitter.emit("A")
+            kairosNetwork.transact { assertEquals(firstStateA.sample(), 2) }
+
+            // active a new entry B
+            reset.emit("B")
+            val firstStateB =
+                kairosNetwork.transact {
+                    val stateMap: Map<String, State<Int>> = activeStatesByKey.sample()
+                    assertEquals(stateMap.keys, setOf("A", "B"))
+                    stateMap.getValue("B").also {
+                        assertEquals(it.sample(), 0)
+                        assertEquals(firstStateA.sample(), 2)
+                    }
+                }
+
+            // emit once to B
+            emitter.emit("B")
+            kairosNetwork.transact {
+                assertEquals(firstStateA.sample(), 2)
+                assertEquals(firstStateB.sample(), 1)
+            }
+
+            // activate a new entry for A, disabling the old entry
+            reset.emit("A")
+            val secondStateA =
+                kairosNetwork.transact {
+                    val stateMap: Map<String, State<Int>> = activeStatesByKey.sample()
+                    assertEquals(stateMap.keys, setOf("A", "B"))
+                    stateMap.getValue("A").also {
+                        assertEquals(it.sample(), 0)
+                        assertEquals(firstStateB.sample(), 1)
+                    }
+                }
+
+            // emit to A: the new A state updates, but the old one does not
+            emitter.emit("A")
+            kairosNetwork.transact {
+                assertEquals(firstStateA.sample(), 2)
+                assertEquals(secondStateA.sample(), 1)
+            }
+        }
+    }
+
+    private fun runSample(
+        dispatcher: TestDispatcher = UnconfinedTestDispatcher(),
+        block: BuildScope.() -> Unit,
+    ) {
+        runTest(dispatcher, timeout = 1.seconds) {
+            val kairosNetwork = backgroundScope.launchKairosNetwork()
+            backgroundScope.launch { kairosNetwork.activateSpec { block() } }
+        }
+    }
+}
+
+private fun <T> assertEquals(actual: T, expected: T) = Assert.assertEquals(expected, actual)
diff --git a/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt
index 150b462..ffe6e95 100644
--- a/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt
+++ b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt
@@ -1,14 +1,12 @@
 package com.android.systemui.kairos
 
 import com.android.systemui.kairos.util.Either
-import com.android.systemui.kairos.util.Either.Left
-import com.android.systemui.kairos.util.Either.Right
+import com.android.systemui.kairos.util.Either.First
+import com.android.systemui.kairos.util.Either.Second
 import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.None
-import com.android.systemui.kairos.util.just
+import com.android.systemui.kairos.util.Maybe.Absent
 import com.android.systemui.kairos.util.map
 import com.android.systemui.kairos.util.maybe
-import com.android.systemui.kairos.util.none
 import kotlin.time.Duration
 import kotlin.time.Duration.Companion.seconds
 import kotlin.time.DurationUnit
@@ -142,7 +140,7 @@
 
                 // convert Eventss to States so that they can be combined
                 val combined =
-                    left.holdState("left" to 0).combineWith(right.holdState("right" to 0)) { l, r ->
+                    left.holdState("left" to 0).combine(right.holdState("right" to 0)) { l, r ->
                         l to r
                     }
                 combined.changes // get State changes
@@ -590,7 +588,7 @@
         intStopEmitter.emit(Unit) // intAH.complete()
         runCurrent()
 
-        // assertEquals(just(10), network.await())
+        // assertEquals(present(10), network.await())
     }
 
     @Test
@@ -692,12 +690,12 @@
             }
         runCurrent()
 
-        emitter.emit(Left(10))
+        emitter.emit(First(10))
         runCurrent()
 
         assertEquals(20, result.value)
 
-        emitter.emit(Right(30))
+        emitter.emit(Second(30))
         runCurrent()
 
         assertEquals(-30, result.value)
@@ -908,7 +906,7 @@
             activateSpecWithResult(network) {
                 val bA = updater.map { it * 2 }.holdState(0)
                 val bB = updater.holdState(0)
-                val combineD: State<Pair<Int, Int>> = bA.combineWith(bB) { a, b -> a to b }
+                val combineD: State<Pair<Int, Int>> = bA.combine(bB) { a, b -> a to b }
                 val sampleS = emitter.sample(combineD) { _, b -> b }
                 sampleS.nextDeferred()
             }
@@ -1142,16 +1140,13 @@
                                                     val eRemoved =
                                                         childChangeById
                                                             .eventsForKey(childId)
-                                                            .filter { it === None }
+                                                            .filter { it === Absent }
                                                             .onEach {
                                                                 println(
                                                                     "removing? (groupId=$groupId, childId=$childId)"
                                                                 )
                                                             }
-                                                            .nextOnly(
-                                                                name =
-                                                                    "eRemoved(groupId=$groupId, childId=$childId)"
-                                                            )
+                                                            .nextOnly()
 
                                                     val addChild: Events<Maybe<State<String>>> =
                                                         now.map { mChild }
@@ -1168,13 +1163,9 @@
                                                                     "removeChild (groupId=$groupId, childId=$childId)"
                                                                 )
                                                             }
-                                                            .map { none() }
+                                                            .map { Maybe.absent() }
 
-                                                    addChild.mergeWith(
-                                                        removeChild,
-                                                        name =
-                                                            "childUpdatesMerged(groupId=$groupId, childId=$childId)",
-                                                    ) { _, _ ->
+                                                    addChild.mergeWith(removeChild) { _, _ ->
                                                         error("unexpected coincidence")
                                                     }
                                                 }
@@ -1182,7 +1173,7 @@
                                         }
                                     val mergeIncrementally: Events<Map<Int, Maybe<State<String>>>> =
                                         map.onEach { println("merge patch: $it") }
-                                            .mergeIncrementallyPromptly(name = "mergeIncrementally")
+                                            .mergeEventsIncrementallyPromptly()
                                     mergeIncrementally
                                         .onEach { println("foldmap patch: $it") }
                                         .foldStateMapIncrementally()
@@ -1203,14 +1194,14 @@
         val emitter2 = network.mutableEvents<Map<Int, Maybe<StateFlow<String>>>>()
         println()
         println("init outer 0")
-        e.emit(mapOf(0 to just(emitter2.onEach { println("emitter2 emit: $it") })))
+        e.emit(mapOf(0 to Maybe.present(emitter2.onEach { println("emitter2 emit: $it") })))
         runCurrent()
 
         assertEquals(mapOf(0 to emptyMap()), state.value)
 
         println()
         println("init inner 10")
-        emitter2.emit(mapOf(10 to just(MutableStateFlow("(0, 10)"))))
+        emitter2.emit(mapOf(10 to Maybe.present(MutableStateFlow("(0, 10)"))))
         runCurrent()
 
         assertEquals(mapOf(0 to mapOf(10 to "(0, 10)")), state.value)
@@ -1218,19 +1209,19 @@
         // replace
         println()
         println("replace inner 10")
-        emitter2.emit(mapOf(10 to just(MutableStateFlow("(1, 10)"))))
+        emitter2.emit(mapOf(10 to Maybe.present(MutableStateFlow("(1, 10)"))))
         runCurrent()
 
         assertEquals(mapOf(0 to mapOf(10 to "(1, 10)")), state.value)
 
         // remove
-        emitter2.emit(mapOf(10 to none()))
+        emitter2.emit(mapOf(10 to Maybe.absent()))
         runCurrent()
 
         assertEquals(mapOf(0 to emptyMap()), state.value)
 
         // add again
-        emitter2.emit(mapOf(10 to just(MutableStateFlow("(2, 10)"))))
+        emitter2.emit(mapOf(10 to Maybe.present(MutableStateFlow("(2, 10)"))))
         runCurrent()
 
         assertEquals(mapOf(0 to mapOf(10 to "(2, 10)")), state.value)
@@ -1242,9 +1233,9 @@
         // batch update
         emitter2.emit(
             mapOf(
-                10 to none(),
-                11 to just(MutableStateFlow("(0, 11)")),
-                12 to just(MutableStateFlow("(0, 12)")),
+                10 to Maybe.absent(),
+                11 to Maybe.present(MutableStateFlow("(0, 11)")),
+                12 to Maybe.present(MutableStateFlow("(0, 12)")),
             )
         )
         runCurrent()
@@ -1278,7 +1269,7 @@
         }
 
         var outerCount = 0
-        val laseventss: StateFlow<Pair<StateFlow<Int?>, StateFlow<Int?>>> =
+        val lastEvent: StateFlow<Pair<StateFlow<Int?>, StateFlow<Int?>>> =
             flowOfFlows
                 .map { it.stateIn(backgroundScope, SharingStarted.Eagerly, null) }
                 .pairwise(MutableStateFlow(null))
@@ -1296,18 +1287,18 @@
 
         assertEquals(1, outerCount)
         //        assertEquals(1, incCount.subscriptionCount)
-        assertNull(laseventss.value.second.value)
+        assertNull(lastEvent.value.second.value)
 
         incCount.emit(Unit)
         runCurrent()
 
         println("checking")
-        assertEquals(1, laseventss.value.second.value)
+        assertEquals(1, lastEvent.value.second.value)
 
         incCount.emit(Unit)
         runCurrent()
 
-        assertEquals(2, laseventss.value.second.value)
+        assertEquals(2, lastEvent.value.second.value)
 
         newCount.emit(newFlow())
         runCurrent()
@@ -1315,9 +1306,9 @@
         runCurrent()
 
         // verify old flow is not getting updates
-        assertEquals(2, laseventss.value.first.value)
+        assertEquals(2, lastEvent.value.first.value)
         // but the new one is
-        assertEquals(1, laseventss.value.second.value)
+        assertEquals(1, lastEvent.value.second.value)
     }
 
     @Test
@@ -1326,7 +1317,7 @@
         var observedCount: Int? = null
         activateSpec(network) {
             val (c, j) = asyncScope { input.foldState(0) { _, x -> x + 1 } }
-            deferredBuildScopeAction { c.get().observe { observedCount = it } }
+            deferredBuildScopeAction { c.value.observe { observedCount = it } }
         }
         runCurrent()
         assertEquals(0, observedCount)
@@ -1385,7 +1376,7 @@
             activateSpec(network) {
                 val handle =
                     input.observe {
-                        effectCoroutineScope.launch {
+                        launch {
                             runningCount++
                             awaitClose { runningCount-- }
                         }
@@ -1420,7 +1411,7 @@
         val specJob =
             activateSpec(network) {
                 input.takeUntil(stopper).observe {
-                    effectCoroutineScope.launch {
+                    launch {
                         runningCount++
                         awaitClose { runningCount-- }
                     }
diff --git a/packages/Vcn/service-b/Android.bp b/packages/Vcn/service-b/Android.bp
index 1370b06..97574e6 100644
--- a/packages/Vcn/service-b/Android.bp
+++ b/packages/Vcn/service-b/Android.bp
@@ -39,9 +39,7 @@
     name: "connectivity-utils-service-vcn-internal",
     sdk_version: "module_current",
     min_sdk_version: "30",
-    srcs: [
-        ":framework-connectivity-shared-srcs",
-    ],
+    srcs: ["service-utils/**/*.java"],
     libs: [
         "framework-annotations-lib",
         "unsupportedappusage",
diff --git a/packages/Vcn/service-b/service-utils/android/util/LocalLog.java b/packages/Vcn/service-b/service-utils/android/util/LocalLog.java
new file mode 100644
index 0000000..5955d93
--- /dev/null
+++ b/packages/Vcn/service-b/service-utils/android/util/LocalLog.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.SystemClock;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Iterator;
+
+/**
+ * @hide
+ */
+// Exported to Mainline modules; cannot use annotations
+// @android.ravenwood.annotation.RavenwoodKeepWholeClass
+// TODO: b/374174952 This is an exact copy of frameworks/base/core/java/android/util/LocalLog.java.
+// This file is only used in "service-connectivity-b-platform" before the VCN modularization flag
+// is fully ramped. When the flag is fully ramped and the development is finalized, this file can
+// be removed.
+public final class LocalLog {
+
+    private final Deque<String> mLog;
+    private final int mMaxLines;
+
+    /**
+     * {@code true} to use log timestamps expressed in local date/time, {@code false} to use log
+     * timestamped expressed with the elapsed realtime clock and UTC system clock. {@code false} is
+     * useful when logging behavior that modifies device time zone or system clock.
+     */
+    private final boolean mUseLocalTimestamps;
+
+    @UnsupportedAppUsage
+    public LocalLog(int maxLines) {
+        this(maxLines, true /* useLocalTimestamps */);
+    }
+
+    public LocalLog(int maxLines, boolean useLocalTimestamps) {
+        mMaxLines = Math.max(0, maxLines);
+        mLog = new ArrayDeque<>(mMaxLines);
+        mUseLocalTimestamps = useLocalTimestamps;
+    }
+
+    @UnsupportedAppUsage
+    public void log(String msg) {
+        if (mMaxLines <= 0) {
+            return;
+        }
+        final String logLine;
+        if (mUseLocalTimestamps) {
+            logLine = LocalDateTime.now() + " - " + msg;
+        } else {
+            logLine = Duration.ofMillis(SystemClock.elapsedRealtime())
+                    + " / " + Instant.now() + " - " + msg;
+        }
+        append(logLine);
+    }
+
+    private synchronized void append(String logLine) {
+        while (mLog.size() >= mMaxLines) {
+            mLog.remove();
+        }
+        mLog.add(logLine);
+    }
+
+    @UnsupportedAppUsage
+    public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        dump(pw);
+    }
+
+    public synchronized void dump(PrintWriter pw) {
+        dump("", pw);
+    }
+
+    /**
+     * Dumps the content of local log to print writer with each log entry predeced with indent
+     *
+     * @param indent indent that precedes each log entry
+     * @param pw printer writer to write into
+     */
+    public synchronized void dump(String indent, PrintWriter pw) {
+        Iterator<String> itr = mLog.iterator();
+        while (itr.hasNext()) {
+            pw.printf("%s%s\n", indent, itr.next());
+        }
+    }
+
+    public synchronized void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        reverseDump(pw);
+    }
+
+    public synchronized void reverseDump(PrintWriter pw) {
+        Iterator<String> itr = mLog.descendingIterator();
+        while (itr.hasNext()) {
+            pw.println(itr.next());
+        }
+    }
+
+    // @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+    public synchronized void clear() {
+        mLog.clear();
+    }
+
+    public static class ReadOnlyLocalLog {
+        private final LocalLog mLog;
+        ReadOnlyLocalLog(LocalLog log) {
+            mLog = log;
+        }
+        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+        public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+            mLog.dump(pw);
+        }
+        public void dump(PrintWriter pw) {
+            mLog.dump(pw);
+        }
+        public void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) {
+            mLog.reverseDump(pw);
+        }
+        public void reverseDump(PrintWriter pw) {
+            mLog.reverseDump(pw);
+        }
+    }
+
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public ReadOnlyLocalLog readOnlyLocalLog() {
+        return new ReadOnlyLocalLog(this);
+    }
+}
\ No newline at end of file
diff --git a/packages/Vcn/service-b/service-utils/com/android/internal/util/WakeupMessage.java b/packages/Vcn/service-b/service-utils/com/android/internal/util/WakeupMessage.java
new file mode 100644
index 0000000..7db62f8
--- /dev/null
+++ b/packages/Vcn/service-b/service-utils/com/android/internal/util/WakeupMessage.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2025 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.util;
+
+import android.app.AlarmManager;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+ /**
+ * An AlarmListener that sends the specified message to a Handler and keeps the system awake until
+ * the message is processed.
+ *
+ * This is useful when using the AlarmManager direct callback interface to wake up the system and
+ * request that an object whose API consists of messages (such as a StateMachine) perform some
+ * action.
+ *
+ * In this situation, using AlarmManager.onAlarmListener by itself will wake up the system to send
+ * the message, but does not guarantee that the system will be awake until the target object has
+ * processed it. This is because as soon as the onAlarmListener sends the message and returns, the
+ * AlarmManager releases its wakelock and the system is free to go to sleep again.
+ */
+// TODO: b/374174952 This is an exact copy of
+// frameworks/base/core/java/com/android/internal/util/WakeupMessage.java.
+// This file is only used in "service-connectivity-b-platform" before the VCN modularization flag
+// is fully ramped. When the flag is fully ramped and the development is finalized, this file can
+// be removed.
+public class WakeupMessage implements AlarmManager.OnAlarmListener {
+    private final AlarmManager mAlarmManager;
+
+    @VisibleForTesting
+    protected final Handler mHandler;
+    @VisibleForTesting
+    protected final String mCmdName;
+    @VisibleForTesting
+    protected final int mCmd, mArg1, mArg2;
+    @VisibleForTesting
+    protected final Object mObj;
+    private final Runnable mRunnable;
+    private boolean mScheduled;
+
+    public WakeupMessage(Context context, Handler handler,
+            String cmdName, int cmd, int arg1, int arg2, Object obj) {
+        mAlarmManager = getAlarmManager(context);
+        mHandler = handler;
+        mCmdName = cmdName;
+        mCmd = cmd;
+        mArg1 = arg1;
+        mArg2 = arg2;
+        mObj = obj;
+        mRunnable = null;
+    }
+
+    public WakeupMessage(Context context, Handler handler, String cmdName, int cmd, int arg1) {
+        this(context, handler, cmdName, cmd, arg1, 0, null);
+    }
+
+    public WakeupMessage(Context context, Handler handler,
+            String cmdName, int cmd, int arg1, int arg2) {
+        this(context, handler, cmdName, cmd, arg1, arg2, null);
+    }
+
+    public WakeupMessage(Context context, Handler handler, String cmdName, int cmd) {
+        this(context, handler, cmdName, cmd, 0, 0, null);
+    }
+
+    public WakeupMessage(Context context, Handler handler, String cmdName, Runnable runnable) {
+        mAlarmManager = getAlarmManager(context);
+        mHandler = handler;
+        mCmdName = cmdName;
+        mCmd = 0;
+        mArg1 = 0;
+        mArg2 = 0;
+        mObj = null;
+        mRunnable = runnable;
+    }
+
+    private static AlarmManager getAlarmManager(Context context) {
+        return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+    }
+
+    /**
+     * Schedule the message to be delivered at the time in milliseconds of the
+     * {@link android.os.SystemClock#elapsedRealtime SystemClock.elapsedRealtime()} clock and wakeup
+     * the device when it goes off. If schedule is called multiple times without the message being
+     * dispatched then the alarm is rescheduled to the new time.
+     */
+    public synchronized void schedule(long when) {
+        mAlarmManager.setExact(
+                AlarmManager.ELAPSED_REALTIME_WAKEUP, when, mCmdName, this, mHandler);
+        mScheduled = true;
+    }
+
+    /**
+     * Cancel all pending messages. This includes alarms that may have been fired, but have not been
+     * run on the handler yet.
+     */
+    public synchronized void cancel() {
+        if (mScheduled) {
+            mAlarmManager.cancel(this);
+            mScheduled = false;
+        }
+    }
+
+    @Override
+    public void onAlarm() {
+        // Once this method is called the alarm has already been fired and removed from
+        // AlarmManager (it is still partially tracked, but only for statistics). The alarm can now
+        // be marked as unscheduled so that it can be rescheduled in the message handler.
+        final boolean stillScheduled;
+        synchronized (this) {
+            stillScheduled = mScheduled;
+            mScheduled = false;
+        }
+        if (stillScheduled) {
+            Message msg;
+            if (mRunnable == null) {
+                msg = mHandler.obtainMessage(mCmd, mArg1, mArg2, mObj);
+            } else {
+                msg = Message.obtain(mHandler, mRunnable);
+            }
+            mHandler.dispatchMessage(msg);
+            msg.recycle();
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt b/packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt
index 3630727..6ec39d9 100644
--- a/packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt
+++ b/packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt
@@ -1,5 +1,2 @@
-rule android.util.IndentingPrintWriter android.net.vcn.module.repackaged.android.util.IndentingPrintWriter
 rule android.util.LocalLog android.net.vcn.module.repackaged.android.util.LocalLog
-rule com.android.internal.util.IndentingPrintWriter android.net.vcn.module.repackaged.com.android.internal.util.IndentingPrintWriter
-rule com.android.internal.util.MessageUtils android.net.vcn.module.repackaged.com.android.internal.util.MessageUtils
 rule com.android.internal.util.WakeupMessage android.net.vcn.module.repackaged.com.android.internal.util.WakeupMessage
\ No newline at end of file
diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto
index 6489905..3a38152 100644
--- a/proto/src/system_messages.proto
+++ b/proto/src/system_messages.proto
@@ -420,5 +420,9 @@
     // Notify the user that accessibility floating menu is hidden.
     // Package: com.android.systemui
     NOTE_A11Y_FLOATING_MENU_HIDDEN = 1009;
+
+    // Notify the hearing aid user that input device can be changed to builtin device or hearing device.
+    // Package: android
+    NOTE_HEARING_DEVICE_INPUT_SWITCH = 1012;
   }
 }
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 59043a83..8e99842 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -345,14 +345,41 @@
     ],
 }
 
+// We define our own version of platform_compat_config's here, because:
+// - The original version (e.g. "framework-platform-compat-config) is built from
+//   the output file of the device side jar, rather than the host jar, meaning
+//   they're slow to build because they depend on D8/R8 output.
+// - The original services one ("services-platform-compat-config") is built from services.jar,
+//   which includes service.permission, which is very slow to rebuild because of kotlin.
+//
+// Because we're re-defining the same compat-IDs that are defined elsewhere,
+// they should all have `include_in_merged_xml: false`. Otherwise, generating
+// merged_compat_config.xml would fail due to duplicate IDs.
+//
+// These module names must end with "compat-config" because these will be used as the filename,
+// and at runtime, we only loads files that match `*compat-config.xml`.
+platform_compat_config {
+    name: "ravenwood-framework-platform-compat-config",
+    src: ":framework-minus-apex-for-host",
+    include_in_merged_xml: false,
+    visibility: ["//visibility:private"],
+}
+
+platform_compat_config {
+    name: "ravenwood-services.core-platform-compat-config",
+    src: ":services.core-for-host",
+    include_in_merged_xml: false,
+    visibility: ["//visibility:private"],
+}
+
 filegroup {
     name: "ravenwood-data",
     device_common_srcs: [
         ":system-build.prop",
         ":framework-res",
         ":ravenwood-empty-res",
-        ":framework-platform-compat-config",
-        ":services-platform-compat-config",
+        ":ravenwood-framework-platform-compat-config",
+        ":ravenwood-services.core-platform-compat-config",
         "texts/ravenwood-build.prop",
     ],
     device_first_srcs: [
@@ -616,6 +643,10 @@
         "android.test.mock.ravenwood",
         "ravenwood-helper-runtime",
         "hoststubgen-helper-runtime.ravenwood",
+
+        // Note, when we include other services.* jars, we'll need to add
+        // platform_compat_config for that module too.
+        // See ravenwood-services.core-platform-compat-config above.
         "services.core.ravenwood-jarjar",
         "services.fakes.ravenwood-jarjar",
 
diff --git a/ravenwood/CleanSpec.mk b/ravenwood/CleanSpec.mk
new file mode 100644
index 0000000..50d2fab
--- /dev/null
+++ b/ravenwood/CleanSpec.mk
@@ -0,0 +1,45 @@
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# If you don't need to do a full clean build but would like to touch
+# a file or delete some intermediate files, add a clean step to the end
+# of the list.  These steps will only be run once, if they haven't been
+# run before.
+#
+# E.g.:
+#     $(call add-clean-step, touch -c external/sqlite/sqlite3.h)
+#     $(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/STATIC_LIBRARIES/libz_intermediates)
+#
+# Always use "touch -c" and "rm -f" or "rm -rf" to gracefully deal with
+# files that are missing or have been moved.
+#
+# Use $(PRODUCT_OUT) to get to the "out/target/product/blah/" directory.
+# Use $(OUT_DIR) to refer to the "out" directory.
+#
+# If you need to re-do something that's already mentioned, just copy
+# the command and add it to the bottom of the list.  E.g., if a change
+# that you made last week required touching a file and a change you
+# made today requires touching the same file, just copy the old
+# touch step and add it to the end of the list.
+#
+# *****************************************************************
+# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST ABOVE THE BANNER
+# *****************************************************************
+
+$(call add-clean-step, rm -rf $(OUT_DIR)/host/linux-x86/testcases/ravenwood-runtime)
+
+# ******************************************************************
+# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST ABOVE THIS BANNER
+# ******************************************************************
diff --git a/ravenwood/tests/bivalenttest/Android.bp b/ravenwood/tests/bivalenttest/Android.bp
index ac545df..c4086c5 100644
--- a/ravenwood/tests/bivalenttest/Android.bp
+++ b/ravenwood/tests/bivalenttest/Android.bp
@@ -40,6 +40,7 @@
 
         "junit-params",
         "platform-parametric-runner-lib",
+        "platform-compat-test-rules",
 
         // To make sure it won't cause VerifyError (b/324063814)
         "platformprotosnano",
diff --git a/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/compat/RavenwoodCompatFrameworkTest.kt b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/compat/RavenwoodCompatFrameworkTest.kt
index 882c91c..540b082 100644
--- a/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/compat/RavenwoodCompatFrameworkTest.kt
+++ b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/compat/RavenwoodCompatFrameworkTest.kt
@@ -16,31 +16,52 @@
 package com.android.ravenwoodtest.bivalenttest.compat
 
 import android.app.compat.CompatChanges
+import android.compat.testing.PlatformCompatChangeRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.internal.ravenwood.RavenwoodEnvironment.CompatIdsForTest
-import org.junit.Assert
+import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
 class RavenwoodCompatFrameworkTest {
+
+    @get:Rule
+    val compatRule = PlatformCompatChangeRule()
+
     @Test
     fun testEnabled() {
-        Assert.assertTrue(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_1))
+        assertTrue(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_1))
     }
 
     @Test
     fun testDisabled() {
-        Assert.assertFalse(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_2))
+        assertFalse(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_2))
     }
 
     @Test
     fun testEnabledAfterSForUApps() {
-        Assert.assertTrue(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_3))
+        assertTrue(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_3))
     }
 
     @Test
     fun testEnabledAfterUForUApps() {
-        Assert.assertFalse(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_4))
+        assertFalse(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_4))
+    }
+
+    @Test
+    @EnableCompatChanges(CompatIdsForTest.TEST_COMPAT_ID_5)
+    fun testEnableCompatChanges() {
+        assertTrue(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_5))
+    }
+
+    @Test
+    @DisableCompatChanges(CompatIdsForTest.TEST_COMPAT_ID_5)
+    fun testDisableCompatChanges() {
+        assertFalse(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_5))
     }
 }
diff --git a/ravenwood/tools/hoststubgen/scripts/dump-jar b/ravenwood/tools/hoststubgen/scripts/dump-jar
index 8765245..998357b 100755
--- a/ravenwood/tools/hoststubgen/scripts/dump-jar
+++ b/ravenwood/tools/hoststubgen/scripts/dump-jar
@@ -89,7 +89,7 @@
     # - Some other transient lines
     # - Sometimes the javap shows mysterious warnings, so remove them too.
     #
-    # `/PATTERN-1/,/PATTERN-1/{//!d}` is a trick to delete lines between two patterns, without
+    # `/PATTERN-1/,/PATTERN-2/{//!d}` is a trick to delete lines between two patterns, without
     # the start and the end lines.
     sed -e 's/#[0-9][0-9]*/#x/g' \
         -e 's/^\( *\)[0-9][0-9]*:/\1x:/' \
diff --git a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
index 6d8d7b7..cc704b2 100644
--- a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
+++ b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
@@ -27,7 +27,7 @@
 import com.android.hoststubgen.filters.KeepNativeFilter
 import com.android.hoststubgen.filters.OutputFilter
 import com.android.hoststubgen.filters.SanitizationFilter
-import com.android.hoststubgen.filters.TextFileFilterPolicyParser
+import com.android.hoststubgen.filters.TextFileFilterPolicyBuilder
 import com.android.hoststubgen.filters.printAsTextPolicy
 import com.android.hoststubgen.utils.ClassFilter
 import com.android.hoststubgen.visitors.BaseAdapter
@@ -179,9 +179,9 @@
         // Next, "text based" filter, which allows to override polices without touching
         // the target code.
         if (options.policyOverrideFiles.isNotEmpty()) {
-            val parser = TextFileFilterPolicyParser(allClasses, filter)
-            options.policyOverrideFiles.forEach(parser::parse)
-            filter = parser.createOutputFilter()
+            val builder = TextFileFilterPolicyBuilder(allClasses, filter)
+            options.policyOverrideFiles.forEach(builder::parse)
+            filter = builder.createOutputFilter()
         }
 
         // Apply the implicit filter.
diff --git a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
index 7462a8c..be1b6ca 100644
--- a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
+++ b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
@@ -23,10 +23,12 @@
 import com.android.hoststubgen.log
 import com.android.hoststubgen.normalizeTextLine
 import com.android.hoststubgen.whitespaceRegex
-import java.io.File
-import java.io.PrintWriter
-import java.util.regex.Pattern
 import org.objectweb.asm.tree.ClassNode
+import java.io.BufferedReader
+import java.io.FileReader
+import java.io.PrintWriter
+import java.io.Reader
+import java.util.regex.Pattern
 
 /**
  * Print a class node as a "keep" policy.
@@ -48,7 +50,7 @@
 
 private const val FILTER_REASON = "file-override"
 
-private enum class SpecialClass {
+enum class SpecialClass {
     NotSpecial,
     Aidl,
     FeatureFlags,
@@ -56,10 +58,58 @@
     RFile,
 }
 
-class TextFileFilterPolicyParser(
+/**
+ * This receives [TextFileFilterPolicyBuilder] parsing result.
+ */
+interface PolicyFileProcessor {
+    /** "package" directive. */
+    fun onPackage(name: String, policy: FilterPolicyWithReason)
+
+    /** "rename" directive. */
+    fun onRename(pattern: Pattern, prefix: String)
+
+    /** "class" directive. */
+    fun onSimpleClassStart(className: String)
+    fun onSimpleClassPolicy(className: String, policy: FilterPolicyWithReason)
+    fun onSimpleClassEnd(className: String)
+
+    fun onSubClassPolicy(superClassName: String, policy: FilterPolicyWithReason)
+    fun onRedirectionClass(fromClassName: String, toClassName: String)
+    fun onClassLoadHook(className: String, callback: String)
+    fun onSpecialClassPolicy(type: SpecialClass, policy: FilterPolicyWithReason)
+
+    /** "field" directive. */
+    fun onField(className: String, fieldName: String, policy: FilterPolicyWithReason)
+
+    /** "method" directive. */
+    fun onSimpleMethodPolicy(
+        className: String,
+        methodName: String,
+        methodDesc: String,
+        policy: FilterPolicyWithReason,
+    )
+    fun onMethodInClassReplace(
+        className: String,
+        methodName: String,
+        methodDesc: String,
+        targetName: String,
+        policy: FilterPolicyWithReason,
+    )
+    fun onMethodOutClassReplace(
+        className: String,
+        methodName: String,
+        methodDesc: String,
+        replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec,
+        policy: FilterPolicyWithReason,
+    )
+}
+
+class TextFileFilterPolicyBuilder(
     private val classes: ClassNodes,
     fallback: OutputFilter
 ) {
+    private val parser = TextFileFilterPolicyParser()
+
     private val subclassFilter = SubclassFilter(classes, fallback)
     private val packageFilter = PackageFilter(subclassFilter)
     private val imf = InMemoryOutputFilter(classes, packageFilter)
@@ -71,30 +121,19 @@
     private val methodReplaceSpec =
         mutableListOf<TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec>()
 
-    private lateinit var currentClassName: String
-
     /**
-     * Read a given "policy" file and return as an [OutputFilter]
+     * Parse a given policy file. This method can be called multiple times to read from
+     * multiple files. To get the resulting filter, use [createOutputFilter]
      */
     fun parse(file: String) {
-        log.i("Loading offloaded annotations from $file ...")
-        log.withIndent {
-            var lineNo = 0
-            try {
-                File(file).forEachLine {
-                    lineNo++
-                    val line = normalizeTextLine(it)
-                    if (line.isEmpty()) {
-                        return@forEachLine // skip empty lines.
-                    }
-                    parseLine(line)
-                }
-            } catch (e: ParseException) {
-                throw e.withSourceInfo(file, lineNo)
-            }
-        }
+        // We may parse multiple files, but we reuse the same parser, because the parser
+        // will make sure there'll be no dupplicating "special class" policies.
+        parser.parse(FileReader(file), file, Processor())
     }
 
+    /**
+     * Generate the resulting [OutputFilter].
+     */
     fun createOutputFilter(): OutputFilter {
         var ret: OutputFilter = imf
         if (typeRenameSpec.isNotEmpty()) {
@@ -112,14 +151,200 @@
         return ret
     }
 
+    private inner class Processor : PolicyFileProcessor {
+        override fun onPackage(name: String, policy: FilterPolicyWithReason) {
+            packageFilter.addPolicy(name, policy)
+        }
+
+        override fun onRename(pattern: Pattern, prefix: String) {
+            typeRenameSpec += TextFilePolicyRemapperFilter.TypeRenameSpec(
+                pattern, prefix
+            )
+        }
+
+        override fun onSimpleClassStart(className: String) {
+        }
+
+        override fun onSimpleClassEnd(className: String) {
+        }
+
+        override fun onSimpleClassPolicy(className: String, policy: FilterPolicyWithReason) {
+            imf.setPolicyForClass(className, policy)
+        }
+
+        override fun onSubClassPolicy(
+            superClassName: String,
+            policy: FilterPolicyWithReason,
+            ) {
+            log.i("class extends $superClassName")
+            subclassFilter.addPolicy( superClassName, policy)
+        }
+
+        override fun onRedirectionClass(fromClassName: String, toClassName: String) {
+            imf.setRedirectionClass(fromClassName, toClassName)
+        }
+
+        override fun onClassLoadHook(className: String, callback: String) {
+            imf.setClassLoadHook(className, callback)
+        }
+
+        override fun onSpecialClassPolicy(
+            type: SpecialClass,
+            policy: FilterPolicyWithReason,
+        ) {
+            log.i("class special $type $policy")
+            when (type) {
+                SpecialClass.NotSpecial -> {} // Shouldn't happen
+
+                SpecialClass.Aidl -> {
+                    aidlPolicy = policy
+                }
+
+                SpecialClass.FeatureFlags -> {
+                    featureFlagsPolicy = policy
+                }
+
+                SpecialClass.Sysprops -> {
+                    syspropsPolicy = policy
+                }
+
+                SpecialClass.RFile -> {
+                    rFilePolicy = policy
+                }
+            }
+        }
+
+        override fun onField(className: String, fieldName: String, policy: FilterPolicyWithReason) {
+            imf.setPolicyForField(className, fieldName, policy)
+        }
+
+        override fun onSimpleMethodPolicy(
+            className: String,
+            methodName: String,
+            methodDesc: String,
+            policy: FilterPolicyWithReason,
+        ) {
+            imf.setPolicyForMethod(className, methodName, methodDesc, policy)
+        }
+
+        override fun onMethodInClassReplace(
+            className: String,
+            methodName: String,
+            methodDesc: String,
+            targetName: String,
+            policy: FilterPolicyWithReason,
+        ) {
+            imf.setPolicyForMethod(className, methodName, methodDesc, policy)
+
+            // Make sure to keep the target method.
+            imf.setPolicyForMethod(
+                className,
+                targetName,
+                methodDesc,
+                FilterPolicy.Keep.withReason(FILTER_REASON)
+            )
+            // Set up the rename.
+            imf.setRenameTo(className, targetName, methodDesc, methodName)
+        }
+
+        override fun onMethodOutClassReplace(
+            className: String,
+            methodName: String,
+            methodDesc: String,
+            replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec,
+            policy: FilterPolicyWithReason,
+        ) {
+            imf.setPolicyForMethod(className, methodName, methodDesc, policy)
+            methodReplaceSpec.add(replaceSpec)
+        }
+    }
+}
+
+/**
+ * Parses a filer policy text file.
+ */
+class TextFileFilterPolicyParser {
+    private lateinit var processor: PolicyFileProcessor
+    private var currentClassName: String? = null
+
+    private var aidlPolicy: FilterPolicyWithReason? = null
+    private var featureFlagsPolicy: FilterPolicyWithReason? = null
+    private var syspropsPolicy: FilterPolicyWithReason? = null
+    private var rFilePolicy: FilterPolicyWithReason? = null
+
+    /** Name of the file that's currently being processed.  */
+    var filename: String? = null
+        private set
+
+    /** 1-based line number in the current file */
+    var lineNumber = -1
+        private set
+
+    /**
+     * Parse a given "policy" file.
+     */
+    fun parse(reader: Reader, inputName: String, processor: PolicyFileProcessor) {
+        filename = inputName
+
+        log.i("Parsing text policy file $inputName ...")
+        this.processor = processor
+        BufferedReader(reader).use { rd ->
+            lineNumber = 0
+            try {
+                while (true) {
+                    var line = rd.readLine()
+                    if (line == null) {
+                        break
+                    }
+                    lineNumber++
+                    line = normalizeTextLine(line) // Remove comment and trim.
+                    if (line.isEmpty()) {
+                        continue
+                    }
+                    parseLine(line)
+                }
+                finishLastClass()
+            } catch (e: ParseException) {
+                throw e.withSourceInfo(inputName, lineNumber)
+            }
+        }
+    }
+
+    private fun finishLastClass() {
+        currentClassName?.let { className ->
+            processor.onSimpleClassEnd(className)
+            currentClassName = null
+        }
+    }
+
+    private fun ensureInClass(directive: String): String {
+        return currentClassName ?:
+            throw ParseException("Directive '$directive' must follow a 'class' directive")
+    }
+
     private fun parseLine(line: String) {
         val fields = line.split(whitespaceRegex).toTypedArray()
         when (fields[0].lowercase()) {
-            "p", "package" -> parsePackage(fields)
-            "c", "class" -> parseClass(fields)
-            "f", "field" -> parseField(fields)
-            "m", "method" -> parseMethod(fields)
-            "r", "rename" -> parseRename(fields)
+            "p", "package" -> {
+                finishLastClass()
+                parsePackage(fields)
+            }
+            "c", "class" -> {
+                finishLastClass()
+                parseClass(fields)
+            }
+            "f", "field" -> {
+                ensureInClass("field")
+                parseField(fields)
+            }
+            "m", "method" -> {
+                ensureInClass("method")
+                parseMethod(fields)
+            }
+            "r", "rename" -> {
+                finishLastClass()
+                parseRename(fields)
+            }
             else -> throw ParseException("Unknown directive \"${fields[0]}\"")
         }
     }
@@ -184,20 +409,20 @@
         if (!policy.isUsableWithClasses) {
             throw ParseException("Package can't have policy '$policy'")
         }
-        packageFilter.addPolicy(name, policy.withReason(FILTER_REASON))
+        processor.onPackage(name, policy.withReason(FILTER_REASON))
     }
 
     private fun parseClass(fields: Array<String>) {
         if (fields.size < 3) {
             throw ParseException("Class ('c') expects 2 fields.")
         }
-        currentClassName = fields[1]
+        val className = fields[1]
 
         // superClass is set when the class name starts with a "*".
-        val superClass = resolveExtendingClass(currentClassName)
+        val superClass = resolveExtendingClass(className)
 
         // :aidl, etc?
-        val classType = resolveSpecialClass(currentClassName)
+        val classType = resolveSpecialClass(className)
 
         if (fields[2].startsWith("!")) {
             if (classType != SpecialClass.NotSpecial) {
@@ -208,7 +433,8 @@
             }
             // It's a redirection class.
             val toClass = fields[2].substring(1)
-            imf.setRedirectionClass(currentClassName, toClass)
+
+            processor.onRedirectionClass(className, toClass)
         } else if (fields[2].startsWith("~")) {
             if (classType != SpecialClass.NotSpecial) {
                 // We could support it, but not needed at least for now.
@@ -218,7 +444,8 @@
             }
             // It's a class-load hook
             val callback = fields[2].substring(1)
-            imf.setClassLoadHook(currentClassName, callback)
+
+            processor.onClassLoadHook(className, callback)
         } else {
             val policy = parsePolicy(fields[2])
             if (!policy.isUsableWithClasses) {
@@ -229,26 +456,27 @@
                 SpecialClass.NotSpecial -> {
                     // TODO: Duplicate check, etc
                     if (superClass == null) {
-                        imf.setPolicyForClass(
-                            currentClassName, policy.withReason(FILTER_REASON)
-                        )
+                        currentClassName = className
+                        processor.onSimpleClassStart(className)
+                        processor.onSimpleClassPolicy(className, policy.withReason(FILTER_REASON))
                     } else {
-                        subclassFilter.addPolicy(
+                        processor.onSubClassPolicy(
                             superClass,
-                            policy.withReason("extends $superClass")
+                            policy.withReason("extends $superClass"),
                         )
                     }
                 }
-
                 SpecialClass.Aidl -> {
                     if (aidlPolicy != null) {
                         throw ParseException(
                             "Policy for AIDL classes already defined"
                         )
                     }
-                    aidlPolicy = policy.withReason(
+                    val p = policy.withReason(
                         "$FILTER_REASON (special-class AIDL)"
                     )
+                    processor.onSpecialClassPolicy(classType, p)
+                    aidlPolicy = p
                 }
 
                 SpecialClass.FeatureFlags -> {
@@ -257,9 +485,11 @@
                             "Policy for feature flags already defined"
                         )
                     }
-                    featureFlagsPolicy = policy.withReason(
+                    val p = policy.withReason(
                         "$FILTER_REASON (special-class feature flags)"
                     )
+                    processor.onSpecialClassPolicy(classType, p)
+                    featureFlagsPolicy = p
                 }
 
                 SpecialClass.Sysprops -> {
@@ -268,9 +498,11 @@
                             "Policy for sysprops already defined"
                         )
                     }
-                    syspropsPolicy = policy.withReason(
+                    val p = policy.withReason(
                         "$FILTER_REASON (special-class sysprops)"
                     )
+                    processor.onSpecialClassPolicy(classType, p)
+                    syspropsPolicy = p
                 }
 
                 SpecialClass.RFile -> {
@@ -279,9 +511,11 @@
                             "Policy for R file already defined"
                         )
                     }
-                    rFilePolicy = policy.withReason(
+                    val p = policy.withReason(
                         "$FILTER_REASON (special-class R file)"
                     )
+                    processor.onSpecialClassPolicy(classType, p)
+                    rFilePolicy = p
                 }
             }
         }
@@ -296,17 +530,16 @@
         if (!policy.isUsableWithFields) {
             throw ParseException("Field can't have policy '$policy'")
         }
-        require(this::currentClassName.isInitialized)
 
         // TODO: Duplicate check, etc
-        imf.setPolicyForField(currentClassName, name, policy.withReason(FILTER_REASON))
+        processor.onField(currentClassName!!, name, policy.withReason(FILTER_REASON))
     }
 
     private fun parseMethod(fields: Array<String>) {
         if (fields.size < 3 || fields.size > 4) {
             throw ParseException("Method ('m') expects 3 or 4 fields.")
         }
-        val name = fields[1]
+        val methodName = fields[1]
         val signature: String
         val policyStr: String
         if (fields.size <= 3) {
@@ -323,44 +556,48 @@
             throw ParseException("Method can't have policy '$policy'")
         }
 
-        require(this::currentClassName.isInitialized)
+        val className = currentClassName!!
 
-        imf.setPolicyForMethod(
-            currentClassName, name, signature,
-            policy.withReason(FILTER_REASON)
-        )
-        if (policy == FilterPolicy.Substitute) {
-            val fromName = policyStr.substring(1)
+        val policyWithReason = policy.withReason(FILTER_REASON)
+        if (policy != FilterPolicy.Substitute) {
+            processor.onSimpleMethodPolicy(className, methodName, signature, policyWithReason)
+        } else {
+            val targetName = policyStr.substring(1)
 
-            if (fromName == name) {
+            if (targetName == methodName) {
                 throw ParseException(
                     "Substitution must have a different name"
                 )
             }
 
-            // Set the policy for the "from" method.
-            imf.setPolicyForMethod(
-                currentClassName, fromName, signature,
-                FilterPolicy.Keep.withReason(FILTER_REASON)
-            )
-
-            val classAndMethod = splitWithLastPeriod(fromName)
+            val classAndMethod = splitWithLastPeriod(targetName)
             if (classAndMethod != null) {
                 // If the substitution target contains a ".", then
                 // it's a method call redirect.
-                methodReplaceSpec.add(
-                    TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec(
-                        currentClassName.toJvmClassName(),
-                        name,
+                val spec = TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec(
+                        currentClassName!!.toJvmClassName(),
+                        methodName,
                         signature,
                         classAndMethod.first.toJvmClassName(),
                         classAndMethod.second,
                     )
+                processor.onMethodOutClassReplace(
+                    className,
+                    methodName,
+                    signature,
+                    spec,
+                    policyWithReason,
                 )
             } else {
                 // It's an in-class replace.
                 // ("@RavenwoodReplace" equivalent)
-                imf.setRenameTo(currentClassName, fromName, signature, name)
+                processor.onMethodInClassReplace(
+                    className,
+                    methodName,
+                    signature,
+                    targetName,
+                    policyWithReason,
+                )
             }
         }
     }
@@ -378,7 +615,7 @@
         // applied. (Which is needed for services.jar)
         val prefix = fields[2].trimStart('/')
 
-        typeRenameSpec += TextFilePolicyRemapperFilter.TypeRenameSpec(
+        processor.onRename(
             pattern, prefix
         )
     }
diff --git a/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh b/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh
index b389a67..8408a18 100755
--- a/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh
+++ b/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh
@@ -34,10 +34,11 @@
 
 SCRIPT_NAME="${0##*/}"
 
-GOLDEN_DIR=golden-output
+GOLDEN_DIR=${GOLDEN_DIR:-golden-output}
 mkdir -p $GOLDEN_DIR
 
-DIFF_CMD=${DIFF:-diff -u --ignore-blank-lines --ignore-space-change}
+# TODO(b/388562869) We shouldn't need `--ignore-matching-lines`, but the golden files may not have the "Constant pool:" lines.
+DIFF_CMD=${DIFF_CMD:-diff -u --ignore-blank-lines --ignore-space-change --ignore-matching-lines='^\(Constant.pool:\|{\)$'}
 
 update=0
 three_way=0
@@ -62,7 +63,7 @@
 shift $(($OPTIND - 1))
 
 # Build the dump files, which are the input of this test.
-run m  dump-jar tiny-framework-dump-test
+run ${BUILD_CMD:=m} dump-jar tiny-framework-dump-test
 
 
 # Get the path to the generate text files. (not the golden files.)
diff --git a/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py b/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py
index 88fa492..c35d6d1 100755
--- a/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py
+++ b/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py
@@ -28,8 +28,11 @@
 
 # Run diff.
 def run_diff(file1, file2):
+    # TODO(b/388562869) We shouldn't need `--ignore-matching-lines`, but the golden files may not have the "Constant pool:" lines.
     command = ['diff', '-u', '--ignore-blank-lines',
-               '--ignore-space-change', file1, file2]
+               '--ignore-space-change',
+               '--ignore-matching-lines=^\(Constant.pool:\|{\)$',
+               file1, file2]
     print(' '.join(command))
     result = subprocess.run(command, stderr=sys.stdout)
 
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 37d045b..8e037c3 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -413,6 +413,7 @@
     private SparseArray<SurfaceControl> mA11yOverlayLayers = new SparseArray<>();
 
     private final FlashNotificationsController mFlashNotificationsController;
+    private final HearingDevicePhoneCallNotificationController mHearingDeviceNotificationController;
     private final UserManagerInternal mUmi;
 
     private AccessibilityUserState getCurrentUserStateLocked() {
@@ -541,7 +542,8 @@
             MagnificationController magnificationController,
             @Nullable AccessibilityInputFilter inputFilter,
             ProxyManager proxyManager,
-            PermissionEnforcer permissionEnforcer) {
+            PermissionEnforcer permissionEnforcer,
+            HearingDevicePhoneCallNotificationController hearingDeviceNotificationController) {
         super(permissionEnforcer);
         mContext = context;
         mPowerManager =  (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
@@ -569,6 +571,11 @@
         // TODO(b/255426725): not used on tests
         mVisibleBgUserIds = null;
         mInputManager = context.getSystemService(InputManager.class);
+        if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) {
+            mHearingDeviceNotificationController = hearingDeviceNotificationController;
+        } else {
+            mHearingDeviceNotificationController = null;
+        }
 
         init();
     }
@@ -618,6 +625,12 @@
         } else {
             mVisibleBgUserIds = null;
         }
+        if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) {
+            mHearingDeviceNotificationController = new HearingDevicePhoneCallNotificationController(
+                    context);
+        } else {
+            mHearingDeviceNotificationController = null;
+        }
 
         init();
     }
@@ -630,6 +643,11 @@
         if (enableTalkbackAndMagnifierKeyGestures()) {
             mInputManager.registerKeyGestureEventHandler(mKeyGestureEventHandler);
         }
+        if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) {
+            if (mHearingDeviceNotificationController != null) {
+                mHearingDeviceNotificationController.startListenForCallState();
+            }
+        }
         disableAccessibilityMenuToMigrateIfNeeded();
     }
 
diff --git a/services/accessibility/java/com/android/server/accessibility/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/AutoclickController.java
index a69ecec..b94fa2f 100644
--- a/services/accessibility/java/com/android/server/accessibility/AutoclickController.java
+++ b/services/accessibility/java/com/android/server/accessibility/AutoclickController.java
@@ -18,8 +18,11 @@
 
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 
+import static com.android.server.accessibility.AutoclickIndicatorView.SHOW_INDICATOR_DELAY_TIME;
+
 import android.accessibilityservice.AccessibilityTrace;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.database.ContentObserver;
@@ -67,7 +70,7 @@
 
     // Lazily created on the first mouse motion event.
     private ClickScheduler mClickScheduler;
-    private ClickDelayObserver mClickDelayObserver;
+    private AutoclickSettingsObserver mAutoclickSettingsObserver;
     private AutoclickIndicatorScheduler mAutoclickIndicatorScheduler;
     private AutoclickIndicatorView mAutoclickIndicatorView;
     private WindowManager mWindowManager;
@@ -87,14 +90,17 @@
         if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
             if (mClickScheduler == null) {
                 Handler handler = new Handler(mContext.getMainLooper());
-                mClickScheduler =
-                        new ClickScheduler(handler, AccessibilityManager.AUTOCLICK_DELAY_DEFAULT);
-                mClickDelayObserver = new ClickDelayObserver(mUserId, handler);
-                mClickDelayObserver.start(mContext.getContentResolver(), mClickScheduler);
-
                 if (Flags.enableAutoclickIndicator()) {
                     initiateAutoclickIndicator(handler);
                 }
+
+                mClickScheduler =
+                        new ClickScheduler(handler, AccessibilityManager.AUTOCLICK_DELAY_DEFAULT);
+                mAutoclickSettingsObserver = new AutoclickSettingsObserver(mUserId, handler);
+                mAutoclickSettingsObserver.start(
+                        mContext.getContentResolver(),
+                        mClickScheduler,
+                        mAutoclickIndicatorScheduler);
             }
 
             handleMouseMotion(event, policyFlags);
@@ -154,9 +160,9 @@
 
     @Override
     public void onDestroy() {
-        if (mClickDelayObserver != null) {
-            mClickDelayObserver.stop();
-            mClickDelayObserver = null;
+        if (mAutoclickSettingsObserver != null) {
+            mAutoclickSettingsObserver.stop();
+            mAutoclickSettingsObserver = null;
         }
         if (mClickScheduler != null) {
             mClickScheduler.cancel();
@@ -189,19 +195,24 @@
     }
 
     /**
-     * Observes setting value for autoclick delay, and updates ClickScheduler delay whenever the
-     * setting value changes.
+     * Observes autoclick setting values, and updates ClickScheduler delay and indicator size
+     * whenever the setting value changes.
      */
-    final private static class ClickDelayObserver extends ContentObserver {
+    final private static class AutoclickSettingsObserver extends ContentObserver {
         /** URI used to identify the autoclick delay setting with content resolver. */
         private final Uri mAutoclickDelaySettingUri = Settings.Secure.getUriFor(
                 Settings.Secure.ACCESSIBILITY_AUTOCLICK_DELAY);
 
+        /** URI used to identify the autoclick cursor area size setting with content resolver. */
+        private final Uri mAutoclickCursorAreaSizeSettingUri =
+                Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE);
+
         private ContentResolver mContentResolver;
         private ClickScheduler mClickScheduler;
+        private AutoclickIndicatorScheduler mAutoclickIndicatorScheduler;
         private final int mUserId;
 
-        public ClickDelayObserver(int userId, Handler handler) {
+        public AutoclickSettingsObserver(int userId, Handler handler) {
             super(handler);
             mUserId = userId;
         }
@@ -214,11 +225,13 @@
          *     changes.
          * @param clickScheduler ClickScheduler that should be updated when click delay changes.
          * @throws IllegalStateException If internal state is already setup when the method is
-         *         called.
+         *     called.
          * @throws NullPointerException If any of the arguments is a null pointer.
          */
-        public void start(@NonNull ContentResolver contentResolver,
-                @NonNull ClickScheduler clickScheduler) {
+        public void start(
+                @NonNull ContentResolver contentResolver,
+                @NonNull ClickScheduler clickScheduler,
+                @Nullable AutoclickIndicatorScheduler autoclickIndicatorScheduler) {
             if (mContentResolver != null || mClickScheduler != null) {
                 throw new IllegalStateException("Observer already started.");
             }
@@ -231,11 +244,20 @@
 
             mContentResolver = contentResolver;
             mClickScheduler = clickScheduler;
+            mAutoclickIndicatorScheduler = autoclickIndicatorScheduler;
             mContentResolver.registerContentObserver(mAutoclickDelaySettingUri, false, this,
                     mUserId);
 
             // Initialize mClickScheduler's initial delay value.
             onChange(true, mAutoclickDelaySettingUri);
+
+            if (Flags.enableAutoclickIndicator()) {
+                // Register observer to listen to cursor area size setting change.
+                mContentResolver.registerContentObserver(
+                        mAutoclickCursorAreaSizeSettingUri, false, this, mUserId);
+                // Initialize mAutoclickIndicatorView's initial size.
+                onChange(true, mAutoclickCursorAreaSizeSettingUri);
+            }
         }
 
         /**
@@ -246,7 +268,7 @@
          */
         public void stop() {
             if (mContentResolver == null || mClickScheduler == null) {
-                throw new IllegalStateException("ClickDelayObserver not started.");
+                throw new IllegalStateException("AutoclickSettingsObserver not started.");
             }
 
             mContentResolver.unregisterContentObserver(this);
@@ -260,6 +282,18 @@
                         AccessibilityManager.AUTOCLICK_DELAY_DEFAULT, mUserId);
                 mClickScheduler.updateDelay(delay);
             }
+            if (Flags.enableAutoclickIndicator()
+                    && mAutoclickCursorAreaSizeSettingUri.equals(uri)) {
+                int size =
+                        Settings.Secure.getIntForUser(
+                                mContentResolver,
+                                Settings.Secure.ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE,
+                                AccessibilityManager.AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT,
+                                mUserId);
+                if (mAutoclickIndicatorScheduler != null) {
+                    mAutoclickIndicatorScheduler.updateCursorAreaSize(size);
+                }
+            }
         }
     }
 
@@ -286,8 +320,6 @@
         }
 
         public void update() {
-            // TODO(b/383901288): update delay time once determined by UX.
-            long SHOW_INDICATOR_DELAY_TIME = 150;
             long scheduledShowIndicatorTime =
                     SystemClock.uptimeMillis() + SHOW_INDICATOR_DELAY_TIME;
             // If there already is a scheduled show indicator at time before the updated time, just
@@ -317,6 +349,10 @@
             mScheduledShowIndicatorTime = -1;
             mHandler.removeCallbacks(this);
         }
+
+        public void updateCursorAreaSize(int size) {
+            mAutoclickIndicatorView.setRadius(size);
+        }
     }
 
     /**
@@ -432,6 +468,10 @@
          */
         public void updateDelay(int delay) {
             mDelay = delay;
+
+            if (Flags.enableAutoclickIndicator() && mAutoclickIndicatorView != null) {
+                mAutoclickIndicatorView.setAnimationDuration(delay - SHOW_INDICATOR_DELAY_TIME);
+            }
         }
 
         /**
diff --git a/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java b/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java
index 54c31e5..bf50151 100644
--- a/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java
+++ b/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java
@@ -16,25 +16,44 @@
 
 package com.android.server.accessibility;
 
+import static android.view.accessibility.AccessibilityManager.AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT;
+
+import android.animation.ValueAnimator;
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Paint;
+import android.graphics.RectF;
 import android.util.DisplayMetrics;
 import android.view.View;
+import android.view.accessibility.AccessibilityManager;
+import android.view.animation.LinearInterpolator;
 
 // A visual indicator for the autoclick feature.
 public class AutoclickIndicatorView extends View {
     private static final String TAG = AutoclickIndicatorView.class.getSimpleName();
 
-    // TODO(b/383901288): allow users to customize the indicator area.
-    static final float RADIUS = 50;
+    // TODO(b/383901288): update delay time once determined by UX.
+    static final int SHOW_INDICATOR_DELAY_TIME = 150;
+
+    static final int MINIMAL_ANIMATION_DURATION = 50;
+
+    private float mRadius = AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT;
 
     private final Paint mPaint;
 
+    private final ValueAnimator mAnimator;
+
+    private final RectF mRingRect;
+
     // x and y coordinates of the visual indicator.
     private float mX;
     private float mY;
 
+    // Current sweep angle of the animated ring.
+    private float mSweepAngle;
+
+    private int mAnimationDuration = AccessibilityManager.AUTOCLICK_DELAY_DEFAULT;
+
     // Status of whether the visual indicator should display or not.
     private boolean showIndicator = false;
 
@@ -46,6 +65,18 @@
         mPaint.setARGB(255, 52, 103, 235);
         mPaint.setStyle(Paint.Style.STROKE);
         mPaint.setStrokeWidth(10);
+
+        mAnimator = ValueAnimator.ofFloat(0, 360);
+        mAnimator.setDuration(mAnimationDuration);
+        mAnimator.setInterpolator(new LinearInterpolator());
+        mAnimator.addUpdateListener(
+                animation -> {
+                    mSweepAngle = (float) animation.getAnimatedValue();
+                    // Redraw the view with the updated angle.
+                    invalidate();
+                });
+
+        mRingRect = new RectF();
     }
 
     @Override
@@ -53,7 +84,12 @@
         super.onDraw(canvas);
 
         if (showIndicator) {
-            canvas.drawCircle(mX, mY, RADIUS, mPaint);
+            mRingRect.set(
+                    /* left= */ mX - mRadius,
+                    /* top= */ mY - mRadius,
+                    /* right= */ mX + mRadius,
+                    /* bottom= */ mY + mRadius);
+            canvas.drawArc(mRingRect, /* startAngle= */ -90, mSweepAngle, false, mPaint);
         }
     }
 
@@ -72,13 +108,24 @@
         mY = y;
     }
 
+    public void setRadius(int radius) {
+        mRadius = radius;
+    }
+
     public void redrawIndicator() {
         showIndicator = true;
         invalidate();
+        mAnimator.start();
     }
 
     public void clearIndicator() {
         showIndicator = false;
+        mAnimator.cancel();
         invalidate();
     }
+
+    public void setAnimationDuration(int duration) {
+        mAnimationDuration = Math.max(duration, MINIMAL_ANIMATION_DURATION);
+        mAnimator.setDuration(mAnimationDuration);
+    }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java b/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java
new file mode 100644
index 0000000..d06daf5
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.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.accessibility;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothUuid;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.MediaRecorder;
+import android.os.Bundle;
+import android.telephony.TelephonyCallback;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.internal.R;
+import com.android.internal.messages.nano.SystemMessageProto;
+import com.android.internal.notification.SystemNotificationChannels;
+import com.android.internal.util.ArrayUtils;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+/**
+ * A controller class to handle notification for hearing device during phone calls.
+ */
+public class HearingDevicePhoneCallNotificationController {
+
+    private final TelephonyManager mTelephonyManager;
+    private final TelephonyCallback mTelephonyListener;
+    private final Executor mCallbackExecutor;
+
+    public HearingDevicePhoneCallNotificationController(@NonNull Context context) {
+        mTelephonyListener = new CallStateListener(context);
+        mTelephonyManager = context.getSystemService(TelephonyManager.class);
+        mCallbackExecutor = Executors.newSingleThreadExecutor();
+    }
+
+    @VisibleForTesting
+    HearingDevicePhoneCallNotificationController(@NonNull Context context,
+            TelephonyCallback telephonyCallback) {
+        mTelephonyListener = telephonyCallback;
+        mTelephonyManager = context.getSystemService(TelephonyManager.class);
+        mCallbackExecutor = context.getMainExecutor();
+    }
+
+    /**
+     * Registers a telephony callback to listen for call state changed to handle notification for
+     * hearing device during phone calls.
+     */
+    public void startListenForCallState() {
+        mTelephonyManager.registerTelephonyCallback(mCallbackExecutor, mTelephonyListener);
+    }
+
+    /**
+     * A telephony callback listener to listen to call state changes and show/dismiss notification
+     */
+    @VisibleForTesting
+    static class CallStateListener extends TelephonyCallback implements
+            TelephonyCallback.CallStateListener {
+
+        private static final String TAG =
+                "HearingDevice_CallStateListener";
+        private static final String ACTION_SWITCH_TO_BUILTIN_MIC =
+                "com.android.server.accessibility.hearingdevice.action.SWITCH_TO_BUILTIN_MIC";
+        private static final String ACTION_SWITCH_TO_HEARING_MIC =
+                "com.android.server.accessibility.hearingdevice.action.SWITCH_TO_HEARING_MIC";
+        private static final String ACTION_BLUETOOTH_DEVICE_DETAILS =
+                "com.android.settings.BLUETOOTH_DEVICE_DETAIL_SETTINGS";
+        private static final String KEY_BLUETOOTH_ADDRESS = "device_address";
+        private static final String EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args";
+        private static final int MICROPHONE_SOURCE_VOICE_COMMUNICATION =
+                MediaRecorder.AudioSource.VOICE_COMMUNICATION;
+        private static final AudioDeviceAttributes BUILTIN_MIC = new AudioDeviceAttributes(
+                AudioDeviceAttributes.ROLE_INPUT, AudioDeviceInfo.TYPE_BUILTIN_MIC, "");
+
+        private final Context mContext;
+        private NotificationManager mNotificationManager;
+        private AudioManager mAudioManager;
+        private BroadcastReceiver mHearingDeviceActionReceiver;
+        private BluetoothDevice mHearingDevice;
+        private boolean mIsNotificationShown = false;
+
+        CallStateListener(@NonNull Context context) {
+            mContext = context;
+        }
+
+        @Override
+        @SuppressLint("AndroidFrameworkRequiresPermission")
+        public void onCallStateChanged(int state) {
+            // NotificationManagerService and AudioService are all initialized after
+            // AccessibilityManagerService.
+            // Can not get them in constructor. Need to get these services until callback is
+            // triggered.
+            mNotificationManager = mContext.getSystemService(NotificationManager.class);
+            mAudioManager = mContext.getSystemService(AudioManager.class);
+            if (mNotificationManager == null || mAudioManager == null) {
+                Log.w(TAG, "NotificationManager or AudioManager is not prepare yet.");
+                return;
+            }
+
+            if (state == TelephonyManager.CALL_STATE_IDLE) {
+                dismissNotificationIfNeeded();
+
+                if (mHearingDevice != null) {
+                    // reset to its original status
+                    setMicrophonePreferredForCalls(mHearingDevice.isMicrophonePreferredForCalls());
+                }
+                mHearingDevice = null;
+            }
+            if (state == TelephonyManager.CALL_STATE_OFFHOOK) {
+                mHearingDevice = getSupportedInputHearingDeviceInfo(
+                        mAudioManager.getAvailableCommunicationDevices());
+                if (mHearingDevice != null) {
+                    showNotificationIfNeeded();
+                }
+            }
+        }
+
+        private void showNotificationIfNeeded() {
+            if (mIsNotificationShown) {
+                return;
+            }
+
+            showNotification(mHearingDevice.isMicrophonePreferredForCalls());
+            mIsNotificationShown = true;
+        }
+
+        private void dismissNotificationIfNeeded() {
+            if (!mIsNotificationShown) {
+                return;
+            }
+
+            dismissNotification();
+            mIsNotificationShown = false;
+        }
+
+        private void showNotification(boolean useRemoteMicrophone) {
+            mNotificationManager.notify(
+                    SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH,
+                    createSwitchInputNotification(useRemoteMicrophone));
+            registerReceiverIfNeeded();
+        }
+
+        private void dismissNotification() {
+            unregisterReceiverIfNeeded();
+            mNotificationManager.cancel(
+                    SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH);
+        }
+
+        private BluetoothDevice getSupportedInputHearingDeviceInfo(List<AudioDeviceInfo> infoList) {
+            final BluetoothAdapter bluetoothAdapter = mContext.getSystemService(
+                    BluetoothManager.class).getAdapter();
+            if (bluetoothAdapter == null) {
+                return null;
+            }
+            if (!isHapClientSupported()) {
+                return null;
+            }
+
+            final Set<String> inputDeviceAddress = Arrays.stream(
+                    mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).map(
+                    AudioDeviceInfo::getAddress).collect(Collectors.toSet());
+
+            //TODO: b/370812132 - Need to update if TYPE_LEA_HEARING_AID is added
+            final AudioDeviceInfo hearingDeviceInfo = infoList.stream()
+                    .filter(info -> info.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET)
+                    .filter(info -> inputDeviceAddress.contains(info.getAddress()))
+                    .filter(info -> isHapClientDevice(bluetoothAdapter, info))
+                    .findAny()
+                    .orElse(null);
+
+            return (hearingDeviceInfo != null) ? bluetoothAdapter.getRemoteDevice(
+                    hearingDeviceInfo.getAddress()) : null;
+        }
+
+        @VisibleForTesting
+        boolean isHapClientDevice(BluetoothAdapter bluetoothAdapter, AudioDeviceInfo info) {
+            BluetoothDevice device = bluetoothAdapter.getRemoteDevice(info.getAddress());
+            return ArrayUtils.contains(device.getUuids(), BluetoothUuid.HAS);
+        }
+
+        @VisibleForTesting
+        boolean isHapClientSupported() {
+            return BluetoothAdapter.getDefaultAdapter().getSupportedProfiles().contains(
+                    BluetoothProfile.HAP_CLIENT);
+        }
+
+        private Notification createSwitchInputNotification(boolean useRemoteMicrophone) {
+            return new Notification.Builder(mContext,
+                    SystemNotificationChannels.ACCESSIBILITY_HEARING_DEVICE)
+                    .setContentTitle(getSwitchInputTitle(useRemoteMicrophone))
+                    .setContentText(getSwitchInputMessage(useRemoteMicrophone))
+                    .setSmallIcon(R.drawable.ic_settings_24dp)
+                    .setColor(mContext.getResources().getColor(
+                            com.android.internal.R.color.system_notification_accent_color))
+                    .setLocalOnly(true)
+                    .setCategory(Notification.CATEGORY_SYSTEM)
+                    .setContentIntent(createPendingIntent(ACTION_BLUETOOTH_DEVICE_DETAILS))
+                    .setActions(buildSwitchInputAction(useRemoteMicrophone),
+                            buildOpenSettingsAction())
+                    .build();
+        }
+
+        private Notification.Action buildSwitchInputAction(boolean useRemoteMicrophone) {
+            return useRemoteMicrophone
+                    ? new Notification.Action.Builder(null,
+                            mContext.getString(R.string.hearing_device_notification_switch_button),
+                            createPendingIntent(ACTION_SWITCH_TO_BUILTIN_MIC)).build()
+                    : new Notification.Action.Builder(null,
+                            mContext.getString(R.string.hearing_device_notification_switch_button),
+                            createPendingIntent(ACTION_SWITCH_TO_HEARING_MIC)).build();
+        }
+
+        private Notification.Action buildOpenSettingsAction() {
+            return new Notification.Action.Builder(null,
+                    mContext.getString(R.string.hearing_device_notification_settings_button),
+                    createPendingIntent(ACTION_BLUETOOTH_DEVICE_DETAILS)).build();
+        }
+
+        private PendingIntent createPendingIntent(String action) {
+            final Intent intent = new Intent(action);
+
+            switch (action) {
+                case ACTION_SWITCH_TO_BUILTIN_MIC, ACTION_SWITCH_TO_HEARING_MIC -> {
+                    intent.setPackage(mContext.getPackageName());
+                    return PendingIntent.getBroadcast(mContext, /* requestCode = */ 0, intent,
+                            PendingIntent.FLAG_IMMUTABLE);
+                }
+                case ACTION_BLUETOOTH_DEVICE_DETAILS -> {
+                    Bundle bundle = new Bundle();
+                    bundle.putString(KEY_BLUETOOTH_ADDRESS, mHearingDevice.getAddress());
+                    intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle);
+                    intent.addFlags(
+                            Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+                    return PendingIntent.getActivity(mContext, /* requestCode = */ 0, intent,
+                            PendingIntent.FLAG_IMMUTABLE);
+                }
+            }
+            return null;
+        }
+
+        private void setMicrophonePreferredForCalls(boolean useRemoteMicrophone) {
+            if (useRemoteMicrophone) {
+                switchToHearingMic();
+            } else {
+                switchToBuiltinMic();
+            }
+        }
+
+        @SuppressLint("AndroidFrameworkRequiresPermission")
+        private void switchToBuiltinMic() {
+            mAudioManager.clearPreferredDevicesForCapturePreset(
+                    MICROPHONE_SOURCE_VOICE_COMMUNICATION);
+            mAudioManager.setPreferredDeviceForCapturePreset(MICROPHONE_SOURCE_VOICE_COMMUNICATION,
+                    BUILTIN_MIC);
+        }
+
+        @SuppressLint("AndroidFrameworkRequiresPermission")
+        private void switchToHearingMic() {
+            // clear config to let audio manager to determine next priority device. We can assume
+            // user connects to hearing device here, so next priority device should be hearing
+            // device.
+            mAudioManager.clearPreferredDevicesForCapturePreset(
+                    MICROPHONE_SOURCE_VOICE_COMMUNICATION);
+        }
+
+        private void registerReceiverIfNeeded() {
+            if (mHearingDeviceActionReceiver != null) {
+                return;
+            }
+            mHearingDeviceActionReceiver = new HearingDeviceActionReceiver();
+            final IntentFilter intentFilter = new IntentFilter();
+            intentFilter.addAction(ACTION_SWITCH_TO_BUILTIN_MIC);
+            intentFilter.addAction(ACTION_SWITCH_TO_HEARING_MIC);
+            mContext.registerReceiver(mHearingDeviceActionReceiver, intentFilter,
+                    Manifest.permission.MANAGE_ACCESSIBILITY, null, Context.RECEIVER_NOT_EXPORTED);
+        }
+
+        private void unregisterReceiverIfNeeded() {
+            if (mHearingDeviceActionReceiver == null) {
+                return;
+            }
+            mContext.unregisterReceiver(mHearingDeviceActionReceiver);
+            mHearingDeviceActionReceiver = null;
+        }
+
+        private CharSequence getSwitchInputTitle(boolean useRemoteMicrophone) {
+            return useRemoteMicrophone
+                    ? mContext.getString(
+                            R.string.hearing_device_switch_phone_mic_notification_title)
+                    : mContext.getString(
+                            R.string.hearing_device_switch_hearing_mic_notification_title);
+        }
+
+        private CharSequence getSwitchInputMessage(boolean useRemoteMicrophone) {
+            return useRemoteMicrophone
+                    ? mContext.getString(
+                            R.string.hearing_device_switch_phone_mic_notification_text)
+                    : mContext.getString(
+                            R.string.hearing_device_switch_hearing_mic_notification_text);
+        }
+
+        private class HearingDeviceActionReceiver extends BroadcastReceiver {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                final String action = intent.getAction();
+                if (TextUtils.isEmpty(action)) {
+                    return;
+                }
+
+                if (ACTION_SWITCH_TO_BUILTIN_MIC.equals(action)) {
+                    switchToBuiltinMic();
+                    showNotification(/* useRemoteMicrophone= */ false);
+                } else if (ACTION_SWITCH_TO_HEARING_MIC.equals(action)) {
+                    switchToHearingMic();
+                    showNotification(/* useRemoteMicrophone= */ true);
+                }
+            }
+        }
+    }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
index f15b8ee..cd46b38 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
@@ -38,7 +38,7 @@
     // Pointer-related constants
     // This constant captures the current implementation detail that
     // pointer IDs are between 0 and 31 inclusive (subject to change).
-    // (See MAX_POINTER_ID in frameworks/base/include/ui/Input.h)
+    // (See MAX_POINTER_ID in frameworks/native/include/input/Input.h)
     public static final int MAX_POINTER_COUNT = 32;
     // Constant referring to the ids bits of all pointers.
     public static final int ALL_POINTER_ID_BITS = 0xFFFFFFFF;
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index 57d33f1a..4376444 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -50,7 +50,10 @@
 import android.app.appsearch.observer.SchemaChangeInfo;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
+import android.content.pm.SigningInfo;
 import android.os.Binder;
 import android.os.CancellationSignal;
 import android.os.IBinder;
@@ -292,7 +295,8 @@
                                     safeExecuteAppFunctionCallback,
                                     /* bindFlags= */ Context.BIND_AUTO_CREATE
                                             | Context.BIND_FOREGROUND_SERVICE,
-                                    callerBinder);
+                                    callerBinder,
+                                    callingUid);
                         })
                 .exceptionally(
                         ex -> {
@@ -444,7 +448,8 @@
             @NonNull ICancellationSignal cancellationSignalTransport,
             @NonNull SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback,
             int bindFlags,
-            @NonNull IBinder callerBinder) {
+            @NonNull IBinder callerBinder,
+            int callingUid) {
         CancellationSignal cancellationSignal =
                 CancellationSignal.fromTransport(cancellationSignalTransport);
         ICancellationCallback cancellationCallback =
@@ -465,7 +470,11 @@
                         new RunAppFunctionServiceCallback(
                                 requestInternal,
                                 cancellationCallback,
-                                safeExecuteAppFunctionCallback),
+                                safeExecuteAppFunctionCallback,
+                                getPackageSigningInfo(
+                                        targetUser,
+                                        requestInternal.getCallingPackage(),
+                                        callingUid)),
                         callerBinder);
 
         if (!bindServiceResult) {
@@ -477,6 +486,23 @@
         }
     }
 
+    @NonNull
+    private SigningInfo getPackageSigningInfo(
+            @NonNull UserHandle targetUser, @NonNull String packageName, int uid) {
+        Objects.requireNonNull(packageName);
+        Objects.requireNonNull(targetUser);
+
+        PackageInfo packageInfo;
+        packageInfo =
+                Objects.requireNonNull(
+                        mPackageManagerInternal.getPackageInfo(
+                                packageName,
+                                PackageManager.GET_SIGNING_CERTIFICATES,
+                                uid,
+                                targetUser.getIdentifier()));
+        return Objects.requireNonNull(packageInfo.signingInfo);
+    }
+
     private AppSearchManager getAppSearchManagerAsUser(@NonNull UserHandle userHandle) {
         return mContext.createContextAsUser(userHandle, /* flags= */ 0)
                 .getSystemService(AppSearchManager.class);
diff --git a/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java b/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java
index 4cba8ec..0cec09d 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java
@@ -23,6 +23,7 @@
 import android.app.appfunctions.ICancellationCallback;
 import android.app.appfunctions.IExecuteAppFunctionCallback;
 import android.app.appfunctions.SafeOneTimeExecuteAppFunctionCallback;
+import android.content.pm.SigningInfo;
 import android.os.SystemClock;
 import android.util.Slog;
 
@@ -38,14 +39,17 @@
     private final ExecuteAppFunctionAidlRequest mRequestInternal;
     private final SafeOneTimeExecuteAppFunctionCallback mSafeExecuteAppFunctionCallback;
     private final ICancellationCallback mCancellationCallback;
+    private final SigningInfo mCallerSigningInfo;
 
     public RunAppFunctionServiceCallback(
             ExecuteAppFunctionAidlRequest requestInternal,
             ICancellationCallback cancellationCallback,
-            SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback) {
-        this.mRequestInternal = requestInternal;
-        this.mSafeExecuteAppFunctionCallback = safeExecuteAppFunctionCallback;
-        this.mCancellationCallback = cancellationCallback;
+            SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback,
+            SigningInfo callerSigningInfo) {
+        mRequestInternal = requestInternal;
+        mSafeExecuteAppFunctionCallback = safeExecuteAppFunctionCallback;
+        mCancellationCallback = cancellationCallback;
+        mCallerSigningInfo = callerSigningInfo;
     }
 
     @Override
@@ -58,6 +62,7 @@
             service.executeAppFunction(
                     mRequestInternal.getClientRequest(),
                     mRequestInternal.getCallingPackage(),
+                    mCallerSigningInfo,
                     mCancellationCallback,
                     new IExecuteAppFunctionCallback.Stub() {
                         @Override
diff --git a/services/backup/java/com/android/server/backup/BackupManagerService.java b/services/backup/java/com/android/server/backup/BackupManagerService.java
index 3f6ede9..8804faf 100644
--- a/services/backup/java/com/android/server/backup/BackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/BackupManagerService.java
@@ -22,9 +22,9 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
-import android.app.ActivityManager;
 import android.app.admin.DevicePolicyManager;
 import android.app.backup.BackupManager;
+import android.app.backup.BackupManagerInternal;
 import android.app.backup.BackupRestoreEventLogger.DataTypeResult;
 import android.app.backup.IBackupManager;
 import android.app.backup.IBackupManagerMonitor;
@@ -60,6 +60,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.DumpUtils;
+import com.android.server.LocalServices;
 import com.android.server.SystemConfig;
 import com.android.server.SystemService;
 import com.android.server.backup.utils.RandomAccessFileUtils;
@@ -91,7 +92,7 @@
  * privileged callers (currently {@link DevicePolicyManager}). If called on {@link
  * UserHandle#USER_SYSTEM}, backup is disabled for all users.
  */
-public class BackupManagerService extends IBackupManager.Stub {
+public class BackupManagerService extends IBackupManager.Stub implements BackupManagerInternal {
     public static final String TAG = "BackupManagerService";
     public static final boolean DEBUG = true;
     public static final boolean MORE_DEBUG = false;
@@ -191,7 +192,6 @@
         }
     }
 
-    // TODO: Remove this when we implement DI by injecting in the construtor.
     @VisibleForTesting
     Handler getBackupHandler() {
         return mHandler;
@@ -637,51 +637,28 @@
     }
 
     @Override
-    public void agentConnectedForUser(int userId, String packageName, IBinder agent)
-            throws RemoteException {
-        if (isUserReadyForBackup(userId)) {
-            agentConnected(userId, packageName, agent);
+    public void agentConnectedForUser(String packageName, @UserIdInt int userId, IBinder agent) {
+        if (!isUserReadyForBackup(userId)) {
+            return;
         }
-    }
 
-    @Override
-    public void agentConnected(String packageName, IBinder agent) throws RemoteException {
-        agentConnectedForUser(binderGetCallingUserId(), packageName, agent);
-    }
-
-    /**
-     * Callback: a requested backup agent has been instantiated. This should only be called from the
-     * {@link ActivityManager}.
-     */
-    public void agentConnected(@UserIdInt int userId, String packageName, IBinder agentBinder) {
-        UserBackupManagerService userBackupManagerService =
-                getServiceForUserIfCallerHasPermission(userId, "agentConnected()");
+        UserBackupManagerService userBackupManagerService = getServiceForUserIfCallerHasPermission(
+                userId, "agentConnected()");
 
         if (userBackupManagerService != null) {
             userBackupManagerService.getBackupAgentConnectionManager().agentConnected(packageName,
-                    agentBinder);
+                    agent);
         }
     }
 
     @Override
-    public void agentDisconnectedForUser(int userId, String packageName) throws RemoteException {
-        if (isUserReadyForBackup(userId)) {
-            agentDisconnected(userId, packageName);
+    public void agentDisconnectedForUser(String packageName, @UserIdInt int userId) {
+        if (!isUserReadyForBackup(userId)) {
+            return;
         }
-    }
 
-    @Override
-    public void agentDisconnected(String packageName) throws RemoteException {
-        agentDisconnectedForUser(binderGetCallingUserId(), packageName);
-    }
-
-    /**
-     * Callback: a backup agent has failed to come up, or has unexpectedly quit. This should only be
-     * called from the {@link ActivityManager}.
-     */
-    public void agentDisconnected(@UserIdInt int userId, String packageName) {
-        UserBackupManagerService userBackupManagerService =
-                getServiceForUserIfCallerHasPermission(userId, "agentDisconnected()");
+        UserBackupManagerService userBackupManagerService = getServiceForUserIfCallerHasPermission(
+                userId, "agentDisconnected()");
 
         if (userBackupManagerService != null) {
             userBackupManagerService.getBackupAgentConnectionManager().agentDisconnected(
@@ -1688,7 +1665,7 @@
      * @param userId User id on which the backup operation is being requested.
      * @param message A message to include in the exception if it is thrown.
      */
-    void enforceCallingPermissionOnUserId(@UserIdInt int userId, String message) {
+    private void enforceCallingPermissionOnUserId(@UserIdInt int userId, String message) {
         if (binderGetCallingUserId() != userId) {
             mContext.enforceCallingOrSelfPermission(
                     Manifest.permission.INTERACT_ACROSS_USERS_FULL, message);
@@ -1697,6 +1674,8 @@
 
     /** Implementation to receive lifecycle event callbacks for system services. */
     public static class Lifecycle extends SystemService {
+        private final BackupManagerService mBackupManagerService;
+
         public Lifecycle(Context context) {
             this(context, new BackupManagerService(context));
         }
@@ -1704,12 +1683,14 @@
         @VisibleForTesting
         Lifecycle(Context context, BackupManagerService backupManagerService) {
             super(context);
+            mBackupManagerService = backupManagerService;
             sInstance = backupManagerService;
+            LocalServices.addService(BackupManagerInternal.class, mBackupManagerService);
         }
 
         @Override
         public void onStart() {
-            publishService(Context.BACKUP_SERVICE, BackupManagerService.sInstance);
+            publishService(Context.BACKUP_SERVICE, mBackupManagerService);
         }
 
         @Override
@@ -1717,17 +1698,17 @@
             // Starts the backup service for this user if backup is active for this user. Offloads
             // work onto the handler thread {@link #mHandlerThread} to keep unlock time low since
             // backup is not essential for device functioning.
-            sInstance.postToHandler(
+            mBackupManagerService.postToHandler(
                     () -> {
-                        sInstance.updateDefaultBackupUserIdIfNeeded();
-                        sInstance.startServiceForUser(user.getUserIdentifier());
-                        sInstance.mHasFirstUserUnlockedSinceBoot = true;
+                        mBackupManagerService.updateDefaultBackupUserIdIfNeeded();
+                        mBackupManagerService.startServiceForUser(user.getUserIdentifier());
+                        mBackupManagerService.mHasFirstUserUnlockedSinceBoot = true;
                     });
         }
 
         @Override
         public void onUserStopping(@NonNull TargetUser user) {
-            sInstance.onStopUser(user.getUserIdentifier());
+            mBackupManagerService.onStopUser(user.getUserIdentifier());
         }
 
         @VisibleForTesting
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 418f3a1..0e2e505 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -109,6 +109,8 @@
 import java.io.PrintWriter;
 import java.util.Collection;
 import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 
 @SuppressLint("LongLogTag")
 public class CompanionDeviceManagerService extends SystemService {
@@ -226,7 +228,8 @@
         if (associations.isEmpty()) return;
 
         mCompanionExemptionProcessor.updateAtm(userId, associations);
-        mCompanionExemptionProcessor.updateAutoRevokeExemptions();
+        ExecutorService executor = Executors.newSingleThreadExecutor();
+        executor.execute(mCompanionExemptionProcessor::updateAutoRevokeExemptions);
     }
 
     @Override
diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java
index d3e808f..7456c50 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -265,8 +265,8 @@
         mInputManagerInternal.setPointerIconVisible(visible, displayId);
     }
 
-    void setMousePointerAccelerationEnabled(boolean enabled, int displayId) {
-        mInputManagerInternal.setMousePointerAccelerationEnabled(enabled, displayId);
+    void setMouseScalingEnabled(boolean enabled, int displayId) {
+        mInputManagerInternal.setMouseScalingEnabled(enabled, displayId);
     }
 
     void setDisplayEligibilityForPointerCapture(boolean isEligible, int displayId) {
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 6bf60bf..260ea75 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -1518,7 +1518,7 @@
 
         final long token = Binder.clearCallingIdentity();
         try {
-            mInputController.setMousePointerAccelerationEnabled(false, displayId);
+            mInputController.setMouseScalingEnabled(false, displayId);
             mInputController.setDisplayEligibilityForPointerCapture(/* isEligible= */ false,
                     displayId);
             if (isTrustedDisplay) {
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 1f3b316..40726b4 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -176,7 +176,7 @@
     public VirtualDeviceManagerService(Context context) {
         super(context);
         mImpl = new VirtualDeviceManagerImpl();
-        mNativeImpl = Flags.enableNativeVdm() ? new VirtualDeviceManagerNativeImpl() : null;
+        mNativeImpl = new VirtualDeviceManagerNativeImpl();
         mLocalService = new LocalService();
     }
 
@@ -208,9 +208,7 @@
     @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES)
     public void onStart() {
         publishBinderService(Context.VIRTUAL_DEVICE_SERVICE, mImpl);
-        if (Flags.enableNativeVdm()) {
-            publishBinderService(VIRTUAL_DEVICE_NATIVE_SERVICE, mNativeImpl);
-        }
+        publishBinderService(VIRTUAL_DEVICE_NATIVE_SERVICE, mNativeImpl);
         publishLocalService(VirtualDeviceManagerInternal.class, mLocalService);
         ActivityTaskManagerInternal activityTaskManagerInternal = getLocalService(
                 ActivityTaskManagerInternal.class);
@@ -769,7 +767,7 @@
                     params,
                     /* activityListener= */ null,
                     /* soundEffectListener= */ null);
-            return new VirtualDeviceManager.VirtualDevice(mImpl, getContext(), virtualDevice);
+            return new VirtualDeviceManager.VirtualDevice(getContext(), virtualDevice);
         }
 
         @Override
diff --git a/services/core/Android.bp b/services/core/Android.bp
index dc83064..d6bffcb 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -132,6 +132,7 @@
     srcs: [
         ":android.hardware.tv.hdmi.connection-V1-java-source",
         ":android.hardware.tv.hdmi.earc-V1-java-source",
+        ":android.hardware.tv.mediaquality-V1-java-source",
         ":statslog-art-java-gen",
         ":statslog-contexthub-java-gen",
         ":services.core-aidl-sources",
diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java
index 778c686..1d914c8 100644
--- a/services/core/java/com/android/server/BinaryTransparencyService.java
+++ b/services/core/java/com/android/server/BinaryTransparencyService.java
@@ -729,8 +729,10 @@
                 private void printModuleDetails(ModuleInfo moduleInfo, final PrintWriter pw) {
                     pw.println("--- Module Details ---");
                     pw.println("Module name: " + moduleInfo.getName());
-                    pw.println("Module visibility: "
-                            + (moduleInfo.isHidden() ? "hidden" : "visible"));
+                    if (!android.content.pm.Flags.removeHiddenModuleUsage()) {
+                        pw.println("Module visibility: "
+                        + (moduleInfo.isHidden() ? "hidden" : "visible"));
+                    }
                 }
 
                 private void printAppDetails(PackageInfo packageInfo,
@@ -1708,7 +1710,7 @@
     private class PackageUpdatedReceiver extends BroadcastReceiver {
         @Override
         public void onReceive(Context context, Intent intent) {
-            if (!intent.getAction().equals(Intent.ACTION_PACKAGE_ADDED)) {
+            if (!Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) {
                 return;
             }
 
diff --git a/services/core/java/com/android/server/GestureLauncherService.java b/services/core/java/com/android/server/GestureLauncherService.java
index dce9760..6459016 100644
--- a/services/core/java/com/android/server/GestureLauncherService.java
+++ b/services/core/java/com/android/server/GestureLauncherService.java
@@ -66,8 +66,7 @@
 /**
  * The service that listens for gestures detected in sensor firmware and starts the intent
  * accordingly.
- * <p>For now, only camera launch gesture is supported, and in the future, more gestures can be
- * added.</p>
+ *
  * @hide
  */
 public class GestureLauncherService extends SystemService {
@@ -109,10 +108,22 @@
     @VisibleForTesting
     static final int EMERGENCY_GESTURE_POWER_BUTTON_COOLDOWN_PERIOD_MS_MAX = 5000;
 
-    /** Indicates camera should be launched on power double tap. */
+    /** Configuration value indicating double tap power gesture is disabled. */
+    @VisibleForTesting static final int DOUBLE_TAP_POWER_DISABLED_MODE = 0;
+
+    /** Configuration value indicating double tap power gesture should launch camera. */
+    @VisibleForTesting static final int DOUBLE_TAP_POWER_LAUNCH_CAMERA_MODE = 1;
+
+    /**
+     * Configuration value indicating double tap power gesture should launch one of many target
+     * actions.
+     */
+    @VisibleForTesting static final int DOUBLE_TAP_POWER_MULTI_TARGET_MODE = 2;
+
+    /** Indicates camera launch is selected as target action for multi target double tap power. */
     @VisibleForTesting static final int LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER = 0;
 
-    /** Indicates wallet should be launched on power double tap. */
+    /** Indicates wallet launch is selected as target action for multi target double tap power. */
     @VisibleForTesting static final int LAUNCH_WALLET_ON_DOUBLE_TAP_POWER = 1;
 
     /** Number of taps required to launch the double tap shortcut (either camera or wallet). */
@@ -228,6 +239,7 @@
             return mId;
         }
     }
+
     public GestureLauncherService(Context context) {
         this(context, new MetricsLogger(),
                 QuickAccessWalletClient.create(context), new UiEventLoggerImpl());
@@ -289,16 +301,15 @@
                     Settings.Secure.getUriFor(
                             Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE),
                     false, mSettingObserver, mUserId);
-        } else {
-            mContext.getContentResolver().registerContentObserver(
-                    Settings.Secure.getUriFor(Settings.Secure.CAMERA_GESTURE_DISABLED),
-                    false, mSettingObserver, mUserId);
-            mContext.getContentResolver().registerContentObserver(
-                    Settings.Secure.getUriFor(
-                            Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED),
-                    false, mSettingObserver, mUserId);
         }
         mContext.getContentResolver().registerContentObserver(
+                Settings.Secure.getUriFor(Settings.Secure.CAMERA_GESTURE_DISABLED),
+                false, mSettingObserver, mUserId);
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Secure.getUriFor(
+                        Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED),
+                false, mSettingObserver, mUserId);
+        mContext.getContentResolver().registerContentObserver(
                 Settings.Secure.getUriFor(Settings.Secure.CAMERA_LIFT_TRIGGER_ENABLED),
                 false, mSettingObserver, mUserId);
         mContext.getContentResolver().registerContentObserver(
@@ -468,23 +479,27 @@
                         Settings.Secure.CAMERA_GESTURE_DISABLED, 0, userId) == 0);
     }
 
-
     /** Checks if camera should be launched on double press of the power button. */
     public static boolean isCameraDoubleTapPowerSettingEnabled(Context context, int userId) {
-        boolean res;
-
-        if (launchWalletOptionOnPowerDoubleTap()) {
-            res = isDoubleTapPowerGestureSettingEnabled(context, userId)
-                    && getDoubleTapPowerGestureAction(context, userId)
-                    == LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER;
-        } else {
-            // These are legacy settings that will be deprecated once the option to launch both
-            // wallet and camera has been created.
-            res = isCameraDoubleTapPowerEnabled(context.getResources())
+        if (!launchWalletOptionOnPowerDoubleTap()) {
+            return isCameraDoubleTapPowerEnabled(context.getResources())
                     && (Settings.Secure.getIntForUser(context.getContentResolver(),
                     Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED, 0, userId) == 0);
         }
-        return res;
+
+        final int doubleTapPowerGestureSettingMode = getDoubleTapPowerGestureMode(
+                context.getResources());
+
+        return switch (doubleTapPowerGestureSettingMode) {
+            case DOUBLE_TAP_POWER_LAUNCH_CAMERA_MODE -> Settings.Secure.getIntForUser(
+                    context.getContentResolver(),
+                    Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED, 0, userId) == 0;
+            case DOUBLE_TAP_POWER_MULTI_TARGET_MODE ->
+                    isMultiTargetDoubleTapPowerGestureSettingEnabled(context, userId)
+                            && getDoubleTapPowerGestureAction(context, userId)
+                            == LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER;
+            default -> false;
+        };
     }
 
     /** Checks if wallet should be launched on double tap of the power button. */
@@ -493,7 +508,9 @@
             return false;
         }
 
-        return isDoubleTapPowerGestureSettingEnabled(context, userId)
+        return getDoubleTapPowerGestureMode(context.getResources())
+                == DOUBLE_TAP_POWER_MULTI_TARGET_MODE
+                && isMultiTargetDoubleTapPowerGestureSettingEnabled(context, userId)
                 && getDoubleTapPowerGestureAction(context, userId)
                 == LAUNCH_WALLET_ON_DOUBLE_TAP_POWER;
     }
@@ -515,6 +532,34 @@
                 isDefaultEmergencyGestureEnabled(context.getResources()) ? 1 : 0, userId) != 0;
     }
 
+    /** Gets the double tap power gesture mode. */
+    private static int getDoubleTapPowerGestureMode(Resources resources) {
+        return resources.getInteger(R.integer.config_doubleTapPowerGestureMode);
+    }
+
+    /**
+     * Whether the setting for multi target double tap power gesture is enabled.
+     *
+     * <p>Multi target double tap power gesture allows the user to choose one of many target actions
+     * when double tapping the power button.
+     * </p>
+     */
+    private static boolean isMultiTargetDoubleTapPowerGestureSettingEnabled(Context context,
+            int userId) {
+        return Settings.Secure.getIntForUser(
+                context.getContentResolver(),
+                Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE_ENABLED,
+                getDoubleTapPowerGestureMode(context.getResources())
+                        == DOUBLE_TAP_POWER_MULTI_TARGET_MODE ? 1 : 0,
+                userId)
+                == 1;
+    }
+
+    /** Gets the selected target action for the multi target double tap power gesture.
+     *
+     * <p>The target actions are defined in {@link Settings.Secure#DOUBLE_TAP_POWER_BUTTON_GESTURE}.
+     * </p>
+     */
     private static int getDoubleTapPowerGestureAction(Context context, int userId) {
         return Settings.Secure.getIntForUser(
                 context.getContentResolver(),
@@ -523,20 +568,6 @@
                 userId);
     }
 
-    /** Whether the shortcut to launch app on power double press is enabled. */
-    private static boolean isDoubleTapPowerGestureSettingEnabled(Context context, int userId) {
-        return Settings.Secure.getIntForUser(
-                context.getContentResolver(),
-                Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE_ENABLED,
-                isDoubleTapConfigEnabled(context.getResources()) ? 1 : 0,
-                userId)
-                == 1;
-    }
-
-    private static boolean isDoubleTapConfigEnabled(Resources resources) {
-        return resources.getBoolean(R.bool.config_doubleTapPowerGestureEnabled);
-    }
-
     /**
      * Gets power button cooldown period in milliseconds after emergency gesture is triggered. The
      * value is capped at a maximum
@@ -595,7 +626,7 @@
                         || isCameraLiftTriggerEnabled(resources)
                         || isEmergencyGestureEnabled(resources);
         if (launchWalletOptionOnPowerDoubleTap()) {
-            res |= isDoubleTapConfigEnabled(resources);
+            res |= getDoubleTapPowerGestureMode(resources) != DOUBLE_TAP_POWER_DISABLED_MODE;
         } else {
             res |= isCameraDoubleTapPowerEnabled(resources);
         }
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 41b4cbd..6cca7d1 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -8950,18 +8950,12 @@
         if (!mAm.mConstants.mFgsStartRestrictionCheckCallerTargetSdk) {
             return true; // In this case, we only check the service's target SDK level.
         }
-        final int callingUid;
-        if (Flags.newFgsRestrictionLogic()) {
-            // We always consider SYSTEM_UID to target S+, so just enable the restrictions.
-            if (actualCallingUid == Process.SYSTEM_UID) {
-                return true;
-            }
-            callingUid = actualCallingUid;
-        } else {
-            // Legacy logic used mRecentCallingUid.
-            callingUid = r.mRecentCallingUid;
+        // We always consider SYSTEM_UID to target S+, so just enable the restrictions.
+        if (actualCallingUid == Process.SYSTEM_UID) {
+            return true;
         }
-        if (!CompatChanges.isChangeEnabled(FGS_BG_START_RESTRICTION_CHANGE_ID, callingUid)) {
+        if (!CompatChanges.isChangeEnabled(FGS_BG_START_RESTRICTION_CHANGE_ID,
+                actualCallingUid)) {
             return false; // If the caller targets < S, then we still disable the restrictions.
         }
 
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index b536dc5..0603c45 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -256,7 +256,7 @@
 import android.app.WaitResult;
 import android.app.assist.ActivityId;
 import android.app.backup.BackupAnnotations.BackupDestination;
-import android.app.backup.IBackupManager;
+import android.app.backup.BackupManagerInternal;
 import android.app.compat.CompatChanges;
 import android.app.job.JobParameters;
 import android.app.usage.UsageEvents;
@@ -4490,11 +4490,8 @@
                 final int userId = app.userId;
                 final String packageName = app.info.packageName;
                 mHandler.post(() -> {
-                    try {
-                        getBackupManager().agentDisconnectedForUser(userId, packageName);
-                    } catch (RemoteException e) {
-                        // Can't happen; the backup manager is local
-                    }
+                    LocalServices.getService(BackupManagerInternal.class).agentDisconnectedForUser(
+                            packageName, userId);
                 });
             }
         } else {
@@ -12864,6 +12861,28 @@
                 }
             }
 
+            final long kernelCmaUsage = Debug.getKernelCmaUsageKb();
+            if (kernelCmaUsage >= 0) {
+                pw.print("      Kernel CMA: ");
+                pw.println(stringifyKBSize(kernelCmaUsage));
+                // CMA memory can be in one of the following four states:
+                //
+                // 1. Free, in which case it is accounted for as part of MemFree, which
+                //    is already considered in the lostRAM calculation below.
+                //
+                // 2. Allocated as part of a userspace allocated, in which case it is
+                //    already accounted for in the total PSS value that was computed.
+                //
+                // 3. Allocated for storing compressed memory (ZRAM) on Android kernels.
+                //    This is accounted for by calculating the amount of memory ZRAM
+                //    consumes and including it in the lostRAM calculuation.
+                //
+                // 4. Allocated by a kernel driver, in which case, it is currently not
+                //    attributed to any term that has been derived thus far. Since the
+                //    allocations come from a kernel driver, add it to kernelUsed.
+                kernelUsed += kernelCmaUsage;
+            }
+
              // Note: ION/DMA-BUF heap pools are reclaimable and hence, they are included as part of
              // memInfo.getCachedSizeKb().
             final long lostRAM = memInfo.getTotalSizeKb()
@@ -13381,12 +13400,32 @@
                 proto.write(MemInfoDumpProto.CACHED_KERNEL_KB, memInfo.getCachedSizeKb());
                 proto.write(MemInfoDumpProto.FREE_KB, memInfo.getFreeSizeKb());
             }
+            // CMA memory can be in one of the following four states:
+            //
+            // 1. Free, in which case it is accounted for as part of MemFree, which
+            //    is already considered in the lostRAM calculation below.
+            //
+            // 2. Allocated as part of a userspace allocated, in which case it is
+            //    already accounted for in the total PSS value that was computed.
+            //
+            // 3. Allocated for storing compressed memory (ZRAM) on Android Kernels.
+            //    This is accounted for by calculating hte amount of memory ZRAM
+            //    consumes and including it in the lostRAM calculation.
+            //
+            // 4. Allocated by a kernel driver, in which case, it is currently not
+            //    attributed to any term that has been derived thus far, so subtract
+            //    it from lostRAM.
+            long kernelCmaUsage = Debug.getKernelCmaUsageKb();
+            if (kernelCmaUsage < 0) {
+                kernelCmaUsage = 0;
+            }
             long lostRAM = memInfo.getTotalSizeKb()
                     - (ss[INDEX_TOTAL_PSS] - ss[INDEX_TOTAL_SWAP_PSS])
                     - memInfo.getFreeSizeKb() - memInfo.getCachedSizeKb()
                     // NR_SHMEM is subtracted twice (getCachedSizeKb() and getKernelUsedSizeKb())
                     + memInfo.getShmemSizeKb()
-                    - memInfo.getKernelUsedSizeKb() - memInfo.getZramTotalSizeKb();
+                    - memInfo.getKernelUsedSizeKb() - memInfo.getZramTotalSizeKb()
+                    - kernelCmaUsage;
             proto.write(MemInfoDumpProto.USED_PSS_KB, ss[INDEX_TOTAL_PSS] - cachedPss);
             proto.write(MemInfoDumpProto.USED_KERNEL_KB, memInfo.getKernelUsedSizeKb());
             proto.write(MemInfoDumpProto.LOST_RAM_KB, lostRAM);
@@ -13505,11 +13544,8 @@
             if (DEBUG_BACKUP || DEBUG_CLEANUP) Slog.d(TAG_CLEANUP, "App "
                     + backupTarget.appInfo + " died during backup");
             mHandler.post(() -> {
-                try {
-                    getBackupManager().agentDisconnectedForUser(app.userId, app.info.packageName);
-                } catch (RemoteException e) {
-                    // can't happen; backup manager is local
-                }
+                LocalServices.getService(BackupManagerInternal.class).agentDisconnectedForUser(
+                        app.info.packageName, app.userId);
             });
         }
 
@@ -14223,9 +14259,8 @@
 
         final long oldIdent = Binder.clearCallingIdentity();
         try {
-            getBackupManager().agentConnectedForUser(userId, agentPackageName, agent);
-        } catch (RemoteException e) {
-            // can't happen; the backup manager service is local
+            LocalServices.getService(BackupManagerInternal.class).agentConnectedForUser(
+                    agentPackageName, userId, agent);
         } catch (Exception e) {
             Slog.w(TAG, "Exception trying to deliver BackupAgent binding: ");
             e.printStackTrace();
@@ -14565,7 +14600,7 @@
                     app.mProfile.addHostingComponentType(HOSTING_COMPONENT_TYPE_INSTRUMENTATION);
                 }
 
-                app.setActiveInstrumentation(activeInstr);
+                mProcessStateController.setActiveInstrumentation(app, activeInstr);
                 activeInstr.mFinished = false;
                 activeInstr.mSourceUid = callingUid;
                 activeInstr.mRunningProcesses.add(app);
@@ -14711,7 +14746,7 @@
                         abiOverride,
                         ZYGOTE_POLICY_FLAG_EMPTY);
 
-                app.setActiveInstrumentation(activeInstr);
+                mProcessStateController.setActiveInstrumentation(app, activeInstr);
                 activeInstr.mFinished = false;
                 activeInstr.mSourceUid = callingUid;
                 activeInstr.mRunningProcesses.add(app);
@@ -14848,7 +14883,7 @@
                 }
 
                 instr.removeProcess(app);
-                app.setActiveInstrumentation(null);
+                mProcessStateController.setActiveInstrumentation(app, null);
             }
             app.mProfile.clearHostingComponentType(HOSTING_COMPONENT_TYPE_INSTRUMENTATION);
 
@@ -16617,7 +16652,7 @@
         }
 
         @Override
-        public void onUserRemoved(@UserIdInt int userId) {
+        public void onUserRemoving(@UserIdInt int userId) {
             // Clean up any ActivityTaskManager state (by telling it the user is stopped)
             mAtmInternal.onUserStopped(userId);
             // Clean up various services by removing the user
@@ -16631,6 +16666,12 @@
         }
 
         @Override
+        public void onUserRemoved(int userId) {
+            // Clean up UserController state
+            mUserController.onUserRemoved(userId);
+        }
+
+        @Override
         public boolean startUserInBackground(final int userId) {
             return ActivityManagerService.this.startUserInBackground(userId);
         }
@@ -19374,7 +19415,7 @@
             }
             if (preventIntentRedirectCollectNestedKeysOnServerIfNotCollected()) {
                 // this flag will be ramped to public.
-                intent.collectExtraIntentKeys();
+                intent.collectExtraIntentKeys(true);
             }
         }
 
@@ -19440,8 +19481,4 @@
         }
         return token;
     }
-
-    private IBackupManager getBackupManager() {
-        return IBackupManager.Stub.asInterface(ServiceManager.getService(Context.BACKUP_SERVICE));
-    }
 }
diff --git a/services/core/java/com/android/server/am/AppProfiler.java b/services/core/java/com/android/server/am/AppProfiler.java
index 6b24df4..225c7ca 100644
--- a/services/core/java/com/android/server/am/AppProfiler.java
+++ b/services/core/java/com/android/server/am/AppProfiler.java
@@ -2477,13 +2477,15 @@
                             // This is the wildcard mode, where every process brought up for
                             // the target instrumentation should be included.
                             if (aInstr.mTargetInfo.packageName.equals(app.info.packageName)) {
-                                app.setActiveInstrumentation(aInstr);
+                                mService.mProcessStateController.setActiveInstrumentation(app,
+                                        aInstr);
                                 aInstr.mRunningProcesses.add(app);
                             }
                         } else {
                             for (String proc : aInstr.mTargetProcesses) {
                                 if (proc.equals(app.processName)) {
-                                    app.setActiveInstrumentation(aInstr);
+                                    mService.mProcessStateController.setActiveInstrumentation(app,
+                                            aInstr);
                                     aInstr.mRunningProcesses.add(app);
                                     break;
                                 }
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 9c569db..3abcd4e 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -156,7 +156,6 @@
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.Trace;
-import android.os.UserHandle;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Slog;
@@ -3401,13 +3400,10 @@
     }
 
     private static int getCpuCapability(ProcessRecord app, long nowUptime) {
+        // Note: persistent processes get all capabilities, including CPU_TIME.
         final UidRecord uidRec = app.getUidRecord();
         if (uidRec != null && uidRec.isCurAllowListed()) {
-            // Process has user visible activities.
-            return PROCESS_CAPABILITY_CPU_TIME;
-        }
-        if (UserHandle.isCore(app.uid)) {
-            // Make sure all system components are not frozen.
+            // Process is in the power allowlist.
             return PROCESS_CAPABILITY_CPU_TIME;
         }
         if (app.mState.getCachedHasVisibleActivities()) {
@@ -3418,6 +3414,12 @@
             // It running a short fgs, just give it cpu time.
             return PROCESS_CAPABILITY_CPU_TIME;
         }
+        if (app.mReceivers.numberOfCurReceivers() > 0) {
+            return PROCESS_CAPABILITY_CPU_TIME;
+        }
+        if (app.hasActiveInstrumentation()) {
+            return PROCESS_CAPABILITY_CPU_TIME;
+        }
         // TODO(b/370817323): Populate this method with all of the reasons to keep a process
         //  unfrozen.
         return 0;
diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java
index 3817ba1..0b78901 100644
--- a/services/core/java/com/android/server/am/PendingIntentRecord.java
+++ b/services/core/java/com/android/server/am/PendingIntentRecord.java
@@ -305,6 +305,10 @@
         this.stringName = null;
     }
 
+    @VisibleForTesting TempAllowListDuration getAllowlistDurationLocked(IBinder allowlistToken) {
+        return mAllowlistDuration.get(allowlistToken);
+    }
+
     void setAllowBgActivityStarts(IBinder token, int flags) {
         if (token == null) return;
         if ((flags & FLAG_ACTIVITY_SENDER) != 0) {
@@ -323,6 +327,12 @@
         mAllowBgActivityStartsForActivitySender.remove(token);
         mAllowBgActivityStartsForBroadcastSender.remove(token);
         mAllowBgActivityStartsForServiceSender.remove(token);
+        if (mAllowlistDuration != null) {
+            mAllowlistDuration.remove(token);
+            if (mAllowlistDuration.isEmpty()) {
+                mAllowlistDuration = null;
+            }
+        }
     }
 
     public void registerCancelListenerLocked(IResultReceiver receiver) {
@@ -703,7 +713,7 @@
         return res;
     }
 
-    private BackgroundStartPrivileges getBackgroundStartPrivilegesForActivitySender(
+    @VisibleForTesting BackgroundStartPrivileges getBackgroundStartPrivilegesForActivitySender(
             IBinder allowlistToken) {
         return mAllowBgActivityStartsForActivitySender.contains(allowlistToken)
                 ? BackgroundStartPrivileges.allowBackgroundActivityStarts(allowlistToken)
diff --git a/services/core/java/com/android/server/am/ProcessStateController.java b/services/core/java/com/android/server/am/ProcessStateController.java
index 5789922..f44fb06 100644
--- a/services/core/java/com/android/server/am/ProcessStateController.java
+++ b/services/core/java/com/android/server/am/ProcessStateController.java
@@ -246,12 +246,11 @@
     }
 
     /**
-     * Set what sched group to grant a process due to running a broadcast.
-     * {@link ProcessList.SCHED_GROUP_UNDEFINED} means the process is not running a broadcast.
+     * Sets an active instrumentation running within the given process.
      */
-    public void setBroadcastSchedGroup(@NonNull ProcessRecord proc, int schedGroup) {
-        // TODO(b/302575389): Migrate state pulled from BroadcastQueue to a pushed model
-        throw new UnsupportedOperationException("Not implemented yet");
+    public void setActiveInstrumentation(@NonNull ProcessRecord proc,
+            ActiveInstrumentation activeInstrumentation) {
+        proc.setActiveInstrumentation(activeInstrumentation);
     }
 
     /********************* Process Visibility State Events *********************/
@@ -587,6 +586,34 @@
         psr.updateHasTopStartedAlmostPerceptibleServices();
     }
 
+    /************************ Broadcast Receiver State Events **************************/
+    /**
+     * Set what sched group to grant a process due to running a broadcast.
+     * {@link ProcessList.SCHED_GROUP_UNDEFINED} means the process is not running a broadcast.
+     */
+    public void setBroadcastSchedGroup(@NonNull ProcessRecord proc, int schedGroup) {
+        // TODO(b/302575389): Migrate state pulled from BroadcastQueue to a pushed model
+        throw new UnsupportedOperationException("Not implemented yet");
+    }
+
+    /**
+     * Note that the process has started processing a broadcast receiver.
+     */
+    public boolean incrementCurReceivers(@NonNull ProcessRecord app) {
+        // TODO(b/302575389): Migrate state pulled from ATMS to a pushed model
+        // maybe used ActivityStateFlags instead.
+        throw new UnsupportedOperationException("Not implemented yet");
+    }
+
+    /**
+     * Note that the process has finished processing a broadcast receiver.
+     */
+    public boolean decrementCurReceivers(@NonNull ProcessRecord app) {
+        // TODO(b/302575389): Migrate state pulled from ATMS to a pushed model
+        // maybe used ActivityStateFlags instead.
+        throw new UnsupportedOperationException("Not implemented yet");
+    }
+
     /**
      * Builder for ProcessStateController.
      */
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index 92d33c9..ca34a13 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -278,24 +278,21 @@
      * Whether to use the new "while-in-use permission" logic for FGS start
      */
     private boolean useNewWiuLogic_forStart() {
-        return Flags.newFgsRestrictionLogic() // This flag should only be set on V+
-                && CompatChanges.isChangeEnabled(USE_NEW_WIU_LOGIC_FOR_START, appInfo.uid);
+        return CompatChanges.isChangeEnabled(USE_NEW_WIU_LOGIC_FOR_START, appInfo.uid);
     }
 
     /**
      * Whether to use the new "while-in-use permission" logic for capabilities
      */
     private boolean useNewWiuLogic_forCapabilities() {
-        return Flags.newFgsRestrictionLogic() // This flag should only be set on V+
-                && CompatChanges.isChangeEnabled(USE_NEW_WIU_LOGIC_FOR_CAPABILITIES, appInfo.uid);
+        return CompatChanges.isChangeEnabled(USE_NEW_WIU_LOGIC_FOR_CAPABILITIES, appInfo.uid);
     }
 
     /**
      * Whether to use the new "FGS BG start exemption" logic.
      */
     private boolean useNewBfslLogic() {
-        return Flags.newFgsRestrictionLogic() // This flag should only be set on V+
-                && CompatChanges.isChangeEnabled(USE_NEW_BFSL_LOGIC, appInfo.uid);
+        return CompatChanges.isChangeEnabled(USE_NEW_BFSL_LOGIC, appInfo.uid);
     }
 
 
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index c31b9ef..d76c04a 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -125,7 +125,6 @@
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.policy.IKeyguardDismissCallback;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.ObjectUtils;
@@ -160,7 +159,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.Consumer;
+import java.util.function.BiConsumer;
 
 /**
  * Helper class for {@link ActivityManagerService} responsible for multi-user functionality.
@@ -226,14 +225,6 @@
     private static final int USER_SWITCH_CALLBACKS_TIMEOUT_MS = 5 * 1000;
 
     /**
-     * Amount of time waited for {@link WindowManagerService#dismissKeyguard} callbacks to be
-     * called after dismissing the keyguard.
-     * Otherwise, we should move on to dismiss the dialog {@link #dismissUserSwitchDialog()}
-     * and report user switch is complete {@link #REPORT_USER_SWITCH_COMPLETE_MSG}.
-     */
-    private static final int DISMISS_KEYGUARD_TIMEOUT_MS = 2 * 1000;
-
-    /**
      * Time after last scheduleOnUserCompletedEvent() call at which USER_COMPLETED_EVENT_MSG will be
      * scheduled (although it may fire sooner instead).
      * When it fires, {@link #reportOnUserCompletedEvent} will be processed.
@@ -454,11 +445,6 @@
         public void onUserCreated(UserInfo user, Object token) {
             onUserAdded(user);
         }
-
-        @Override
-        public void onUserRemoved(UserInfo user) {
-            UserController.this.onUserRemoved(user.id);
-        }
     };
 
     UserController(ActivityManagerService service) {
@@ -1920,8 +1906,14 @@
                 return false;
             }
 
-            mHandler.post(() -> startUserInternalOnHandler(userId, oldUserId, userStartMode,
-                    unlockListener, callingUid, callingPid));
+            final Runnable continueStartUserInternal = () -> continueStartUserInternal(userInfo,
+                    oldUserId, userStartMode, unlockListener, callingUid, callingPid);
+            if (foreground) {
+                mHandler.post(() -> dispatchOnBeforeUserSwitching(userId, () ->
+                        mHandler.post(continueStartUserInternal)));
+            } else {
+                continueStartUserInternal.run();
+            }
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
@@ -1929,11 +1921,11 @@
         return true;
     }
 
-    private void startUserInternalOnHandler(int userId, int oldUserId, int userStartMode,
+    private void continueStartUserInternal(UserInfo userInfo, int oldUserId, int userStartMode,
             IProgressListener unlockListener, int callingUid, int callingPid) {
         final TimingsTraceAndSlog t = new TimingsTraceAndSlog();
         final boolean foreground = userStartMode == USER_START_MODE_FOREGROUND;
-        final UserInfo userInfo = getUserInfo(userId);
+        final int userId = userInfo.id;
 
         boolean needStart = false;
         boolean updateUmState = false;
@@ -1995,7 +1987,6 @@
             // it should be moved outside, but for now it's not as there are many calls to
             // external components here afterwards
             updateProfileRelatedCaches();
-            dispatchOnBeforeUserSwitching(userId);
             mInjector.getWindowManager().setCurrentUser(userId);
             mInjector.reportCurWakefulnessUsageEvent();
             // Once the internal notion of the active user has switched, we lock the device
@@ -2004,7 +1995,7 @@
                 mInjector.getWindowManager().setSwitchingUser(true);
                 // Only lock if the user has a secure keyguard PIN/Pattern/Pwd
                 if (mInjector.getKeyguardManager().isDeviceSecure(userId)) {
-                    // Make sure the device is locked before moving on with the user switch
+                    Slogf.d(TAG, "Locking the device before moving on with the user switch");
                     mInjector.lockDeviceNowAndWaitForKeyguardShown();
                 }
             }
@@ -2296,25 +2287,42 @@
         mUserSwitchObservers.finishBroadcast();
     }
 
-    private void dispatchOnBeforeUserSwitching(@UserIdInt int newUserId) {
+    private void dispatchOnBeforeUserSwitching(@UserIdInt int newUserId, Runnable onComplete) {
         final TimingsTraceAndSlog t = new TimingsTraceAndSlog();
         t.traceBegin("dispatchOnBeforeUserSwitching-" + newUserId);
-        final int observerCount = mUserSwitchObservers.beginBroadcast();
-        for (int i = 0; i < observerCount; i++) {
-            final String name = "#" + i + " " + mUserSwitchObservers.getBroadcastCookie(i);
-            t.traceBegin("onBeforeUserSwitching-" + name);
+        final AtomicBoolean isFirst = new AtomicBoolean(true);
+        startTimeoutForOnBeforeUserSwitching(isFirst, onComplete);
+        informUserSwitchObservers((observer, callback) -> {
             try {
-                mUserSwitchObservers.getBroadcastItem(i).onBeforeUserSwitching(newUserId);
+                observer.onBeforeUserSwitching(newUserId, callback);
             } catch (RemoteException e) {
-                // Ignore
-            } finally {
-                t.traceEnd();
+                // ignore
             }
-        }
-        mUserSwitchObservers.finishBroadcast();
+        }, () -> {
+            if (isFirst.getAndSet(false)) {
+                onComplete.run();
+            }
+        }, "onBeforeUserSwitching");
         t.traceEnd();
     }
 
+    private void startTimeoutForOnBeforeUserSwitching(AtomicBoolean isFirst,
+            Runnable onComplete) {
+        final long timeout = getUserSwitchTimeoutMs();
+        mHandler.postDelayed(() -> {
+            if (isFirst.getAndSet(false)) {
+                String unresponsiveObservers;
+                synchronized (mLock) {
+                    unresponsiveObservers = String.join(", ", mCurWaitingUserSwitchCallbacks);
+                }
+                Slogf.e(TAG, "Timeout on dispatchOnBeforeUserSwitching. These UserSwitchObservers "
+                        + "did not respond in " + timeout + "ms: " + unresponsiveObservers + ".");
+                onComplete.run();
+            }
+        }, timeout);
+    }
+
+
     /** Called on handler thread */
     @VisibleForTesting
     void dispatchUserSwitchComplete(@UserIdInt int oldUserId, @UserIdInt int newUserId) {
@@ -2527,70 +2535,76 @@
         t.traceBegin("dispatchUserSwitch-" + oldUserId + "-to-" + newUserId);
 
         EventLog.writeEvent(EventLogTags.UC_DISPATCH_USER_SWITCH, oldUserId, newUserId);
-
-        final int observerCount = mUserSwitchObservers.beginBroadcast();
-        if (observerCount > 0) {
-            final ArraySet<String> curWaitingUserSwitchCallbacks = new ArraySet<>();
-            synchronized (mLock) {
-                uss.switching = true;
-                mCurWaitingUserSwitchCallbacks = curWaitingUserSwitchCallbacks;
+        uss.switching = true;
+        informUserSwitchObservers((observer, callback) -> {
+            try {
+                observer.onUserSwitching(newUserId, callback);
+            } catch (RemoteException e) {
+                // ignore
             }
-            final AtomicInteger waitingCallbacksCount = new AtomicInteger(observerCount);
-            final long userSwitchTimeoutMs = getUserSwitchTimeoutMs();
-            final long dispatchStartedTime = SystemClock.elapsedRealtime();
-            for (int i = 0; i < observerCount; i++) {
-                final long dispatchStartedTimeForObserver = SystemClock.elapsedRealtime();
-                try {
-                    // Prepend with unique prefix to guarantee that keys are unique
-                    final String name = "#" + i + " " + mUserSwitchObservers.getBroadcastCookie(i);
-                    synchronized (mLock) {
-                        curWaitingUserSwitchCallbacks.add(name);
-                    }
-                    final IRemoteCallback callback = new IRemoteCallback.Stub() {
-                        @Override
-                        public void sendResult(Bundle data) throws RemoteException {
-                            asyncTraceEnd("onUserSwitching-" + name, newUserId);
-                            synchronized (mLock) {
-                                long delayForObserver = SystemClock.elapsedRealtime()
-                                        - dispatchStartedTimeForObserver;
-                                if (delayForObserver > LONG_USER_SWITCH_OBSERVER_WARNING_TIME_MS) {
-                                    Slogf.w(TAG, "User switch slowed down by observer " + name
-                                            + ": result took " + delayForObserver
-                                            + " ms to process.");
-                                }
-
-                                long totalDelay = SystemClock.elapsedRealtime()
-                                        - dispatchStartedTime;
-                                if (totalDelay > userSwitchTimeoutMs) {
-                                    Slogf.e(TAG, "User switch timeout: observer " + name
-                                            + "'s result was received " + totalDelay
-                                            + " ms after dispatchUserSwitch.");
-                                }
-
-                                curWaitingUserSwitchCallbacks.remove(name);
-                                // Continue switching if all callbacks have been notified and
-                                // user switching session is still valid
-                                if (waitingCallbacksCount.decrementAndGet() == 0
-                                        && (curWaitingUserSwitchCallbacks
-                                        == mCurWaitingUserSwitchCallbacks)) {
-                                    sendContinueUserSwitchLU(uss, oldUserId, newUserId);
-                                }
-                            }
-                        }
-                    };
-                    asyncTraceBegin("onUserSwitching-" + name, newUserId);
-                    mUserSwitchObservers.getBroadcastItem(i).onUserSwitching(newUserId, callback);
-                } catch (RemoteException e) {
-                    // Ignore
-                }
-            }
-        } else {
+        }, () -> {
             synchronized (mLock) {
                 sendContinueUserSwitchLU(uss, oldUserId, newUserId);
             }
+        }, "onUserSwitching");
+        t.traceEnd();
+    }
+
+    void informUserSwitchObservers(BiConsumer<IUserSwitchObserver, IRemoteCallback> consumer,
+            final Runnable onComplete, String trace) {
+        final int observerCount = mUserSwitchObservers.beginBroadcast();
+        if (observerCount == 0) {
+            onComplete.run();
+            mUserSwitchObservers.finishBroadcast();
+            return;
+        }
+        final ArraySet<String> curWaitingUserSwitchCallbacks = new ArraySet<>();
+        synchronized (mLock) {
+            mCurWaitingUserSwitchCallbacks = curWaitingUserSwitchCallbacks;
+        }
+        final AtomicInteger waitingCallbacksCount = new AtomicInteger(observerCount);
+        final long userSwitchTimeoutMs = getUserSwitchTimeoutMs();
+        final long dispatchStartedTime = SystemClock.elapsedRealtime();
+        for (int i = 0; i < observerCount; i++) {
+            final long dispatchStartedTimeForObserver = SystemClock.elapsedRealtime();
+            // Prepend with unique prefix to guarantee that keys are unique
+            final String name = "#" + i + " " + mUserSwitchObservers.getBroadcastCookie(i);
+            synchronized (mLock) {
+                curWaitingUserSwitchCallbacks.add(name);
+            }
+            final IRemoteCallback callback = new IRemoteCallback.Stub() {
+                @Override
+                public void sendResult(Bundle data) throws RemoteException {
+                    asyncTraceEnd(trace + "-" + name, 0);
+                    synchronized (mLock) {
+                        long delayForObserver = SystemClock.elapsedRealtime()
+                                - dispatchStartedTimeForObserver;
+                        if (delayForObserver > LONG_USER_SWITCH_OBSERVER_WARNING_TIME_MS) {
+                            Slogf.w(TAG, "User switch slowed down by observer " + name
+                                    + ": result took " + delayForObserver
+                                    + " ms to process. " + trace);
+                        }
+                        long totalDelay = SystemClock.elapsedRealtime() - dispatchStartedTime;
+                        if (totalDelay > userSwitchTimeoutMs) {
+                            Slogf.e(TAG, "User switch timeout: observer " + name
+                                    + "'s result was received " + totalDelay
+                                    + " ms after dispatchUserSwitch. " + trace);
+                        }
+                        curWaitingUserSwitchCallbacks.remove(name);
+                        // Continue switching if all callbacks have been notified and
+                        // user switching session is still valid
+                        if (waitingCallbacksCount.decrementAndGet() == 0
+                                && (curWaitingUserSwitchCallbacks
+                                == mCurWaitingUserSwitchCallbacks)) {
+                            onComplete.run();
+                        }
+                    }
+                }
+            };
+            asyncTraceBegin(trace + "-" + name, 0);
+            consumer.accept(mUserSwitchObservers.getBroadcastItem(i), callback);
         }
         mUserSwitchObservers.finishBroadcast();
-        t.traceEnd(); // end dispatchUserSwitch-
     }
 
     @GuardedBy("mLock")
@@ -2611,7 +2625,7 @@
 
         EventLog.writeEvent(EventLogTags.UC_CONTINUE_USER_SWITCH, oldUserId, newUserId);
 
-        // Do the keyguard dismiss and dismiss the user switching dialog later
+        // Dismiss the user switching dialog and complete the user switch
         mHandler.removeMessages(COMPLETE_USER_SWITCH_MSG);
         mHandler.sendMessage(mHandler.obtainMessage(
                 COMPLETE_USER_SWITCH_MSG, oldUserId, newUserId));
@@ -2626,31 +2640,17 @@
 
     @VisibleForTesting
     void completeUserSwitch(int oldUserId, int newUserId) {
-        final boolean isUserSwitchUiEnabled = isUserSwitchUiEnabled();
-        // serialize each conditional step
-        await(
-                // STEP 1 - If there is no challenge set, dismiss the keyguard right away
-                isUserSwitchUiEnabled && !mInjector.getKeyguardManager().isDeviceSecure(newUserId),
-                mInjector::dismissKeyguard,
-                () -> await(
-                        // STEP 2 - If user switch ui was enabled, dismiss user switch dialog
-                        isUserSwitchUiEnabled,
-                        this::dismissUserSwitchDialog,
-                        () -> {
-                            // STEP 3 - Send REPORT_USER_SWITCH_COMPLETE_MSG to broadcast
-                            // ACTION_USER_SWITCHED & call UserSwitchObservers.onUserSwitchComplete
-                            mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG);
-                            mHandler.sendMessage(mHandler.obtainMessage(
-                                    REPORT_USER_SWITCH_COMPLETE_MSG, oldUserId, newUserId));
-                        }
-                ));
-    }
-
-    private void await(boolean condition, Consumer<Runnable> conditionalStep, Runnable nextStep) {
-        if (condition) {
-            conditionalStep.accept(nextStep);
+        final Runnable runnable = () -> {
+            // Send REPORT_USER_SWITCH_COMPLETE_MSG to broadcast ACTION_USER_SWITCHED and call
+            // onUserSwitchComplete on UserSwitchObservers.
+            mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG);
+            mHandler.sendMessage(mHandler.obtainMessage(
+                    REPORT_USER_SWITCH_COMPLETE_MSG, oldUserId, newUserId));
+        };
+        if (isUserSwitchUiEnabled()) {
+            dismissUserSwitchDialog(runnable);
         } else {
-            nextStep.run();
+            runnable.run();
         }
     }
 
@@ -3352,10 +3352,12 @@
                 if (mUserProfileGroupIds.keyAt(i) == userId
                         || mUserProfileGroupIds.valueAt(i) == userId) {
                     mUserProfileGroupIds.removeAt(i);
-
                 }
             }
             mCurrentProfileIds = ArrayUtils.removeInt(mCurrentProfileIds, userId);
+            mUserLru.remove((Integer) userId);
+            mStartedUsers.remove(userId);
+            updateStartedUserArrayLU();
         }
     }
 
@@ -4098,33 +4100,6 @@
             return IStorageManager.Stub.asInterface(ServiceManager.getService("mount"));
         }
 
-        protected void dismissKeyguard(Runnable runnable) {
-            final AtomicBoolean isFirst = new AtomicBoolean(true);
-            final Runnable runOnce = () -> {
-                if (isFirst.getAndSet(false)) {
-                    runnable.run();
-                }
-            };
-
-            mHandler.postDelayed(runOnce, DISMISS_KEYGUARD_TIMEOUT_MS);
-            getWindowManager().dismissKeyguard(new IKeyguardDismissCallback.Stub() {
-                @Override
-                public void onDismissError() throws RemoteException {
-                    mHandler.post(runOnce);
-                }
-
-                @Override
-                public void onDismissSucceeded() throws RemoteException {
-                    mHandler.post(runOnce);
-                }
-
-                @Override
-                public void onDismissCancelled() throws RemoteException {
-                    mHandler.post(runOnce);
-                }
-            }, /* message= */ null);
-        }
-
         boolean isHeadlessSystemUserMode() {
             return UserManager.isHeadlessSystemUserMode();
         }
@@ -4149,6 +4124,7 @@
 
         void lockDeviceNowAndWaitForKeyguardShown() {
             if (getWindowManager().isKeyguardLocked()) {
+                Slogf.w(TAG, "Not locking the device since the keyguard is already locked");
                 return;
             }
 
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 295e044..8a63f9a 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -359,7 +359,7 @@
     private static final Duration RATE_LIMITER_WINDOW = Duration.ofMillis(10);
     private final RateLimiter mRateLimiter = new RateLimiter(RATE_LIMITER_WINDOW);
 
-    volatile @NonNull HistoricalRegistry mHistoricalRegistry = new HistoricalRegistry(this);
+    volatile @NonNull HistoricalRegistry mHistoricalRegistry;
 
     /*
      * These are app op restrictions imposed per user from various parties.
@@ -1039,6 +1039,8 @@
         // will not exist and the nonce will be UNSET.
         AppOpsManager.invalidateAppOpModeCache();
         AppOpsManager.disableAppOpModeCache();
+
+        mHistoricalRegistry = new HistoricalRegistry(this, context);
     }
 
     public void publish() {
diff --git a/services/core/java/com/android/server/appop/AttributedOp.java b/services/core/java/com/android/server/appop/AttributedOp.java
index 4d114b4..9dd09ce 100644
--- a/services/core/java/com/android/server/appop/AttributedOp.java
+++ b/services/core/java/com/android/server/appop/AttributedOp.java
@@ -113,7 +113,7 @@
         mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid,
                 parent.packageName, persistentDeviceId, tag, uidState, flags, accessTime,
                 AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE,
-                DiscreteRegistry.ACCESS_TYPE_NOTE_OP, accessCount);
+                DiscreteOpsRegistry.ACCESS_TYPE_NOTE_OP, accessCount);
     }
 
     /**
@@ -257,7 +257,8 @@
         if (isStarted) {
             mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid,
                     parent.packageName, persistentDeviceId, tag, uidState, flags, startTime,
-                    attributionFlags, attributionChainId, DiscreteRegistry.ACCESS_TYPE_START_OP, 1);
+                    attributionFlags, attributionChainId,
+                    DiscreteOpsRegistry.ACCESS_TYPE_START_OP, 1);
         }
     }
 
@@ -344,8 +345,8 @@
                     parent.packageName, persistentDeviceId, tag, event.getUidState(),
                     event.getFlags(), finishedEvent.getNoteTime(), finishedEvent.getDuration(),
                     event.getAttributionFlags(), event.getAttributionChainId(),
-                    isPausing ? DiscreteRegistry.ACCESS_TYPE_PAUSE_OP
-                            : DiscreteRegistry.ACCESS_TYPE_FINISH_OP);
+                    isPausing ? DiscreteOpsRegistry.ACCESS_TYPE_PAUSE_OP
+                            : DiscreteOpsRegistry.ACCESS_TYPE_FINISH_OP);
 
             if (!isPausing) {
                 mAppOpsService.mInProgressStartOpEventPool.release(event);
@@ -453,7 +454,7 @@
             mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid,
                     parent.packageName, persistentDeviceId, tag, event.getUidState(),
                     event.getFlags(), startTime, event.getAttributionFlags(),
-                    event.getAttributionChainId(), DiscreteRegistry.ACCESS_TYPE_RESUME_OP, 1);
+                    event.getAttributionChainId(), DiscreteOpsRegistry.ACCESS_TYPE_RESUME_OP, 1);
             if (shouldSendActive) {
                 mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid,
                         parent.packageName, tag, event.getVirtualDeviceId(), true,
diff --git a/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java b/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java
new file mode 100644
index 0000000..e4c36cc
--- /dev/null
+++ b/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java
@@ -0,0 +1,387 @@
+/*
+ * 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.appop;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.database.DatabaseErrorHandler;
+import android.database.DefaultDatabaseErrorHandler;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteRawStatement;
+import android.os.Environment;
+import android.util.IntArray;
+import android.util.Slog;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+class DiscreteOpsDbHelper extends SQLiteOpenHelper {
+    private static final String LOG_TAG = "DiscreteOpsDbHelper";
+    static final String DATABASE_NAME = "app_op_history.db";
+    private static final int DATABASE_VERSION = 1;
+    private static final boolean DEBUG = false;
+
+    DiscreteOpsDbHelper(@NonNull Context context, @NonNull File databaseFile) {
+        super(context, databaseFile.getAbsolutePath(), null, DATABASE_VERSION,
+                new DiscreteOpsDatabaseErrorHandler());
+        setOpenParams(getDatabaseOpenParams());
+    }
+
+    private static SQLiteDatabase.OpenParams getDatabaseOpenParams() {
+        return new SQLiteDatabase.OpenParams.Builder()
+                .addOpenFlags(SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING)
+                .build();
+    }
+
+    @NonNull
+    static File getDatabaseFile() {
+        return new File(new File(Environment.getDataSystemDirectory(), "appops"), DATABASE_NAME);
+    }
+
+    @Override
+    public void onConfigure(SQLiteDatabase db) {
+        db.execSQL("PRAGMA synchronous = NORMAL");
+    }
+
+    @Override
+    public void onCreate(SQLiteDatabase db) {
+        db.execSQL(DiscreteOpsTable.CREATE_TABLE_SQL);
+        db.execSQL(DiscreteOpsTable.CREATE_INDEX_SQL);
+    }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+    }
+
+    void insertDiscreteOps(@NonNull List<DiscreteOpsSqlRegistry.DiscreteOp> opEvents) {
+        if (opEvents.isEmpty()) {
+            return;
+        }
+
+        SQLiteDatabase db = getWritableDatabase();
+        // TODO (b/383157289) what if database is busy and can't start a transaction? will read
+        //  more about it and can be done in a follow up cl.
+        db.beginTransaction();
+        try (SQLiteRawStatement statement = db.createRawStatement(
+                DiscreteOpsTable.INSERT_TABLE_SQL)) {
+            for (DiscreteOpsSqlRegistry.DiscreteOp event : opEvents) {
+                try {
+                    statement.bindInt(DiscreteOpsTable.UID_INDEX, event.getUid());
+                    bindTextOrNull(statement, DiscreteOpsTable.PACKAGE_NAME_INDEX,
+                            event.getPackageName());
+                    bindTextOrNull(statement, DiscreteOpsTable.DEVICE_ID_INDEX,
+                            event.getDeviceId());
+                    statement.bindInt(DiscreteOpsTable.OP_CODE_INDEX, event.getOpCode());
+                    bindTextOrNull(statement, DiscreteOpsTable.ATTRIBUTION_TAG_INDEX,
+                            event.getAttributionTag());
+                    statement.bindLong(DiscreteOpsTable.ACCESS_TIME_INDEX, event.getAccessTime());
+                    statement.bindLong(
+                            DiscreteOpsTable.ACCESS_DURATION_INDEX, event.getDuration());
+                    statement.bindInt(DiscreteOpsTable.UID_STATE_INDEX, event.getUidState());
+                    statement.bindInt(DiscreteOpsTable.OP_FLAGS_INDEX, event.getOpFlags());
+                    statement.bindInt(DiscreteOpsTable.ATTRIBUTION_FLAGS_INDEX,
+                            event.getAttributionFlags());
+                    statement.bindLong(DiscreteOpsTable.CHAIN_ID_INDEX, event.getChainId());
+                    statement.step();
+                } catch (Exception exception) {
+                    Slog.e(LOG_TAG, "Error inserting the discrete op: " + event, exception);
+                } finally {
+                    statement.reset();
+                }
+            }
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    private void bindTextOrNull(SQLiteRawStatement statement, int index, @Nullable String text) {
+        if (text == null) {
+            statement.bindNull(index);
+        } else {
+            statement.bindText(index, text);
+        }
+    }
+
+    /**
+     * This will be used as an offset for inserting new chain id in discrete ops table.
+     */
+    long getLargestAttributionChainId() {
+        long chainId = 0;
+        try {
+            SQLiteDatabase db = getReadableDatabase();
+            db.beginTransactionReadOnly();
+            try (SQLiteRawStatement statement =
+                     db.createRawStatement(DiscreteOpsTable.SELECT_MAX_ATTRIBUTION_CHAIN_ID)) {
+                if (statement.step()) {
+                    chainId = statement.getColumnLong(0);
+                    if (chainId < 0) {
+                        chainId = 0;
+                    }
+                }
+                db.setTransactionSuccessful();
+            } finally {
+                db.endTransaction();
+            }
+        } catch (SQLiteException exception) {
+            Slog.e(LOG_TAG, "Error reading attribution chain id", exception);
+        }
+        return chainId;
+    }
+
+    void execSQL(@NonNull String sql) {
+        execSQL(sql, null);
+    }
+
+    void execSQL(@NonNull String sql, Object[] bindArgs) {
+        if (DEBUG) {
+            Slog.i(LOG_TAG, "DB execSQL, sql: " + sql);
+        }
+        SQLiteDatabase db = getWritableDatabase();
+        if (bindArgs == null) {
+            db.execSQL(sql);
+        } else {
+            db.execSQL(sql, bindArgs);
+        }
+    }
+
+    /**
+     * Returns a list of {@link DiscreteOpsSqlRegistry.DiscreteOp} based on the given filters.
+     */
+    List<DiscreteOpsSqlRegistry.DiscreteOp> getDiscreteOps(
+            @AppOpsManager.HistoricalOpsRequestFilter int requestFilters,
+            int uidFilter, @Nullable String packageNameFilter,
+            @Nullable String attributionTagFilter, IntArray opCodesFilter, int opFlagsFilter,
+            long beginTime, long endTime, int limit, String orderByColumn) {
+        List<SQLCondition> conditions = prepareConditions(beginTime, endTime, requestFilters,
+                uidFilter, packageNameFilter,
+                attributionTagFilter, opCodesFilter, opFlagsFilter);
+        String sql = buildSql(conditions, orderByColumn, limit);
+
+        SQLiteDatabase db = getReadableDatabase();
+        List<DiscreteOpsSqlRegistry.DiscreteOp> results = new ArrayList<>();
+        db.beginTransactionReadOnly();
+        try (SQLiteRawStatement statement = db.createRawStatement(sql)) {
+            int size = conditions.size();
+            for (int i = 0; i < size; i++) {
+                SQLCondition condition = conditions.get(i);
+                if (DEBUG) {
+                    Slog.i(LOG_TAG, condition + ", binding value = " + condition.mFilterValue);
+                }
+                switch (condition.mColumnFilter) {
+                    case PACKAGE_NAME, ATTR_TAG -> statement.bindText(i + 1,
+                            condition.mFilterValue.toString());
+                    case UID, OP_CODE_EQUAL, OP_FLAGS -> statement.bindInt(i + 1,
+                            Integer.parseInt(condition.mFilterValue.toString()));
+                    case BEGIN_TIME, END_TIME -> statement.bindLong(i + 1,
+                            Long.parseLong(condition.mFilterValue.toString()));
+                    case OP_CODE_IN -> Slog.d(LOG_TAG, "No binding for In operator");
+                    default -> Slog.w(LOG_TAG, "unknown sql condition " + condition);
+                }
+            }
+
+            while (statement.step()) {
+                int uid = statement.getColumnInt(0);
+                String packageName = statement.getColumnText(1);
+                String deviceId = statement.getColumnText(2);
+                int opCode = statement.getColumnInt(3);
+                String attributionTag = statement.getColumnText(4);
+                long accessTime = statement.getColumnLong(5);
+                long duration = statement.getColumnLong(6);
+                int uidState = statement.getColumnInt(7);
+                int opFlags = statement.getColumnInt(8);
+                int attributionFlags = statement.getColumnInt(9);
+                long chainId = statement.getColumnLong(10);
+                DiscreteOpsSqlRegistry.DiscreteOp event = new DiscreteOpsSqlRegistry.DiscreteOp(uid,
+                        packageName, attributionTag, deviceId, opCode,
+                        opFlags, attributionFlags, uidState, chainId, accessTime, duration);
+                results.add(event);
+            }
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+        return results;
+    }
+
+    private String buildSql(List<SQLCondition> conditions, String orderByColumn, int limit) {
+        StringBuilder sql = new StringBuilder(DiscreteOpsTable.SELECT_TABLE_DATA);
+        if (!conditions.isEmpty()) {
+            sql.append(" WHERE ");
+            int size = conditions.size();
+            for (int i = 0; i < size; i++) {
+                sql.append(conditions.get(i).toString());
+                if (i < size - 1) {
+                    sql.append(" AND ");
+                }
+            }
+        }
+
+        if (orderByColumn != null) {
+            sql.append(" ORDER BY ").append(orderByColumn);
+        }
+        if (limit > 0) {
+            sql.append(" LIMIT ").append(limit);
+        }
+        if (DEBUG) {
+            Slog.i(LOG_TAG, "Sql query " + sql);
+        }
+        return sql.toString();
+    }
+
+    /**
+     * Creates where conditions for package, uid, attribution tag and app op codes,
+     * app op codes condition does not support argument binding.
+     */
+    private List<SQLCondition> prepareConditions(long beginTime, long endTime, int requestFilters,
+            int uid, @Nullable String packageName, @Nullable String attributionTag,
+            IntArray opCodes, int opFlags) {
+        final List<SQLCondition> conditions = new ArrayList<>();
+
+        if (beginTime != -1) {
+            conditions.add(new SQLCondition(ColumnFilter.BEGIN_TIME, beginTime));
+        }
+        if (endTime != -1) {
+            conditions.add(new SQLCondition(ColumnFilter.END_TIME, endTime));
+        }
+        if (opFlags != 0) {
+            conditions.add(new SQLCondition(ColumnFilter.OP_FLAGS, opFlags));
+        }
+
+        if (requestFilters != 0) {
+            if ((requestFilters & AppOpsManager.FILTER_BY_PACKAGE_NAME) != 0) {
+                conditions.add(new SQLCondition(ColumnFilter.PACKAGE_NAME, packageName));
+            }
+            if ((requestFilters & AppOpsManager.FILTER_BY_UID) != 0) {
+                conditions.add(new SQLCondition(ColumnFilter.UID, uid));
+
+            }
+            if ((requestFilters & AppOpsManager.FILTER_BY_ATTRIBUTION_TAG) != 0) {
+                conditions.add(new SQLCondition(ColumnFilter.ATTR_TAG, attributionTag));
+            }
+            // filter op codes
+            if (opCodes != null && opCodes.size() == 1) {
+                conditions.add(new SQLCondition(ColumnFilter.OP_CODE_EQUAL, opCodes.get(0)));
+            } else if (opCodes != null && opCodes.size() > 1) {
+                StringBuilder b = new StringBuilder();
+                int size = opCodes.size();
+                for (int i = 0; i < size; i++) {
+                    b.append(opCodes.get(i));
+                    if (i < size - 1) {
+                        b.append(", ");
+                    }
+                }
+                conditions.add(new SQLCondition(ColumnFilter.OP_CODE_IN, b.toString()));
+            }
+        }
+        return conditions;
+    }
+
+    /**
+     * This class prepares a where clause condition for discrete ops table column.
+     */
+    static final class SQLCondition {
+        private final ColumnFilter mColumnFilter;
+        private final Object mFilterValue;
+
+        SQLCondition(ColumnFilter columnFilter, Object filterValue) {
+            mColumnFilter = columnFilter;
+            mFilterValue = filterValue;
+        }
+
+        @Override
+        public String toString() {
+            if (mColumnFilter == ColumnFilter.OP_CODE_IN) {
+                return mColumnFilter + " ( " + mFilterValue + " )";
+            }
+            return mColumnFilter.toString();
+        }
+    }
+
+    /**
+     * This enum describes the where clause conditions for different columns in discrete ops
+     * table.
+     */
+    private enum ColumnFilter {
+        PACKAGE_NAME(DiscreteOpsTable.Columns.PACKAGE_NAME + " = ? "),
+        UID(DiscreteOpsTable.Columns.UID + " = ? "),
+        ATTR_TAG(DiscreteOpsTable.Columns.ATTRIBUTION_TAG + " = ? "),
+        END_TIME(DiscreteOpsTable.Columns.ACCESS_TIME + " < ? "),
+        OP_CODE_EQUAL(DiscreteOpsTable.Columns.OP_CODE + " = ? "),
+        BEGIN_TIME(DiscreteOpsTable.Columns.ACCESS_TIME + " + "
+                + DiscreteOpsTable.Columns.ACCESS_DURATION + " > ? "),
+        OP_FLAGS("(" + DiscreteOpsTable.Columns.OP_FLAGS + " & ? ) != 0"),
+        OP_CODE_IN(DiscreteOpsTable.Columns.OP_CODE + " IN ");
+
+        final String mCondition;
+
+        ColumnFilter(String condition) {
+            mCondition = condition;
+        }
+
+        @Override
+        public String toString() {
+            return mCondition;
+        }
+    }
+
+    static final class DiscreteOpsDatabaseErrorHandler implements DatabaseErrorHandler {
+        private final DefaultDatabaseErrorHandler mDefaultDatabaseErrorHandler =
+                new DefaultDatabaseErrorHandler();
+
+        @Override
+        public void onCorruption(SQLiteDatabase dbObj) {
+            Slog.e(LOG_TAG, "discrete ops database got corrupted.");
+            mDefaultDatabaseErrorHandler.onCorruption(dbObj);
+        }
+    }
+
+    // USED for testing only
+    List<DiscreteOpsSqlRegistry.DiscreteOp> getAllDiscreteOps(@NonNull String sql) {
+        SQLiteDatabase db = getReadableDatabase();
+        List<DiscreteOpsSqlRegistry.DiscreteOp> results = new ArrayList<>();
+        db.beginTransactionReadOnly();
+        try (SQLiteRawStatement statement = db.createRawStatement(sql)) {
+            while (statement.step()) {
+                int uid = statement.getColumnInt(0);
+                String packageName = statement.getColumnText(1);
+                String deviceId = statement.getColumnText(2);
+                int opCode = statement.getColumnInt(3);
+                String attributionTag = statement.getColumnText(4);
+                long accessTime = statement.getColumnLong(5);
+                long duration = statement.getColumnLong(6);
+                int uidState = statement.getColumnInt(7);
+                int opFlags = statement.getColumnInt(8);
+                int attributionFlags = statement.getColumnInt(9);
+                long chainId = statement.getColumnLong(10);
+                DiscreteOpsSqlRegistry.DiscreteOp event = new DiscreteOpsSqlRegistry.DiscreteOp(uid,
+                        packageName, attributionTag, deviceId, opCode,
+                        opFlags, attributionFlags, uidState, chainId, accessTime, duration);
+                results.add(event);
+            }
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+        return results;
+    }
+}
diff --git a/services/core/java/com/android/server/appop/DiscreteOpsMigrationHelper.java b/services/core/java/com/android/server/appop/DiscreteOpsMigrationHelper.java
new file mode 100644
index 0000000..c38ee55
--- /dev/null
+++ b/services/core/java/com/android/server/appop/DiscreteOpsMigrationHelper.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appop;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper class for migrating discrete ops from xml to sqlite
+ */
+public class DiscreteOpsMigrationHelper {
+    /**
+     * migrate discrete ops from xml to sqlite.
+     */
+    static void migrateDiscreteOpsToSqlite(DiscreteOpsXmlRegistry xmlRegistry,
+            DiscreteOpsSqlRegistry sqlRegistry) {
+        DiscreteOpsXmlRegistry.DiscreteOps xmlOps = xmlRegistry.getAllDiscreteOps();
+        List<DiscreteOpsSqlRegistry.DiscreteOp> discreteOps = getSqlDiscreteOps(xmlOps);
+        sqlRegistry.migrateXmlData(discreteOps, xmlOps.mChainIdOffset);
+        xmlRegistry.deleteDiscreteOpsDir();
+    }
+
+    /**
+     * rollback discrete ops from sqlite to xml.
+     */
+    static void migrateDiscreteOpsToXml(DiscreteOpsSqlRegistry sqlRegistry,
+            DiscreteOpsXmlRegistry xmlRegistry) {
+        List<DiscreteOpsSqlRegistry.DiscreteOp> sqlOps = sqlRegistry.getAllDiscreteOps();
+        DiscreteOpsXmlRegistry.DiscreteOps xmlOps = getXmlDiscreteOps(sqlOps);
+        xmlRegistry.migrateSqliteData(xmlOps);
+        sqlRegistry.deleteDatabase();
+    }
+
+    /**
+     * Convert sqlite flat rows to hierarchical data.
+     */
+    private static DiscreteOpsXmlRegistry.DiscreteOps getXmlDiscreteOps(
+            List<DiscreteOpsSqlRegistry.DiscreteOp> discreteOps) {
+        DiscreteOpsXmlRegistry.DiscreteOps xmlOps =
+                new DiscreteOpsXmlRegistry.DiscreteOps(0);
+        if (discreteOps.isEmpty()) {
+            return xmlOps;
+        }
+
+        for (DiscreteOpsSqlRegistry.DiscreteOp discreteOp : discreteOps) {
+            xmlOps.addDiscreteAccess(discreteOp.getOpCode(), discreteOp.getUid(),
+                    discreteOp.getPackageName(), discreteOp.getDeviceId(),
+                    discreteOp.getAttributionTag(), discreteOp.getOpFlags(),
+                    discreteOp.getUidState(),
+                    discreteOp.getAccessTime(), discreteOp.getDuration(),
+                    discreteOp.getAttributionFlags(), (int) discreteOp.getChainId());
+        }
+        return xmlOps;
+    }
+
+    /**
+     * Convert xml (hierarchical) data to flat row based data.
+     */
+    private static List<DiscreteOpsSqlRegistry.DiscreteOp> getSqlDiscreteOps(
+            DiscreteOpsXmlRegistry.DiscreteOps discreteOps) {
+        List<DiscreteOpsSqlRegistry.DiscreteOp> opEvents = new ArrayList<>();
+
+        if (discreteOps.isEmpty()) {
+            return opEvents;
+        }
+
+        discreteOps.mUids.forEach((uid, discreteUidOps) -> {
+            discreteUidOps.mPackages.forEach((packageName, packageOps) -> {
+                packageOps.mPackageOps.forEach((opcode, ops) -> {
+                    ops.mDeviceAttributedOps.forEach((deviceId, deviceOps) -> {
+                        deviceOps.mAttributedOps.forEach((tag, attributedOps) -> {
+                            for (DiscreteOpsXmlRegistry.DiscreteOpEvent attributedOp :
+                                    attributedOps) {
+                                DiscreteOpsSqlRegistry.DiscreteOp
+                                        opModel = new DiscreteOpsSqlRegistry.DiscreteOp(uid,
+                                        packageName, tag,
+                                        deviceId, opcode, attributedOp.mOpFlag,
+                                        attributedOp.mAttributionFlags,
+                                        attributedOp.mUidState, attributedOp.mAttributionChainId,
+                                        attributedOp.mNoteTime,
+                                        attributedOp.mNoteDuration);
+                                opEvents.add(opModel);
+                            }
+                        });
+                    });
+                });
+            });
+        });
+
+        return opEvents;
+    }
+}
diff --git a/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java
new file mode 100644
index 0000000..88b3f6d
--- /dev/null
+++ b/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java
@@ -0,0 +1,298 @@
+/*
+ * 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.appop;
+
+import static android.app.AppOpsManager.OP_CAMERA;
+import static android.app.AppOpsManager.OP_COARSE_LOCATION;
+import static android.app.AppOpsManager.OP_EMERGENCY_LOCATION;
+import static android.app.AppOpsManager.OP_FINE_LOCATION;
+import static android.app.AppOpsManager.OP_FLAG_SELF;
+import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXIED;
+import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXY;
+import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION;
+import static android.app.AppOpsManager.OP_MONITOR_LOCATION;
+import static android.app.AppOpsManager.OP_PHONE_CALL_CAMERA;
+import static android.app.AppOpsManager.OP_PHONE_CALL_MICROPHONE;
+import static android.app.AppOpsManager.OP_PROCESS_OUTGOING_CALLS;
+import static android.app.AppOpsManager.OP_READ_ICC_SMS;
+import static android.app.AppOpsManager.OP_READ_SMS;
+import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO;
+import static android.app.AppOpsManager.OP_RECEIVE_SANDBOX_TRIGGER_AUDIO;
+import static android.app.AppOpsManager.OP_RECORD_AUDIO;
+import static android.app.AppOpsManager.OP_RESERVED_FOR_TESTING;
+import static android.app.AppOpsManager.OP_SEND_SMS;
+import static android.app.AppOpsManager.OP_SMS_FINANCIAL_TRANSACTIONS;
+import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
+import static android.app.AppOpsManager.OP_WRITE_ICC_SMS;
+import static android.app.AppOpsManager.OP_WRITE_SMS;
+
+import static java.lang.Long.min;
+import static java.lang.Math.max;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AppOpsManager;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.permission.flags.Flags;
+import android.provider.DeviceConfig;
+import android.util.Slog;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.FrameworkStatsLog;
+
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.text.SimpleDateFormat;
+import java.time.Duration;
+import java.util.Date;
+import java.util.Set;
+
+/**
+ * This class provides interface for xml and sqlite implementation. Implementation manages
+ * information about recent accesses to ops for permission usage timeline.
+ * <p>
+ * The discrete history is kept for limited time (initial default is 24 hours, set in
+ * {@link DiscreteOpsRegistry#sDiscreteHistoryCutoff} and discarded after that.
+ * <p>
+ * Discrete history is quantized to reduce resources footprint. By default, quantization is set to
+ * one minute in {@link DiscreteOpsRegistry#sDiscreteHistoryQuantization}. All access times are
+ * aligned to the closest quantized time. All durations (except -1, meaning no duration) are
+ * rounded up to the closest quantized interval.
+ * <p>
+ * When data is queried through API, events are deduplicated and for every time quant there can
+ * be only one {@link AppOpsManager.AttributedOpEntry}. Each entry contains information about
+ * different accesses which happened in specified time quant - across dimensions of
+ * {@link AppOpsManager.UidState} and {@link AppOpsManager.OpFlags}. For each dimension
+ * it is only possible to know if at least one access happened in the time quant.
+ * <p>
+ * INITIALIZATION: We can initialize persistence only after the system is ready
+ * as we need to check the optional configuration override from the settings
+ * database which is not initialized at the time the app ops service is created. This class
+ * relies on {@link HistoricalRegistry} for controlling that no calls are allowed until then. All
+ * outside calls are going through {@link HistoricalRegistry}.
+ *
+ */
+abstract class DiscreteOpsRegistry {
+    private static final String TAG = DiscreteOpsRegistry.class.getSimpleName();
+
+    static final boolean DEBUG_LOG = false;
+    static final String PROPERTY_DISCRETE_HISTORY_CUTOFF = "discrete_history_cutoff_millis";
+    static final String PROPERTY_DISCRETE_HISTORY_QUANTIZATION =
+            "discrete_history_quantization_millis";
+    static final String PROPERTY_DISCRETE_FLAGS = "discrete_history_op_flags";
+    static final String PROPERTY_DISCRETE_OPS_LIST = "discrete_history_ops_cslist";
+    static final String DEFAULT_DISCRETE_OPS = OP_FINE_LOCATION + "," + OP_COARSE_LOCATION
+            + "," + OP_EMERGENCY_LOCATION + "," + OP_CAMERA + "," + OP_RECORD_AUDIO + ","
+            + OP_PHONE_CALL_MICROPHONE + "," + OP_PHONE_CALL_CAMERA + ","
+            + OP_RECEIVE_AMBIENT_TRIGGER_AUDIO + "," + OP_RECEIVE_SANDBOX_TRIGGER_AUDIO
+            + "," + OP_RESERVED_FOR_TESTING;
+    static final int[] sDiscreteOpsToLog =
+            new int[]{OP_FINE_LOCATION, OP_COARSE_LOCATION, OP_EMERGENCY_LOCATION, OP_CAMERA,
+                    OP_RECORD_AUDIO, OP_PHONE_CALL_MICROPHONE, OP_PHONE_CALL_CAMERA,
+                    OP_RECEIVE_AMBIENT_TRIGGER_AUDIO, OP_RECEIVE_SANDBOX_TRIGGER_AUDIO, OP_READ_SMS,
+                    OP_WRITE_SMS, OP_SEND_SMS, OP_READ_ICC_SMS, OP_WRITE_ICC_SMS,
+                    OP_SMS_FINANCIAL_TRANSACTIONS, OP_SYSTEM_ALERT_WINDOW, OP_MONITOR_LOCATION,
+                    OP_MONITOR_HIGH_POWER_LOCATION, OP_PROCESS_OUTGOING_CALLS,
+            };
+
+    static final long DEFAULT_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(7).toMillis();
+    static final long MAXIMUM_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(30).toMillis();
+    // The duration for which the data is kept, default is 7 days and max 30 days enforced.
+    static long sDiscreteHistoryCutoff;
+
+    static final long DEFAULT_DISCRETE_HISTORY_QUANTIZATION = Duration.ofMinutes(1).toMillis();
+    // discrete ops are rounded up to quantization time, meaning we record one op per time bucket
+    // in case of duplicate op events.
+    static long sDiscreteHistoryQuantization;
+
+    static int[] sDiscreteOps;
+    static int sDiscreteFlags;
+
+    static final int OP_FLAGS_DISCRETE = OP_FLAG_SELF | OP_FLAG_TRUSTED_PROXIED
+            | OP_FLAG_TRUSTED_PROXY;
+
+    boolean mDebugMode = false;
+
+    static final int ACCESS_TYPE_NOTE_OP =
+            FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__NOTE_OP;
+    static final int ACCESS_TYPE_START_OP =
+            FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__START_OP;
+    static final int ACCESS_TYPE_FINISH_OP =
+            FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__FINISH_OP;
+    static final int ACCESS_TYPE_PAUSE_OP =
+            FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__PAUSE_OP;
+    static final int ACCESS_TYPE_RESUME_OP =
+            FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__RESUME_OP;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = {"ACCESS_TYPE_"}, value = {
+            ACCESS_TYPE_NOTE_OP,
+            ACCESS_TYPE_START_OP,
+            ACCESS_TYPE_FINISH_OP,
+            ACCESS_TYPE_PAUSE_OP,
+            ACCESS_TYPE_RESUME_OP
+    })
+    @interface AccessType {}
+
+    void systemReady() {
+        DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_PRIVACY,
+                AsyncTask.THREAD_POOL_EXECUTOR, (DeviceConfig.Properties p) -> {
+                    setDiscreteHistoryParameters(p);
+                });
+        setDiscreteHistoryParameters(DeviceConfig.getProperties(DeviceConfig.NAMESPACE_PRIVACY));
+    }
+
+    abstract void recordDiscreteAccess(int uid, String packageName, @NonNull String deviceId,
+            int op, @Nullable String attributionTag, @AppOpsManager.OpFlags int flags,
+            @AppOpsManager.UidState int uidState, long accessTime, long accessDuration,
+            @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId,
+            @DiscreteOpsRegistry.AccessType int accessType);
+
+    /**
+     * A periodic callback from {@link AppOpsService} to flush the in memory events to disk.
+     * The shutdown callback is also plugged into it.
+     * <p>
+     * This method flushes in memory records to disk, and also clears old records from disk.
+     */
+    abstract void writeAndClearOldAccessHistory();
+
+    /** Remove all discrete op events. */
+    abstract void clearHistory();
+
+    /** Remove all discrete op events for given UID and package. */
+    abstract void clearHistory(int uid, String packageName);
+
+    /**
+     * Offset access time by given timestamp, new access time would be accessTime - offsetMillis.
+     */
+    abstract void offsetHistory(long offset);
+
+    abstract  void addFilteredDiscreteOpsToHistoricalOps(AppOpsManager.HistoricalOps result,
+            long beginTimeMillis, long endTimeMillis,
+            @AppOpsManager.HistoricalOpsRequestFilter int filter, int uidFilter,
+            @Nullable String packageNameFilter, @Nullable String[] opNamesFilter,
+            @Nullable String attributionTagFilter, @AppOpsManager.OpFlags int flagsFilter,
+            Set<String> attributionExemptPkgs);
+
+    abstract void dump(@NonNull PrintWriter pw, int uidFilter, @Nullable String packageNameFilter,
+            @Nullable String attributionTagFilter,
+            @AppOpsManager.HistoricalOpsRequestFilter int filter, int dumpOp,
+            @NonNull SimpleDateFormat sdf, @NonNull Date date, @NonNull String prefix,
+            int nDiscreteOps);
+
+    void setDebugMode(boolean debugMode) {
+        this.mDebugMode = debugMode;
+    }
+
+    static long discretizeTimeStamp(long timeStamp) {
+        return timeStamp / sDiscreteHistoryQuantization * sDiscreteHistoryQuantization;
+
+    }
+
+    static long discretizeDuration(long duration) {
+        return duration == -1 ? -1 : (duration + sDiscreteHistoryQuantization - 1)
+                / sDiscreteHistoryQuantization * sDiscreteHistoryQuantization;
+    }
+
+    static boolean isDiscreteOp(int op, @AppOpsManager.OpFlags int flags) {
+        if (!ArrayUtils.contains(sDiscreteOps, op)) {
+            return false;
+        }
+        if ((flags & (sDiscreteFlags)) == 0) {
+            return false;
+        }
+        return true;
+    }
+
+    // could this be impl detail of discrete registry, just one test is using the method
+    // abstract DiscreteRegistry.DiscreteOps getAllDiscreteOps();
+
+    private void setDiscreteHistoryParameters(DeviceConfig.Properties p) {
+        if (p.getKeyset().contains(PROPERTY_DISCRETE_HISTORY_CUTOFF)) {
+            sDiscreteHistoryCutoff = p.getLong(PROPERTY_DISCRETE_HISTORY_CUTOFF,
+                    DEFAULT_DISCRETE_HISTORY_CUTOFF);
+            if (!Build.IS_DEBUGGABLE && !mDebugMode) {
+                sDiscreteHistoryCutoff = min(MAXIMUM_DISCRETE_HISTORY_CUTOFF,
+                        sDiscreteHistoryCutoff);
+            }
+        } else {
+            sDiscreteHistoryCutoff = DEFAULT_DISCRETE_HISTORY_CUTOFF;
+        }
+        if (p.getKeyset().contains(PROPERTY_DISCRETE_HISTORY_QUANTIZATION)) {
+            sDiscreteHistoryQuantization = p.getLong(PROPERTY_DISCRETE_HISTORY_QUANTIZATION,
+                    DEFAULT_DISCRETE_HISTORY_QUANTIZATION);
+            if (!Build.IS_DEBUGGABLE && !mDebugMode) {
+                sDiscreteHistoryQuantization = max(DEFAULT_DISCRETE_HISTORY_QUANTIZATION,
+                        sDiscreteHistoryQuantization);
+            }
+        } else {
+            sDiscreteHistoryQuantization = DEFAULT_DISCRETE_HISTORY_QUANTIZATION;
+        }
+        sDiscreteFlags = p.getKeyset().contains(PROPERTY_DISCRETE_FLAGS) ? sDiscreteFlags =
+                p.getInt(PROPERTY_DISCRETE_FLAGS, OP_FLAGS_DISCRETE) : OP_FLAGS_DISCRETE;
+        sDiscreteOps = p.getKeyset().contains(PROPERTY_DISCRETE_OPS_LIST) ? parseOpsList(
+                p.getString(PROPERTY_DISCRETE_OPS_LIST, DEFAULT_DISCRETE_OPS)) : parseOpsList(
+                DEFAULT_DISCRETE_OPS);
+    }
+
+    private static int[] parseOpsList(String opsList) {
+        String[] strArr;
+        if (opsList.isEmpty()) {
+            strArr = new String[0];
+        } else {
+            strArr = opsList.split(",");
+        }
+        int nOps = strArr.length;
+        int[] result = new int[nOps];
+        try {
+            for (int i = 0; i < nOps; i++) {
+                result[i] = Integer.parseInt(strArr[i]);
+            }
+        } catch (NumberFormatException e) {
+            Slog.e(TAG, "Failed to parse Discrete ops list: " + e.getMessage());
+            return parseOpsList(DEFAULT_DISCRETE_OPS);
+        }
+        return result;
+    }
+
+    /**
+     * Whether app op access tacking is enabled and a metric event should be logged.
+     */
+    static boolean shouldLogAccess(int op) {
+        return Flags.appopAccessTrackingLoggingEnabled()
+                && ArrayUtils.contains(sDiscreteOpsToLog, op);
+    }
+
+    String getAttributionTag(String attributionTag, String packageName) {
+        if (attributionTag == null || packageName == null) {
+            return attributionTag;
+        }
+        int firstChar = 0;
+        if (attributionTag.startsWith(packageName)) {
+            firstChar = packageName.length();
+            if (firstChar < attributionTag.length() && attributionTag.charAt(firstChar)
+                    == '.') {
+                firstChar++;
+            }
+        }
+        return attributionTag.substring(firstChar);
+    }
+
+}
diff --git a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java
new file mode 100644
index 0000000..4b3981c
--- /dev/null
+++ b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java
@@ -0,0 +1,689 @@
+/*
+ * 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.appop;
+
+import static android.app.AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE;
+import static android.app.AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR;
+import static android.app.AppOpsManager.ATTRIBUTION_FLAG_RECEIVER;
+import static android.app.AppOpsManager.ATTRIBUTION_FLAG_TRUSTED;
+import static android.app.AppOpsManager.flagsToString;
+import static android.app.AppOpsManager.getUidStateName;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.util.ArraySet;
+import android.util.IntArray;
+import android.util.LongSparseArray;
+import android.util.Slog;
+
+import com.android.internal.util.FrameworkStatsLog;
+import com.android.server.ServiceThread;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * This class handles sqlite persistence layer for discrete ops.
+ */
+public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
+    private static final String TAG = "DiscreteOpsSqlRegistry";
+
+    private final Context mContext;
+    private final DiscreteOpsDbHelper mDiscreteOpsDbHelper;
+    private final SqliteWriteHandler mSqliteWriteHandler;
+    private final DiscreteOpCache mDiscreteOpCache = new DiscreteOpCache(512);
+    private static final long THREE_HOURS = Duration.ofHours(3).toMillis();
+    private static final int WRITE_CACHE_EVICTED_OP_EVENTS = 1;
+    private static final int DELETE_OLD_OP_EVENTS = 2;
+    // Attribution chain id is used to identify an attribution source chain, This is
+    // set for startOp only. PermissionManagerService resets this ID on device restart, so
+    // we use previously persisted chain id as offset, and add it to chain id received from
+    // permission manager service.
+    private long mChainIdOffset;
+    private final File mDatabaseFile;
+
+    DiscreteOpsSqlRegistry(Context context) {
+        this(context, DiscreteOpsDbHelper.getDatabaseFile());
+    }
+
+    DiscreteOpsSqlRegistry(Context context, File databaseFile) {
+        ServiceThread thread = new ServiceThread(TAG, Process.THREAD_PRIORITY_BACKGROUND, true);
+        thread.start();
+        mContext = context;
+        mDatabaseFile = databaseFile;
+        mSqliteWriteHandler = new SqliteWriteHandler(thread.getLooper());
+        mDiscreteOpsDbHelper = new DiscreteOpsDbHelper(context, databaseFile);
+        mChainIdOffset = mDiscreteOpsDbHelper.getLargestAttributionChainId();
+    }
+
+    @Override
+    void recordDiscreteAccess(int uid, String packageName,
+            @NonNull String deviceId, int op,
+            @Nullable String attributionTag, int flags, int uidState,
+            long accessTime, long accessDuration, int attributionFlags, int attributionChainId,
+            int accessType) {
+        if (shouldLogAccess(op)) {
+            FrameworkStatsLog.write(FrameworkStatsLog.APP_OP_ACCESS_TRACKED, uid, op, accessType,
+                    uidState, flags, attributionFlags,
+                    getAttributionTag(attributionTag, packageName),
+                    attributionChainId);
+        }
+
+        if (!isDiscreteOp(op, flags)) {
+            return;
+        }
+
+        long offsetChainId = attributionChainId;
+        if (attributionChainId != ATTRIBUTION_CHAIN_ID_NONE) {
+            offsetChainId = attributionChainId + mChainIdOffset;
+            // PermissionManagerService chain id reached the max value,
+            // reset offset, it's going to be very rare.
+            if (attributionChainId == Integer.MAX_VALUE) {
+                mChainIdOffset = offsetChainId;
+            }
+        }
+        DiscreteOp discreteOpEvent = new DiscreteOp(uid, packageName, attributionTag, deviceId, op,
+                flags, attributionFlags, uidState, offsetChainId, accessTime, accessDuration);
+        mDiscreteOpCache.add(discreteOpEvent);
+    }
+
+    @Override
+    void writeAndClearOldAccessHistory() {
+        // Let the sql impl also follow the same disk write frequencies as xml,
+        // controlled by AppOpsService.
+        mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.getAllEventsAndClear());
+        if (!mSqliteWriteHandler.hasMessages(DELETE_OLD_OP_EVENTS)) {
+            if (mSqliteWriteHandler.sendEmptyMessageDelayed(DELETE_OLD_OP_EVENTS, THREE_HOURS)) {
+                Slog.w(TAG, "DELETE_OLD_OP_EVENTS is not queued");
+            }
+        }
+    }
+
+    @Override
+    void clearHistory() {
+        mDiscreteOpCache.clear();
+        mDiscreteOpsDbHelper.execSQL(DiscreteOpsTable.DELETE_TABLE_DATA);
+    }
+
+    @Override
+    void clearHistory(int uid, String packageName) {
+        mDiscreteOpCache.clear(uid, packageName);
+        mDiscreteOpsDbHelper.execSQL(DiscreteOpsTable.DELETE_DATA_FOR_UID_PACKAGE,
+                new Object[]{uid, packageName});
+    }
+
+    @Override
+    void offsetHistory(long offset) {
+        mDiscreteOpCache.offsetTimestamp(offset);
+        mDiscreteOpsDbHelper.execSQL(DiscreteOpsTable.OFFSET_ACCESS_TIME,
+                new Object[]{offset});
+    }
+
+    private IntArray getAppOpCodes(@AppOpsManager.HistoricalOpsRequestFilter int filter,
+            @Nullable String[] opNamesFilter) {
+        if ((filter & AppOpsManager.FILTER_BY_OP_NAMES) != 0) {
+            IntArray opCodes = new IntArray(opNamesFilter.length);
+            for (int i = 0; i < opNamesFilter.length; i++) {
+                int op;
+                try {
+                    op = AppOpsManager.strOpToOp(opNamesFilter[i]);
+                } catch (IllegalArgumentException ex) {
+                    Slog.w(TAG, "Appop `" + opNamesFilter[i] + "` is not recognized.");
+                    continue;
+                }
+                opCodes.add(op);
+            }
+            return opCodes;
+        }
+        return null;
+    }
+
+    @Override
+    void addFilteredDiscreteOpsToHistoricalOps(AppOpsManager.HistoricalOps result,
+            long beginTimeMillis, long endTimeMillis, int filter, int uidFilter,
+            @Nullable String packageNameFilter,
+            @Nullable String[] opNamesFilter,
+            @Nullable String attributionTagFilter, int opFlagsFilter,
+            Set<String> attributionExemptPkgs) {
+        // flush the cache into database before read.
+        writeAndClearOldAccessHistory();
+        boolean assembleChains = attributionExemptPkgs != null;
+        IntArray opCodes = getAppOpCodes(filter, opNamesFilter);
+        List<DiscreteOp> discreteOps = mDiscreteOpsDbHelper.getDiscreteOps(filter, uidFilter,
+                packageNameFilter, attributionTagFilter, opCodes, opFlagsFilter, beginTimeMillis,
+                endTimeMillis, -1, null);
+
+        LongSparseArray<AttributionChain> attributionChains = null;
+        if (assembleChains) {
+            attributionChains = createAttributionChains(discreteOps, attributionExemptPkgs);
+        }
+
+        int nEvents = discreteOps.size();
+        for (int j = 0; j < nEvents; j++) {
+            DiscreteOp event = discreteOps.get(j);
+            AppOpsManager.OpEventProxyInfo proxy = null;
+            if (assembleChains && event.mChainId != ATTRIBUTION_CHAIN_ID_NONE) {
+                AttributionChain chain = attributionChains.get(event.mChainId);
+                if (chain != null && chain.isComplete()
+                        && chain.isStart(event)
+                        && chain.mLastVisibleEvent != null) {
+                    DiscreteOp proxyEvent = chain.mLastVisibleEvent;
+                    proxy = new AppOpsManager.OpEventProxyInfo(proxyEvent.mUid,
+                            proxyEvent.mPackageName, proxyEvent.mAttributionTag);
+                }
+            }
+            result.addDiscreteAccess(event.mOpCode, event.mUid, event.mPackageName,
+                    event.mAttributionTag, event.mUidState, event.mOpFlags,
+                    event.mDiscretizedAccessTime, event.mDiscretizedDuration, proxy);
+        }
+    }
+
+    @Override
+    void dump(@NonNull PrintWriter pw, int uidFilter, @Nullable String packageNameFilter,
+            @Nullable String attributionTagFilter,
+            @AppOpsManager.HistoricalOpsRequestFilter int filter, int dumpOp,
+            @NonNull SimpleDateFormat sdf, @NonNull Date date, @NonNull String prefix,
+            int nDiscreteOps) {
+        writeAndClearOldAccessHistory();
+        IntArray opCodes = new IntArray();
+        if (dumpOp != AppOpsManager.OP_NONE) {
+            opCodes.add(dumpOp);
+        }
+        List<DiscreteOp> discreteOps = mDiscreteOpsDbHelper.getDiscreteOps(filter, uidFilter,
+                packageNameFilter, attributionTagFilter, opCodes, 0, -1,
+                -1, nDiscreteOps, DiscreteOpsTable.Columns.ACCESS_TIME);
+
+        pw.print(prefix);
+        pw.print("Largest chain id: ");
+        pw.print(mDiscreteOpsDbHelper.getLargestAttributionChainId());
+        pw.println();
+        pw.println("UID|PACKAGE_NAME|DEVICE_ID|OP_NAME|ATTRIBUTION_TAG|UID_STATE|OP_FLAGS|"
+                + "ATTR_FLAGS|CHAIN_ID|ACCESS_TIME|DURATION");
+        int discreteOpsCount = discreteOps.size();
+        for (int i = 0; i < discreteOpsCount; i++) {
+            DiscreteOp event = discreteOps.get(i);
+            date.setTime(event.mAccessTime);
+            pw.println(event.mUid + "|" + event.mPackageName + "|" + event.mDeviceId + "|"
+                    + AppOpsManager.opToName(event.mOpCode) + "|" + event.mAttributionTag + "|"
+                    + getUidStateName(event.mUidState) + "|"
+                    + flagsToString(event.mOpFlags) + "|" + event.mAttributionFlags + "|"
+                    + event.mChainId + "|"
+                    + sdf.format(date) + "|" + event.mDuration);
+        }
+        pw.println();
+    }
+
+    void migrateXmlData(List<DiscreteOp> opEvents, int chainIdOffset) {
+        mChainIdOffset = chainIdOffset;
+        mDiscreteOpsDbHelper.insertDiscreteOps(opEvents);
+    }
+
+    LongSparseArray<AttributionChain> createAttributionChains(
+            List<DiscreteOp> discreteOps, Set<String> attributionExemptPkgs) {
+        LongSparseArray<AttributionChain> chains = new LongSparseArray<>();
+        final int count = discreteOps.size();
+
+        for (int i = 0; i < count; i++) {
+            DiscreteOp opEvent = discreteOps.get(i);
+            if (opEvent.mChainId == ATTRIBUTION_CHAIN_ID_NONE
+                    || (opEvent.mAttributionFlags & ATTRIBUTION_FLAG_TRUSTED) == 0) {
+                continue;
+            }
+            AttributionChain chain = chains.get(opEvent.mChainId);
+            if (chain == null) {
+                chain = new AttributionChain(attributionExemptPkgs);
+                chains.put(opEvent.mChainId, chain);
+            }
+            chain.addEvent(opEvent);
+        }
+        return chains;
+    }
+
+    static class AttributionChain {
+        List<DiscreteOp> mChain = new ArrayList<>();
+        Set<String> mExemptPkgs;
+        DiscreteOp mStartEvent = null;
+        DiscreteOp mLastVisibleEvent = null;
+
+        AttributionChain(Set<String> exemptPkgs) {
+            mExemptPkgs = exemptPkgs;
+        }
+
+        boolean isComplete() {
+            return !mChain.isEmpty() && getStart() != null && isEnd(mChain.get(mChain.size() - 1));
+        }
+
+        DiscreteOp getStart() {
+            return mChain.isEmpty() || !isStart(mChain.get(0)) ? null : mChain.get(0);
+        }
+
+        private boolean isEnd(DiscreteOp event) {
+            return event != null
+                    && (event.mAttributionFlags & ATTRIBUTION_FLAG_ACCESSOR) != 0;
+        }
+
+        private boolean isStart(DiscreteOp event) {
+            return event != null
+                    && (event.mAttributionFlags & ATTRIBUTION_FLAG_RECEIVER) != 0;
+        }
+
+        DiscreteOp getLastVisible() {
+            // Search all nodes but the first one, which is the start node
+            for (int i = mChain.size() - 1; i > 0; i--) {
+                DiscreteOp event = mChain.get(i);
+                if (!mExemptPkgs.contains(event.mPackageName)) {
+                    return event;
+                }
+            }
+            return null;
+        }
+
+        void addEvent(DiscreteOp opEvent) {
+            // check if we have a matching event except duration.
+            DiscreteOp matchingItem = null;
+            for (int i = 0; i < mChain.size(); i++) {
+                DiscreteOp item = mChain.get(i);
+                if (item.equalsExceptDuration(opEvent)) {
+                    matchingItem = item;
+                    break;
+                }
+            }
+
+            if (matchingItem != null) {
+                // exact match or existing event has longer duration
+                if (matchingItem.mDuration == opEvent.mDuration
+                        || matchingItem.mDuration > opEvent.mDuration) {
+                    return;
+                }
+                mChain.remove(matchingItem);
+            }
+
+            if (mChain.isEmpty() || isEnd(opEvent)) {
+                mChain.add(opEvent);
+            } else if (isStart(opEvent)) {
+                mChain.add(0, opEvent);
+            } else {
+                for (int i = 0; i < mChain.size(); i++) {
+                    DiscreteOp currEvent = mChain.get(i);
+                    if ((!isStart(currEvent)
+                            && currEvent.mAccessTime > opEvent.mAccessTime)
+                            || (i == mChain.size() - 1 && isEnd(currEvent))) {
+                        mChain.add(i, opEvent);
+                        break;
+                    } else if (i == mChain.size() - 1) {
+                        mChain.add(opEvent);
+                        break;
+                    }
+                }
+            }
+            mStartEvent = isComplete() ? getStart() : null;
+            mLastVisibleEvent = isComplete() ? getLastVisible() : null;
+        }
+    }
+
+    /**
+     * Handler to write asynchronously to sqlite database.
+     */
+    class SqliteWriteHandler extends Handler {
+        SqliteWriteHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case WRITE_CACHE_EVICTED_OP_EVENTS:
+                    List<DiscreteOp> opEvents = (List<DiscreteOp>) msg.obj;
+                    mDiscreteOpsDbHelper.insertDiscreteOps(opEvents);
+                    break;
+                case DELETE_OLD_OP_EVENTS:
+                    long cutOffTimeStamp = System.currentTimeMillis() - sDiscreteHistoryCutoff;
+                    mDiscreteOpsDbHelper.execSQL(
+                            DiscreteOpsTable.DELETE_TABLE_DATA_BEFORE_ACCESS_TIME,
+                            new Object[]{cutOffTimeStamp});
+                    break;
+                default:
+                    throw new IllegalStateException("Unexpected value: " + msg.what);
+            }
+        }
+    }
+
+    /**
+     * A write cache for discrete ops. The noteOp, start/finishOp discrete op events are written to
+     * the cache first.
+     * <p>
+     * These events are persisted into sqlite database
+     * 1) Periodic interval, controlled by {@link AppOpsService}
+     * 2) When total events in the cache exceeds cache limit.
+     * 3) During read call we flush the whole cache to sqlite.
+     * 4) During shutdown.
+     */
+    class DiscreteOpCache {
+        private final int mCapacity;
+        private final ArraySet<DiscreteOp> mCache;
+
+        DiscreteOpCache(int capacity) {
+            mCapacity = capacity;
+            mCache = new ArraySet<>();
+        }
+
+        public void add(DiscreteOp opEvent) {
+            synchronized (this) {
+                if (mCache.contains(opEvent)) {
+                    return;
+                }
+                mCache.add(opEvent);
+                if (mCache.size() >= mCapacity) {
+                    if (DEBUG_LOG) {
+                        Slog.i(TAG, "Current discrete ops cache size: " + mCache.size());
+                    }
+                    List<DiscreteOp> evictedEvents = evict();
+                    if (DEBUG_LOG) {
+                        Slog.i(TAG, "Evicted discrete ops size: " + evictedEvents.size());
+                    }
+                    // if nothing to evict, just write the whole cache to disk
+                    if (evictedEvents.isEmpty()) {
+                        Slog.w(TAG, "No discrete ops event is evicted, write cache to db.");
+                        evictedEvents.addAll(mCache);
+                        mCache.clear();
+                    }
+                    mSqliteWriteHandler.obtainMessage(WRITE_CACHE_EVICTED_OP_EVENTS, evictedEvents);
+                }
+            }
+        }
+
+        /**
+         * Evict entries older than {@link DiscreteOpsRegistry#sDiscreteHistoryQuantization}.
+         */
+        private List<DiscreteOp> evict() {
+            synchronized (this) {
+                List<DiscreteOp> evictedEvents = new ArrayList<>();
+                Set<DiscreteOp> snapshot = new ArraySet<>(mCache);
+                long evictionTimestamp = System.currentTimeMillis() - sDiscreteHistoryQuantization;
+                evictionTimestamp = discretizeTimeStamp(evictionTimestamp);
+                for (DiscreteOp opEvent : snapshot) {
+                    if (opEvent.mDiscretizedAccessTime <= evictionTimestamp) {
+                        evictedEvents.add(opEvent);
+                        mCache.remove(opEvent);
+                    }
+                }
+                return evictedEvents;
+            }
+        }
+
+        /**
+         * Remove all the entries from cache.
+         *
+         * @return return all removed entries.
+         */
+        public List<DiscreteOp> getAllEventsAndClear() {
+            synchronized (this) {
+                List<DiscreteOp> cachedOps = new ArrayList<>(mCache.size());
+                if (mCache.isEmpty()) {
+                    return cachedOps;
+                }
+                cachedOps.addAll(mCache);
+                mCache.clear();
+                return cachedOps;
+            }
+        }
+
+        /**
+         * Remove all entries from the cache.
+         */
+        public void clear() {
+            synchronized (this) {
+                mCache.clear();
+            }
+        }
+
+        /**
+         * Offset access time by given offset milliseconds.
+         */
+        public void offsetTimestamp(long offsetMillis) {
+            synchronized (this) {
+                List<DiscreteOp> cachedOps = new ArrayList<>(mCache);
+                mCache.clear();
+                for (DiscreteOp discreteOp : cachedOps) {
+                    add(new DiscreteOp(discreteOp.getUid(), discreteOp.mPackageName,
+                            discreteOp.getAttributionTag(), discreteOp.getDeviceId(),
+                            discreteOp.mOpCode, discreteOp.mOpFlags,
+                            discreteOp.getAttributionFlags(), discreteOp.getUidState(),
+                            discreteOp.getChainId(), discreteOp.mAccessTime - offsetMillis,
+                            discreteOp.getDuration())
+                    );
+                }
+            }
+        }
+
+        /** Remove cached events for given UID and package. */
+        public void clear(int uid, String packageName) {
+            synchronized (this) {
+                Set<DiscreteOp> snapshot = new ArraySet<>(mCache);
+                for (DiscreteOp currentEvent : snapshot) {
+                    if (Objects.equals(packageName, currentEvent.mPackageName)
+                            && uid == currentEvent.getUid()) {
+                        mCache.remove(currentEvent);
+                    }
+                }
+            }
+        }
+    }
+
+    /** Immutable discrete op object. */
+    static class DiscreteOp {
+        private final int mUid;
+        private final String mPackageName;
+        private final String mAttributionTag;
+        private final String mDeviceId;
+        private final int mOpCode;
+        private final int mOpFlags;
+        private final int mAttributionFlags;
+        private final int mUidState;
+        private final long mChainId;
+        private final long mAccessTime;
+        private final long mDuration;
+        // store discretized timestamp to avoid repeated calculations.
+        private final long mDiscretizedAccessTime;
+        private final long mDiscretizedDuration;
+
+        DiscreteOp(int uid, String packageName, String attributionTag, String deviceId,
+                int opCode,
+                int mOpFlags, int mAttributionFlags, int uidState, long chainId, long accessTime,
+                long duration) {
+            this.mUid = uid;
+            this.mPackageName = packageName.intern();
+            this.mAttributionTag = attributionTag;
+            this.mDeviceId = deviceId;
+            this.mOpCode = opCode;
+            this.mOpFlags = mOpFlags;
+            this.mAttributionFlags = mAttributionFlags;
+            this.mUidState = uidState;
+            this.mChainId = chainId;
+            this.mAccessTime = accessTime;
+            this.mDiscretizedAccessTime = discretizeTimeStamp(accessTime);
+            this.mDuration = duration;
+            this.mDiscretizedDuration = discretizeDuration(duration);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof DiscreteOp that)) return false;
+
+            if (mUid != that.mUid) return false;
+            if (mOpCode != that.mOpCode) return false;
+            if (mOpFlags != that.mOpFlags) return false;
+            if (mAttributionFlags != that.mAttributionFlags) return false;
+            if (mUidState != that.mUidState) return false;
+            if (mChainId != that.mChainId) return false;
+            if (!Objects.equals(mPackageName, that.mPackageName)) {
+                return false;
+            }
+            if (!Objects.equals(mAttributionTag, that.mAttributionTag)) {
+                return false;
+            }
+            if (!Objects.equals(mDeviceId, that.mDeviceId)) {
+                return false;
+            }
+            if (mDiscretizedAccessTime != that.mDiscretizedAccessTime) {
+                return false;
+            }
+            return mDiscretizedDuration == that.mDiscretizedDuration;
+        }
+
+        @Override
+        public int hashCode() {
+            int result = mUid;
+            result = 31 * result + (mPackageName != null ? mPackageName.hashCode() : 0);
+            result = 31 * result + (mAttributionTag != null ? mAttributionTag.hashCode() : 0);
+            result = 31 * result + (mDeviceId != null ? mDeviceId.hashCode() : 0);
+            result = 31 * result + mOpCode;
+            result = 31 * result + mOpFlags;
+            result = 31 * result + mAttributionFlags;
+            result = 31 * result + mUidState;
+            result = 31 * result + Objects.hash(mChainId);
+            result = 31 * result + Objects.hash(mDiscretizedAccessTime);
+            result = 31 * result + Objects.hash(mDiscretizedDuration);
+            return result;
+        }
+
+        public boolean equalsExceptDuration(DiscreteOp that) {
+            if (mUid != that.mUid) return false;
+            if (mOpCode != that.mOpCode) return false;
+            if (mOpFlags != that.mOpFlags) return false;
+            if (mAttributionFlags != that.mAttributionFlags) return false;
+            if (mUidState != that.mUidState) return false;
+            if (mChainId != that.mChainId) return false;
+            if (!Objects.equals(mPackageName, that.mPackageName)) {
+                return false;
+            }
+            if (!Objects.equals(mAttributionTag, that.mAttributionTag)) {
+                return false;
+            }
+            if (!Objects.equals(mDeviceId, that.mDeviceId)) {
+                return false;
+            }
+            return mAccessTime == that.mAccessTime;
+        }
+
+        @Override
+        public String toString() {
+            return "DiscreteOp{"
+                    + "uid=" + mUid
+                    + ", packageName='" + mPackageName + '\''
+                    + ", attributionTag='" + mAttributionTag + '\''
+                    + ", deviceId='" + mDeviceId + '\''
+                    + ", opCode=" + AppOpsManager.opToName(mOpCode)
+                    + ", opFlag=" + flagsToString(mOpFlags)
+                    + ", attributionFlag=" + mAttributionFlags
+                    + ", uidState=" + getUidStateName(mUidState)
+                    + ", chainId=" + mChainId
+                    + ", accessTime=" + mAccessTime
+                    + ", duration=" + mDuration + '}';
+        }
+
+        public int getUid() {
+            return mUid;
+        }
+
+        public String getPackageName() {
+            return mPackageName;
+        }
+
+        public String getAttributionTag() {
+            return mAttributionTag;
+        }
+
+        public String getDeviceId() {
+            return mDeviceId;
+        }
+
+        public int getOpCode() {
+            return mOpCode;
+        }
+
+        @AppOpsManager.OpFlags
+        public int getOpFlags() {
+            return mOpFlags;
+        }
+
+
+        @AppOpsManager.AttributionFlags
+        public int getAttributionFlags() {
+            return mAttributionFlags;
+        }
+
+        @AppOpsManager.UidState
+        public int getUidState() {
+            return mUidState;
+        }
+
+        public long getChainId() {
+            return mChainId;
+        }
+
+        public long getAccessTime() {
+            return mAccessTime;
+        }
+
+        public long getDuration() {
+            return mDuration;
+        }
+    }
+
+    // API for tests only, can be removed or changed.
+    void recordDiscreteAccess(DiscreteOp discreteOpEvent) {
+        mDiscreteOpCache.add(discreteOpEvent);
+    }
+
+    // API for tests only, can be removed or changed.
+    List<DiscreteOp> getCachedDiscreteOps() {
+        return new ArrayList<>(mDiscreteOpCache.mCache);
+    }
+
+    // API for tests only, can be removed or changed.
+    List<DiscreteOp> getAllDiscreteOps() {
+        List<DiscreteOp> ops = new ArrayList<>(mDiscreteOpCache.mCache);
+        ops.addAll(mDiscreteOpsDbHelper.getAllDiscreteOps(DiscreteOpsTable.SELECT_TABLE_DATA));
+        return ops;
+    }
+
+    // API for testing and migration
+    long getLargestAttributionChainId() {
+        return mDiscreteOpsDbHelper.getLargestAttributionChainId();
+    }
+
+    // API for testing and migration
+    void deleteDatabase() {
+        mDiscreteOpsDbHelper.close();
+        mContext.deleteDatabase(mDatabaseFile.getName());
+    }
+}
diff --git a/services/core/java/com/android/server/appop/DiscreteOpsTable.java b/services/core/java/com/android/server/appop/DiscreteOpsTable.java
new file mode 100644
index 0000000..9cb19aa
--- /dev/null
+++ b/services/core/java/com/android/server/appop/DiscreteOpsTable.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.server.appop;
+
+
+/**
+ * SQLite table for storing app op accesses.
+ */
+final class DiscreteOpsTable {
+    private static final String TABLE_NAME = "app_op_accesses";
+    private static final String INDEX_APP_OP = "app_op_access_index";
+
+    static final class Columns {
+        /** Auto increment primary key. */
+        static final String ID = "id";
+        /** UID of the package accessing private data. */
+        static final String UID = "uid";
+        /** Package accessing private data. */
+        static final String PACKAGE_NAME = "package_name";
+        /** The device from which the private data is accessed. */
+        static final String DEVICE_ID = "device_id";
+        /** Op code representing private data i.e. location, mic etc. */
+        static final String OP_CODE = "op_code";
+        /** Attribution tag provided when accessing the private data. */
+        static final String ATTRIBUTION_TAG = "attribution_tag";
+        /** Timestamp when private data is accessed, number of milliseconds that have passed
+         * since Unix epoch */
+        static final String ACCESS_TIME = "access_time";
+        /** For how long the private data is accessed. */
+        static final String ACCESS_DURATION = "access_duration";
+        /** App process state, whether the app is in foreground, background or cached etc. */
+        static final String UID_STATE = "uid_state";
+        /** App op flags */
+        static final String OP_FLAGS = "op_flags";
+        /** Attribution flags */
+        static final String ATTRIBUTION_FLAGS = "attribution_flags";
+        /** Chain id */
+        static final String CHAIN_ID = "chain_id";
+    }
+
+    static final int UID_INDEX = 1;
+    static final int PACKAGE_NAME_INDEX = 2;
+    static final int DEVICE_ID_INDEX = 3;
+    static final int OP_CODE_INDEX = 4;
+    static final int ATTRIBUTION_TAG_INDEX = 5;
+    static final int ACCESS_TIME_INDEX = 6;
+    static final int ACCESS_DURATION_INDEX = 7;
+    static final int UID_STATE_INDEX = 8;
+    static final int OP_FLAGS_INDEX = 9;
+    static final int ATTRIBUTION_FLAGS_INDEX = 10;
+    static final int CHAIN_ID_INDEX = 11;
+
+    static final String CREATE_TABLE_SQL = "CREATE TABLE IF NOT EXISTS "
+            + TABLE_NAME + "("
+            + Columns.ID + " INTEGER PRIMARY KEY,"
+            + Columns.UID + " INTEGER,"
+            + Columns.PACKAGE_NAME + " TEXT,"
+            + Columns.DEVICE_ID + " TEXT NOT NULL,"
+            + Columns.OP_CODE + " INTEGER,"
+            + Columns.ATTRIBUTION_TAG + " TEXT,"
+            + Columns.ACCESS_TIME + " INTEGER,"
+            + Columns.ACCESS_DURATION + " INTEGER,"
+            + Columns.UID_STATE + " INTEGER,"
+            + Columns.OP_FLAGS + " INTEGER,"
+            + Columns.ATTRIBUTION_FLAGS + " INTEGER,"
+            + Columns.CHAIN_ID + " INTEGER"
+            + ")";
+
+    static final String INSERT_TABLE_SQL = "INSERT INTO " + TABLE_NAME + "("
+            + Columns.UID + ", "
+            + Columns.PACKAGE_NAME + ", "
+            + Columns.DEVICE_ID + ", "
+            + Columns.OP_CODE + ", "
+            + Columns.ATTRIBUTION_TAG + ", "
+            + Columns.ACCESS_TIME + ", "
+            + Columns.ACCESS_DURATION + ", "
+            + Columns.UID_STATE + ", "
+            + Columns.OP_FLAGS + ", "
+            + Columns.ATTRIBUTION_FLAGS + ", "
+            + Columns.CHAIN_ID + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+
+    static final String SELECT_MAX_ATTRIBUTION_CHAIN_ID = "SELECT MAX(" + Columns.CHAIN_ID + ")"
+            + " FROM " + TABLE_NAME;
+
+    static final String SELECT_TABLE_DATA = "SELECT DISTINCT "
+            + Columns.UID + ","
+            + Columns.PACKAGE_NAME + ","
+            + Columns.DEVICE_ID + ","
+            + Columns.OP_CODE + ","
+            + Columns.ATTRIBUTION_TAG + ","
+            + Columns.ACCESS_TIME + ","
+            + Columns.ACCESS_DURATION + ","
+            + Columns.UID_STATE + ","
+            + Columns.OP_FLAGS + ","
+            + Columns.ATTRIBUTION_FLAGS + ","
+            + Columns.CHAIN_ID
+            + " FROM " + TABLE_NAME;
+
+    static final String DELETE_TABLE_DATA = "DELETE FROM " + TABLE_NAME;
+
+    static final String DELETE_TABLE_DATA_BEFORE_ACCESS_TIME = "DELETE FROM " + TABLE_NAME
+            + " WHERE " + Columns.ACCESS_TIME + " < ?";
+
+    static final String DELETE_DATA_FOR_UID_PACKAGE = "DELETE FROM " + DiscreteOpsTable.TABLE_NAME
+            + " WHERE " + Columns.UID + " = ? AND " + Columns.PACKAGE_NAME + " = ?";
+
+    static final String OFFSET_ACCESS_TIME = "UPDATE " + DiscreteOpsTable.TABLE_NAME
+            + " SET " + Columns.ACCESS_TIME + " = ACCESS_TIME - ?";
+
+    // Index on access time, uid and op code
+    static final String CREATE_INDEX_SQL = "CREATE INDEX IF NOT EXISTS "
+            + INDEX_APP_OP + " ON " + TABLE_NAME
+            + " (" + Columns.ACCESS_TIME + ", " + Columns.UID + ", " + Columns.OP_CODE + ")";
+}
diff --git a/services/core/java/com/android/server/appop/DiscreteOpsTestingShim.java b/services/core/java/com/android/server/appop/DiscreteOpsTestingShim.java
new file mode 100644
index 0000000..1523cca
--- /dev/null
+++ b/services/core/java/com/android/server/appop/DiscreteOpsTestingShim.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appop;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AppOpsManager;
+import android.os.SystemClock;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.Slog;
+
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A testing class, which supports both xml and sqlite persistence for discrete ops, the class
+ * logs warning if there is a mismatch in the behavior.
+ */
+class DiscreteOpsTestingShim extends DiscreteOpsRegistry {
+    private static final String LOG_TAG = "DiscreteOpsTestingShim";
+    private final DiscreteOpsRegistry mXmlRegistry;
+    private final DiscreteOpsRegistry mSqlRegistry;
+
+    DiscreteOpsTestingShim(DiscreteOpsRegistry xmlRegistry,
+            DiscreteOpsRegistry sqlRegistry) {
+        mXmlRegistry = xmlRegistry;
+        mSqlRegistry = sqlRegistry;
+    }
+
+    @Override
+    void recordDiscreteAccess(int uid, String packageName, @NonNull String deviceId, int op,
+            @Nullable String attributionTag, int flags, int uidState, long accessTime,
+            long accessDuration, int attributionFlags, int attributionChainId, int accessType) {
+        long start = SystemClock.uptimeMillis();
+        mXmlRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, attributionTag, flags,
+                uidState, accessTime, accessDuration, attributionFlags, attributionChainId,
+                accessType);
+        long start2 = SystemClock.uptimeMillis();
+        mSqlRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, attributionTag, flags,
+                uidState, accessTime, accessDuration, attributionFlags, attributionChainId,
+                accessType);
+        long end = SystemClock.uptimeMillis();
+        long xmlTimeTaken = start2 - start;
+        long sqlTimeTaken = end - start2;
+        Log.i(LOG_TAG,
+                "recordDiscreteAccess: XML time taken : " + xmlTimeTaken + ", SQL time taken : "
+                        + sqlTimeTaken + ", diff (sql - xml): " + (sqlTimeTaken - xmlTimeTaken));
+    }
+
+
+    @Override
+    void writeAndClearOldAccessHistory() {
+        mXmlRegistry.writeAndClearOldAccessHistory();
+        mSqlRegistry.writeAndClearOldAccessHistory();
+    }
+
+    @Override
+    void clearHistory() {
+        mXmlRegistry.clearHistory();
+        mSqlRegistry.clearHistory();
+    }
+
+    @Override
+    void clearHistory(int uid, String packageName) {
+        mXmlRegistry.clearHistory(uid, packageName);
+        mSqlRegistry.clearHistory(uid, packageName);
+    }
+
+    @Override
+    void offsetHistory(long offset) {
+        mXmlRegistry.offsetHistory(offset);
+        mSqlRegistry.offsetHistory(offset);
+    }
+
+    @Override
+    void addFilteredDiscreteOpsToHistoricalOps(AppOpsManager.HistoricalOps result,
+            long beginTimeMillis, long endTimeMillis, int filter, int uidFilter,
+            @Nullable String packageNameFilter, @Nullable String[] opNamesFilter,
+            @Nullable String attributionTagFilter, int flagsFilter,
+            Set<String> attributionExemptPkgs) {
+        AppOpsManager.HistoricalOps result2 =
+                new AppOpsManager.HistoricalOps(beginTimeMillis, endTimeMillis);
+
+        long start = System.currentTimeMillis();
+        mXmlRegistry.addFilteredDiscreteOpsToHistoricalOps(result2, beginTimeMillis, endTimeMillis,
+                filter, uidFilter, packageNameFilter, opNamesFilter, attributionTagFilter,
+                flagsFilter, attributionExemptPkgs);
+        long start2 = System.currentTimeMillis();
+        mSqlRegistry.addFilteredDiscreteOpsToHistoricalOps(result, beginTimeMillis, endTimeMillis,
+                filter, uidFilter, packageNameFilter, opNamesFilter, attributionTagFilter,
+                flagsFilter, attributionExemptPkgs);
+        long end = System.currentTimeMillis();
+        long xmlTimeTaken = start2 - start;
+        long sqlTimeTaken = end - start2;
+        try {
+            assertHistoricalOpsAreEquals(result, result2);
+        } catch (Exception ex) {
+            Slog.e(LOG_TAG, "different output when reading discrete ops", ex);
+        }
+        Log.i(LOG_TAG, "Read: XML time taken : " + xmlTimeTaken + ", SQL time taken : "
+                + sqlTimeTaken + ", diff (sql - xml): " + (sqlTimeTaken - xmlTimeTaken));
+    }
+
+    void assertHistoricalOpsAreEquals(AppOpsManager.HistoricalOps sqlResult,
+            AppOpsManager.HistoricalOps xmlResult) {
+        assertEquals(sqlResult.getUidCount(), xmlResult.getUidCount());
+        int uidCount = sqlResult.getUidCount();
+
+        for (int i = 0; i < uidCount; i++) {
+            AppOpsManager.HistoricalUidOps sqlUidOps = sqlResult.getUidOpsAt(i);
+            AppOpsManager.HistoricalUidOps xmlUidOps = xmlResult.getUidOpsAt(i);
+            Slog.i(LOG_TAG, "sql uid: " + sqlUidOps.getUid() + ", xml uid: " + xmlUidOps.getUid());
+            assertEquals(sqlUidOps.getUid(), xmlUidOps.getUid());
+            assertEquals(sqlUidOps.getPackageCount(), xmlUidOps.getPackageCount());
+
+            int packageCount = sqlUidOps.getPackageCount();
+            for (int p = 0; p < packageCount; p++) {
+                AppOpsManager.HistoricalPackageOps sqlPackageOps = sqlUidOps.getPackageOpsAt(p);
+                AppOpsManager.HistoricalPackageOps xmlPackageOps = xmlUidOps.getPackageOpsAt(p);
+                Slog.i(LOG_TAG, "sql package: " + sqlPackageOps.getPackageName() + ", xml package: "
+                        + xmlPackageOps.getPackageName());
+                assertEquals(sqlPackageOps.getPackageName(), xmlPackageOps.getPackageName());
+                assertEquals(sqlPackageOps.getAttributedOpsCount(),
+                        xmlPackageOps.getAttributedOpsCount());
+
+                int attrCount = sqlPackageOps.getAttributedOpsCount();
+                for (int a = 0; a < attrCount; a++) {
+                    AppOpsManager.AttributedHistoricalOps sqlAttrOps =
+                            sqlPackageOps.getAttributedOpsAt(a);
+                    AppOpsManager.AttributedHistoricalOps xmlAttrOps =
+                            xmlPackageOps.getAttributedOpsAt(a);
+                    Slog.i(LOG_TAG, "sql tag: " + sqlAttrOps.getTag() + ", xml tag: "
+                            + xmlAttrOps.getTag());
+                    assertEquals(sqlAttrOps.getTag(), xmlAttrOps.getTag());
+                    assertEquals(sqlAttrOps.getOpCount(), xmlAttrOps.getOpCount());
+
+                    int opCount = sqlAttrOps.getOpCount();
+                    for (int o = 0; o < opCount; o++) {
+                        AppOpsManager.HistoricalOp sqlHistoricalOp = sqlAttrOps.getOpAt(o);
+                        AppOpsManager.HistoricalOp xmlHistoricalOp = xmlAttrOps.getOpAt(o);
+                        Slog.i(LOG_TAG, "sql op: " + sqlHistoricalOp.getOpName() + ", xml op: "
+                                + xmlHistoricalOp.getOpName());
+                        assertEquals(sqlHistoricalOp.getOpName(), xmlHistoricalOp.getOpName());
+                        assertEquals(sqlHistoricalOp.getDiscreteAccessCount(),
+                                xmlHistoricalOp.getDiscreteAccessCount());
+
+                        int accessCount = sqlHistoricalOp.getDiscreteAccessCount();
+                        for (int x = 0; x < accessCount; x++) {
+                            AppOpsManager.AttributedOpEntry sqlOpEntry =
+                                    sqlHistoricalOp.getDiscreteAccessAt(x);
+                            AppOpsManager.AttributedOpEntry xmlOpEntry =
+                                    xmlHistoricalOp.getDiscreteAccessAt(x);
+                            Slog.i(LOG_TAG, "sql keys: " + sqlOpEntry.collectKeys() + ", xml keys: "
+                                    + xmlOpEntry.collectKeys());
+                            assertEquals(sqlOpEntry.collectKeys(), xmlOpEntry.collectKeys());
+                            assertEquals(sqlOpEntry.isRunning(), xmlOpEntry.isRunning());
+                            ArraySet<Long> keys = sqlOpEntry.collectKeys();
+                            final int keyCount = keys.size();
+                            for (int k = 0; k < keyCount; k++) {
+                                final long key = keys.valueAt(k);
+                                final int flags = extractFlagsFromKey(key);
+                                assertEquals(sqlOpEntry.getLastDuration(flags),
+                                        xmlOpEntry.getLastDuration(flags));
+                                assertEquals(sqlOpEntry.getLastProxyInfo(flags),
+                                        xmlOpEntry.getLastProxyInfo(flags));
+                                assertEquals(sqlOpEntry.getLastAccessTime(flags),
+                                        xmlOpEntry.getLastAccessTime(flags));
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    // code duplicated for assertions
+    private static final int FLAGS_MASK = 0xFFFFFFFF;
+
+    public static int extractFlagsFromKey(@AppOpsManager.DataBucketKey long key) {
+        return (int) (key & FLAGS_MASK);
+    }
+
+    private void assertEquals(Object actual, Object expected) {
+        if (!Objects.equals(actual, expected)) {
+            throw new IllegalStateException("Actual (" + actual + ") is not equal to expected ("
+                    + expected + ")");
+        }
+    }
+
+    @Override
+    void dump(@NonNull PrintWriter pw, int uidFilter, @Nullable String packageNameFilter,
+            @Nullable String attributionTagFilter, int filter, int dumpOp,
+            @NonNull SimpleDateFormat sdf, @NonNull Date date, @NonNull String prefix,
+            int nDiscreteOps) {
+        mXmlRegistry.dump(pw, uidFilter, packageNameFilter, attributionTagFilter, filter, dumpOp,
+                sdf, date, prefix, nDiscreteOps);
+        pw.println("--------------------------------------------------------");
+        pw.println("--------------------------------------------------------");
+        mSqlRegistry.dump(pw, uidFilter, packageNameFilter, attributionTagFilter, filter, dumpOp,
+                sdf, date, prefix, nDiscreteOps);
+    }
+}
diff --git a/services/core/java/com/android/server/appop/DiscreteRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsXmlRegistry.java
similarity index 84%
rename from services/core/java/com/android/server/appop/DiscreteRegistry.java
rename to services/core/java/com/android/server/appop/DiscreteOpsXmlRegistry.java
index 7f161f6..a6e3fc7 100644
--- a/services/core/java/com/android/server/appop/DiscreteRegistry.java
+++ b/services/core/java/com/android/server/appop/DiscreteOpsXmlRegistry.java
@@ -24,48 +24,20 @@
 import static android.app.AppOpsManager.FILTER_BY_OP_NAMES;
 import static android.app.AppOpsManager.FILTER_BY_PACKAGE_NAME;
 import static android.app.AppOpsManager.FILTER_BY_UID;
-import static android.app.AppOpsManager.OP_CAMERA;
-import static android.app.AppOpsManager.OP_COARSE_LOCATION;
-import static android.app.AppOpsManager.OP_EMERGENCY_LOCATION;
-import static android.app.AppOpsManager.OP_FINE_LOCATION;
 import static android.app.AppOpsManager.OP_FLAGS_ALL;
-import static android.app.AppOpsManager.OP_FLAG_SELF;
-import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXIED;
-import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXY;
-import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION;
-import static android.app.AppOpsManager.OP_MONITOR_LOCATION;
 import static android.app.AppOpsManager.OP_NONE;
-import static android.app.AppOpsManager.OP_PHONE_CALL_CAMERA;
-import static android.app.AppOpsManager.OP_PHONE_CALL_MICROPHONE;
-import static android.app.AppOpsManager.OP_PROCESS_OUTGOING_CALLS;
-import static android.app.AppOpsManager.OP_READ_ICC_SMS;
-import static android.app.AppOpsManager.OP_READ_SMS;
-import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO;
-import static android.app.AppOpsManager.OP_RECEIVE_SANDBOX_TRIGGER_AUDIO;
-import static android.app.AppOpsManager.OP_RECORD_AUDIO;
-import static android.app.AppOpsManager.OP_RESERVED_FOR_TESTING;
-import static android.app.AppOpsManager.OP_SEND_SMS;
-import static android.app.AppOpsManager.OP_SMS_FINANCIAL_TRANSACTIONS;
-import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
-import static android.app.AppOpsManager.OP_WRITE_ICC_SMS;
-import static android.app.AppOpsManager.OP_WRITE_SMS;
 import static android.app.AppOpsManager.flagsToString;
 import static android.app.AppOpsManager.getUidStateName;
 import static android.companion.virtual.VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT;
 
-import static java.lang.Long.min;
 import static java.lang.Math.max;
 
-import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.AppOpsManager;
-import android.os.AsyncTask;
-import android.os.Build;
 import android.os.Environment;
 import android.os.FileUtils;
 import android.permission.flags.Flags;
-import android.provider.DeviceConfig;
 import android.util.ArrayMap;
 import android.util.AtomicFile;
 import android.util.Slog;
@@ -84,10 +56,7 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.PrintWriter;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
 import java.text.SimpleDateFormat;
-import java.time.Duration;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
@@ -99,100 +68,30 @@
 import java.util.Set;
 
 /**
- * This class manages information about recent accesses to ops for permission usage timeline.
+ * Xml persistence implementation for discrete ops.
  *
- * The discrete history is kept for limited time (initial default is 24 hours, set in
- * {@link DiscreteRegistry#sDiscreteHistoryCutoff) and discarded after that.
- *
- * Discrete history is quantized to reduce resources footprint. By default quantization is set to
- * one minute in {@link DiscreteRegistry#sDiscreteHistoryQuantization}. All access times are aligned
- * to the closest quantized time. All durations (except -1, meaning no duration) are rounded up to
- * the closest quantized interval.
- *
- * When data is queried through API, events are deduplicated and for every time quant there can
- * be only one {@link AppOpsManager.AttributedOpEntry}. Each entry contains information about
- * different accesses which happened in specified time quant - across dimensions of
- * {@link AppOpsManager.UidState} and {@link AppOpsManager.OpFlags}. For each dimension
- * it is only possible to know if at least one access happened in the time quant.
- *
+ * <p>
  * Every time state is saved (default is 30 minutes), memory state is dumped to a
  * new file and memory state is cleared. Files older than time limit are deleted
  * during the process.
- *
+ * <p>
  * When request comes in, files are read and requested information is collected
  * and delivered. Information is cached in memory until the next state save (up to 30 minutes), to
  * avoid reading disk if more API calls come in a quick succession.
- *
+ * <p>
  * THREADING AND LOCKING:
- * For in-memory transactions this class relies on {@link DiscreteRegistry#mInMemoryLock}. It is
- * assumed that the same lock is used for in-memory transactions in {@link AppOpsService},
- * {@link HistoricalRegistry}, and {@link DiscreteRegistry}.
- * {@link DiscreteRegistry#recordDiscreteAccess(int, String, int, String, int, int, long, long)}
- * must only be called while holding this lock.
- * {@link DiscreteRegistry#mOnDiskLock} is used when disk transactions are performed.
- * It is very important to release {@link DiscreteRegistry#mInMemoryLock} as soon as possible, as
- * no AppOps related transactions across the system can be performed while it is held.
+ * For in-memory transactions this class relies on {@link DiscreteOpsXmlRegistry#mInMemoryLock}.
+ * It is assumed that the same lock is used for in-memory transactions in {@link AppOpsService},
+ * {@link HistoricalRegistry}, and {@link DiscreteOpsXmlRegistry }.
+ * {@link DiscreteOpsRegistry#recordDiscreteAccess} must only be called while holding this lock.
+ * {@link DiscreteOpsXmlRegistry#mOnDiskLock} is used when disk transactions are performed.
+ * It is very important to release {@link DiscreteOpsXmlRegistry#mInMemoryLock} as soon as
+ * possible, as no AppOps related transactions across the system can be performed while it is held.
  *
- * INITIALIZATION: We can initialize persistence only after the system is ready
- * as we need to check the optional configuration override from the settings
- * database which is not initialized at the time the app ops service is created. This class
- * relies on {@link HistoricalRegistry} for controlling that no calls are allowed until then. All
- * outside calls are going through {@link HistoricalRegistry}, where
- * {@link HistoricalRegistry#isPersistenceInitializedMLocked()} check is done.
  */
-
-final class DiscreteRegistry {
+class DiscreteOpsXmlRegistry extends DiscreteOpsRegistry {
     static final String DISCRETE_HISTORY_FILE_SUFFIX = "tl";
-    private static final String TAG = DiscreteRegistry.class.getSimpleName();
-
-    private static final String PROPERTY_DISCRETE_HISTORY_CUTOFF = "discrete_history_cutoff_millis";
-    private static final String PROPERTY_DISCRETE_HISTORY_QUANTIZATION =
-            "discrete_history_quantization_millis";
-    private static final String PROPERTY_DISCRETE_FLAGS = "discrete_history_op_flags";
-    private static final String PROPERTY_DISCRETE_OPS_LIST = "discrete_history_ops_cslist";
-    private static final String DEFAULT_DISCRETE_OPS = OP_FINE_LOCATION + "," + OP_COARSE_LOCATION
-            + "," + OP_EMERGENCY_LOCATION + "," + OP_CAMERA + "," + OP_RECORD_AUDIO + ","
-            + OP_PHONE_CALL_MICROPHONE + "," + OP_PHONE_CALL_CAMERA + ","
-            + OP_RECEIVE_AMBIENT_TRIGGER_AUDIO + "," + OP_RECEIVE_SANDBOX_TRIGGER_AUDIO
-            + "," + OP_RESERVED_FOR_TESTING;
-    private static final int[] sDiscreteOpsToLog =
-            new int[]{OP_FINE_LOCATION, OP_COARSE_LOCATION, OP_EMERGENCY_LOCATION, OP_CAMERA,
-                    OP_RECORD_AUDIO, OP_PHONE_CALL_MICROPHONE, OP_PHONE_CALL_CAMERA,
-                    OP_RECEIVE_AMBIENT_TRIGGER_AUDIO, OP_RECEIVE_SANDBOX_TRIGGER_AUDIO, OP_READ_SMS,
-                    OP_WRITE_SMS, OP_SEND_SMS, OP_READ_ICC_SMS, OP_WRITE_ICC_SMS,
-                    OP_SMS_FINANCIAL_TRANSACTIONS, OP_SYSTEM_ALERT_WINDOW, OP_MONITOR_LOCATION,
-                    OP_MONITOR_HIGH_POWER_LOCATION, OP_PROCESS_OUTGOING_CALLS,
-            };
-    private static final long DEFAULT_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(7).toMillis();
-    private static final long MAXIMUM_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(30).toMillis();
-    private static final long DEFAULT_DISCRETE_HISTORY_QUANTIZATION =
-            Duration.ofMinutes(1).toMillis();
-
-    static final int ACCESS_TYPE_NOTE_OP =
-            FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__NOTE_OP;
-    static final int ACCESS_TYPE_START_OP =
-            FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__START_OP;
-    static final int ACCESS_TYPE_FINISH_OP =
-            FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__FINISH_OP;
-    static final int ACCESS_TYPE_PAUSE_OP =
-            FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__PAUSE_OP;
-    static final int ACCESS_TYPE_RESUME_OP =
-            FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__RESUME_OP;
-
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef(prefix = {"ACCESS_TYPE_"}, value = {
-            ACCESS_TYPE_NOTE_OP,
-            ACCESS_TYPE_START_OP,
-            ACCESS_TYPE_FINISH_OP,
-            ACCESS_TYPE_PAUSE_OP,
-            ACCESS_TYPE_RESUME_OP
-    })
-    public @interface AccessType {}
-
-    private static long sDiscreteHistoryCutoff;
-    private static long sDiscreteHistoryQuantization;
-    private static int[] sDiscreteOps;
-    private static int sDiscreteFlags;
+    private static final String TAG = DiscreteOpsXmlRegistry.class.getSimpleName();
 
     private static final String TAG_HISTORY = "h";
     private static final String ATTR_VERSION = "v";
@@ -221,9 +120,6 @@
     private static final String ATTR_ATTRIBUTION_FLAGS = "af";
     private static final String ATTR_CHAIN_ID = "ci";
 
-    private static final int OP_FLAGS_DISCRETE = OP_FLAG_SELF | OP_FLAG_TRUSTED_PROXIED
-            | OP_FLAG_TRUSTED_PROXY;
-
     // Lock for read/write access to on disk state
     private final Object mOnDiskLock = new Object();
 
@@ -239,14 +135,12 @@
     @GuardedBy("mOnDiskLock")
     private DiscreteOps mCachedOps = null;
 
-    private boolean mDebugMode = false;
-
-    DiscreteRegistry(Object inMemoryLock) {
-        this(inMemoryLock, new File(new File(Environment.getDataSystemDirectory(), "appops"),
-                "discrete"));
+    DiscreteOpsXmlRegistry(Object inMemoryLock) {
+        this(inMemoryLock, getDiscreteOpsDir());
     }
 
-    DiscreteRegistry(Object inMemoryLock, File discreteAccessDir) {
+    // constructor for tests.
+    DiscreteOpsXmlRegistry(Object inMemoryLock, File discreteAccessDir) {
         mInMemoryLock = inMemoryLock;
         synchronized (mOnDiskLock) {
             mDiscreteAccessDir = discreteAccessDir;
@@ -258,40 +152,8 @@
         }
     }
 
-    void systemReady() {
-        DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_PRIVACY,
-                AsyncTask.THREAD_POOL_EXECUTOR, (DeviceConfig.Properties p) -> {
-                    setDiscreteHistoryParameters(p);
-                });
-        setDiscreteHistoryParameters(DeviceConfig.getProperties(DeviceConfig.NAMESPACE_PRIVACY));
-    }
-
-    private void setDiscreteHistoryParameters(DeviceConfig.Properties p) {
-        if (p.getKeyset().contains(PROPERTY_DISCRETE_HISTORY_CUTOFF)) {
-            sDiscreteHistoryCutoff = p.getLong(PROPERTY_DISCRETE_HISTORY_CUTOFF,
-                    DEFAULT_DISCRETE_HISTORY_CUTOFF);
-            if (!Build.IS_DEBUGGABLE && !mDebugMode) {
-                sDiscreteHistoryCutoff = min(MAXIMUM_DISCRETE_HISTORY_CUTOFF,
-                        sDiscreteHistoryCutoff);
-            }
-        } else {
-            sDiscreteHistoryCutoff = DEFAULT_DISCRETE_HISTORY_CUTOFF;
-        }
-        if (p.getKeyset().contains(PROPERTY_DISCRETE_HISTORY_QUANTIZATION)) {
-            sDiscreteHistoryQuantization = p.getLong(PROPERTY_DISCRETE_HISTORY_QUANTIZATION,
-                    DEFAULT_DISCRETE_HISTORY_QUANTIZATION);
-            if (!Build.IS_DEBUGGABLE && !mDebugMode) {
-                sDiscreteHistoryQuantization = max(DEFAULT_DISCRETE_HISTORY_QUANTIZATION,
-                        sDiscreteHistoryQuantization);
-            }
-        } else {
-            sDiscreteHistoryQuantization = DEFAULT_DISCRETE_HISTORY_QUANTIZATION;
-        }
-        sDiscreteFlags = p.getKeyset().contains(PROPERTY_DISCRETE_FLAGS) ? sDiscreteFlags =
-                p.getInt(PROPERTY_DISCRETE_FLAGS, OP_FLAGS_DISCRETE) : OP_FLAGS_DISCRETE;
-        sDiscreteOps = p.getKeyset().contains(PROPERTY_DISCRETE_OPS_LIST) ? parseOpsList(
-                p.getString(PROPERTY_DISCRETE_OPS_LIST, DEFAULT_DISCRETE_OPS)) : parseOpsList(
-                DEFAULT_DISCRETE_OPS);
+    static File getDiscreteOpsDir() {
+        return new File(new File(Environment.getDataSystemDirectory(), "appops"), "discrete");
     }
 
     void recordDiscreteAccess(int uid, String packageName, @NonNull String deviceId, int op,
@@ -300,17 +162,9 @@
             @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId,
             @AccessType int accessType) {
         if (shouldLogAccess(op)) {
-            int firstChar = 0;
-            if (attributionTag != null && attributionTag.startsWith(packageName)) {
-                firstChar = packageName.length();
-                if (firstChar < attributionTag.length() && attributionTag.charAt(firstChar)
-                        == '.') {
-                    firstChar++;
-                }
-            }
             FrameworkStatsLog.write(FrameworkStatsLog.APP_OP_ACCESS_TRACKED, uid, op, accessType,
                     uidState, flags, attributionFlags,
-                    attributionTag == null ? null : attributionTag.substring(firstChar),
+                    getAttributionTag(attributionTag, packageName),
                     attributionChainId);
         }
 
@@ -331,7 +185,7 @@
         }
     }
 
-    void writeAndClearAccessHistory() {
+    void writeAndClearOldAccessHistory() {
         synchronized (mOnDiskLock) {
             if (mDiscreteAccessDir == null) {
                 Slog.d(TAG, "State not saved - persistence not initialized.");
@@ -350,6 +204,22 @@
         }
     }
 
+    void migrateSqliteData(DiscreteOps sqliteOps) {
+        synchronized (mOnDiskLock) {
+            if (mDiscreteAccessDir == null) {
+                Slog.d(TAG, "State not saved - persistence not initialized.");
+                return;
+            }
+            synchronized (mInMemoryLock) {
+                mDiscreteOps.mLargestChainId = sqliteOps.mLargestChainId;
+                mDiscreteOps.mChainIdOffset = sqliteOps.mChainIdOffset;
+            }
+            if (!sqliteOps.isEmpty()) {
+                persistDiscreteOpsLocked(sqliteOps);
+            }
+        }
+    }
+
     void addFilteredDiscreteOpsToHistoricalOps(AppOpsManager.HistoricalOps result,
             long beginTimeMillis, long endTimeMillis,
             @AppOpsManager.HistoricalOpsRequestFilter int filter, int uidFilter,
@@ -369,7 +239,7 @@
         discreteOps.applyToHistoricalOps(result, attributionChains);
     }
 
-    private int readLargestChainIdFromDiskLocked() {
+    int readLargestChainIdFromDiskLocked() {
         final File[] files = mDiscreteAccessDir.listFiles();
         if (files != null && files.length > 0) {
             File latestFile = null;
@@ -497,6 +367,13 @@
         }
     }
 
+    void deleteDiscreteOpsDir() {
+        synchronized (mOnDiskLock) {
+            mCachedOps = null;
+            FileUtils.deleteContentsAndDir(mDiscreteAccessDir);
+        }
+    }
+
     void clearHistory(int uid, String packageName) {
         synchronized (mOnDiskLock) {
             DiscreteOps discreteOps;
@@ -1506,26 +1383,6 @@
         }
     }
 
-    private static int[] parseOpsList(String opsList) {
-        String[] strArr;
-        if (opsList.isEmpty()) {
-            strArr = new String[0];
-        } else {
-            strArr = opsList.split(",");
-        }
-        int nOps = strArr.length;
-        int[] result = new int[nOps];
-        try {
-            for (int i = 0; i < nOps; i++) {
-                result[i] = Integer.parseInt(strArr[i]);
-            }
-        } catch (NumberFormatException e) {
-            Slog.e(TAG, "Failed to parse Discrete ops list: " + e.getMessage());
-            return parseOpsList(DEFAULT_DISCRETE_OPS);
-        }
-        return result;
-    }
-
     private static List<DiscreteOpEvent> stableListMerge(List<DiscreteOpEvent> a,
             List<DiscreteOpEvent> b) {
         int nA = a.size();
@@ -1570,34 +1427,4 @@
         }
         return result;
     }
-
-    private static boolean isDiscreteOp(int op, @AppOpsManager.OpFlags int flags) {
-        if (!ArrayUtils.contains(sDiscreteOps, op)) {
-            return false;
-        }
-        if ((flags & (sDiscreteFlags)) == 0) {
-            return false;
-        }
-        return true;
-    }
-
-    private static boolean shouldLogAccess(int op) {
-        return Flags.appopAccessTrackingLoggingEnabled()
-                && ArrayUtils.contains(sDiscreteOpsToLog, op);
-    }
-
-    private static long discretizeTimeStamp(long timeStamp) {
-        return timeStamp / sDiscreteHistoryQuantization * sDiscreteHistoryQuantization;
-
-    }
-
-    private static long discretizeDuration(long duration) {
-        return duration == -1 ? -1 : (duration + sDiscreteHistoryQuantization - 1)
-                / sDiscreteHistoryQuantization * sDiscreteHistoryQuantization;
-    }
-
-    void setDebugMode(boolean debugMode) {
-        this.mDebugMode = debugMode;
-    }
 }
-
diff --git a/services/core/java/com/android/server/appop/HistoricalRegistry.java b/services/core/java/com/android/server/appop/HistoricalRegistry.java
index 5e67f26..ba391d0 100644
--- a/services/core/java/com/android/server/appop/HistoricalRegistry.java
+++ b/services/core/java/com/android/server/appop/HistoricalRegistry.java
@@ -35,6 +35,7 @@
 import android.app.AppOpsManager.OpHistoryFlags;
 import android.app.AppOpsManager.UidState;
 import android.content.ContentResolver;
+import android.content.Context;
 import android.database.ContentObserver;
 import android.net.Uri;
 import android.os.Build;
@@ -45,6 +46,7 @@
 import android.os.Process;
 import android.os.RemoteCallback;
 import android.os.UserHandle;
+import android.permission.flags.Flags;
 import android.provider.Settings;
 import android.util.ArraySet;
 import android.util.LongSparseArray;
@@ -135,7 +137,7 @@
     private static final String PARAMETER_DELIMITER = ",";
     private static final String PARAMETER_ASSIGNMENT = "=";
 
-    private volatile @NonNull DiscreteRegistry mDiscreteRegistry;
+    private volatile @NonNull DiscreteOpsRegistry mDiscreteRegistry;
 
     @GuardedBy("mLock")
     private @NonNull LinkedList<HistoricalOps> mPendingWrites = new LinkedList<>();
@@ -196,13 +198,30 @@
     @GuardedBy("mOnDiskLock")
     private Persistence mPersistence;
 
-    HistoricalRegistry(@NonNull Object lock) {
+    private final Context mContext;
+
+    HistoricalRegistry(@NonNull Object lock, Context context) {
         mInMemoryLock = lock;
-        mDiscreteRegistry = new DiscreteRegistry(lock);
+        mContext = context;
+        if (Flags.enableSqliteAppopsAccesses()) {
+            mDiscreteRegistry = new DiscreteOpsSqlRegistry(context);
+            if (DiscreteOpsXmlRegistry.getDiscreteOpsDir().exists()) {
+                DiscreteOpsSqlRegistry sqlRegistry = (DiscreteOpsSqlRegistry) mDiscreteRegistry;
+                DiscreteOpsXmlRegistry xmlRegistry = new DiscreteOpsXmlRegistry(context);
+                DiscreteOpsMigrationHelper.migrateDiscreteOpsToSqlite(xmlRegistry, sqlRegistry);
+            }
+        } else {
+            mDiscreteRegistry = new DiscreteOpsXmlRegistry(context);
+            if (DiscreteOpsDbHelper.getDatabaseFile().exists()) { // roll-back sqlite
+                DiscreteOpsSqlRegistry sqlRegistry = new DiscreteOpsSqlRegistry(context);
+                DiscreteOpsXmlRegistry xmlRegistry = (DiscreteOpsXmlRegistry) mDiscreteRegistry;
+                DiscreteOpsMigrationHelper.migrateDiscreteOpsToXml(sqlRegistry, xmlRegistry);
+            }
+        }
     }
 
     HistoricalRegistry(@NonNull HistoricalRegistry other) {
-        this(other.mInMemoryLock);
+        this(other.mInMemoryLock, other.mContext);
         mMode = other.mMode;
         mBaseSnapshotInterval = other.mBaseSnapshotInterval;
         mIntervalCompressionMultiplier = other.mIntervalCompressionMultiplier;
@@ -475,7 +494,7 @@
             @NonNull String deviceId, @Nullable String attributionTag, @UidState int uidState,
             @OpFlags int flags, long accessTime,
             @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId,
-            @DiscreteRegistry.AccessType int accessType, int accessCount) {
+            @DiscreteOpsRegistry.AccessType int accessType, int accessCount) {
         synchronized (mInMemoryLock) {
             if (mMode == AppOpsManager.HISTORICAL_MODE_ENABLED_ACTIVE) {
                 if (!isPersistenceInitializedMLocked()) {
@@ -512,7 +531,7 @@
             @NonNull String deviceId, @Nullable String attributionTag, @UidState int uidState,
             @OpFlags int flags, long eventStartTime, long increment,
             @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId,
-            @DiscreteRegistry.AccessType int accessType) {
+            @DiscreteOpsRegistry.AccessType int accessType) {
         synchronized (mInMemoryLock) {
             if (mMode == AppOpsManager.HISTORICAL_MODE_ENABLED_ACTIVE) {
                 if (!isPersistenceInitializedMLocked()) {
@@ -648,7 +667,7 @@
     }
 
     void writeAndClearDiscreteHistory() {
-        mDiscreteRegistry.writeAndClearAccessHistory();
+        mDiscreteRegistry.writeAndClearOldAccessHistory();
     }
 
     void clearAllHistory() {
@@ -743,7 +762,7 @@
             }
             persistPendingHistory(pendingWrites);
         }
-        mDiscreteRegistry.writeAndClearAccessHistory();
+        mDiscreteRegistry.writeAndClearOldAccessHistory();
     }
 
     private void persistPendingHistory(@NonNull List<HistoricalOps> pendingWrites) {
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 0fd4716..b9b0670 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -47,6 +47,7 @@
 import static android.media.AudioManager.RINGER_MODE_SILENT;
 import static android.media.AudioManager.RINGER_MODE_VIBRATE;
 import static android.media.AudioManager.STREAM_SYSTEM;
+import static android.media.IAudioManagerNative.HardeningType;
 import static android.media.audio.Flags.autoPublicVolumeApiHardening;
 import static android.media.audio.Flags.automaticBtDeviceType;
 import static android.media.audio.Flags.concurrentAudioRecordBypassPermission;
@@ -151,6 +152,7 @@
 import android.media.FadeManagerConfiguration;
 import android.media.IAudioDeviceVolumeDispatcher;
 import android.media.IAudioFocusDispatcher;
+import android.media.IAudioManagerNative;
 import android.media.IAudioModeDispatcher;
 import android.media.IAudioRoutesObserver;
 import android.media.IAudioServerStateDispatcher;
@@ -242,6 +244,7 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
+import android.util.SystemPropertySetter;
 import android.view.Display;
 import android.view.KeyEvent;
 import android.view.accessibility.AccessibilityManager;
@@ -834,6 +837,18 @@
     private final UserRestrictionsListener mUserRestrictionsListener =
             new AudioServiceUserRestrictionsListener();
 
+    private final IAudioManagerNative mNativeShim = new IAudioManagerNative.Stub() {
+        // oneway
+        @Override
+        public void playbackHardeningEvent(int uid, byte type, boolean bypassed) {
+        }
+
+        @Override
+        public void permissionUpdateBarrier() {
+            AudioService.this.permissionUpdateBarrier();
+        }
+    };
+
     // List of binder death handlers for setMode() client processes.
     // The last process to have called setMode() is at the top of the list.
     // package-private so it can be accessed in AudioDeviceBroker.getSetModeDeathHandlers
@@ -2810,6 +2825,11 @@
                 args, callback, resultReceiver);
     }
 
+    @Override
+    public IAudioManagerNative getNativeInterface() {
+        return mNativeShim;
+    }
+
     /** @see AudioManager#getSurroundFormats() */
     @Override
     public Map<Integer, Boolean> getSurroundFormats() {
@@ -7213,7 +7233,7 @@
         final int pid = Binder.getCallingPid();
         final String eventSource = new StringBuilder("setBluetoothA2dpOn(").append(on)
                 .append(") from u/pid:").append(uid).append("/")
-                .append(pid).toString();
+                .append(pid).append(" src:AudioService.setBtA2dpOn").toString();
 
         new MediaMetrics.Item(MediaMetrics.Name.AUDIO_DEVICE
                 + MediaMetrics.SEPARATOR + "setBluetoothA2dpOn")
@@ -8570,6 +8590,12 @@
         return true;
     }
 
+    private boolean shouldPreserveVolume(boolean userSwitch, VolumeGroupState vgs) {
+        // as for STREAM_MUSIC, preserve volume from one user to the next except
+        // Android Automotive platform
+        return (userSwitch && vgs.isMusic()) && !isPlatformAutomotive();
+    }
+
     private void readVolumeGroupsSettings(boolean userSwitch) {
         synchronized (mSettingsLock) {
             synchronized (VolumeStreamState.class) {
@@ -8578,8 +8604,7 @@
                 }
                 for (int i = 0; i < sVolumeGroupStates.size(); i++) {
                     VolumeGroupState vgs = sVolumeGroupStates.valueAt(i);
-                    // as for STREAM_MUSIC, preserve volume from one user to the next.
-                    if (!(userSwitch && vgs.isMusic())) {
+                    if (!shouldPreserveVolume(userSwitch, vgs)) {
                         vgs.clearIndexCache();
                         vgs.readSettings();
                     }
@@ -9018,6 +9043,11 @@
             mIndexMap.clear();
         }
 
+        private @UserIdInt int getVolumePersistenceUserId() {
+            return isMusic() && !isPlatformAutomotive()
+                    ? UserHandle.USER_SYSTEM : UserHandle.USER_CURRENT;
+        }
+
         private void persistVolumeGroup(int device) {
             // No need to persist the index if the volume group is backed up
             // by a public stream type as this is redundant
@@ -9035,7 +9065,7 @@
             boolean success = mSettings.putSystemIntForUser(mContentResolver,
                     getSettingNameForDevice(device),
                     getIndex(device),
-                    isMusic() ? UserHandle.USER_SYSTEM : UserHandle.USER_CURRENT);
+                    getVolumePersistenceUserId());
             if (!success) {
                 Log.e(TAG, "persistVolumeGroup failed for group " +  mAudioVolumeGroup.name());
             }
@@ -9058,7 +9088,7 @@
                     String name = getSettingNameForDevice(device);
                     index = mSettings.getSystemIntForUser(
                             mContentResolver, name, defaultIndex,
-                            isMusic() ? UserHandle.USER_SYSTEM : UserHandle.USER_CURRENT);
+                            getVolumePersistenceUserId());
                     if (index == -1) {
                         continue;
                     }
@@ -11032,7 +11062,7 @@
         @GuardedBy("mLock")
         private void updateLocked() {
             String n = Long.toString(mToken++);
-            SystemProperties.set(PermissionManager.CACHE_KEY_PACKAGE_INFO_NOTIFY, n);
+            SystemPropertySetter.setWithRetry(PermissionManager.CACHE_KEY_PACKAGE_INFO_NOTIFY, n);
         }
 
         private void trigger() {
diff --git a/services/core/java/com/android/server/compat/CompatConfig.java b/services/core/java/com/android/server/compat/CompatConfig.java
index e89f43b..20c3327 100644
--- a/services/core/java/com/android/server/compat/CompatConfig.java
+++ b/services/core/java/com/android/server/compat/CompatConfig.java
@@ -876,7 +876,28 @@
     }
 
     @Nullable
+    @android.ravenwood.annotation.RavenwoodReplace(
+            blockedBy = PackageManager.class,
+            reason = "PackageManager.getApplicationInfo() isn't supported yet")
     private Long getVersionCodeOrNull(String packageName) {
+        return getVersionCodeOrNullImpl(packageName);
+    }
+
+    @SuppressWarnings("unused")
+    @Nullable
+    private Long getVersionCodeOrNull$ravenwood(String packageName) {
+        try {
+            // It's possible that the context is mocked, try the real method first
+            return getVersionCodeOrNullImpl(packageName);
+        } catch (Throwable e) {
+            // For now, Ravenwood doesn't support the concept of "app updates", so let's
+            // just use a fixed version code for all packages.
+            return 1L;
+        }
+    }
+
+    @Nullable
+    private Long getVersionCodeOrNullImpl(String packageName) {
         try {
             ApplicationInfo applicationInfo = mContext.getPackageManager().getApplicationInfo(
                     packageName, MATCH_ANY_USER);
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
index 5d9db65..d89db8d 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
@@ -312,6 +312,13 @@
                 mProcessObserver);
     }
 
+    @Override
+    public void onBootPhase(int phase) {
+        if (phase == PHASE_SYSTEM_SERVICES_READY) {
+            mDeviceStatePolicy.getDeviceStateProvider().onSystemReady();
+        }
+    }
+
     @VisibleForTesting
     Handler getHandler() {
         return mHandler;
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateProvider.java b/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
index 8d07609..8a8ebc2 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
@@ -91,6 +91,11 @@
     @interface SupportedStatesUpdatedReason {}
 
     /**
+     * Called when the system boot phase advances to PHASE_SYSTEM_SERVICES_READY.
+     */
+    default void onSystemReady() {};
+
+    /**
      * Registers a listener for changes in provider state.
      * <p>
      * It is <b>required</b> that
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index c8192e5..b530da2 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -2246,10 +2246,6 @@
 
     @GuardedBy("mSyncRoot")
     private void handleLogicalDisplayDisconnectedLocked(LogicalDisplay display) {
-        if (!mFlags.isConnectedDisplayManagementEnabled()) {
-            Slog.e(TAG, "DisplayDisconnected shouldn't be received when the flag is off");
-            return;
-        }
         releaseDisplayAndEmitEvent(display, DisplayManagerGlobal.EVENT_DISPLAY_DISCONNECTED);
         mExternalDisplayPolicy.handleLogicalDisplayDisconnectedLocked(display);
     }
@@ -2315,11 +2311,6 @@
 
     @SuppressLint("AndroidFrameworkRequiresPermission")
     private void handleLogicalDisplayConnectedLocked(LogicalDisplay display) {
-        if (!mFlags.isConnectedDisplayManagementEnabled()) {
-            Slog.e(TAG, "DisplayConnected shouldn't be received when the flag is off");
-            return;
-        }
-
         setupLogicalDisplay(display);
 
         if (ExternalDisplayPolicy.isExternalDisplayLocked(display)) {
@@ -2346,9 +2337,6 @@
     private void handleLogicalDisplayAddedLocked(LogicalDisplay display) {
         final int displayId = display.getDisplayIdLocked();
         final boolean isDefault = displayId == Display.DEFAULT_DISPLAY;
-        if (!mFlags.isConnectedDisplayManagementEnabled()) {
-            setupLogicalDisplay(display);
-        }
 
         // Wake up waitForDefaultDisplay.
         if (isDefault) {
@@ -2443,21 +2431,17 @@
     }
 
     private void handleLogicalDisplayRemovedLocked(@NonNull LogicalDisplay display) {
-        // With display management, the display is removed when disabled, and it might still exist.
+        // The display is removed when disabled, and it might still exist.
         // Resources must only be released when the disconnected signal is received.
-        if (mFlags.isConnectedDisplayManagementEnabled()) {
-            if (display.isValidLocked()) {
-                updateViewportPowerStateLocked(display);
-            }
+        if (display.isValidLocked()) {
+            updateViewportPowerStateLocked(display);
+        }
 
-            // Note: This method is only called if the display was enabled before being removed.
-            sendDisplayEventLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_REMOVED);
+        // Note: This method is only called if the display was enabled before being removed.
+        sendDisplayEventLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_REMOVED);
 
-            if (display.isValidLocked()) {
-                applyDisplayChangedLocked(display);
-            }
-        } else {
-            releaseDisplayAndEmitEvent(display, DisplayManagerGlobal.EVENT_DISPLAY_REMOVED);
+        if (display.isValidLocked()) {
+            applyDisplayChangedLocked(display);
         }
         if (mDisplayTopologyCoordinator != null) {
             mDisplayTopologyCoordinator.onDisplayRemoved(display.getDisplayIdLocked());
@@ -4565,13 +4549,11 @@
             final int callingPid = Binder.getCallingPid();
             final int callingUid = Binder.getCallingUid();
 
-            if (mFlags.isConnectedDisplayManagementEnabled()) {
-                if ((internalEventFlagsMask
-                        & DisplayManagerGlobal
-                        .INTERNAL_EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) {
-                    mContext.enforceCallingOrSelfPermission(MANAGE_DISPLAYS,
-                            "Permission required to get signals about connection events.");
-                }
+            if ((internalEventFlagsMask
+                    & DisplayManagerGlobal
+                    .INTERNAL_EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) {
+                mContext.enforceCallingOrSelfPermission(MANAGE_DISPLAYS,
+                        "Permission required to get signals about connection events.");
             }
 
             final long token = Binder.clearCallingIdentity();
diff --git a/services/core/java/com/android/server/display/DisplayManagerShellCommand.java b/services/core/java/com/android/server/display/DisplayManagerShellCommand.java
index e46397b..f6b2591 100644
--- a/services/core/java/com/android/server/display/DisplayManagerShellCommand.java
+++ b/services/core/java/com/android/server/display/DisplayManagerShellCommand.java
@@ -179,12 +179,10 @@
         pw.println("    Sets brightness to docked + idle screen brightness mode");
         pw.println("  undock");
         pw.println("    Sets brightness to active (normal) screen brightness mode");
-        if (mFlags.isConnectedDisplayManagementEnabled()) {
-            pw.println("  enable-display DISPLAY_ID");
-            pw.println("    Enable the DISPLAY_ID. Only possible if this is a connected display.");
-            pw.println("  disable-display DISPLAY_ID");
-            pw.println("    Disable the DISPLAY_ID. Only possible if this is a connected display.");
-        }
+        pw.println("  enable-display DISPLAY_ID");
+        pw.println("    Enable the DISPLAY_ID. Only possible if this is a connected display.");
+        pw.println("  disable-display DISPLAY_ID");
+        pw.println("    Disable the DISPLAY_ID. Only possible if this is a connected display.");
         pw.println("  power-reset DISPLAY_ID");
         pw.println("    Turn the DISPLAY_ID power to a state the display supposed to have.");
         pw.println("  power-off DISPLAY_ID");
@@ -601,11 +599,6 @@
     }
 
     private int setDisplayEnabled(boolean enable) {
-        if (!mFlags.isConnectedDisplayManagementEnabled()) {
-            getErrPrintWriter()
-                    .println("Error: external display management is not available on this device.");
-            return 1;
-        }
         final String displayIdText = getNextArg();
         if (displayIdText == null) {
             getErrPrintWriter().println("Error: no displayId specified");
diff --git a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java
index 519763a..a47853c 100644
--- a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java
+++ b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java
@@ -142,14 +142,6 @@
             mDisplayIdsWaitingForBootCompletion.clear();
         }
 
-        if (!mFlags.isConnectedDisplayManagementEnabled()) {
-            if (DEBUG) {
-                Slog.d(TAG, "External display management is not enabled on your device:"
-                                    + " cannot register thermal listener.");
-            }
-            return;
-        }
-
         if (!mFlags.isConnectedDisplayErrorHandlingEnabled()) {
             if (DEBUG) {
                 Slog.d(TAG, "ConnectedDisplayErrorHandlingEnabled is not enabled on your device:"
@@ -173,14 +165,6 @@
             return;
         }
 
-        if (!mFlags.isConnectedDisplayManagementEnabled()) {
-            if (DEBUG) {
-                Slog.d(TAG, "setExternalDisplayEnabledLocked: External display management is not"
-                                    + " enabled on your device, cannot enable/disable display.");
-            }
-            return;
-        }
-
         if (enabled && !isExternalDisplayAllowed()) {
             Slog.w(TAG, "setExternalDisplayEnabledLocked: External display can not be enabled"
                                 + " because it is currently not allowed.");
@@ -202,14 +186,6 @@
             return;
         }
 
-        if (!mFlags.isConnectedDisplayManagementEnabled()) {
-            if (DEBUG) {
-                Slog.d(TAG, "handleExternalDisplayConnectedLocked connected display management"
-                                    + " flag is off");
-            }
-            return;
-        }
-
         if (!mIsBootCompleted) {
             mDisplayIdsWaitingForBootCompletion.add(logicalDisplay.getDisplayIdLocked());
             return;
@@ -251,10 +227,6 @@
     void handleLogicalDisplayDisconnectedLocked(@NonNull final LogicalDisplay logicalDisplay) {
         // Type of the display here is always UNKNOWN, so we can't verify it is an external display
 
-        if (!mFlags.isConnectedDisplayManagementEnabled()) {
-            return;
-        }
-
         var displayId = logicalDisplay.getDisplayIdLocked();
         if (mDisplayIdsWaitingForBootCompletion.remove(displayId)) {
             return;
@@ -271,10 +243,6 @@
             return;
         }
 
-        if (!mFlags.isConnectedDisplayManagementEnabled()) {
-            return;
-        }
-
         mExternalDisplayStatsService.onDisplayAdded(logicalDisplay.getDisplayIdLocked());
     }
 
@@ -289,10 +257,6 @@
             }
         }
 
-        if (!mFlags.isConnectedDisplayManagementEnabled()) {
-            return;
-        }
-
         if (isShown) {
             mExternalDisplayStatsService.onPresentationWindowAdded(displayId);
         } else {
@@ -306,12 +270,6 @@
             return;
         }
 
-        if (!mFlags.isConnectedDisplayManagementEnabled()) {
-            Slog.e(TAG, "disableExternalDisplayLocked shouldn't be called when the"
-                                + " connected display management flag is off");
-            return;
-        }
-
         if (!mFlags.isConnectedDisplayErrorHandlingEnabled()) {
             if (DEBUG) {
                 Slog.d(TAG, "disableExternalDisplayLocked shouldn't be called when the"
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index 0069215..ecc8896 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -823,18 +823,13 @@
                 if (wasPreviouslyUpdated) {
                     // The display isn't actually removed from our internal data structures until
                     // after the notification is sent; see {@link #sendUpdatesForDisplaysLocked}.
-                    if (mFlags.isConnectedDisplayManagementEnabled()) {
-                        if (mDisplaysEnabledCache.get(displayId)) {
-                            // We still need to send LOGICAL_DISPLAY_EVENT_DISCONNECTED
-                            reloop = true;
-                            logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_REMOVED;
-                        } else {
-                            mUpdatedLogicalDisplays.delete(displayId);
-                            logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_DISCONNECTED;
-                        }
+                    if (mDisplaysEnabledCache.get(displayId)) {
+                        // We still need to send LOGICAL_DISPLAY_EVENT_DISCONNECTED
+                        reloop = true;
+                        logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_REMOVED;
                     } else {
                         mUpdatedLogicalDisplays.delete(displayId);
-                        logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_REMOVED;
+                        logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_DISCONNECTED;
                     }
                 } else {
                     // This display never left this class, safe to remove without notification
@@ -845,20 +840,15 @@
 
             // The display is new.
             } else if (!wasPreviouslyUpdated) {
-                if (mFlags.isConnectedDisplayManagementEnabled()) {
-                    // We still need to send LOGICAL_DISPLAY_EVENT_ADDED
-                    reloop = true;
-                    logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_CONNECTED;
-                } else {
-                    logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_ADDED;
-                }
+                // We still need to send LOGICAL_DISPLAY_EVENT_ADDED
+                reloop = true;
+                logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_CONNECTED;
             // Underlying displays device has changed to a different one.
             } else if (!TextUtils.equals(mTempDisplayInfo.uniqueId, newDisplayInfo.uniqueId)) {
                 logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_SWAPPED;
 
             // Something about the display device has changed.
-            } else if (mFlags.isConnectedDisplayManagementEnabled()
-                    && wasPreviouslyEnabled != isCurrentlyEnabled) {
+            } else if (wasPreviouslyEnabled != isCurrentlyEnabled) {
                 int event = isCurrentlyEnabled ? LOGICAL_DISPLAY_EVENT_ADDED :
                         LOGICAL_DISPLAY_EVENT_REMOVED;
                 logicalDisplayEventMask |= event;
@@ -936,17 +926,13 @@
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_DEVICE_STATE_TRANSITION);
         sendUpdatesForGroupsLocked(DISPLAY_GROUP_EVENT_ADDED);
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_REMOVED);
-        if (mFlags.isConnectedDisplayManagementEnabled()) {
-            sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_DISCONNECTED);
-        }
+        sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_DISCONNECTED);
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_BASIC_CHANGED);
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED);
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_STATE_CHANGED);
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_FRAME_RATE_OVERRIDES_CHANGED);
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_SWAPPED);
-        if (mFlags.isConnectedDisplayManagementEnabled()) {
-            sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_CONNECTED);
-        }
+        sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_CONNECTED);
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_ADDED);
         sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_HDR_SDR_RATIO_CHANGED);
         sendUpdatesForGroupsLocked(DISPLAY_GROUP_EVENT_CHANGED);
@@ -996,23 +982,15 @@
                         + "display=" + id + " with device=" + uniqueId);
             }
 
-            if (mFlags.isConnectedDisplayManagementEnabled()) {
-                if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_ADDED) {
-                    mDisplaysEnabledCache.put(id, true);
-                } else if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_REMOVED) {
-                    mDisplaysEnabledCache.delete(id);
-                }
+            if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_ADDED) {
+                mDisplaysEnabledCache.put(id, true);
+            } else if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_REMOVED) {
+                mDisplaysEnabledCache.delete(id);
             }
 
             mListener.onLogicalDisplayEventLocked(display, logicalDisplayEvent);
 
-            if (mFlags.isConnectedDisplayManagementEnabled()) {
-                if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_DISCONNECTED) {
-                    mLogicalDisplays.delete(id);
-                }
-            } else if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_REMOVED) {
-                // We wait until we sent the EVENT_REMOVED event before actually removing the
-                // display.
+            if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_DISCONNECTED) {
                 mLogicalDisplays.delete(id);
             }
         }
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index addfbf1..4e57d67 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -42,10 +42,6 @@
             Flags.FLAG_ENABLE_PORT_IN_DISPLAY_LAYOUT,
             Flags::enablePortInDisplayLayout);
 
-    private final FlagState mConnectedDisplayManagementFlagState = new FlagState(
-            Flags.FLAG_ENABLE_CONNECTED_DISPLAY_MANAGEMENT,
-            Flags::enableConnectedDisplayManagement);
-
     private final FlagState mAdaptiveToneImprovements1 = new FlagState(
             Flags.FLAG_ENABLE_ADAPTIVE_TONE_IMPROVEMENTS_1,
             Flags::enableAdaptiveToneImprovements1);
@@ -269,11 +265,6 @@
         return mPortInDisplayLayoutFlagState.isEnabled();
     }
 
-    /** Returns whether connected display management is enabled or not. */
-    public boolean isConnectedDisplayManagementEnabled() {
-        return mConnectedDisplayManagementFlagState.isEnabled();
-    }
-
     /** Returns whether power throttling clamper is enabled on not. */
     public boolean isPowerThrottlingClamperEnabled() {
         return mPowerThrottlingClamperFlagState.isEnabled();
@@ -572,7 +563,6 @@
         pw.println(" " + mAdaptiveToneImprovements2);
         pw.println(" " + mBackUpSmoothDisplayAndForcePeakRefreshRateFlagState);
         pw.println(" " + mConnectedDisplayErrorHandlingFlagState);
-        pw.println(" " + mConnectedDisplayManagementFlagState);
         pw.println(" " + mDisplayOffloadFlagState);
         pw.println(" " + mExternalDisplayLimitModeState);
         pw.println(" " + mDisplayTopology);
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index eccbbb1..afae07c 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -29,14 +29,6 @@
 }
 
 flag {
-    name: "enable_connected_display_management"
-    namespace: "display_manager"
-    description: "Feature flag for Connected Display management"
-    bug: "280739508"
-    is_fixed_read_only: true
-}
-
-flag {
     name: "enable_power_throttling_clamper"
     namespace: "display_manager"
     description: "Feature flag for Power Throttling Clamper"
diff --git a/services/core/java/com/android/server/flags/services.aconfig b/services/core/java/com/android/server/flags/services.aconfig
index eea5c98..4505d0e 100644
--- a/services/core/java/com/android/server/flags/services.aconfig
+++ b/services/core/java/com/android/server/flags/services.aconfig
@@ -78,3 +78,11 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "datetime_notifications"
+    # "location" is used by the Android System Time team for feature flags.
+    namespace: "location"
+    description: "Enable the time notifications feature, a toggle to enable/disable time-related notifications in Date & Time Settings"
+    bug: "283267917"
+}
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
index c384b54..9349ea5 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
@@ -1030,10 +1030,20 @@
     }
 
     @ServiceThreadOnly
+    void addAndStartAction(final HdmiCecFeatureAction action, final boolean remove) {
+        assertRunOnServiceThread();
+        if (hasAction(action.getClass()) && remove) {
+            // If the action is currently running, remove it and restart it.
+            Slog.i(TAG, action.getClass().getName() + " is in progress. Restarting.");
+            removeAction(action.getClass());
+        }
+        addAndStartAction(action);
+    }
+
+    @ServiceThreadOnly
     void startNewAvbAudioStatusAction(int targetAddress) {
         assertRunOnServiceThread();
-        removeAction(AbsoluteVolumeAudioStatusAction.class);
-        addAndStartAction(new AbsoluteVolumeAudioStatusAction(this, targetAddress));
+        addAndStartAction(new AbsoluteVolumeAudioStatusAction(this, targetAddress), true);
     }
 
     @ServiceThreadOnly
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
index 1e90ab2..510e4f5 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
@@ -317,11 +317,7 @@
         if ((systemAudioOnPowerOnProp == ALWAYS_SYSTEM_AUDIO_CONTROL_ON_POWER_ON)
                 || ((systemAudioOnPowerOnProp == USE_LAST_STATE_SYSTEM_AUDIO_CONTROL_ON_POWER_ON)
                 && lastSystemAudioControlStatus && isSystemAudioControlFeatureEnabled())) {
-            if (hasAction(SystemAudioInitiationActionFromAvr.class)) {
-                Slog.i(TAG, "SystemAudioInitiationActionFromAvr is in progress. Restarting.");
-                removeAction(SystemAudioInitiationActionFromAvr.class);
-            }
-            addAndStartAction(new SystemAudioInitiationActionFromAvr(this));
+            addAndStartAction(new SystemAudioInitiationActionFromAvr(this), true);
         }
     }
 
@@ -457,6 +453,7 @@
             HdmiLogger.debug("AVR device is not directly connected with TV");
             return Constants.ABORT_NOT_IN_CORRECT_MODE;
         } else {
+            // Action has been removed if it existed, do not attempt to remove again before start.
             addAndStartAction(new ArcInitiationActionFromAvr(this));
             return Constants.HANDLED;
         }
@@ -477,11 +474,9 @@
                     && !getActions(ArcTerminationActionFromAvr.class).get(0).mCallbacks.isEmpty()) {
                 IHdmiControlCallback callback =
                         getActions(ArcTerminationActionFromAvr.class).get(0).mCallbacks.get(0);
-                removeAction(ArcTerminationActionFromAvr.class);
-                addAndStartAction(new ArcTerminationActionFromAvr(this, callback));
+                addAndStartAction(new ArcTerminationActionFromAvr(this, callback), true);
             } else {
-                removeAction(ArcTerminationActionFromAvr.class);
-                addAndStartAction(new ArcTerminationActionFromAvr(this));
+                addAndStartAction(new ArcTerminationActionFromAvr(this), true);
             }
             return Constants.HANDLED;
         }
@@ -1036,11 +1031,7 @@
     void onSystemAudioControlFeatureSupportChanged(boolean enabled) {
         setSystemAudioControlFeatureEnabled(enabled);
         if (enabled) {
-            if (hasAction(SystemAudioInitiationActionFromAvr.class)) {
-                Slog.i(TAG, "SystemAudioInitiationActionFromAvr is in progress. Restarting.");
-                removeAction(SystemAudioInitiationActionFromAvr.class);
-            }
-            addAndStartAction(new SystemAudioInitiationActionFromAvr(this));
+            addAndStartAction(new SystemAudioInitiationActionFromAvr(this), true);
         }
     }
 
@@ -1221,8 +1212,7 @@
         removeAction(ArcTerminationActionFromAvr.class);
         if (SystemProperties.getBoolean(Constants.PROPERTY_ARC_SUPPORT, true)
                 && isDirectConnectToTv() && !isArcEnabled()) {
-            removeAction(ArcInitiationActionFromAvr.class);
-            addAndStartAction(new ArcInitiationActionFromAvr(this));
+            addAndStartAction(new ArcInitiationActionFromAvr(this), true);
         }
     }
 
@@ -1367,10 +1357,6 @@
         if (mService.isDeviceDiscoveryHandledByPlayback()) {
             return;
         }
-        if (hasAction(DeviceDiscoveryAction.class)) {
-            Slog.i(TAG, "Device Discovery Action is in progress. Restarting.");
-            removeAction(DeviceDiscoveryAction.class);
-        }
         DeviceDiscoveryAction action = new DeviceDiscoveryAction(this,
                 new DeviceDiscoveryCallback() {
                     @Override
@@ -1380,7 +1366,7 @@
                         }
                     }
                 });
-        addAndStartAction(action);
+        addAndStartAction(action, true);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java
index 0b667fc..86abbc4 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java
@@ -21,7 +21,6 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.hardware.display.DeviceProductInfo;
 import android.hardware.hdmi.HdmiControlManager;
 import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.IHdmiControlCallback;
@@ -32,7 +31,6 @@
 import android.os.SystemProperties;
 import android.sysprop.HdmiProperties;
 import android.util.Slog;
-import android.view.Display;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.LocalePicker;
@@ -151,10 +149,6 @@
     private void launchDeviceDiscovery() {
         assertRunOnServiceThread();
         clearDeviceInfoList();
-        if (hasAction(DeviceDiscoveryAction.class)) {
-            Slog.i(TAG, "Device Discovery Action is in progress. Restarting.");
-            removeAction(DeviceDiscoveryAction.class);
-        }
         DeviceDiscoveryAction action = new DeviceDiscoveryAction(this,
                 new DeviceDiscoveryAction.DeviceDiscoveryCallback() {
                     @Override
@@ -163,25 +157,21 @@
                             mService.getHdmiCecNetwork().addCecDevice(info);
                         }
 
-                        // Since we removed all devices when it starts and device discovery action
-                        // does not poll local devices, we should put device info of local device
-                        // manually here.
+                        // Since we removed all devices when it starts and device discovery
+                        // action does not poll local devices, we should put device info of
+                        // local device manually here.
                         for (HdmiCecLocalDevice device : mService.getAllCecLocalDevices()) {
                             mService.getHdmiCecNetwork().addCecDevice(device.getDeviceInfo());
                         }
 
-                        List<HotplugDetectionAction> hotplugActions =
-                                getActions(HotplugDetectionAction.class);
-                        if (hotplugActions.isEmpty()) {
+                        if (!hasAction(HotplugDetectionAction.class)) {
                             addAndStartAction(
-                                    new HotplugDetectionAction(HdmiCecLocalDevicePlayback.this));
+                                    new HotplugDetectionAction(
+                                            HdmiCecLocalDevicePlayback.this));
                         }
 
                         if (mService.isHdmiControlEnhancedBehaviorFlagEnabled()) {
-                            List<PowerStatusMonitorActionFromPlayback>
-                                    powerStatusMonitorActionsFromPlayback =
-                                    getActions(PowerStatusMonitorActionFromPlayback.class);
-                            if (powerStatusMonitorActionsFromPlayback.isEmpty()) {
+                            if (!hasAction(PowerStatusMonitorActionFromPlayback.class)) {
                                 addAndStartAction(
                                         new PowerStatusMonitorActionFromPlayback(
                                                 HdmiCecLocalDevicePlayback.this));
@@ -189,7 +179,7 @@
                         }
                     }
                 });
-        addAndStartAction(action);
+        addAndStartAction(action, true);
     }
 
     @Override
@@ -235,8 +225,16 @@
             invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
             return;
         }
-        removeAction(DeviceSelectActionFromPlayback.class);
-        addAndStartAction(new DeviceSelectActionFromPlayback(this, targetDevice, callback));
+        List<DeviceSelectActionFromPlayback> actions = getActions(
+                DeviceSelectActionFromPlayback.class);
+        if (!actions.isEmpty()) {
+            DeviceSelectActionFromPlayback action = actions.get(0);
+            if (action.getTargetAddress() == targetDevice.getLogicalAddress()) {
+                return;
+            }
+        }
+        addAndStartAction(new DeviceSelectActionFromPlayback(this, targetDevice, callback),
+                true);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 424102c..3d6d34b 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -220,10 +220,6 @@
         List<HdmiCecMessage> bufferedActiveSource = mDelayedMessageBuffer
                 .getBufferedMessagesWithOpcode(Constants.MESSAGE_ACTIVE_SOURCE);
         if (bufferedActiveSource.isEmpty()) {
-            if (hasAction(RequestActiveSourceAction.class)) {
-                Slog.i(TAG, "RequestActiveSourceAction is in progress. Restarting.");
-                removeAction(RequestActiveSourceAction.class);
-            }
             addAndStartAction(new RequestActiveSourceAction(this, new IHdmiControlCallback.Stub() {
                 @Override
                 public void onComplete(int result) {
@@ -231,7 +227,7 @@
                         launchRoutingControl(routingForBootup);
                     }
                 }
-            }));
+            }), true);
         } else {
             addCecDeviceForBufferedActiveSource(bufferedActiveSource.get(0));
         }
@@ -328,8 +324,15 @@
             invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
             return;
         }
-        removeAction(DeviceSelectActionFromTv.class);
-        addAndStartAction(new DeviceSelectActionFromTv(this, targetDevice, callback));
+        List<DeviceSelectActionFromTv> actions = getActions(DeviceSelectActionFromTv.class);
+        if (!actions.isEmpty()) {
+            DeviceSelectActionFromTv action = actions.get(0);
+            if (action.getTargetAddress() == targetDevice.getLogicalAddress()) {
+                return;
+            }
+        }
+        addAndStartAction(new DeviceSelectActionFromTv(this, targetDevice, callback),
+                true);
     }
 
     @ServiceThreadOnly
@@ -475,9 +478,8 @@
                 HdmiCecMessageBuilder.buildRoutingChange(
                         getDeviceInfo().getLogicalAddress(), oldPath, newPath);
         mService.sendCecCommand(routingChange);
-        removeAction(RoutingControlAction.class);
         addAndStartAction(
-                new RoutingControlAction(this, newPath, callback));
+                new RoutingControlAction(this, newPath, callback), true);
     }
 
     @ServiceThreadOnly
@@ -801,16 +803,12 @@
                         mSelectRequestBuffer.process();
                         resetSelectRequestBuffer();
 
-                        List<HotplugDetectionAction> hotplugActions
-                                = getActions(HotplugDetectionAction.class);
-                        if (hotplugActions.isEmpty()) {
+                        if (!hasAction(HotplugDetectionAction.class)) {
                             addAndStartAction(
                                     new HotplugDetectionAction(HdmiCecLocalDeviceTv.this));
                         }
 
-                        List<PowerStatusMonitorAction> powerStatusActions
-                                = getActions(PowerStatusMonitorAction.class);
-                        if (powerStatusActions.isEmpty()) {
+                        if (!hasAction(PowerStatusMonitorAction.class)) {
                             addAndStartAction(
                                     new PowerStatusMonitorAction(HdmiCecLocalDeviceTv.this));
                         }
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 35ef18b..bd8b67b9 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -17,7 +17,6 @@
 package com.android.server.hdmi;
 
 import static android.media.tv.flags.Flags.hdmiControlEnhancedBehavior;
-
 import static android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_ADD_DEVICE;
 import static android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE;
 import static android.hardware.hdmi.HdmiControlManager.EARC_FEATURE_DISABLED;
@@ -107,7 +106,6 @@
 import android.util.SparseArray;
 import android.view.Display;
 import android.view.KeyEvent;
-import android.view.WindowManager;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -1218,9 +1216,6 @@
                 audioSystem.terminateSystemAudioMode();
             }
             if (isArcEnabled) {
-                if (audioSystem.hasAction(ArcTerminationActionFromAvr.class)) {
-                    audioSystem.removeAction(ArcTerminationActionFromAvr.class);
-                }
                 audioSystem.addAndStartAction(new ArcTerminationActionFromAvr(audioSystem,
                         new IHdmiControlCallback.Stub() {
                             @Override
@@ -1228,7 +1223,7 @@
                                 mAddressAllocated = false;
                                 initializeCecLocalDevices(INITIATED_BY_SOUNDBAR_MODE);
                             }
-                        }));
+                        }), true);
             }
         }
         if (!isArcEnabled) {
diff --git a/services/core/java/com/android/server/hdmi/PowerStatusMonitorActionFromPlayback.java b/services/core/java/com/android/server/hdmi/PowerStatusMonitorActionFromPlayback.java
index 9a3cde1..d05ded5 100644
--- a/services/core/java/com/android/server/hdmi/PowerStatusMonitorActionFromPlayback.java
+++ b/services/core/java/com/android/server/hdmi/PowerStatusMonitorActionFromPlayback.java
@@ -68,6 +68,8 @@
 
     private boolean handleReportPowerStatusFromTv(HdmiCecMessage cmd) {
         int powerStatus = cmd.getParams()[0] & 0xFF;
+        mState = STATE_WAIT_FOR_NEXT_MONITORING;
+        addTimer(mState, MONITORING_INTERVAL_MS);
         if (powerStatus == POWER_STATUS_STANDBY) {
             Slog.d(TAG, "TV reported it turned off, going to sleep.");
             source().getService().standby();
diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java
index 9f785ac..93fdbc7 100644
--- a/services/core/java/com/android/server/input/InputGestureManager.java
+++ b/services/core/java/com/android/server/input/InputGestureManager.java
@@ -19,6 +19,7 @@
 import static android.hardware.input.InputGestureData.createKeyTrigger;
 
 import static com.android.hardware.input.Flags.enableTalkbackAndMagnifierKeyGestures;
+import static com.android.hardware.input.Flags.enableVoiceAccessKeyGestures;
 import static com.android.hardware.input.Flags.keyboardA11yShortcutControl;
 import static com.android.server.flags.Flags.newBugreportKeyboardShortcut;
 import static com.android.window.flags.Flags.enableMoveToNextDisplayShortcut;
@@ -240,6 +241,13 @@
                     KeyEvent.META_META_ON | KeyEvent.META_ALT_ON,
                     KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK));
         }
+        if (enableVoiceAccessKeyGestures()) {
+            systemShortcuts.add(
+                    createKeyGesture(
+                            KeyEvent.KEYCODE_V,
+                            KeyEvent.META_META_ON | KeyEvent.META_ALT_ON,
+                            KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS));
+        }
         if (enableTaskResizingKeyboardShortcuts()) {
             systemShortcuts.add(createKeyGesture(
                     KeyEvent.KEYCODE_LEFT_BRACKET,
@@ -308,6 +316,31 @@
         }
     }
 
+    @Nullable
+    public InputGestureData getInputGesture(int userId, InputGestureData.Trigger trigger) {
+        synchronized (mGestureLock) {
+            if (mBlockListedTriggers.contains(trigger)) {
+                return new InputGestureData.Builder().setTrigger(trigger).setKeyGestureType(
+                        KeyGestureEvent.KEY_GESTURE_TYPE_SYSTEM_RESERVED).build();
+            }
+            if (trigger instanceof InputGestureData.KeyTrigger keyTrigger) {
+                if (KeyEvent.isModifierKey(keyTrigger.getKeycode()) ||
+                        KeyEvent.isSystemKey(keyTrigger.getKeycode())) {
+                    return new InputGestureData.Builder().setTrigger(trigger).setKeyGestureType(
+                            KeyGestureEvent.KEY_GESTURE_TYPE_SYSTEM_RESERVED).build();
+                }
+            }
+            InputGestureData gestureData = mSystemShortcuts.get(trigger);
+            if (gestureData != null) {
+                return gestureData;
+            }
+            if (!mCustomInputGestures.contains(userId)) {
+                return null;
+            }
+            return mCustomInputGestures.get(userId).get(trigger);
+        }
+    }
+
     @InputManager.CustomInputGestureResult
     public int addCustomInputGesture(int userId, InputGestureData newGesture) {
         synchronized (mGestureLock) {
diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java
index bc44fed..4e5c720 100644
--- a/services/core/java/com/android/server/input/InputManagerInternal.java
+++ b/services/core/java/com/android/server/input/InputManagerInternal.java
@@ -104,13 +104,16 @@
     public abstract PointF getCursorPosition(int displayId);
 
     /**
-     * Enables or disables pointer acceleration for mouse movements.
+     * Set whether all pointer scaling, including linear scaling based on the
+     * user's pointer speed setting, should be enabled or disabled for mice.
      *
      * Note that this only affects pointer movements from mice (that is, pointing devices which send
      * relative motions, including trackballs and pointing sticks), not from other pointer devices
      * such as touchpads and styluses.
+     *
+     * Scaling is enabled by default on new displays until it is explicitly disabled.
      */
-    public abstract void setMousePointerAccelerationEnabled(boolean enabled, int displayId);
+    public abstract void setMouseScalingEnabled(boolean enabled, int displayId);
 
     /**
      * Sets the eligibility of windows on a given display for pointer capture. If a display is
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 559b4ae..2ba35d6 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -1382,9 +1382,9 @@
         mNative.setPointerSpeed(speed);
     }
 
-    private void setMousePointerAccelerationEnabled(boolean enabled, int displayId) {
+    private void setMouseScalingEnabled(boolean enabled, int displayId) {
         updateAdditionalDisplayInputProperties(displayId,
-                properties -> properties.mousePointerAccelerationEnabled = enabled);
+                properties -> properties.mouseScalingEnabled = enabled);
     }
 
     private void setPointerIconVisible(boolean visible, int displayId) {
@@ -2232,8 +2232,8 @@
                     pw.println("displayId: " + mAdditionalDisplayInputProperties.keyAt(i));
                     final AdditionalDisplayInputProperties properties =
                             mAdditionalDisplayInputProperties.valueAt(i);
-                    pw.println("mousePointerAccelerationEnabled: "
-                            + properties.mousePointerAccelerationEnabled);
+                    pw.println("mouseScalingEnabled: "
+                            + properties.mouseScalingEnabled);
                     pw.println("pointerIconVisible: " + properties.pointerIconVisible);
                 }
             } finally {
@@ -3061,6 +3061,16 @@
 
     @Override
     @PermissionManuallyEnforced
+    public AidlInputGestureData getInputGesture(@UserIdInt int userId,
+            @NonNull AidlInputGestureData.Trigger trigger) {
+        enforceManageKeyGesturePermission();
+
+        Objects.requireNonNull(trigger);
+        return mKeyGestureController.getInputGesture(userId, trigger);
+    }
+
+    @Override
+    @PermissionManuallyEnforced
     public int addCustomInputGesture(@UserIdInt int userId,
             @NonNull AidlInputGestureData inputGestureData) {
         enforceManageKeyGesturePermission();
@@ -3575,8 +3585,8 @@
         }
 
         @Override
-        public void setMousePointerAccelerationEnabled(boolean enabled, int displayId) {
-            InputManagerService.this.setMousePointerAccelerationEnabled(enabled, displayId);
+        public void setMouseScalingEnabled(boolean enabled, int displayId) {
+            InputManagerService.this.setMouseScalingEnabled(enabled, displayId);
         }
 
         @Override
@@ -3716,15 +3726,15 @@
     private static class AdditionalDisplayInputProperties {
 
         static final boolean DEFAULT_POINTER_ICON_VISIBLE = true;
-        static final boolean DEFAULT_MOUSE_POINTER_ACCELERATION_ENABLED = true;
+        static final boolean DEFAULT_MOUSE_SCALING_ENABLED = true;
 
         /**
-         * Whether to enable mouse pointer acceleration on this display. Note that this only affects
+         * Whether to enable mouse pointer scaling on this display. Note that this only affects
          * pointer movements from mice (that is, pointing devices which send relative motions,
          * including trackballs and pointing sticks), not from other pointer devices such as
          * touchpads and styluses.
          */
-        public boolean mousePointerAccelerationEnabled;
+        public boolean mouseScalingEnabled;
 
         // Whether the pointer icon should be visible or hidden on this display.
         public boolean pointerIconVisible;
@@ -3734,12 +3744,12 @@
         }
 
         public boolean allDefaults() {
-            return mousePointerAccelerationEnabled == DEFAULT_MOUSE_POINTER_ACCELERATION_ENABLED
+            return mouseScalingEnabled == DEFAULT_MOUSE_SCALING_ENABLED
                     && pointerIconVisible == DEFAULT_POINTER_ICON_VISIBLE;
         }
 
         public void reset() {
-            mousePointerAccelerationEnabled = DEFAULT_MOUSE_POINTER_ACCELERATION_ENABLED;
+            mouseScalingEnabled = DEFAULT_MOUSE_SCALING_ENABLED;
             pointerIconVisible = DEFAULT_POINTER_ICON_VISIBLE;
         }
     }
@@ -3754,14 +3764,14 @@
                 mAdditionalDisplayInputProperties.put(displayId, properties);
             }
             final boolean oldPointerIconVisible = properties.pointerIconVisible;
-            final boolean oldMouseAccelerationEnabled = properties.mousePointerAccelerationEnabled;
+            final boolean oldMouseScalingEnabled = properties.mouseScalingEnabled;
             updater.accept(properties);
             if (oldPointerIconVisible != properties.pointerIconVisible) {
                 mNative.setPointerIconVisibility(displayId, properties.pointerIconVisible);
             }
-            if (oldMouseAccelerationEnabled != properties.mousePointerAccelerationEnabled) {
-                mNative.setMousePointerAccelerationEnabled(displayId,
-                        properties.mousePointerAccelerationEnabled);
+            if (oldMouseScalingEnabled != properties.mouseScalingEnabled) {
+                mNative.setMouseScalingEnabled(displayId,
+                        properties.mouseScalingEnabled);
             }
             if (properties.allDefaults()) {
                 mAdditionalDisplayInputProperties.remove(displayId);
diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java
index febf24e..e25ea4b 100644
--- a/services/core/java/com/android/server/input/InputSettingsObserver.java
+++ b/services/core/java/com/android/server/input/InputSettingsObserver.java
@@ -74,6 +74,8 @@
                 Map.entry(Settings.System.getUriFor(
                                 Settings.System.MOUSE_POINTER_ACCELERATION_ENABLED),
                         (reason) -> updateMouseAccelerationEnabled()),
+                Map.entry(Settings.System.getUriFor(Settings.System.MOUSE_SCROLLING_SPEED),
+                        (reason) -> updateMouseScrollingSpeed()),
                 Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_POINTER_SPEED),
                         (reason) -> updateTouchpadPointerSpeed()),
                 Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_NATURAL_SCROLLING),
@@ -199,6 +201,11 @@
                 InputSettings.isMousePointerAccelerationEnabled(mContext));
     }
 
+    private void updateMouseScrollingSpeed() {
+        mNative.setMouseScrollingSpeed(
+                constrainPointerSpeedValue(InputSettings.getMouseScrollingSpeed(mContext)));
+    }
+
     private void updateTouchpadPointerSpeed() {
         mNative.setTouchpadPointerSpeed(
                 constrainPointerSpeedValue(InputSettings.getTouchpadPointerSpeed(mContext)));
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index 5f7ad27..fb5ce5b 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -18,6 +18,8 @@
 
 import static android.content.pm.PackageManager.FEATURE_LEANBACK;
 import static android.content.pm.PackageManager.FEATURE_WATCH;
+import static android.os.UserManager.isVisibleBackgroundUsersEnabled;
+import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE;
 
 import static com.android.hardware.input.Flags.enableNew25q2Keycodes;
@@ -55,7 +57,6 @@
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.view.Display;
 import android.view.InputDevice;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
@@ -64,6 +65,8 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.policy.IShortcutService;
+import com.android.server.LocalServices;
+import com.android.server.pm.UserManagerInternal;
 import com.android.server.policy.KeyCombinationManager;
 
 import java.util.ArrayDeque;
@@ -159,6 +162,10 @@
     /** Currently fully consumed key codes per device */
     private final SparseArray<Set<Integer>> mConsumedKeysForDevice = new SparseArray<>();
 
+    private final UserManagerInternal mUserManagerInternal;
+
+    private final boolean mVisibleBackgroundUsersEnabled = isVisibleBackgroundUsersEnabled();
+
     KeyGestureController(Context context, Looper looper, InputDataStore inputDataStore) {
         mContext = context;
         mHandler = new Handler(looper, this::handleMessage);
@@ -180,6 +187,7 @@
         mAppLaunchShortcutManager = new AppLaunchShortcutManager(mContext);
         mInputGestureManager = new InputGestureManager(mContext);
         mInputDataStore = inputDataStore;
+        mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
         initBehaviors();
         initKeyCombinationRules();
     }
@@ -449,6 +457,9 @@
     }
 
     public boolean interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
+        if (mVisibleBackgroundUsersEnabled && shouldIgnoreKeyEventForVisibleBackgroundUser(event)) {
+            return false;
+        }
         final boolean interactive = (policyFlags & FLAG_INTERACTIVE) != 0;
         if (InputSettings.doesKeyGestureEventHandlerSupportMultiKeyGestures()
                 && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
@@ -457,6 +468,24 @@
         return false;
     }
 
+    private boolean shouldIgnoreKeyEventForVisibleBackgroundUser(KeyEvent event) {
+        final int displayAssignedUserId = mUserManagerInternal.getUserAssignedToDisplay(
+                event.getDisplayId());
+        final int currentUserId;
+        synchronized (mUserLock) {
+            currentUserId = mCurrentUserId;
+        }
+        if (currentUserId != displayAssignedUserId
+                && !KeyEvent.isVisibleBackgroundUserAllowedKey(event.getKeyCode())) {
+            if (DEBUG) {
+                Slog.w(TAG, "Ignored key event [" + event + "] for visible background user ["
+                        + displayAssignedUserId + "]");
+            }
+            return true;
+        }
+        return false;
+    }
+
     public long interceptKeyBeforeDispatching(IBinder focusedToken, KeyEvent event,
             int policyFlags) {
         // TODO(b/358569822): Handle shortcuts trigger logic here and pass it to appropriate
@@ -895,7 +924,7 @@
     private void handleMultiKeyGesture(int[] keycodes,
             @KeyGestureEvent.KeyGestureType int gestureType, int action, int flags) {
         handleKeyGesture(KeyCharacterMap.VIRTUAL_KEYBOARD, keycodes, /* modifierState= */0,
-                gestureType, action, Display.DEFAULT_DISPLAY, /* focusedToken = */null, flags,
+                gestureType, action, DEFAULT_DISPLAY, /* focusedToken = */null, flags,
                 /* appLaunchData = */null);
     }
 
@@ -903,7 +932,7 @@
             @Nullable AppLaunchData appLaunchData) {
         handleKeyGesture(KeyCharacterMap.VIRTUAL_KEYBOARD, new int[0], /* modifierState= */0,
                 keyGestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE,
-                Display.DEFAULT_DISPLAY, /* focusedToken = */null, /* flags = */0, appLaunchData);
+                DEFAULT_DISPLAY, /* focusedToken = */null, /* flags = */0, appLaunchData);
     }
 
     @VisibleForTesting
@@ -915,6 +944,11 @@
     }
 
     private boolean handleKeyGesture(AidlKeyGestureEvent event, @Nullable IBinder focusedToken) {
+        if (mVisibleBackgroundUsersEnabled && event.displayId != DEFAULT_DISPLAY
+                && shouldIgnoreGestureEventForVisibleBackgroundUser(event.gestureType,
+                event.displayId)) {
+            return false;
+        }
         synchronized (mKeyGestureHandlerRecords) {
             for (KeyGestureHandlerRecord handler : mKeyGestureHandlerRecords.values()) {
                 if (handler.handleKeyGesture(event, focusedToken)) {
@@ -927,6 +961,24 @@
         return false;
     }
 
+    private boolean shouldIgnoreGestureEventForVisibleBackgroundUser(
+            @KeyGestureEvent.KeyGestureType int gestureType, int displayId) {
+        final int displayAssignedUserId = mUserManagerInternal.getUserAssignedToDisplay(displayId);
+        final int currentUserId;
+        synchronized (mUserLock) {
+            currentUserId = mCurrentUserId;
+        }
+        if (currentUserId != displayAssignedUserId
+                && !KeyGestureEvent.isVisibleBackgrounduserAllowedGesture(gestureType)) {
+            if (DEBUG) {
+                Slog.w(TAG, "Ignored gesture event [" + gestureType
+                        + "] for visible background user [" + displayAssignedUserId + "]");
+            }
+            return true;
+        }
+        return false;
+    }
+
     private boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) {
         synchronized (mKeyGestureHandlerRecords) {
             for (KeyGestureHandlerRecord handler : mKeyGestureHandlerRecords.values()) {
@@ -943,7 +995,7 @@
         // TODO(b/358569822): Once we move the gesture detection logic to IMS, we ideally
         //  should not rely on PWM to tell us about the gesture start and end.
         AidlKeyGestureEvent event = createKeyGestureEvent(deviceId, keycodes, modifierState,
-                gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, Display.DEFAULT_DISPLAY,
+                gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, DEFAULT_DISPLAY,
                 /* flags = */0, /* appLaunchData = */null);
         mHandler.obtainMessage(MSG_NOTIFY_KEY_GESTURE_EVENT, event).sendToTarget();
     }
@@ -951,7 +1003,7 @@
     public void handleKeyGesture(int deviceId, int[] keycodes, int modifierState,
             @KeyGestureEvent.KeyGestureType int gestureType) {
         AidlKeyGestureEvent event = createKeyGestureEvent(deviceId, keycodes, modifierState,
-                gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, Display.DEFAULT_DISPLAY,
+                gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, DEFAULT_DISPLAY,
                 /* flags = */0, /* appLaunchData = */null);
         handleKeyGesture(event, null /*focusedToken*/);
     }
@@ -1069,6 +1121,18 @@
     }
 
     @BinderThread
+    @Nullable
+    public AidlInputGestureData getInputGesture(@UserIdInt int userId,
+            @NonNull AidlInputGestureData.Trigger trigger) {
+        InputGestureData gestureData = mInputGestureManager.getInputGesture(userId,
+                InputGestureData.createTriggerFromAidlTrigger(trigger));
+        if (gestureData == null) {
+            return null;
+        }
+        return gestureData.getAidlData();
+    }
+
+    @BinderThread
     @InputManager.CustomInputGestureResult
     public int addCustomInputGesture(@UserIdInt int userId,
             @NonNull AidlInputGestureData inputGestureData) {
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index 7dbde64..4d38c84 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -130,12 +130,14 @@
 
     void setPointerSpeed(int speed);
 
-    void setMousePointerAccelerationEnabled(int displayId, boolean enabled);
+    void setMouseScalingEnabled(int displayId, boolean enabled);
 
     void setMouseReverseVerticalScrollingEnabled(boolean enabled);
 
     void setMouseScrollingAccelerationEnabled(boolean enabled);
 
+    void setMouseScrollingSpeed(int speed);
+
     void setMouseSwapPrimaryButtonEnabled(boolean enabled);
 
     void setMouseAccelerationEnabled(boolean enabled);
@@ -419,7 +421,7 @@
         public native void setPointerSpeed(int speed);
 
         @Override
-        public native void setMousePointerAccelerationEnabled(int displayId, boolean enabled);
+        public native void setMouseScalingEnabled(int displayId, boolean enabled);
 
         @Override
         public native void setMouseReverseVerticalScrollingEnabled(boolean enabled);
@@ -428,6 +430,9 @@
         public native void setMouseScrollingAccelerationEnabled(boolean enabled);
 
         @Override
+        public native void setMouseScrollingSpeed(int speed);
+
+        @Override
         public native void setMouseSwapPrimaryButtonEnabled(boolean enabled);
 
         @Override
diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
index b0dff22..281db0a 100644
--- a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
+++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
@@ -387,8 +387,9 @@
     @GuardedBy("ImfLock.class")
     void setWindowState(IBinder windowToken, @NonNull ImeTargetWindowState newState) {
         final ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken);
-        if (state != null && newState.hasEditorFocused()
-                && newState.mToolType != MotionEvent.TOOL_TYPE_STYLUS) {
+        if (state != null && newState.hasEditorFocused() && (
+                newState.mToolType != MotionEvent.TOOL_TYPE_STYLUS
+                        || Flags.refactorInsetsController())) {
             // Inherit the last requested IME visible state when the target window is still
             // focused with an editor.
             newState.setRequestedImeVisible(state.mRequestedImeVisible);
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
index 87d809b..1e54bee 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
@@ -32,7 +32,10 @@
 import android.hardware.location.IContextHubTransactionCallback;
 import android.os.Binder;
 import android.os.IBinder;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
 import android.os.RemoteException;
+import android.os.WorkSource;
 import android.util.Log;
 import android.util.SparseArray;
 
@@ -54,6 +57,16 @@
     /** Message used by noteOp when this client receives a message from an endpoint. */
     private static final String RECEIVE_MSG_NOTE = "ContextHubEndpointMessageDelivery";
 
+    /** The duration of wakelocks acquired during HAL callbacks */
+    private static final long WAKELOCK_TIMEOUT_MILLIS = 5 * 1000;
+
+    /*
+     * Internal interface used to invoke client callbacks.
+     */
+    interface CallbackConsumer {
+        void accept(IContextHubEndpointCallback callback) throws RemoteException;
+    }
+
     /** The context of the service. */
     private final Context mContext;
 
@@ -134,6 +147,9 @@
 
     private final int mUid;
 
+    /** Wakelock held while nanoapp message are in flight to the client */
+    private final WakeLock mWakeLock;
+
     /* package */ ContextHubEndpointBroker(
             Context context,
             IEndpointCommunication hubInterface,
@@ -158,6 +174,11 @@
 
         mAppOpsManager = context.getSystemService(AppOpsManager.class);
         mAppOpsManager.startWatchingMode(AppOpsManager.OP_NONE, mPackageName, this);
+
+        PowerManager powerManager = context.getSystemService(PowerManager.class);
+        mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
+        mWakeLock.setWorkSource(new WorkSource(mUid, mPackageName));
+        mWakeLock.setReferenceCounted(true);
     }
 
     @Override
@@ -227,6 +248,7 @@
             }
         }
         mEndpointManager.unregisterEndpoint(mEndpointInfo.getIdentifier().getEndpoint());
+        releaseWakeLockOnExit();
     }
 
     @Override
@@ -302,6 +324,13 @@
         }
     }
 
+    @Override
+    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+    public void onCallbackFinished() {
+        super.onCallbackFinished_enforcePermission();
+        releaseWakeLock();
+    }
+
     /** Invoked when the underlying binder of this broker has died at the client process. */
     @Override
     public void binderDied() {
@@ -357,15 +386,13 @@
             mSessionInfoMap.put(sessionId, new SessionInfo(initiator, true));
         }
 
-        if (mContextHubEndpointCallback != null) {
-            try {
-                mContextHubEndpointCallback.onSessionOpenRequest(
-                        sessionId, initiator, serviceDescriptor);
-            } catch (RemoteException e) {
-                Log.e(TAG, "RemoteException while calling onSessionOpenRequest", e);
-                cleanupSessionResources(sessionId);
-                return;
-            }
+        boolean success =
+                invokeCallback(
+                        (consumer) ->
+                                consumer.onSessionOpenRequest(
+                                        sessionId, initiator, serviceDescriptor));
+        if (!success) {
+            cleanupSessionResources(sessionId);
         }
     }
 
@@ -374,14 +401,11 @@
             Log.w(TAG, "Unknown session ID in onCloseEndpointSession: id=" + sessionId);
             return;
         }
-        if (mContextHubEndpointCallback != null) {
-            try {
-                mContextHubEndpointCallback.onSessionClosed(
-                        sessionId, ContextHubServiceUtil.toAppHubEndpointReason(reason));
-            } catch (RemoteException e) {
-                Log.e(TAG, "RemoteException while calling onSessionClosed", e);
-            }
-        }
+
+        invokeCallback(
+                (consumer) ->
+                        consumer.onSessionClosed(
+                                sessionId, ContextHubServiceUtil.toAppHubEndpointReason(reason)));
     }
 
     /* package */ void onEndpointSessionOpenComplete(int sessionId) {
@@ -392,16 +416,30 @@
             }
             mSessionInfoMap.get(sessionId).setSessionState(SessionInfo.SessionState.ACTIVE);
         }
-        if (mContextHubEndpointCallback != null) {
-            try {
-                mContextHubEndpointCallback.onSessionOpenComplete(sessionId);
-            } catch (RemoteException e) {
-                Log.e(TAG, "RemoteException while calling onSessionClosed", e);
-            }
-        }
+
+        invokeCallback((consumer) -> consumer.onSessionOpenComplete(sessionId));
     }
 
     /* package */ void onMessageReceived(int sessionId, HubMessage message) {
+        byte code = onMessageReceivedInternal(sessionId, message);
+        if (code != ErrorCode.OK && message.isResponseRequired()) {
+            sendMessageDeliveryStatus(
+                    sessionId, message.getMessageSequenceNumber(), code);
+        }
+    }
+
+    /* package */ void onMessageDeliveryStatusReceived(
+            int sessionId, int sequenceNumber, byte errorCode) {
+        mTransactionManager.onMessageDeliveryResponse(sequenceNumber, errorCode == ErrorCode.OK);
+    }
+
+    /* package */ boolean hasSessionId(int sessionId) {
+        synchronized (mOpenSessionLock) {
+            return mSessionInfoMap.contains(sessionId);
+        }
+    }
+
+    private byte onMessageReceivedInternal(int sessionId, HubMessage message) {
         HubEndpointInfo remote;
         synchronized (mOpenSessionLock) {
             if (!isSessionActive(sessionId)) {
@@ -411,9 +449,7 @@
                                 + sessionId
                                 + ") with message: "
                                 + message);
-                sendMessageDeliveryStatus(
-                        sessionId, message.getMessageSequenceNumber(), ErrorCode.PERMANENT_ERROR);
-                return;
+                return ErrorCode.PERMANENT_ERROR;
             }
             remote = mSessionInfoMap.get(sessionId).getRemoteEndpointInfo();
         }
@@ -435,31 +471,12 @@
                             + ". "
                             + mPackageName
                             + " doesn't have permission");
-            sendMessageDeliveryStatus(
-                    sessionId, message.getMessageSequenceNumber(), ErrorCode.PERMISSION_DENIED);
-            return;
+            return ErrorCode.PERMISSION_DENIED;
         }
 
-        if (mContextHubEndpointCallback != null) {
-            try {
-                mContextHubEndpointCallback.onMessageReceived(sessionId, message);
-            } catch (RemoteException e) {
-                Log.e(TAG, "RemoteException while calling onMessageReceived", e);
-                sendMessageDeliveryStatus(
-                        sessionId, message.getMessageSequenceNumber(), ErrorCode.TRANSIENT_ERROR);
-            }
-        }
-    }
-
-    /* package */ void onMessageDeliveryStatusReceived(
-            int sessionId, int sequenceNumber, byte errorCode) {
-        mTransactionManager.onMessageDeliveryResponse(sequenceNumber, errorCode == ErrorCode.OK);
-    }
-
-    /* package */ boolean hasSessionId(int sessionId) {
-        synchronized (mOpenSessionLock) {
-            return mSessionInfoMap.contains(sessionId);
-        }
+        boolean success =
+                invokeCallback((consumer) -> consumer.onMessageReceived(sessionId, message));
+        return success ? ErrorCode.OK : ErrorCode.TRANSIENT_ERROR;
     }
 
     /**
@@ -520,4 +537,63 @@
         Collection<String> requiredPermissions = targetEndpointInfo.getRequiredPermissions();
         return ContextHubServiceUtil.hasPermissions(mContext, mPid, mUid, requiredPermissions);
     }
+
+    private void acquireWakeLock() {
+        Binder.withCleanCallingIdentity(
+                () -> {
+                    if (mIsRegistered.get()) {
+                        mWakeLock.acquire(WAKELOCK_TIMEOUT_MILLIS);
+                    }
+                });
+    }
+
+    private void releaseWakeLock() {
+        Binder.withCleanCallingIdentity(
+                () -> {
+                    if (mWakeLock.isHeld()) {
+                        try {
+                            mWakeLock.release();
+                        } catch (RuntimeException e) {
+                            Log.e(TAG, "Releasing the wakelock fails - ", e);
+                        }
+                    }
+                });
+    }
+
+    private void releaseWakeLockOnExit() {
+        Binder.withCleanCallingIdentity(
+                () -> {
+                    while (mWakeLock.isHeld()) {
+                        try {
+                            mWakeLock.release();
+                        } catch (RuntimeException e) {
+                            Log.e(
+                                    TAG,
+                                    "Releasing the wakelock for all acquisitions fails - ",
+                                    e);
+                            break;
+                        }
+                    }
+                });
+    }
+
+    /**
+     * Invokes a callback and acquires a wakelock.
+     *
+     * @param consumer The callback invoke
+     * @return false if the callback threw a RemoteException
+     */
+    private boolean invokeCallback(CallbackConsumer consumer) {
+        if (mContextHubEndpointCallback != null) {
+            acquireWakeLock();
+            try {
+                consumer.accept(mContextHubEndpointCallback);
+            } catch (RemoteException e) {
+                Log.e(TAG, "RemoteException while calling endpoint callback", e);
+                releaseWakeLock();
+                return false;
+            }
+        }
+        return true;
+    }
 }
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java
index 7698a87..740c4f1 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java
@@ -36,6 +36,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
 
 /**
  * A class that manages registration/unregistration of clients and manages messages to/from clients.
@@ -312,15 +313,9 @@
 
     @Override
     public void onCloseEndpointSession(int sessionId, byte reason) {
-        boolean callbackInvoked = false;
-        for (ContextHubEndpointBroker broker : mEndpointMap.values()) {
-            if (broker.hasSessionId(sessionId)) {
-                broker.onCloseEndpointSession(sessionId, reason);
-                callbackInvoked = true;
-                break;
-            }
-        }
-
+        boolean callbackInvoked =
+                invokeCallbackForMatchingSession(
+                        sessionId, (broker) -> broker.onCloseEndpointSession(sessionId, reason));
         if (!callbackInvoked) {
             Log.w(TAG, "onCloseEndpointSession: unknown session ID " + sessionId);
         }
@@ -328,15 +323,9 @@
 
     @Override
     public void onEndpointSessionOpenComplete(int sessionId) {
-        boolean callbackInvoked = false;
-        for (ContextHubEndpointBroker broker : mEndpointMap.values()) {
-            if (broker.hasSessionId(sessionId)) {
-                broker.onEndpointSessionOpenComplete(sessionId);
-                callbackInvoked = true;
-                break;
-            }
-        }
-
+        boolean callbackInvoked =
+                invokeCallbackForMatchingSession(
+                        sessionId, (broker) -> broker.onEndpointSessionOpenComplete(sessionId));
         if (!callbackInvoked) {
             Log.w(TAG, "onEndpointSessionOpenComplete: unknown session ID " + sessionId);
         }
@@ -344,15 +333,9 @@
 
     @Override
     public void onMessageReceived(int sessionId, HubMessage message) {
-        boolean callbackInvoked = false;
-        for (ContextHubEndpointBroker broker : mEndpointMap.values()) {
-            if (broker.hasSessionId(sessionId)) {
-                broker.onMessageReceived(sessionId, message);
-                callbackInvoked = true;
-                break;
-            }
-        }
-
+        boolean callbackInvoked =
+                invokeCallbackForMatchingSession(
+                        sessionId, (broker) -> broker.onMessageReceived(sessionId, message));
         if (!callbackInvoked) {
             Log.w(TAG, "onMessageReceived: unknown session ID " + sessionId);
         }
@@ -360,20 +343,38 @@
 
     @Override
     public void onMessageDeliveryStatusReceived(int sessionId, int sequenceNumber, byte errorCode) {
-        boolean callbackInvoked = false;
-        for (ContextHubEndpointBroker broker : mEndpointMap.values()) {
-            if (broker.hasSessionId(sessionId)) {
-                broker.onMessageDeliveryStatusReceived(sessionId, sequenceNumber, errorCode);
-                callbackInvoked = true;
-                break;
-            }
-        }
-
+        boolean callbackInvoked =
+                invokeCallbackForMatchingSession(
+                        sessionId,
+                        (broker) ->
+                                broker.onMessageDeliveryStatusReceived(
+                                        sessionId, sequenceNumber, errorCode));
         if (!callbackInvoked) {
             Log.w(TAG, "onMessageDeliveryStatusReceived: unknown session ID " + sessionId);
         }
     }
 
+    /**
+     * Invokes a callback for a session with matching ID.
+     *
+     * @param callback The callback to execute
+     * @return true if a callback was executed
+     */
+    private boolean invokeCallbackForMatchingSession(
+            int sessionId, Consumer<ContextHubEndpointBroker> callback) {
+        for (ContextHubEndpointBroker broker : mEndpointMap.values()) {
+            if (broker.hasSessionId(sessionId)) {
+                try {
+                    callback.accept(broker);
+                } catch (RuntimeException e) {
+                    Log.e(TAG, "Exception while invoking callback", e);
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
     /** Unregister the hub (called during init() failure). Silence errors. */
     private void unregisterHub() {
         try {
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 54ed5a9..2615a76 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -756,9 +756,7 @@
     @Override
     public List<HubInfo> getHubs() throws RemoteException {
         super.getHubs_enforcePermission();
-        if (mHubInfoRegistry == null) {
-            return Collections.emptyList();
-        }
+        checkHubDiscoveryPreconditions();
         return mHubInfoRegistry.getHubs();
     }
 
@@ -766,9 +764,7 @@
     @Override
     public List<HubEndpointInfo> findEndpoints(long endpointId) {
         super.findEndpoints_enforcePermission();
-        if (mHubInfoRegistry == null) {
-            return Collections.emptyList();
-        }
+        checkEndpointDiscoveryPreconditions();
         return mHubInfoRegistry.findEndpoints(endpointId);
     }
 
@@ -776,9 +772,7 @@
     @Override
     public List<HubEndpointInfo> findEndpointsWithService(String serviceDescriptor) {
         super.findEndpointsWithService_enforcePermission();
-        if (mHubInfoRegistry == null) {
-            return Collections.emptyList();
-        }
+        checkEndpointDiscoveryPreconditions();
         return mHubInfoRegistry.findEndpointsWithService(serviceDescriptor);
     }
 
@@ -834,6 +828,13 @@
         }
     }
 
+    private void checkHubDiscoveryPreconditions() {
+        if (mHubInfoRegistry == null) {
+            Log.e(TAG, "Hub registry failed to initialize");
+            throw new UnsupportedOperationException("Hub discovery is not supported");
+        }
+    }
+
     /**
      * Creates an internal load transaction callback to be used for old API clients
      *
diff --git a/services/core/java/com/android/server/location/fudger/LocationFudger.java b/services/core/java/com/android/server/location/fudger/LocationFudger.java
index 2757764..28e21b7 100644
--- a/services/core/java/com/android/server/location/fudger/LocationFudger.java
+++ b/services/core/java/com/android/server/location/fudger/LocationFudger.java
@@ -302,6 +302,15 @@
 
     // requires latitude since longitudinal distances change with distance from equator.
     private static double metersToDegreesLongitude(double distance, double lat) {
-        return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR / Math.cos(Math.toRadians(lat));
+        // Needed to convert from longitude distance to longitude degree.
+        // X meters near the poles is more degrees than at the equator.
+        double cosLat = Math.cos(Math.toRadians(lat));
+        // If we are right on top of the pole, the degree is always 0.
+        // We return a very small value instead to avoid divide by zero errors
+        // later on.
+        if (cosLat == 0.0) {
+            return 0.0001;
+        }
+        return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR / cosLat;
     }
 }
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 3f91575..0d0cdd8 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -438,9 +438,9 @@
         }
         LockscreenCredential credential =
                 LockscreenCredential.createUnifiedProfilePassword(newPassword);
-        Arrays.fill(newPasswordChars, '\u0000');
-        Arrays.fill(newPassword, (byte) 0);
-        Arrays.fill(randomLockSeed, (byte) 0);
+        LockPatternUtils.zeroize(newPasswordChars);
+        LockPatternUtils.zeroize(newPassword);
+        LockPatternUtils.zeroize(randomLockSeed);
         return credential;
     }
 
@@ -451,6 +451,7 @@
      * @param profileUserId  profile user Id
      * @param profileUserPassword  profile original password (when it has separated lock).
      */
+    @GuardedBy("mSpManager")
     private void tieProfileLockIfNecessary(int profileUserId,
             LockscreenCredential profileUserPassword) {
         // Only for profiles that shares credential with parent
@@ -909,14 +910,8 @@
                 // Hide notification first, as tie profile lock takes time
                 hideEncryptionNotification(new UserHandle(userId));
 
-                if (android.app.admin.flags.Flags.fixRaceConditionInTieProfileLock()) {
-                    synchronized (mSpManager) {
-                        tieProfileLockIfNecessary(userId, LockscreenCredential.createNone());
-                    }
-                } else {
-                    if (isCredentialSharableWithParent(userId)) {
-                        tieProfileLockIfNecessary(userId, LockscreenCredential.createNone());
-                    }
+                synchronized (mSpManager) {
+                    tieProfileLockIfNecessary(userId, LockscreenCredential.createNone());
                 }
             }
         });
@@ -1380,11 +1375,7 @@
                 mStorage.removeChildProfileLock(userId);
                 removeKeystoreProfileKey(userId);
             } else {
-                if (android.app.admin.flags.Flags.fixRaceConditionInTieProfileLock()) {
-                    synchronized (mSpManager) {
-                        tieProfileLockIfNecessary(userId, profileUserPassword);
-                    }
-                } else {
+                synchronized (mSpManager) {
                     tieProfileLockIfNecessary(userId, profileUserPassword);
                 }
             }
@@ -1546,7 +1537,7 @@
                         + userId);
             }
         } finally {
-            Arrays.fill(password, (byte) 0);
+            LockPatternUtils.zeroize(password);
         }
     }
 
@@ -1579,7 +1570,7 @@
         decryptionResult = cipher.doFinal(encryptedPassword);
         LockscreenCredential credential = LockscreenCredential.createUnifiedProfilePassword(
                 decryptionResult);
-        Arrays.fill(decryptionResult, (byte) 0);
+        LockPatternUtils.zeroize(decryptionResult);
         try {
             long parentSid = getGateKeeperService().getSecureUserId(
                     mUserManager.getProfileParent(userId).id);
@@ -2272,7 +2263,7 @@
         } catch (RemoteException e) {
             Slogf.wtf(TAG, e, "Failed to unlock CE storage for %s user %d", userType, userId);
         } finally {
-            Arrays.fill(secret, (byte) 0);
+            LockPatternUtils.zeroize(secret);
         }
     }
 
diff --git a/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java b/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java
index 21caf76..3d64f18 100644
--- a/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java
+++ b/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java
@@ -26,6 +26,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
+import com.android.internal.widget.LockPatternUtils;
 import com.android.internal.widget.LockscreenCredential;
 
 import java.security.GeneralSecurityException;
@@ -154,7 +155,7 @@
             }
             LockscreenCredential result =
                     LockscreenCredential.createUnifiedProfilePassword(credential);
-            Arrays.fill(credential, (byte) 0);
+            LockPatternUtils.zeroize(credential);
             return result;
         }
     }
@@ -175,7 +176,7 @@
                 Slog.d(TAG, "Cannot delete key", e);
             }
             if (mEncryptedPasswords.contains(userId)) {
-                Arrays.fill(mEncryptedPasswords.get(userId), (byte) 0);
+                LockPatternUtils.zeroize(mEncryptedPasswords.get(userId));
                 mEncryptedPasswords.remove(userId);
             }
         }
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java
index bf1b3c3..85dc811 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java
@@ -162,7 +162,7 @@
             Log.e(TAG, "Unexpected exception thrown during KeySyncTask", e);
         } finally {
             if (mCredential != null) {
-                Arrays.fill(mCredential, (byte) 0); // no longer needed.
+                LockPatternUtils.zeroize(mCredential); // no longer needed.
             }
         }
     }
@@ -506,7 +506,7 @@
 
         try {
             byte[] hash = MessageDigest.getInstance(LOCK_SCREEN_HASH_ALGORITHM).digest(bytes);
-            Arrays.fill(bytes, (byte) 0);
+            LockPatternUtils.zeroize(bytes);
             return hash;
         } catch (NoSuchAlgorithmException e) {
             // Impossible, SHA-256 must be supported on Android.
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
index 54303c0..7d8300a 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
@@ -1082,7 +1082,7 @@
             int keyguardCredentialsType = lockPatternUtilsToKeyguardType(savedCredentialType);
             try (LockscreenCredential credential =
                     createLockscreenCredential(keyguardCredentialsType, decryptedCredentials)) {
-                Arrays.fill(decryptedCredentials, (byte) 0);
+                LockPatternUtils.zeroize(decryptedCredentials);
                 decryptedCredentials = null;
                 VerifyCredentialResponse verifyResponse =
                         lockSettingsService.verifyCredential(credential, userId, 0);
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java
index 0e66746..f1ef333 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java
@@ -19,8 +19,9 @@
 import android.annotation.Nullable;
 import android.util.SparseArray;
 
+import com.android.internal.widget.LockPatternUtils;
+
 import java.util.ArrayList;
-import java.util.Arrays;
 
 import javax.security.auth.Destroyable;
 
@@ -187,8 +188,8 @@
          */
         @Override
         public void destroy() {
-            Arrays.fill(mLskfHash, (byte) 0);
-            Arrays.fill(mKeyClaimant, (byte) 0);
+            LockPatternUtils.zeroize(mLskfHash);
+            LockPatternUtils.zeroize(mKeyClaimant);
         }
     }
 }
diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java
index 68e195d..35bb199 100644
--- a/services/core/java/com/android/server/media/MediaRouterService.java
+++ b/services/core/java/com/android/server/media/MediaRouterService.java
@@ -302,7 +302,9 @@
 
         final long token = Binder.clearCallingIdentity();
         try {
-            mAudioService.setBluetoothA2dpOn(on);
+            if (!Flags.disableSetBluetoothAd2pOnCalls()) {
+                mAudioService.setBluetoothA2dpOn(on);
+            }
         } catch (RemoteException ex) {
             Slog.w(TAG, "RemoteException while calling setBluetoothA2dpOn. on=" + on);
         } finally {
@@ -677,7 +679,9 @@
                 if (DEBUG) {
                     Slog.d(TAG, "restoreBluetoothA2dp(" + a2dpOn + ")");
                 }
-                mAudioService.setBluetoothA2dpOn(a2dpOn);
+                if (!Flags.disableSetBluetoothAd2pOnCalls()) {
+                    mAudioService.setBluetoothA2dpOn(a2dpOn);
+                }
             }
         } catch (RemoteException e) {
             Slog.w(TAG, "RemoteException while calling setBluetoothA2dpOn.");
diff --git a/services/core/java/com/android/server/media/TEST_MAPPING b/services/core/java/com/android/server/media/TEST_MAPPING
index 43e2afd..dbf9915 100644
--- a/services/core/java/com/android/server/media/TEST_MAPPING
+++ b/services/core/java/com/android/server/media/TEST_MAPPING
@@ -1,7 +1,10 @@
 {
   "presubmit": [
     {
-      "name": "CtsMediaBetterTogetherTestCases"
+      "name": "CtsMediaRouterTestCases"
+    },
+    {
+      "name": "CtsMediaSessionTestCases"
     },
     {
       "name": "MediaRouterServiceTests"
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java
index 34bb415..d440d3a 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityService.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java
@@ -18,8 +18,10 @@
 
 import android.content.ContentValues;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
+import android.hardware.tv.mediaquality.IMediaQuality;
 import android.media.quality.AmbientBacklightSettings;
 import android.media.quality.IAmbientBacklightCallback;
 import android.media.quality.IMediaQualityManager;
@@ -34,20 +36,30 @@
 import android.media.quality.SoundProfileHandle;
 import android.os.Binder;
 import android.os.Bundle;
+import android.os.IBinder;
 import android.os.PersistableBundle;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.os.UserHandle;
 import android.util.Log;
+import android.util.Pair;
+import android.util.Slog;
+import android.util.SparseArray;
 
 import com.android.server.SystemService;
+import com.android.server.utils.Slogf;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.UUID;
 import java.util.stream.Collectors;
 
@@ -64,10 +76,14 @@
     private final MediaQualityDbHelper mMediaQualityDbHelper;
     private final BiMap<Long, String> mPictureProfileTempIdMap;
     private final BiMap<Long, String> mSoundProfileTempIdMap;
+    private final PackageManager mPackageManager;
+    private final SparseArray<UserState> mUserStates = new SparseArray<>();
+    private IMediaQuality mMediaQuality;
 
     public MediaQualityService(Context context) {
         super(context);
         mContext = context;
+        mPackageManager = mContext.getPackageManager();
         mPictureProfileTempIdMap = new BiMap<>();
         mSoundProfileTempIdMap = new BiMap<>();
         mMediaQualityDbHelper = new MediaQualityDbHelper(mContext);
@@ -77,6 +93,12 @@
 
     @Override
     public void onStart() {
+        IBinder binder = ServiceManager.getService(IMediaQuality.DESCRIPTOR + "/default");
+        if (binder != null) {
+            Slogf.d(TAG, "binder is not null");
+            mMediaQuality = IMediaQuality.Stub.asInterface(binder);
+        }
+
         publishBinderService(Context.MEDIA_QUALITY_SERVICE, new BinderService());
     }
 
@@ -85,12 +107,20 @@
 
         @Override
         public PictureProfile createPictureProfile(PictureProfile pp, UserHandle user) {
+            if ((pp.getPackageName() != null && !pp.getPackageName().isEmpty()
+                    && !incomingPackageEqualsCallingUidPackage(pp.getPackageName()))
+                    && !hasGlobalPictureQualityServicePermission()) {
+                notifyError(null, PictureProfile.ERROR_NO_PERMISSION,
+                        Binder.getCallingUid(), Binder.getCallingPid());
+            }
+
             SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase();
 
             ContentValues values = getContentValues(null,
                     pp.getProfileType(),
                     pp.getName(),
-                    pp.getPackageName(),
+                    pp.getPackageName() == null || pp.getPackageName().isEmpty()
+                            ? getPackageOfCallingUid() : pp.getPackageName(),
                     pp.getInputId(),
                     pp.getParameters());
 
@@ -104,9 +134,13 @@
 
         @Override
         public void updatePictureProfile(String id, PictureProfile pp, UserHandle user) {
-            Long intId = mPictureProfileTempIdMap.getKey(id);
+            Long dbId = mPictureProfileTempIdMap.getKey(id);
+            if (!hasPermissionToUpdatePictureProfile(dbId, pp)) {
+                notifyError(id, PictureProfile.ERROR_NO_PERMISSION,
+                        Binder.getCallingUid(), Binder.getCallingPid());
+            }
 
-            ContentValues values = getContentValues(intId,
+            ContentValues values = getContentValues(dbId,
                     pp.getProfileType(),
                     pp.getName(),
                     pp.getPackageName(),
@@ -118,27 +152,51 @@
                     null, values);
         }
 
+        private boolean hasPermissionToUpdatePictureProfile(Long dbId, PictureProfile toUpdate) {
+            PictureProfile fromDb = getPictureProfile(dbId);
+            return fromDb.getProfileType() == toUpdate.getProfileType()
+                    && fromDb.getPackageName().equals(toUpdate.getPackageName())
+                    && fromDb.getName().equals(toUpdate.getName())
+                    && fromDb.getName().equals(getPackageOfCallingUid());
+        }
+
         @Override
         public void removePictureProfile(String id, UserHandle user) {
-            Long intId = mPictureProfileTempIdMap.getKey(id);
-            if (intId != null) {
+            Long dbId = mPictureProfileTempIdMap.getKey(id);
+
+            if (!hasPermissionToRemovePictureProfile(dbId)) {
+                notifyError(id, PictureProfile.ERROR_NO_PERMISSION,
+                        Binder.getCallingUid(), Binder.getCallingPid());
+            }
+
+            if (dbId != null) {
                 SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase();
                 String selection = BaseParameters.PARAMETER_ID + " = ?";
-                String[] selectionArgs = {Long.toString(intId)};
-                db.delete(mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, selection,
+                String[] selectionArgs = {Long.toString(dbId)};
+                int result = db.delete(mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, selection,
                         selectionArgs);
-                mPictureProfileTempIdMap.remove(intId);
+                if (result == 0) {
+                    notifyError(id, PictureProfile.ERROR_INVALID_ARGUMENT,
+                            Binder.getCallingUid(), Binder.getCallingPid());
+                }
+                mPictureProfileTempIdMap.remove(dbId);
             }
         }
 
+        private boolean hasPermissionToRemovePictureProfile(Long dbId) {
+            PictureProfile fromDb = getPictureProfile(dbId);
+            return fromDb.getName().equalsIgnoreCase(getPackageOfCallingUid());
+        }
+
         @Override
         public PictureProfile getPictureProfile(int type, String name, Bundle options,
                 UserHandle user) {
             boolean includeParams =
                     options.getBoolean(MediaQualityManager.OPTION_INCLUDE_PARAMETERS, false);
             String selection = BaseParameters.PARAMETER_TYPE + " = ? AND "
-                    + BaseParameters.PARAMETER_NAME + " = ?";
-            String[] selectionArguments = {Integer.toString(type), name};
+                    + BaseParameters.PARAMETER_NAME + " = ? AND "
+                    + BaseParameters.PARAMETER_PACKAGE + " = ?";
+            String[] selectionArguments = {Integer.toString(type), name, getPackageOfCallingUid()};
 
             try (
                     Cursor cursor = getCursorAfterQuerying(
@@ -156,13 +214,42 @@
                     return null;
                 }
                 cursor.moveToFirst();
-                return getPictureProfileWithTempIdFromCursor(cursor);
+                return convertCursorToPictureProfileWithTempId(cursor);
+            }
+        }
+
+        private PictureProfile getPictureProfile(Long dbId) {
+            String selection = BaseParameters.PARAMETER_ID + " = ?";
+            String[] selectionArguments = {Long.toString(dbId)};
+
+            try (
+                    Cursor cursor = getCursorAfterQuerying(
+                            mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME,
+                            getMediaProfileColumns(false), selection, selectionArguments)
+            ) {
+                int count = cursor.getCount();
+                if (count == 0) {
+                    return null;
+                }
+                if (count > 1) {
+                    Log.wtf(TAG, String.format(Locale.US, "%d entries found for id=%d"
+                                    + " in %s. Should only ever be 0 or 1.", count, dbId,
+                            mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME));
+                    return null;
+                }
+                cursor.moveToFirst();
+                return convertCursorToPictureProfileWithTempId(cursor);
             }
         }
 
         @Override
         public List<PictureProfile> getPictureProfilesByPackage(
                 String packageName, Bundle options, UserHandle user) {
+            if (!hasGlobalPictureQualityServicePermission()) {
+                notifyError(null, PictureProfile.ERROR_NO_PERMISSION,
+                        Binder.getCallingUid(), Binder.getCallingPid());
+            }
+
             boolean includeParams =
                     options.getBoolean(MediaQualityManager.OPTION_INCLUDE_PARAMETERS, false);
             String selection = BaseParameters.PARAMETER_PACKAGE + " = ?";
@@ -172,23 +259,31 @@
         }
 
         @Override
-        public List<PictureProfile> getAvailablePictureProfiles(Bundle options, UserHandle user) {
-            String[] packageNames = mContext.getPackageManager().getPackagesForUid(
-                    Binder.getCallingUid());
-            if (packageNames != null && packageNames.length == 1 && !packageNames[0].isEmpty()) {
-                return getPictureProfilesByPackage(packageNames[0], options, user);
+        public List<PictureProfile> getAvailablePictureProfiles(
+                        Bundle options, UserHandle user) {
+            String packageName = getPackageOfCallingUid();
+            if (packageName != null) {
+                return getPictureProfilesByPackage(packageName, options, user);
             }
             return new ArrayList<>();
         }
 
         @Override
         public boolean setDefaultPictureProfile(String profileId, UserHandle user) {
+            if (!hasGlobalPictureQualityServicePermission()) {
+                notifyError(profileId, PictureProfile.ERROR_NO_PERMISSION,
+                        Binder.getCallingUid(), Binder.getCallingPid());
+            }
             // TODO: pass the profile ID to MediaQuality HAL when ready.
             return false;
         }
 
         @Override
         public List<String> getPictureProfilePackageNames(UserHandle user) {
+            if (!hasGlobalPictureQualityServicePermission()) {
+                notifyError(null, PictureProfile.ERROR_NO_PERMISSION,
+                        Binder.getCallingUid(), Binder.getCallingPid());
+            }
             String [] column = {BaseParameters.PARAMETER_PACKAGE};
             List<PictureProfile> pictureProfiles = getPictureProfilesBasedOnConditions(column,
                     null, null);
@@ -210,12 +305,19 @@
 
         @Override
         public SoundProfile createSoundProfile(SoundProfile sp, UserHandle user) {
+            if ((sp.getPackageName() != null && !sp.getPackageName().isEmpty()
+                    && !incomingPackageEqualsCallingUidPackage(sp.getPackageName()))
+                    && !hasGlobalPictureQualityServicePermission()) {
+                //TODO: error handling
+                return null;
+            }
             SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase();
 
             ContentValues values = getContentValues(null,
                     sp.getProfileType(),
                     sp.getName(),
-                    sp.getPackageName(),
+                    sp.getPackageName() == null || sp.getPackageName().isEmpty()
+                            ? getPackageOfCallingUid() : sp.getPackageName(),
                     sp.getInputId(),
                     sp.getParameters());
 
@@ -229,9 +331,14 @@
 
         @Override
         public void updateSoundProfile(String id, SoundProfile sp, UserHandle user) {
-            Long intId = mSoundProfileTempIdMap.getKey(id);
+            Long dbId = mSoundProfileTempIdMap.getKey(id);
 
-            ContentValues values = getContentValues(intId,
+            if (!hasPermissionToUpdateSoundProfile(dbId, sp)) {
+                //TODO: error handling
+                return;
+            }
+
+            ContentValues values = getContentValues(dbId,
                     sp.getProfileType(),
                     sp.getName(),
                     sp.getPackageName(),
@@ -242,27 +349,49 @@
             db.replace(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, null, values);
         }
 
+        private boolean hasPermissionToUpdateSoundProfile(Long dbId, SoundProfile sp) {
+            SoundProfile fromDb = getSoundProfile(dbId);
+            return fromDb.getProfileType() == sp.getProfileType()
+                    && fromDb.getPackageName().equals(sp.getPackageName())
+                    && fromDb.getName().equals(sp.getName())
+                    && fromDb.getName().equals(getPackageOfCallingUid());
+        }
+
         @Override
         public void removeSoundProfile(String id, UserHandle user) {
             Long intId = mSoundProfileTempIdMap.getKey(id);
+            if (!hasPermissionToRemoveSoundProfile(intId)) {
+                //TODO: error handling
+                return;
+            }
+
             if (intId != null) {
                 SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase();
                 String selection = BaseParameters.PARAMETER_ID + " = ?";
                 String[] selectionArgs = {Long.toString(intId)};
-                db.delete(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, selection,
+                int result = db.delete(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, selection,
                         selectionArgs);
+                if (result == 0) {
+                    //TODO: error handling
+                }
                 mSoundProfileTempIdMap.remove(intId);
             }
         }
 
+        private boolean hasPermissionToRemoveSoundProfile(Long dbId) {
+            SoundProfile fromDb = getSoundProfile(dbId);
+            return fromDb.getName().equalsIgnoreCase(getPackageOfCallingUid());
+        }
+
         @Override
-        public SoundProfile getSoundProfile(int type, String id, Bundle options,
+        public SoundProfile getSoundProfile(int type, String name, Bundle options,
                 UserHandle user) {
             boolean includeParams =
                     options.getBoolean(MediaQualityManager.OPTION_INCLUDE_PARAMETERS, false);
             String selection = BaseParameters.PARAMETER_TYPE + " = ? AND "
-                    + BaseParameters.PARAMETER_ID + " = ?";
-            String[] selectionArguments = {String.valueOf(type), id};
+                    + BaseParameters.PARAMETER_NAME + " = ? AND "
+                    + BaseParameters.PARAMETER_PACKAGE + " = ?";
+            String[] selectionArguments = {String.valueOf(type), name, getPackageOfCallingUid()};
 
             try (
                     Cursor cursor = getCursorAfterQuerying(
@@ -275,18 +404,47 @@
                 }
                 if (count > 1) {
                     Log.wtf(TAG, String.format(Locale.US, "%d entries found for id=%s"
-                                    + " in %s. Should only ever be 0 or 1.", count, id,
+                                    + " in %s. Should only ever be 0 or 1.", count, name,
                             mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME));
                     return null;
                 }
                 cursor.moveToFirst();
-                return getSoundProfileWithTempIdFromCursor(cursor);
+                return convertCursorToSoundProfileWithTempId(cursor);
+            }
+        }
+
+        private SoundProfile getSoundProfile(Long dbId) {
+            String selection = BaseParameters.PARAMETER_ID + " = ?";
+            String[] selectionArguments = {Long.toString(dbId)};
+
+            try (
+                    Cursor cursor = getCursorAfterQuerying(
+                            mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME,
+                            getMediaProfileColumns(false), selection, selectionArguments)
+            ) {
+                int count = cursor.getCount();
+                if (count == 0) {
+                    return null;
+                }
+                if (count > 1) {
+                    Log.wtf(TAG, String.format(Locale.US, "%d entries found for id=%s "
+                                    + "in %s. Should only ever be 0 or 1.", count, dbId,
+                            mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME));
+                    return null;
+                }
+                cursor.moveToFirst();
+                return convertCursorToSoundProfileWithTempId(cursor);
             }
         }
 
         @Override
         public List<SoundProfile> getSoundProfilesByPackage(
                 String packageName, Bundle options, UserHandle user) {
+            if (!hasGlobalSoundQualityServicePermission()) {
+                //TODO: error handling
+                return new ArrayList<>();
+            }
+
             boolean includeParams =
                     options.getBoolean(MediaQualityManager.OPTION_INCLUDE_PARAMETERS, false);
             String selection = BaseParameters.PARAMETER_PACKAGE + " = ?";
@@ -296,24 +454,30 @@
         }
 
         @Override
-        public List<SoundProfile> getAvailableSoundProfiles(
-                Bundle options, UserHandle user) {
-            String[] packageNames = mContext.getPackageManager().getPackagesForUid(
-                    Binder.getCallingUid());
-            if (packageNames != null && packageNames.length == 1 && !packageNames[0].isEmpty()) {
-                return getSoundProfilesByPackage(packageNames[0], options, user);
+        public List<SoundProfile> getAvailableSoundProfiles(Bundle options, UserHandle user) {
+            String packageName = getPackageOfCallingUid();
+            if (packageName != null) {
+                return getSoundProfilesByPackage(packageName, options, user);
             }
             return new ArrayList<>();
         }
 
         @Override
         public boolean setDefaultSoundProfile(String profileId, UserHandle user) {
+            if (!hasGlobalSoundQualityServicePermission()) {
+                //TODO: error handling
+                return false;
+            }
             // TODO: pass the profile ID to MediaQuality HAL when ready.
             return false;
         }
 
         @Override
         public List<String> getSoundProfilePackageNames(UserHandle user) {
+            if (!hasGlobalSoundQualityServicePermission()) {
+                //TODO: error handling
+                return new ArrayList<>();
+            }
             String [] column = {BaseParameters.PARAMETER_NAME};
             List<SoundProfile> soundProfiles = getSoundProfilesBasedOnConditions(column,
                     null, null);
@@ -323,6 +487,37 @@
                     .collect(Collectors.toList());
         }
 
+        private String getPackageOfCallingUid() {
+            String[] packageNames = mPackageManager.getPackagesForUid(
+                    Binder.getCallingUid());
+            if (packageNames != null && packageNames.length == 1 && !packageNames[0].isEmpty()) {
+                return packageNames[0];
+            }
+            return null;
+        }
+
+        private boolean incomingPackageEqualsCallingUidPackage(String incomingPackage) {
+            return incomingPackage.equalsIgnoreCase(getPackageOfCallingUid());
+        }
+
+        private boolean hasGlobalPictureQualityServicePermission() {
+            return mPackageManager.checkPermission(android.Manifest.permission
+                            .MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE,
+                    mContext.getPackageName()) == mPackageManager.PERMISSION_GRANTED;
+        }
+
+        private boolean hasGlobalSoundQualityServicePermission() {
+            return mPackageManager.checkPermission(android.Manifest.permission
+                            .MANAGE_GLOBAL_SOUND_QUALITY_SERVICE,
+                    mContext.getPackageName()) == mPackageManager.PERMISSION_GRANTED;
+        }
+
+        private boolean hasReadColorZonesPermission() {
+            return mPackageManager.checkPermission(android.Manifest.permission
+                            .READ_COLOR_ZONES,
+                    mContext.getPackageName()) == mPackageManager.PERMISSION_GRANTED;
+        }
+
         private void populateTempIdMap(BiMap<Long, String> map, Long id) {
             if (id != null && map.getValue(id) == null) {
                 String uuid;
@@ -430,7 +625,7 @@
             return columns.toArray(new String[0]);
         }
 
-        private PictureProfile getPictureProfileWithTempIdFromCursor(Cursor cursor) {
+        private PictureProfile convertCursorToPictureProfileWithTempId(Cursor cursor) {
             return new PictureProfile(
                     getTempId(mPictureProfileTempIdMap, cursor),
                     getType(cursor),
@@ -442,7 +637,7 @@
             );
         }
 
-        private SoundProfile getSoundProfileWithTempIdFromCursor(Cursor cursor) {
+        private SoundProfile convertCursorToSoundProfileWithTempId(Cursor cursor) {
             return new SoundProfile(
                     getTempId(mSoundProfileTempIdMap, cursor),
                     getType(cursor),
@@ -502,7 +697,7 @@
             ) {
                 List<PictureProfile> pictureProfiles = new ArrayList<>();
                 while (cursor.moveToNext()) {
-                    pictureProfiles.add(getPictureProfileWithTempIdFromCursor(cursor));
+                    pictureProfiles.add(convertCursorToPictureProfileWithTempId(cursor));
                 }
                 return pictureProfiles;
             }
@@ -517,30 +712,64 @@
             ) {
                 List<SoundProfile> soundProfiles = new ArrayList<>();
                 while (cursor.moveToNext()) {
-                    soundProfiles.add(getSoundProfileWithTempIdFromCursor(cursor));
+                    soundProfiles.add(convertCursorToSoundProfileWithTempId(cursor));
                 }
                 return soundProfiles;
             }
         }
 
+        private void notifyError(String profileId, int errorCode, int uid, int pid) {
+            UserState userState = getOrCreateUserStateLocked(UserHandle.USER_SYSTEM);
+            int n = userState.mCallbacks.beginBroadcast();
+
+            for (int i = 0; i < n; ++i) {
+                try {
+                    IPictureProfileCallback callback = userState.mCallbacks.getBroadcastItem(i);
+                    Pair<Integer, Integer> pidUid = userState.mCallbackPidUidMap.get(callback);
+
+                    if (pidUid.first == pid && pidUid.second == uid) {
+                        userState.mCallbacks.getBroadcastItem(i).onError(profileId, errorCode);
+                    }
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "failed to report added input to callback", e);
+                }
+            }
+            userState.mCallbacks.finishBroadcast();
+        }
+
         @Override
         public void registerPictureProfileCallback(final IPictureProfileCallback callback) {
+            int callingPid = Binder.getCallingPid();
+            int callingUid = Binder.getCallingUid();
+
+            UserState userState = getOrCreateUserStateLocked(Binder.getCallingUid());
+            userState.mCallbackPidUidMap.put(callback, Pair.create(callingPid, callingUid));
         }
+
         @Override
         public void registerSoundProfileCallback(final ISoundProfileCallback callback) {
         }
 
         @Override
         public void registerAmbientBacklightCallback(IAmbientBacklightCallback callback) {
+            if (!hasReadColorZonesPermission()) {
+                //TODO: error handling
+            }
         }
 
         @Override
         public void setAmbientBacklightSettings(
                 AmbientBacklightSettings settings, UserHandle user) {
+            if (!hasReadColorZonesPermission()) {
+                //TODO: error handling
+            }
         }
 
         @Override
         public void setAmbientBacklightEnabled(boolean enabled, UserHandle user) {
+            if (!hasReadColorZonesPermission()) {
+                //TODO: error handling
+            }
         }
 
         @Override
@@ -551,20 +780,34 @@
 
         @Override
         public List<String> getPictureProfileAllowList(UserHandle user) {
+            if (!hasGlobalPictureQualityServicePermission()) {
+                //TODO: error handling
+                return new ArrayList<>();
+            }
             return new ArrayList<>();
         }
 
         @Override
         public void setPictureProfileAllowList(List<String> packages, UserHandle user) {
+            if (!hasGlobalPictureQualityServicePermission()) {
+                //TODO: error handling
+            }
         }
 
         @Override
         public List<String> getSoundProfileAllowList(UserHandle user) {
+            if (!hasGlobalSoundQualityServicePermission()) {
+                //TODO: error handling
+                return new ArrayList<>();
+            }
             return new ArrayList<>();
         }
 
         @Override
         public void setSoundProfileAllowList(List<String> packages, UserHandle user) {
+            if (!hasGlobalSoundQualityServicePermission()) {
+                //TODO: error handling
+            }
         }
 
         @Override
@@ -574,28 +817,94 @@
 
         @Override
         public void setAutoPictureQualityEnabled(boolean enabled, UserHandle user) {
+            if (!hasGlobalPictureQualityServicePermission()) {
+                //TODO: error handling
+            }
+
+            try {
+                if (mMediaQuality != null) {
+                    mMediaQuality.setAutoPqEnabled(enabled);
+                }
+            } catch (UnsupportedOperationException e) {
+                Slog.e(TAG, "The current device is not supported");
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to set auto picture quality", e);
+            }
         }
 
         @Override
         public boolean isAutoPictureQualityEnabled(UserHandle user) {
+            try {
+                if (mMediaQuality != null) {
+                    return mMediaQuality.getAutoPqEnabled();
+                }
+            } catch (UnsupportedOperationException e) {
+                Slog.e(TAG, "The current device is not supported");
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to get auto picture quality", e);
+            }
             return false;
         }
 
         @Override
         public void setSuperResolutionEnabled(boolean enabled, UserHandle user) {
+            if (!hasGlobalPictureQualityServicePermission()) {
+                //TODO: error handling
+            }
+
+            try {
+                if (mMediaQuality != null) {
+                    mMediaQuality.setAutoSrEnabled(enabled);
+                }
+            } catch (UnsupportedOperationException e) {
+                Slog.e(TAG, "The current device is not supported");
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to set auto super resolution", e);
+            }
         }
 
         @Override
         public boolean isSuperResolutionEnabled(UserHandle user) {
+            try {
+                if (mMediaQuality != null) {
+                    return mMediaQuality.getAutoSrEnabled();
+                }
+            } catch (UnsupportedOperationException e) {
+                Slog.e(TAG, "The current device is not supported");
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to get auto super resolution", e);
+            }
             return false;
         }
 
         @Override
         public void setAutoSoundQualityEnabled(boolean enabled, UserHandle user) {
+            if (!hasGlobalSoundQualityServicePermission()) {
+                //TODO: error handling
+            }
+
+            try {
+                if (mMediaQuality != null) {
+                    mMediaQuality.setAutoAqEnabled(enabled);
+                }
+            } catch (UnsupportedOperationException e) {
+                Slog.e(TAG, "The current device is not supported");
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to set auto audio quality", e);
+            }
         }
 
         @Override
         public boolean isAutoSoundQualityEnabled(UserHandle user) {
+            try {
+                if (mMediaQuality != null) {
+                    return mMediaQuality.getAutoAqEnabled();
+                }
+            } catch (UnsupportedOperationException e) {
+                Slog.e(TAG, "The current device is not supported");
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to get auto audio quality", e);
+            }
             return false;
         }
 
@@ -604,4 +913,38 @@
             return false;
         }
     }
+
+    private class MediaQualityManagerCallbackList extends
+            RemoteCallbackList<IPictureProfileCallback> {
+        @Override
+        public void onCallbackDied(IPictureProfileCallback callback) {
+            //todo
+        }
+    }
+
+    private final class UserState {
+        // A list of callbacks.
+        private final MediaQualityManagerCallbackList mCallbacks =
+                new MediaQualityManagerCallbackList();
+
+        private final Map<IPictureProfileCallback, Pair<Integer, Integer>> mCallbackPidUidMap =
+                new HashMap<>();
+
+        private UserState(Context context, int userId) {
+
+        }
+    }
+
+    private UserState getOrCreateUserStateLocked(int userId) {
+        UserState userState = getUserStateLocked(userId);
+        if (userState == null) {
+            userState = new UserState(mContext, userId);
+            mUserStates.put(userId, userState);
+        }
+        return userState;
+    }
+
+    private UserState getUserStateLocked(int userId) {
+        return mUserStates.get(userId);
+    }
 }
diff --git a/services/core/java/com/android/server/notification/ConditionProviders.java b/services/core/java/com/android/server/notification/ConditionProviders.java
index 0b40d64..3f2c222 100644
--- a/services/core/java/com/android/server/notification/ConditionProviders.java
+++ b/services/core/java/com/android/server/notification/ConditionProviders.java
@@ -325,7 +325,7 @@
         for (int i = 0; i < N; i++) {
             final Condition c = conditions[i];
             if (mCallback != null) {
-                mCallback.onConditionChanged(c.id, c);
+                mCallback.onConditionChanged(c.id, c, info.uid);
             }
         }
     }
@@ -515,7 +515,7 @@
 
     public interface Callback {
         void onServiceAdded(ComponentName component);
-        void onConditionChanged(Uri id, Condition condition);
+        void onConditionChanged(Uri id, Condition condition, int callerUid);
     }
 
 }
diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java
index 4b41696..e47f8ae9 100644
--- a/services/core/java/com/android/server/notification/GroupHelper.java
+++ b/services/core/java/com/android/server/notification/GroupHelper.java
@@ -583,6 +583,15 @@
         final FullyQualifiedGroupKey fullAggregateGroupKey = new FullyQualifiedGroupKey(
                 record.getUserId(), pkgName, sectioner);
 
+        // The notification was part of a different section => trigger regrouping
+        final FullyQualifiedGroupKey prevSectionKey = getPreviousValidSectionKey(record);
+        if (prevSectionKey != null && !fullAggregateGroupKey.equals(prevSectionKey)) {
+            if (DEBUG) {
+                Slog.i(TAG, "Section changed for: " + record);
+            }
+            maybeUngroupOnSectionChanged(record, prevSectionKey);
+        }
+
         // This notification is already aggregated
         if (record.getGroupKey().equals(fullAggregateGroupKey.toString())) {
             return false;
@@ -652,10 +661,33 @@
     }
 
     /**
+     * A notification was added that was previously part of a different section and needs to trigger
+     * GH state cleanup.
+     */
+    private void maybeUngroupOnSectionChanged(NotificationRecord record,
+            FullyQualifiedGroupKey prevSectionKey) {
+        maybeUngroupWithSections(record, prevSectionKey);
+        if (record.getGroupKey().equals(prevSectionKey.toString())) {
+            record.setOverrideGroupKey(null);
+        }
+    }
+
+    /**
      * A notification was added that is app-grouped.
      */
     private void maybeUngroupOnAppGrouped(NotificationRecord record) {
-        maybeUngroupWithSections(record, getSectionGroupKeyWithFallback(record));
+        FullyQualifiedGroupKey currentSectionKey = getSectionGroupKeyWithFallback(record);
+
+        // The notification was part of a different section => trigger regrouping
+        final FullyQualifiedGroupKey prevSectionKey = getPreviousValidSectionKey(record);
+        if (prevSectionKey != null && !prevSectionKey.equals(currentSectionKey)) {
+            if (DEBUG) {
+                Slog.i(TAG, "Section changed for: " + record);
+            }
+            currentSectionKey = prevSectionKey;
+        }
+
+        maybeUngroupWithSections(record, currentSectionKey);
     }
 
     /**
diff --git a/services/core/java/com/android/server/notification/NotificationDelegate.java b/services/core/java/com/android/server/notification/NotificationDelegate.java
index 7cbbe29..5a42505 100644
--- a/services/core/java/com/android/server/notification/NotificationDelegate.java
+++ b/services/core/java/com/android/server/notification/NotificationDelegate.java
@@ -107,4 +107,9 @@
      * @param key the notification key
      */
     void unbundleNotification(String key);
+    /**
+     *  Called when the notification should be rebundled.
+     * @param key the notification key
+     */
+    void rebundleNotification(String key);
 }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index f50e8aa..341038f 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1888,6 +1888,36 @@
                 }
             }
         }
+
+        @Override
+        public void rebundleNotification(String key) {
+            if (!(notificationClassification() && notificationRegroupOnClassification())) {
+                return;
+            }
+            synchronized (mNotificationLock) {
+                NotificationRecord r = mNotificationsByKey.get(key);
+                if (r == null) {
+                    return;
+                }
+
+                if (DBG) {
+                    Slog.v(TAG, "rebundleNotification: " + r);
+                }
+
+                if (r.getBundleType() != Adjustment.TYPE_OTHER) {
+                    final Bundle classifBundle = new Bundle();
+                    classifBundle.putInt(KEY_TYPE, r.getBundleType());
+                    Adjustment adj = new Adjustment(r.getSbn().getPackageName(), r.getKey(),
+                            classifBundle, "rebundle", r.getUserId());
+                    applyAdjustmentLocked(r, adj, /* isPosted= */ true);
+                    mRankingHandler.requestSort();
+                } else {
+                    if (DBG) {
+                        Slog.w(TAG, "Can't rebundle. No valid bundle type for: " + r);
+                    }
+                }
+            }
+        }
     };
 
     NotificationManagerPrivate mNotificationManagerPrivate = new NotificationManagerPrivate() {
@@ -3162,6 +3192,7 @@
             mAssistants.onBootPhaseAppsCanStart();
             mConditionProviders.onBootPhaseAppsCanStart();
             mHistoryManager.onBootPhaseAppsCanStart();
+            mPreferencesHelper.onBootPhaseAppsCanStart();
             migrateDefaultNAS();
             maybeShowInitialReviewPermissionsNotification();
 
@@ -5903,8 +5934,9 @@
         // TODO: b/310620812 - Remove getZenRules() when MODES_API is inlined.
         @Override
         public List<ZenModeConfig.ZenRule> getZenRules() throws RemoteException {
-            enforcePolicyAccess(Binder.getCallingUid(), "getZenRules");
-            return mZenModeHelper.getZenRules(getCallingZenUser());
+            int callingUid = Binder.getCallingUid();
+            enforcePolicyAccess(callingUid, "getZenRules");
+            return mZenModeHelper.getZenRules(getCallingZenUser(), callingUid);
         }
 
         @Override
@@ -5912,15 +5944,17 @@
             if (!android.app.Flags.modesApi()) {
                 throw new IllegalStateException("getAutomaticZenRules called with flag off!");
             }
-            enforcePolicyAccess(Binder.getCallingUid(), "getAutomaticZenRules");
-            return mZenModeHelper.getAutomaticZenRules(getCallingZenUser());
+            int callingUid = Binder.getCallingUid();
+            enforcePolicyAccess(callingUid, "getAutomaticZenRules");
+            return mZenModeHelper.getAutomaticZenRules(getCallingZenUser(), callingUid);
         }
 
         @Override
         public AutomaticZenRule getAutomaticZenRule(String id) throws RemoteException {
             Objects.requireNonNull(id, "Id is null");
-            enforcePolicyAccess(Binder.getCallingUid(), "getAutomaticZenRule");
-            return mZenModeHelper.getAutomaticZenRule(getCallingZenUser(), id);
+            int callingUid = Binder.getCallingUid();
+            enforcePolicyAccess(callingUid, "getAutomaticZenRule");
+            return mZenModeHelper.getAutomaticZenRule(getCallingZenUser(), id, callingUid);
         }
 
         @Override
@@ -6065,8 +6099,9 @@
         @Condition.State
         public int getAutomaticZenRuleState(@NonNull String id) {
             Objects.requireNonNull(id, "id is null");
-            enforcePolicyAccess(Binder.getCallingUid(), "getAutomaticZenRuleState");
-            return mZenModeHelper.getAutomaticZenRuleState(getCallingZenUser(), id);
+            int callingUid = Binder.getCallingUid();
+            enforcePolicyAccess(callingUid, "getAutomaticZenRuleState");
+            return mZenModeHelper.getAutomaticZenRuleState(getCallingZenUser(), id, callingUid);
         }
 
         @Override
@@ -7129,6 +7164,7 @@
                     adjustments.putParcelable(KEY_TYPE, newChannel);
 
                     logClassificationChannelAdjustmentReceived(r, isPosted, classification);
+                    r.setBundleType(classification);
                 }
             }
             r.addAdjustment(adjustment);
@@ -9532,7 +9568,8 @@
                                     || !Objects.equals(oldSbn.getNotification().getGroup(),
                                         n.getNotification().getGroup())
                                     || oldSbn.getNotification().flags
-                                    != n.getNotification().flags) {
+                                    != n.getNotification().flags
+                                    || !old.getChannel().getId().equals(r.getChannel().getId())) {
                                 synchronized (mNotificationLock) {
                                     final String autogroupName =
                                             notificationForceGrouping() ?
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index 93f512b..81af0d8 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -36,10 +36,7 @@
 import android.app.Notification;
 import android.app.NotificationChannel;
 import android.app.Person;
-import android.content.ContentProvider;
-import android.content.ContentResolver;
 import android.content.Context;
-import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.ShortcutInfo;
@@ -48,7 +45,6 @@
 import android.media.AudioSystem;
 import android.metrics.LogMaker;
 import android.net.Uri;
-import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -226,6 +222,9 @@
     // lifetime extended.
     private boolean mCanceledAfterLifetimeExtension = false;
 
+    // type of the bundle if the notification was classified
+    private @Adjustment.Types int mBundleType = Adjustment.TYPE_OTHER;
+
     public NotificationRecord(Context context, StatusBarNotification sbn,
             NotificationChannel channel) {
         this.sbn = sbn;
@@ -471,6 +470,10 @@
             }
         }
 
+        if (android.service.notification.Flags.notificationClassification()) {
+            mBundleType = previous.mBundleType;
+        }
+
         // Don't copy importance information or mGlobalSortKey, recompute them.
     }
 
@@ -1493,23 +1496,14 @@
 
             final Notification notification = getNotification();
             notification.visitUris((uri) -> {
-                if (com.android.server.notification.Flags.notificationVerifyChannelSoundUri()) {
-                    visitGrantableUri(uri, false, false);
-                } else {
-                    oldVisitGrantableUri(uri, false, false);
-                }
+                visitGrantableUri(uri, false, false);
             });
 
             if (notification.getChannelId() != null) {
                 NotificationChannel channel = getChannel();
                 if (channel != null) {
-                    if (com.android.server.notification.Flags.notificationVerifyChannelSoundUri()) {
-                        visitGrantableUri(channel.getSound(), (channel.getUserLockedFields()
-                                & NotificationChannel.USER_LOCKED_SOUND) != 0, true);
-                    } else {
-                        oldVisitGrantableUri(channel.getSound(), (channel.getUserLockedFields()
-                                & NotificationChannel.USER_LOCKED_SOUND) != 0, true);
-                    }
+                    visitGrantableUri(channel.getSound(), (channel.getUserLockedFields()
+                            & NotificationChannel.USER_LOCKED_SOUND) != 0, true);
                 }
             }
         } finally {
@@ -1525,53 +1519,6 @@
      * {@link #mGrantableUris}. Otherwise, this will either log or throw
      * {@link SecurityException} depending on target SDK of enqueuing app.
      */
-    private void oldVisitGrantableUri(Uri uri, boolean userOverriddenUri, boolean isSound) {
-        if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return;
-
-        if (mGrantableUris != null && mGrantableUris.contains(uri)) {
-            return; // already verified this URI
-        }
-
-        final int sourceUid = getSbn().getUid();
-        final long ident = Binder.clearCallingIdentity();
-        try {
-            // This will throw a SecurityException if the caller can't grant.
-            mUgmInternal.checkGrantUriPermission(sourceUid, null,
-                    ContentProvider.getUriWithoutUserId(uri),
-                    Intent.FLAG_GRANT_READ_URI_PERMISSION,
-                    ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(sourceUid)));
-
-            if (mGrantableUris == null) {
-                mGrantableUris = new ArraySet<>();
-            }
-            mGrantableUris.add(uri);
-        } catch (SecurityException e) {
-            if (!userOverriddenUri) {
-                if (isSound) {
-                    mSound = Settings.System.DEFAULT_NOTIFICATION_URI;
-                    Log.w(TAG, "Replacing " + uri + " from " + sourceUid + ": " + e.getMessage());
-                } else {
-                    if (mTargetSdkVersion >= Build.VERSION_CODES.P) {
-                        throw e;
-                    } else {
-                        Log.w(TAG,
-                                "Ignoring " + uri + " from " + sourceUid + ": " + e.getMessage());
-                    }
-                }
-            }
-        } finally {
-            Binder.restoreCallingIdentity(ident);
-        }
-    }
-
-    /**
-     * Note the presence of a {@link Uri} that should have permission granted to
-     * whoever will be rendering it.
-     * <p>
-     * If the enqueuing app has the ability to grant access, it will be added to
-     * {@link #mGrantableUris}. Otherwise, this will either log or throw
-     * {@link SecurityException} depending on target SDK of enqueuing app.
-     */
     private void visitGrantableUri(Uri uri, boolean userOverriddenUri,
             boolean isSound) {
         if (mGrantableUris != null && mGrantableUris.contains(uri)) {
@@ -1689,6 +1636,14 @@
         mCanceledAfterLifetimeExtension = canceledAfterLifetimeExtension;
     }
 
+    public @Adjustment.Types int getBundleType() {
+        return mBundleType;
+    }
+
+    public void setBundleType(@Adjustment.Types int bundleType) {
+        mBundleType = bundleType;
+    }
+
     /**
      * Whether this notification is a conversation notification.
      */
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 15377d6..36eabae 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -82,7 +82,6 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.IntArray;
-import android.util.Log;
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseBooleanArray;
@@ -272,6 +271,15 @@
         updateMediaNotificationFilteringEnabled();
     }
 
+    void onBootPhaseAppsCanStart() {
+        // IpcDataCaches must be invalidated once data becomes available, as queries will only
+        // begin to be cached after the first invalidation signal. At this point, we know about all
+        // notification channels.
+        if (android.app.Flags.nmBinderPerfCacheChannels()) {
+            invalidateNotificationChannelCache();
+        }
+    }
+
     public void readXml(TypedXmlPullParser parser, boolean forRestore, int userId)
             throws XmlPullParserException, IOException {
         int type = parser.getEventType();
@@ -531,12 +539,14 @@
     private PackagePreferences getOrCreatePackagePreferencesLocked(String pkg,
             @UserIdInt int userId, int uid, int importance, int priority, int visibility,
             boolean showBadge, int bubblePreference, long creationTime) {
+        boolean created = false;
         final String key = packagePreferencesKey(pkg, uid);
         PackagePreferences
                 r = (uid == UNKNOWN_UID)
                 ? mRestoredWithoutUids.get(unrestoredPackageKey(pkg, userId))
                 : mPackagePreferences.get(key);
         if (r == null) {
+            created = true;
             r = new PackagePreferences();
             r.pkg = pkg;
             r.uid = uid;
@@ -572,6 +582,9 @@
                 mRestoredWithoutUids.remove(unrestoredPackageKey(pkg, userId));
             }
         }
+        if (android.app.Flags.nmBinderPerfCacheChannels() && created) {
+            invalidateNotificationChannelCache();
+        }
         return r;
     }
 
@@ -664,6 +677,9 @@
         }
         NotificationChannel channel = new NotificationChannel(channelId, label, IMPORTANCE_LOW);
         p.channels.put(channelId, channel);
+        if (android.app.Flags.nmBinderPerfCacheChannels()) {
+            invalidateNotificationChannelCache();
+        }
         return channel;
     }
 
@@ -1171,9 +1187,7 @@
                 // Verify that the app has permission to read the sound Uri
                 // Only check for new channels, as regular apps can only set sound
                 // before creating. See: {@link NotificationChannel#setSound}
-                if (Flags.notificationVerifyChannelSoundUri()) {
-                    PermissionHelper.grantUriPermission(mUgmInternal, channel.getSound(), uid);
-                }
+                PermissionHelper.grantUriPermission(mUgmInternal, channel.getSound(), uid);
 
                 channel.setImportanceLockedByCriticalDeviceFunction(
                         r.defaultAppLockedImportance || r.fixedImportance);
@@ -1208,6 +1222,10 @@
             updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
         }
 
+        if (android.app.Flags.nmBinderPerfCacheChannels() && needsPolicyFileChange) {
+            invalidateNotificationChannelCache();
+        }
+
         return needsPolicyFileChange;
     }
 
@@ -1229,6 +1247,9 @@
             }
             channel.unlockFields(USER_LOCKED_IMPORTANCE);
         }
+        if (android.app.Flags.nmBinderPerfCacheChannels()) {
+            invalidateNotificationChannelCache();
+        }
     }
 
 
@@ -1301,6 +1322,9 @@
             updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
         }
         if (changed) {
+            if (android.app.Flags.nmBinderPerfCacheChannels()) {
+                invalidateNotificationChannelCache();
+            }
             updateConfig();
         }
     }
@@ -1537,6 +1561,10 @@
         if (channelBypassedDnd) {
             updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
         }
+
+        if (android.app.Flags.nmBinderPerfCacheChannels() && deletedChannel) {
+            invalidateNotificationChannelCache();
+        }
         return deletedChannel;
     }
 
@@ -1566,6 +1594,9 @@
             }
             r.channels.remove(channelId);
         }
+        if (android.app.Flags.nmBinderPerfCacheChannels()) {
+            invalidateNotificationChannelCache();
+        }
     }
 
     @Override
@@ -1576,13 +1607,18 @@
             if (r == null) {
                 return;
             }
+            boolean deleted = false;
             int N = r.channels.size() - 1;
             for (int i = N; i >= 0; i--) {
                 String key = r.channels.keyAt(i);
                 if (!DEFAULT_CHANNEL_ID.equals(key)) {
                     r.channels.remove(key);
+                    deleted = true;
                 }
             }
+            if (android.app.Flags.nmBinderPerfCacheChannels() && deleted) {
+                invalidateNotificationChannelCache();
+            }
         }
     }
 
@@ -1613,6 +1649,9 @@
                 }
             }
         }
+        if (android.app.Flags.nmBinderPerfCacheChannels()) {
+            invalidateNotificationChannelCache();
+        }
     }
 
     public void updateDefaultApps(int userId, ArraySet<String> toRemove,
@@ -1642,6 +1681,9 @@
                 }
             }
         }
+        if (android.app.Flags.nmBinderPerfCacheChannels()) {
+            invalidateNotificationChannelCache();
+        }
     }
 
     public NotificationChannelGroup getNotificationChannelGroupWithChannels(String pkg,
@@ -1757,6 +1799,9 @@
         if (groupBypassedDnd) {
             updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
         }
+        if (android.app.Flags.nmBinderPerfCacheChannels() && deletedChannels.size() > 0) {
+            invalidateNotificationChannelCache();
+        }
         return deletedChannels;
     }
 
@@ -1902,8 +1947,13 @@
                 }
             }
         }
-        if (!deletedChannelIds.isEmpty() && mCurrentUserHasChannelsBypassingDnd) {
-            updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+        if (!deletedChannelIds.isEmpty()) {
+            if (mCurrentUserHasChannelsBypassingDnd) {
+                updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+            }
+            if (android.app.Flags.nmBinderPerfCacheChannels()) {
+                invalidateNotificationChannelCache();
+            }
         }
         return deletedChannelIds;
     }
@@ -2196,6 +2246,11 @@
             PackagePreferences prefs = getOrCreatePackagePreferencesLocked(sourcePkg, sourceUid);
             prefs.delegate = new Delegate(delegatePkg, delegateUid, true);
         }
+        if (android.app.Flags.nmBinderPerfCacheChannels()) {
+            // If package delegates change, then which packages can get what channel information
+            // also changes, so we need to clear the cache.
+            invalidateNotificationChannelCache();
+        }
     }
 
     /**
@@ -2208,6 +2263,9 @@
                 prefs.delegate.mEnabled = false;
             }
         }
+        if (android.app.Flags.nmBinderPerfCacheChannels()) {
+            invalidateNotificationChannelCache();
+        }
     }
 
     /**
@@ -2811,18 +2869,24 @@
 
     public void onUserRemoved(int userId) {
         synchronized (mLock) {
+            boolean removed = false;
             int N = mPackagePreferences.size();
             for (int i = N - 1; i >= 0; i--) {
                 PackagePreferences PackagePreferences = mPackagePreferences.valueAt(i);
                 if (UserHandle.getUserId(PackagePreferences.uid) == userId) {
                     mPackagePreferences.removeAt(i);
+                    removed = true;
                 }
             }
+            if (android.app.Flags.nmBinderPerfCacheChannels() && removed) {
+                invalidateNotificationChannelCache();
+            }
         }
     }
 
     protected void onLocaleChanged(Context context, int userId) {
         synchronized (mLock) {
+            boolean updated = false;
             int N = mPackagePreferences.size();
             for (int i = 0; i < N; i++) {
                 PackagePreferences PackagePreferences = mPackagePreferences.valueAt(i);
@@ -2833,10 +2897,14 @@
                                 DEFAULT_CHANNEL_ID).setName(
                                 context.getResources().getString(
                                         R.string.default_notification_channel_label));
+                        updated = true;
                     }
                     // TODO (b/346396459): Localize all reserved channels
                 }
             }
+            if (android.app.Flags.nmBinderPerfCacheChannels() && updated) {
+                invalidateNotificationChannelCache();
+            }
         }
     }
 
@@ -2884,7 +2952,7 @@
                                                     channel.getAudioAttributes().getUsage());
                                     if (Settings.System.DEFAULT_NOTIFICATION_URI.equals(
                                             restoredUri)) {
-                                        Log.w(TAG,
+                                        Slog.w(TAG,
                                                 "Could not restore sound: " + uri + " for channel: "
                                                         + channel);
                                     }
@@ -2922,6 +2990,9 @@
 
         if (updated) {
             updateConfig();
+            if (android.app.Flags.nmBinderPerfCacheChannels()) {
+                invalidateNotificationChannelCache();
+            }
         }
         return updated;
     }
@@ -2939,6 +3010,9 @@
                 p.priority = DEFAULT_PRIORITY;
                 p.visibility = DEFAULT_VISIBILITY;
                 p.showBadge = DEFAULT_SHOW_BADGE;
+                if (android.app.Flags.nmBinderPerfCacheChannels()) {
+                    invalidateNotificationChannelCache();
+                }
             }
         }
     }
@@ -3123,6 +3197,9 @@
                 }
             }
         }
+        if (android.app.Flags.nmBinderPerfCacheChannels()) {
+            invalidateNotificationChannelCache();
+        }
     }
 
     public void migrateNotificationPermissions(List<UserInfo> users) {
@@ -3154,6 +3231,12 @@
         mRankingHandler.requestSort();
     }
 
+    @VisibleForTesting
+    // Utility method for overriding in tests to confirm that the cache gets cleared.
+    protected void invalidateNotificationChannelCache() {
+        NotificationManager.invalidateNotificationChannelCache();
+    }
+
     private static String packagePreferencesKey(String pkg, int uid) {
         return pkg + "|" + uid;
     }
diff --git a/services/core/java/com/android/server/notification/ZenModeConditions.java b/services/core/java/com/android/server/notification/ZenModeConditions.java
index 52d0c41..d44baeb 100644
--- a/services/core/java/com/android/server/notification/ZenModeConditions.java
+++ b/services/core/java/com/android/server/notification/ZenModeConditions.java
@@ -113,15 +113,18 @@
     }
 
     @Override
-    public void onConditionChanged(Uri id, Condition condition) {
+    public void onConditionChanged(Uri id, Condition condition, int callingUid) {
         if (DEBUG) Log.d(TAG, "onConditionChanged " + id + " " + condition);
         ZenModeConfig config = mHelper.getConfig();
         if (config == null) return;
-        final int callingUid = Binder.getCallingUid();
+        if (!Flags.fixCallingUidFromCps()) {
+            // Old behavior: overwrite with known-bad callingUid (always system_server).
+            callingUid = Binder.getCallingUid();
+        }
 
         // This change is known to be for UserHandle.CURRENT because ConditionProviders for
         // background users are not bound.
-        mHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, condition,
+        mHelper.setAutomaticZenRuleStateFromConditionProvider(UserHandle.CURRENT, id, condition,
                 callingUid == Process.SYSTEM_UID ? ZenModeConfig.ORIGIN_SYSTEM
                         : ZenModeConfig.ORIGIN_APP,
                 callingUid);
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index b571d62..0a63f3f 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -413,13 +413,13 @@
     }
 
     // TODO: b/310620812 - Make private (or inline) when MODES_API is inlined.
-    public List<ZenRule> getZenRules(UserHandle user) {
+    public List<ZenRule> getZenRules(UserHandle user, int callingUid) {
         List<ZenRule> rules = new ArrayList<>();
         synchronized (mConfigLock) {
             ZenModeConfig config = getConfigLocked(user);
             if (config == null) return rules;
             for (ZenRule rule : config.automaticRules.values()) {
-                if (canManageAutomaticZenRule(rule)) {
+                if (canManageAutomaticZenRule(rule, callingUid)) {
                     rules.add(rule);
                 }
             }
@@ -432,8 +432,8 @@
      * (which means the owned rules for a regular app, and every rule for system callers) together
      * with their ids.
      */
-    Map<String, AutomaticZenRule> getAutomaticZenRules(UserHandle user) {
-        List<ZenRule> ruleList = getZenRules(user);
+    Map<String, AutomaticZenRule> getAutomaticZenRules(UserHandle user, int callingUid) {
+        List<ZenRule> ruleList = getZenRules(user, callingUid);
         HashMap<String, AutomaticZenRule> rules = new HashMap<>(ruleList.size());
         for (ZenRule rule : ruleList) {
             rules.put(rule.id, zenRuleToAutomaticZenRule(rule));
@@ -441,7 +441,7 @@
         return rules;
     }
 
-    public AutomaticZenRule getAutomaticZenRule(UserHandle user, String id) {
+    public AutomaticZenRule getAutomaticZenRule(UserHandle user, String id, int callingUid) {
         ZenRule rule;
         synchronized (mConfigLock) {
             ZenModeConfig config = getConfigLocked(user);
@@ -449,7 +449,7 @@
             rule = config.automaticRules.get(id);
         }
         if (rule == null) return null;
-        if (canManageAutomaticZenRule(rule)) {
+        if (canManageAutomaticZenRule(rule, callingUid)) {
             return zenRuleToAutomaticZenRule(rule);
         }
         return null;
@@ -591,7 +591,7 @@
                         + " reason=" + reason);
             }
             ZenModeConfig.ZenRule oldRule = config.automaticRules.get(ruleId);
-            if (oldRule == null || !canManageAutomaticZenRule(oldRule)) {
+            if (oldRule == null || !canManageAutomaticZenRule(oldRule, callingUid)) {
                 throw new SecurityException(
                         "Cannot update rules not owned by your condition provider");
             }
@@ -859,7 +859,7 @@
             newConfig = config.copy();
             ZenRule ruleToRemove = newConfig.automaticRules.get(id);
             if (ruleToRemove == null) return false;
-            if (canManageAutomaticZenRule(ruleToRemove)) {
+            if (canManageAutomaticZenRule(ruleToRemove, callingUid)) {
                 newConfig.automaticRules.remove(id);
                 maybePreserveRemovedRule(newConfig, ruleToRemove, origin);
                 if (ruleToRemove.getPkg() != null
@@ -893,7 +893,8 @@
             newConfig = config.copy();
             for (int i = newConfig.automaticRules.size() - 1; i >= 0; i--) {
                 ZenRule rule = newConfig.automaticRules.get(newConfig.automaticRules.keyAt(i));
-                if (Objects.equals(rule.getPkg(), packageName) && canManageAutomaticZenRule(rule)) {
+                if (Objects.equals(rule.getPkg(), packageName)
+                        && canManageAutomaticZenRule(rule, callingUid)) {
                     newConfig.automaticRules.removeAt(i);
                     maybePreserveRemovedRule(newConfig, rule, origin);
                 }
@@ -938,14 +939,14 @@
     }
 
     @Condition.State
-    int getAutomaticZenRuleState(UserHandle user, String id) {
+    int getAutomaticZenRuleState(UserHandle user, String id, int callingUid) {
         synchronized (mConfigLock) {
             ZenModeConfig config = getConfigLocked(user);
             if (config == null) {
                 return Condition.STATE_UNKNOWN;
             }
             ZenRule rule = config.automaticRules.get(id);
-            if (rule == null || !canManageAutomaticZenRule(rule)) {
+            if (rule == null || !canManageAutomaticZenRule(rule, callingUid)) {
                 return Condition.STATE_UNKNOWN;
             }
             if (Flags.modesApi() && Flags.modesUi()) {
@@ -968,7 +969,7 @@
             newConfig = config.copy();
             ZenRule rule = newConfig.automaticRules.get(id);
             if (Flags.modesApi()) {
-                if (rule != null && canManageAutomaticZenRule(rule)) {
+                if (rule != null && canManageAutomaticZenRule(rule, callingUid)) {
                     setAutomaticZenRuleStateLocked(newConfig, Collections.singletonList(rule),
                             condition, origin, callingUid);
                 }
@@ -980,8 +981,8 @@
         }
     }
 
-    void setAutomaticZenRuleState(UserHandle user, Uri ruleDefinition, Condition condition,
-            @ConfigOrigin int origin, int callingUid) {
+    void setAutomaticZenRuleStateFromConditionProvider(UserHandle user, Uri ruleDefinition,
+            Condition condition, @ConfigOrigin int origin, int callingUid) {
         checkSetRuleStateOrigin("setAutomaticZenRuleState(Uri ruleDefinition)", origin);
         ZenModeConfig newConfig;
         synchronized (mConfigLock) {
@@ -992,7 +993,7 @@
             List<ZenRule> matchingRules = findMatchingRules(newConfig, ruleDefinition, condition);
             if (Flags.modesApi()) {
                 for (int i = matchingRules.size() - 1; i >= 0; i--) {
-                    if (!canManageAutomaticZenRule(matchingRules.get(i))) {
+                    if (!canManageAutomaticZenRule(matchingRules.get(i), callingUid)) {
                         matchingRules.remove(i);
                     }
                 }
@@ -1125,15 +1126,21 @@
         return count;
     }
 
-    public boolean canManageAutomaticZenRule(ZenRule rule) {
-        final int callingUid = Binder.getCallingUid();
+    public boolean canManageAutomaticZenRule(ZenRule rule, int callingUid) {
+        if (!com.android.server.notification.Flags.fixCallingUidFromCps()) {
+            // Old behavior: ignore supplied callingUid and instead obtain it here. Will be
+            // incorrect if not currently handling a Binder call.
+            callingUid = Binder.getCallingUid();
+        }
+
         if (callingUid == 0 || callingUid == Process.SYSTEM_UID) {
+            // Checked specifically, because checkCallingPermission() will fail.
             return true;
         } else if (mContext.checkCallingPermission(android.Manifest.permission.MANAGE_NOTIFICATIONS)
                 == PackageManager.PERMISSION_GRANTED) {
             return true;
         } else {
-            String[] packages = mPm.getPackagesForUid(Binder.getCallingUid());
+            String[] packages = mPm.getPackagesForUid(callingUid);
             if (packages != null) {
                 final int packageCount = packages.length;
                 for (int i = 0; i < packageCount; i++) {
@@ -2902,8 +2909,8 @@
     }
 
     /**
-     * Checks that the {@code origin} supplied to {@link #setAutomaticZenRuleState} overloads makes
-     * sense.
+     * Checks that the {@code origin} supplied to {@link #setAutomaticZenRuleState} or
+     * {@link #setAutomaticZenRuleStateFromConditionProvider} makes sense.
      */
     private static void checkSetRuleStateOrigin(String method, @ConfigOrigin int origin) {
         if (!Flags.modesApi()) {
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index f15c23e..c1ca9c2 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -172,16 +172,6 @@
 }
 
 flag {
-  name: "notification_verify_channel_sound_uri"
-  namespace: "systemui"
-  description: "Verify Uri permission for sound when creating a notification channel"
-  bug: "337775777"
-  metadata {
-    purpose: PURPOSE_BUGFIX
-  }
-}
-
-flag {
   name: "notification_vibration_in_sound_uri_for_channel"
   namespace: "systemui"
   description: "Enables sound uri with vibration source in notification channel"
@@ -196,4 +186,14 @@
   metadata {
     purpose: PURPOSE_BUGFIX
   }
-}
\ No newline at end of file
+}
+
+flag {
+  name: "fix_calling_uid_from_cps"
+  namespace: "systemui"
+  description: "Correctly checks zen rule ownership when a CPS notifies with a Condition"
+  bug: "379722187"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/services/core/java/com/android/server/om/IdmapDaemon.java b/services/core/java/com/android/server/om/IdmapDaemon.java
index 1b22154..d33c860 100644
--- a/services/core/java/com/android/server/om/IdmapDaemon.java
+++ b/services/core/java/com/android/server/om/IdmapDaemon.java
@@ -28,6 +28,7 @@
 import android.os.IIdmap2;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.StrictMode;
 import android.os.SystemClock;
 import android.os.SystemService;
 import android.text.TextUtils;
@@ -40,7 +41,6 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * To prevent idmap2d from continuously running, the idmap daemon will terminate after 10 seconds
@@ -66,7 +66,7 @@
 
     private static IdmapDaemon sInstance;
     private volatile IIdmap2 mService;
-    private final AtomicInteger mOpenedCount = new AtomicInteger();
+    private int mOpenedCount = 0;
     private final Object mIdmapToken = new Object();
 
     /**
@@ -74,15 +74,20 @@
      * finalized, the idmap service will be stopped after a period of time unless another connection
      * to the service is open.
      **/
-    private class Connection implements AutoCloseable {
+    private final class Connection implements AutoCloseable {
         @Nullable
         private final IIdmap2 mIdmap2;
         private boolean mOpened = true;
 
-        private Connection(IIdmap2 idmap2) {
+        private Connection() {
+            mIdmap2 = null;
+            mOpened = false;
+        }
+
+        private Connection(@NonNull IIdmap2 idmap2) {
+            mIdmap2 = idmap2;
             synchronized (mIdmapToken) {
-                mOpenedCount.incrementAndGet();
-                mIdmap2 = idmap2;
+                ++mOpenedCount;
             }
         }
 
@@ -94,20 +99,22 @@
                 }
 
                 mOpened = false;
-                if (mOpenedCount.decrementAndGet() != 0) {
+                if (--mOpenedCount != 0) {
                     // Only post the callback to stop the service if the service does not have an
                     // open connection.
                     return;
                 }
 
+                final var service = mService;
                 FgThread.getHandler().postDelayed(() -> {
                     synchronized (mIdmapToken) {
-                        // Only stop the service if the service does not have an open connection.
-                        if (mService == null || mOpenedCount.get() != 0) {
+                        // Only stop the service if it's the one we were scheduled for and
+                        // it does not have an open connection.
+                        if (mService != service || mOpenedCount != 0) {
                             return;
                         }
 
-                        stopIdmapService();
+                        stopIdmapServiceLocked();
                         mService = null;
                     }
                 }, mIdmapToken, SERVICE_TIMEOUT_MS);
@@ -175,6 +182,8 @@
     }
 
     boolean idmapExists(String overlayPath, int userId) {
+        // The only way to verify an idmap is to read its state on disk.
+        final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
         try (Connection c = connect()) {
             final IIdmap2 idmap2 = c.getIdmap2();
             if (idmap2 == null) {
@@ -187,6 +196,8 @@
         } catch (Exception e) {
             Slog.wtf(TAG, "failed to check if idmap exists for " + overlayPath, e);
             return false;
+        } finally {
+            StrictMode.setThreadPolicy(oldPolicy);
         }
     }
 
@@ -242,14 +253,16 @@
         } catch (Exception e) {
             Slog.wtf(TAG, "failed to get all fabricated overlays", e);
         } finally {
-            try {
-                if (c.getIdmap2() != null && iteratorId != -1) {
-                    c.getIdmap2().releaseFabricatedOverlayIterator(iteratorId);
+            if (c != null) {
+                try {
+                    if (c.getIdmap2() != null && iteratorId != -1) {
+                        c.getIdmap2().releaseFabricatedOverlayIterator(iteratorId);
+                    }
+                } catch (RemoteException e) {
+                    // ignore
                 }
-            } catch (RemoteException e) {
-                // ignore
+                c.close();
             }
-            c.close();
         }
         return allInfos;
     }
@@ -271,9 +284,11 @@
     }
 
     @Nullable
-    private IBinder getIdmapService() throws TimeoutException, RemoteException {
+    private IBinder getIdmapServiceLocked() throws TimeoutException, RemoteException {
         try {
-            SystemService.start(IDMAP_DAEMON);
+            if (!SystemService.isRunning(IDMAP_DAEMON)) {
+                SystemService.start(IDMAP_DAEMON);
+            }
         } catch (RuntimeException e) {
             Slog.wtf(TAG, "Failed to enable idmap2 daemon", e);
             if (e.getMessage().contains("failed to set system property")) {
@@ -306,9 +321,11 @@
                         walltimeMillis - endWalltimeMillis + SERVICE_CONNECT_WALLTIME_TIMEOUT_MS));
     }
 
-    private static void stopIdmapService() {
+    private static void stopIdmapServiceLocked() {
         try {
-            SystemService.stop(IDMAP_DAEMON);
+            if (SystemService.isRunning(IDMAP_DAEMON)) {
+                SystemService.stop(IDMAP_DAEMON);
+            }
         } catch (RuntimeException e) {
             // If the idmap daemon cannot be disabled for some reason, it is okay
             // since we already finished invoking idmap.
@@ -326,9 +343,9 @@
                 return new Connection(mService);
             }
 
-            IBinder binder = getIdmapService();
+            IBinder binder = getIdmapServiceLocked();
             if (binder == null) {
-                return new Connection(null);
+                return new Connection();
             }
 
             mService = IIdmap2.Stub.asInterface(binder);
diff --git a/services/core/java/com/android/server/om/OverlayActorEnforcer.java b/services/core/java/com/android/server/om/OverlayActorEnforcer.java
index 38f3939..d806770 100644
--- a/services/core/java/com/android/server/om/OverlayActorEnforcer.java
+++ b/services/core/java/com/android/server/om/OverlayActorEnforcer.java
@@ -19,7 +19,6 @@
 import android.annotation.NonNull;
 import android.content.om.OverlayInfo;
 import android.content.om.OverlayableInfo;
-import android.content.res.Flags;
 import android.net.Uri;
 import android.os.Process;
 import android.text.TextUtils;
@@ -50,6 +49,10 @@
      */
     static Pair<String, ActorState> getPackageNameForActor(@NonNull String actorUriString,
             @NonNull Map<String, Map<String, String>> namedActors) {
+        if (namedActors.isEmpty()) {
+            return Pair.create(null, ActorState.NO_NAMED_ACTORS);
+        }
+
         Uri actorUri = Uri.parse(actorUriString);
 
         String actorScheme = actorUri.getScheme();
@@ -58,10 +61,6 @@
             return Pair.create(null, ActorState.INVALID_OVERLAYABLE_ACTOR_NAME);
         }
 
-        if (namedActors.isEmpty()) {
-            return Pair.create(null, ActorState.NO_NAMED_ACTORS);
-        }
-
         String actorNamespace = actorUri.getAuthority();
         Map<String, String> namespace = namedActors.get(actorNamespace);
         if (ArrayUtils.isEmpty(namespace)) {
@@ -163,15 +162,11 @@
             return ActorState.UNABLE_TO_GET_TARGET_OVERLAYABLE;
         }
 
-        // Framework doesn't have <overlayable> declaration by design, and we still want to be able
-        // to enable its overlays from the packages with the permission.
-        if (targetOverlayable == null
-                && !(Flags.rroControlForAndroidNoOverlayable() && targetPackageName.equals(
-                "android"))) {
+        if (targetOverlayable == null) {
             return ActorState.MISSING_OVERLAYABLE;
         }
 
-        final String actor = targetOverlayable == null ? null : targetOverlayable.actor;
+        String actor = targetOverlayable.actor;
         if (TextUtils.isEmpty(actor)) {
             // If there's no actor defined, fallback to the legacy permission check
             try {
diff --git a/services/core/java/com/android/server/om/OverlayReferenceMapper.java b/services/core/java/com/android/server/om/OverlayReferenceMapper.java
index fdceabe..18de995 100644
--- a/services/core/java/com/android/server/om/OverlayReferenceMapper.java
+++ b/services/core/java/com/android/server/om/OverlayReferenceMapper.java
@@ -26,15 +26,13 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.CollectionUtils;
 import com.android.server.SystemConfig;
 import com.android.server.pm.pkg.AndroidPackage;
 
 import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -121,20 +119,16 @@
                 return actorPair.first;
             }
 
-            @NonNull
+            @Nullable
             @Override
-            public Map<String, Set<String>> getTargetToOverlayables(@NonNull AndroidPackage pkg) {
+            public Pair<String, String> getTargetToOverlayables(@NonNull AndroidPackage pkg) {
                 String target = pkg.getOverlayTarget();
                 if (TextUtils.isEmpty(target)) {
-                    return Collections.emptyMap();
+                    return null;
                 }
 
                 String overlayable = pkg.getOverlayTargetOverlayableName();
-                Map<String, Set<String>> targetToOverlayables = new HashMap<>();
-                Set<String> overlayables = new HashSet<>();
-                overlayables.add(overlayable);
-                targetToOverlayables.put(target, overlayables);
-                return targetToOverlayables;
+                return Pair.create(target, overlayable);
             }
         };
     }
@@ -174,7 +168,7 @@
             }
 
             // TODO(b/135203078): Replace with isOverlay boolean flag check; fix test mocks
-            if (!mProvider.getTargetToOverlayables(pkg).isEmpty()) {
+            if (mProvider.getTargetToOverlayables(pkg) != null) {
                 addOverlay(pkg, otherPkgs, changed);
             }
 
@@ -245,20 +239,17 @@
             String target = targetPkg.getPackageName();
             removeTarget(target, changedPackages);
 
-            Map<String, String> overlayablesToActors = targetPkg.getOverlayables();
-            for (String overlayable : overlayablesToActors.keySet()) {
-                String actor = overlayablesToActors.get(overlayable);
+            final Map<String, String> overlayablesToActors = targetPkg.getOverlayables();
+            for (final var entry : overlayablesToActors.entrySet()) {
+                final String overlayable = entry.getKey();
+                final String actor = entry.getValue();
                 addTargetToMap(actor, target, changedPackages);
 
                 for (AndroidPackage overlayPkg : otherPkgs.values()) {
-                    Map<String, Set<String>> targetToOverlayables =
+                    var targetToOverlayables =
                             mProvider.getTargetToOverlayables(overlayPkg);
-                    Set<String> overlayables = targetToOverlayables.get(target);
-                    if (CollectionUtils.isEmpty(overlayables)) {
-                        continue;
-                    }
-
-                    if (overlayables.contains(overlayable)) {
+                    if (targetToOverlayables != null && targetToOverlayables.first.equals(target)
+                            && Objects.equals(targetToOverlayables.second, overlayable)) {
                         String overlay = overlayPkg.getPackageName();
                         addOverlayToMap(actor, target, overlay, changedPackages);
                     }
@@ -310,25 +301,22 @@
             String overlay = overlayPkg.getPackageName();
             removeOverlay(overlay, changedPackages);
 
-            Map<String, Set<String>> targetToOverlayables =
+            Pair<String, String> targetToOverlayables =
                     mProvider.getTargetToOverlayables(overlayPkg);
-            for (Map.Entry<String, Set<String>> entry : targetToOverlayables.entrySet()) {
-                String target = entry.getKey();
-                Set<String> overlayables = entry.getValue();
+            if (targetToOverlayables != null) {
+                String target = targetToOverlayables.first;
                 AndroidPackage targetPkg = otherPkgs.get(target);
                 if (targetPkg == null) {
-                    continue;
+                    return;
                 }
-
                 String targetPkgName = targetPkg.getPackageName();
                 Map<String, String> overlayableToActor = targetPkg.getOverlayables();
-                for (String overlayable : overlayables) {
-                    String actor = overlayableToActor.get(overlayable);
-                    if (TextUtils.isEmpty(actor)) {
-                        continue;
-                    }
-                    addOverlayToMap(actor, targetPkgName, overlay, changedPackages);
+                String overlayable = targetToOverlayables.second;
+                String actor = overlayableToActor.get(overlayable);
+                if (TextUtils.isEmpty(actor)) {
+                    return;
                 }
+                addOverlayToMap(actor, targetPkgName, overlay, changedPackages);
             }
         }
     }
@@ -430,11 +418,11 @@
         String getActorPkg(@NonNull String actor);
 
         /**
-         * Mock response of multiple overlay tags.
+         * Mock response of overlay tags.
          *
          * TODO(b/119899133): Replace with actual implementation; fix OverlayReferenceMapperTests
          */
-        @NonNull
-        Map<String, Set<String>> getTargetToOverlayables(@NonNull AndroidPackage pkg);
+        @Nullable
+        Pair<String, String> getTargetToOverlayables(@NonNull AndroidPackage pkg);
     }
 }
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 4c70d23..a0fbc00 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -125,7 +125,6 @@
 import android.content.pm.Signature;
 import android.content.pm.SigningDetails;
 import android.content.pm.VerifierInfo;
-import android.content.pm.dex.DexMetadataHelper;
 import android.content.pm.parsing.result.ParseResult;
 import android.content.pm.parsing.result.ParseTypeImpl;
 import android.net.Uri;
@@ -171,7 +170,6 @@
 import com.android.internal.pm.pkg.component.ParsedPermission;
 import com.android.internal.pm.pkg.component.ParsedPermissionGroup;
 import com.android.internal.pm.pkg.parsing.ParsingPackageUtils;
-import com.android.internal.security.VerityUtils;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.CollectionUtils;
 import com.android.server.EventLogTags;
@@ -186,7 +184,6 @@
 import com.android.server.pm.pkg.PackageStateInternal;
 import com.android.server.pm.pkg.SharedLibraryWrapper;
 import com.android.server.rollback.RollbackManagerInternal;
-import com.android.server.security.FileIntegrityService;
 import com.android.server.utils.WatchedArrayMap;
 import com.android.server.utils.WatchedLongSparseArray;
 
@@ -195,7 +192,6 @@
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
-import java.security.DigestException;
 import java.security.DigestInputStream;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
@@ -1165,11 +1161,8 @@
                 }
                 try {
                     doRenameLI(request, parsedPackage);
-                    setUpFsVerity(parsedPackage);
-                } catch (Installer.InstallerException | IOException | DigestException
-                         | NoSuchAlgorithmException | PrepareFailure e) {
-                    request.setError(PackageManagerException.INTERNAL_ERROR_VERITY_SETUP,
-                            "Failed to set up verity: " + e);
+                } catch (PrepareFailure e) {
+                    request.setError(e);
                     return false;
                 }
 
@@ -2322,68 +2315,6 @@
         }
     }
 
-    /**
-     * Set up fs-verity for the given package. For older devices that do not support fs-verity,
-     * this is a no-op.
-     */
-    private void setUpFsVerity(AndroidPackage pkg) throws Installer.InstallerException,
-            PrepareFailure, IOException, DigestException, NoSuchAlgorithmException {
-        if (!PackageManagerServiceUtils.isApkVerityEnabled()) {
-            return;
-        }
-
-        if (isIncrementalPath(pkg.getPath()) && IncrementalManager.getVersion()
-                < IncrementalManager.MIN_VERSION_TO_SUPPORT_FSVERITY) {
-            return;
-        }
-
-        // Collect files we care for fs-verity setup.
-        ArrayMap<String, String> fsverityCandidates = new ArrayMap<>();
-        fsverityCandidates.put(pkg.getBaseApkPath(),
-                VerityUtils.getFsveritySignatureFilePath(pkg.getBaseApkPath()));
-
-        final String dmPath = DexMetadataHelper.buildDexMetadataPathForApk(
-                pkg.getBaseApkPath());
-        if (new File(dmPath).exists()) {
-            fsverityCandidates.put(dmPath, VerityUtils.getFsveritySignatureFilePath(dmPath));
-        }
-
-        for (String path : pkg.getSplitCodePaths()) {
-            fsverityCandidates.put(path, VerityUtils.getFsveritySignatureFilePath(path));
-
-            final String splitDmPath = DexMetadataHelper.buildDexMetadataPathForApk(path);
-            if (new File(splitDmPath).exists()) {
-                fsverityCandidates.put(splitDmPath,
-                        VerityUtils.getFsveritySignatureFilePath(splitDmPath));
-            }
-        }
-
-        var fis = FileIntegrityService.getService();
-        for (Map.Entry<String, String> entry : fsverityCandidates.entrySet()) {
-            try {
-                final String filePath = entry.getKey();
-                if (VerityUtils.hasFsverity(filePath)) {
-                    continue;
-                }
-
-                final String signaturePath = entry.getValue();
-                if (new File(signaturePath).exists()) {
-                    // If signature is provided, enable fs-verity first so that the file can be
-                    // measured for signature check below.
-                    VerityUtils.setUpFsverity(filePath);
-
-                    if (!fis.verifyPkcs7DetachedSignature(signaturePath, filePath)) {
-                        throw new PrepareFailure(PackageManager.INSTALL_FAILED_BAD_SIGNATURE,
-                                "fs-verity signature does not verify against a known key");
-                    }
-                }
-            } catch (IOException e) {
-                throw new PrepareFailure(PackageManager.INSTALL_FAILED_BAD_SIGNATURE,
-                        "Failed to enable fs-verity: " + e);
-            }
-        }
-    }
-
     private PackageFreezer freezePackageForInstall(String packageName, int userId, int installFlags,
             String killReason, int exitInfoReason, InstallRequest request) {
         if ((installFlags & PackageManager.INSTALL_DONT_KILL_APP) != 0) {
@@ -3060,6 +2991,8 @@
         }
 
         if (succeeded) {
+            Slog.i(TAG, "installation completed:" + packageName);
+
             if (Flags.aslInApkAppMetadataSource()
                     && pkgSetting.getAppMetadataSource() == APP_METADATA_SOURCE_APK) {
                 if (!extractAppMetadataFromApk(request.getPkg(),
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index c676043..1b41c36 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -26,7 +26,6 @@
 import static android.content.pm.PackageInstaller.UNARCHIVAL_STATUS_UNSET;
 import static android.content.pm.PackageItemInfo.MAX_SAFE_LABEL_LENGTH;
 import static android.content.pm.PackageManager.INSTALL_FAILED_ABORTED;
-import static android.content.pm.PackageManager.INSTALL_FAILED_BAD_SIGNATURE;
 import static android.content.pm.PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
 import static android.content.pm.PackageManager.INSTALL_FAILED_INTERNAL_ERROR;
 import static android.content.pm.PackageManager.INSTALL_FAILED_INVALID_APK;
@@ -824,8 +823,6 @@
 
     @GuardedBy("mLock")
     private File mInheritedFilesBase;
-    @GuardedBy("mLock")
-    private boolean mVerityFoundForApks;
 
     /**
      * Both flags should be guarded with mLock whenever changes need to be in lockstep.
@@ -864,7 +861,6 @@
             } else {
                 if (DexMetadataHelper.isDexMetadataFile(file)) return false;
             }
-            if (VerityUtils.isFsveritySignatureFile(file)) return false;
             if (ApkChecksums.isDigestOrDigestSignatureFile(file)) return false;
             return true;
         }
@@ -3565,13 +3561,6 @@
                     "Missing existing base package");
         }
 
-        // Default to require only if existing base apk has fs-verity signature.
-        mVerityFoundForApks = PackageManagerServiceUtils.isApkVerityEnabled()
-                && params.mode == SessionParams.MODE_INHERIT_EXISTING
-                && VerityUtils.hasFsverity(pkgInfo.applicationInfo.getBaseCodePath())
-                && (new File(VerityUtils.getFsveritySignatureFilePath(
-                        pkgInfo.applicationInfo.getBaseCodePath()))).exists();
-
         final List<File> removedFiles = getRemovedFilesLocked();
         final List<String> removeSplitList = new ArrayList<>();
         if (!removedFiles.isEmpty()) {
@@ -3972,24 +3961,6 @@
     }
 
     @GuardedBy("mLock")
-    private void maybeStageFsveritySignatureLocked(File origFile, File targetFile,
-            boolean fsVerityRequired) throws PackageManagerException {
-        if (android.security.Flags.deprecateFsvSig()) {
-            return;
-        }
-        final File originalSignature = new File(
-                VerityUtils.getFsveritySignatureFilePath(origFile.getPath()));
-        if (originalSignature.exists()) {
-            final File stagedSignature = new File(
-                    VerityUtils.getFsveritySignatureFilePath(targetFile.getPath()));
-            stageFileLocked(originalSignature, stagedSignature);
-        } else if (fsVerityRequired) {
-            throw new PackageManagerException(INSTALL_FAILED_BAD_SIGNATURE,
-                    "Missing corresponding fs-verity signature to " + origFile);
-        }
-    }
-
-    @GuardedBy("mLock")
     private void maybeStageV4SignatureLocked(File origFile, File targetFile)
             throws PackageManagerException {
         final File originalSignature = new File(origFile.getPath() + V4Signature.EXT);
@@ -4015,11 +3986,6 @@
                 DexMetadataHelper.buildDexMetadataPathForApk(targetFile.getName()));
 
         stageFileLocked(dexMetadataFile, targetDexMetadataFile);
-
-        // Also stage .dm.fsv_sig. .dm may be required to install with fs-verity signature on
-        // supported on older devices.
-        maybeStageFsveritySignatureLocked(dexMetadataFile, targetDexMetadataFile,
-                DexMetadataHelper.isFsVerityRequired());
     }
 
     @FlaggedApi(com.android.art.flags.Flags.FLAG_ART_SERVICE_V3)
@@ -4105,44 +4071,10 @@
     }
 
     @GuardedBy("mLock")
-    private boolean isFsVerityRequiredForApk(File origFile, File targetFile)
-            throws PackageManagerException {
-        if (mVerityFoundForApks) {
-            return true;
-        }
-
-        // We haven't seen .fsv_sig for any APKs. Treat it as not required until we see one.
-        final File originalSignature = new File(
-                VerityUtils.getFsveritySignatureFilePath(origFile.getPath()));
-        if (!originalSignature.exists()) {
-            return false;
-        }
-        mVerityFoundForApks = true;
-
-        // When a signature is found, also check any previous staged APKs since they also need to
-        // have fs-verity signature consistently.
-        for (File file : mResolvedStagedFiles) {
-            if (!file.getName().endsWith(".apk")) {
-                continue;
-            }
-            // Ignore the current targeting file.
-            if (targetFile.getName().equals(file.getName())) {
-                continue;
-            }
-            throw new PackageManagerException(INSTALL_FAILED_BAD_SIGNATURE,
-                    "Previously staged apk is missing fs-verity signature");
-        }
-        return true;
-    }
-
-    @GuardedBy("mLock")
     private void resolveAndStageFileLocked(File origFile, File targetFile, String splitName,
             List<String> artManagedFilePaths) throws PackageManagerException {
         stageFileLocked(origFile, targetFile);
 
-        // Stage APK's fs-verity signature if present.
-        maybeStageFsveritySignatureLocked(origFile, targetFile,
-                isFsVerityRequiredForApk(origFile, targetFile));
         // Stage APK's v4 signature if present, and fs-verity is supported.
         if (android.security.Flags.extendVbChainToUpdatedApk()
                 && VerityUtils.isFsVeritySupported()) {
@@ -4160,16 +4092,6 @@
     }
 
     @GuardedBy("mLock")
-    private void maybeInheritFsveritySignatureLocked(File origFile) {
-        // Inherit the fsverity signature file if present.
-        final File fsveritySignatureFile = new File(
-                VerityUtils.getFsveritySignatureFilePath(origFile.getPath()));
-        if (fsveritySignatureFile.exists()) {
-            mResolvedInheritedFiles.add(fsveritySignatureFile);
-        }
-    }
-
-    @GuardedBy("mLock")
     private void maybeInheritV4SignatureLocked(File origFile) {
         // Inherit the v4 signature file if present.
         final File v4SignatureFile = new File(origFile.getPath() + V4Signature.EXT);
@@ -4182,7 +4104,6 @@
     private void inheritFileLocked(File origFile, List<String> artManagedFilePaths) {
         mResolvedInheritedFiles.add(origFile);
 
-        maybeInheritFsveritySignatureLocked(origFile);
         if (android.security.Flags.extendVbChainToUpdatedApk()) {
             maybeInheritV4SignatureLocked(origFile);
         }
@@ -4193,13 +4114,11 @@
                          artManagedFilePaths, origFile.getPath())) {
                 File artManagedFile = new File(path);
                 mResolvedInheritedFiles.add(artManagedFile);
-                maybeInheritFsveritySignatureLocked(artManagedFile);
             }
         } else {
             final File dexMetadataFile = DexMetadataHelper.findDexMetadataForFile(origFile);
             if (dexMetadataFile != null) {
                 mResolvedInheritedFiles.add(dexMetadataFile);
-                maybeInheritFsveritySignatureLocked(dexMetadataFile);
             }
         }
         // Inherit the digests if present.
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
index 7af39f7..3e376b6 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
@@ -520,22 +520,6 @@
         }
     }
 
-    /** Default is to not use fs-verity since it depends on kernel support. */
-    private static final int FSVERITY_DISABLED = 0;
-
-    /** Standard fs-verity. */
-    private static final int FSVERITY_ENABLED = 2;
-
-    /** Returns true if standard APK Verity is enabled. */
-    static boolean isApkVerityEnabled() {
-        if (android.security.Flags.deprecateFsvSig()) {
-            return false;
-        }
-        return Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.R
-                || SystemProperties.getInt("ro.apk_verity.mode", FSVERITY_DISABLED)
-                        == FSVERITY_ENABLED;
-    }
-
     /**
      * Verifies that signatures match.
      * @returns {@code true} if the compat signatures were matched; otherwise, {@code false}.
diff --git a/services/core/java/com/android/server/pm/ResilientAtomicFile.java b/services/core/java/com/android/server/pm/ResilientAtomicFile.java
index 3aefc5a..473ed61 100644
--- a/services/core/java/com/android/server/pm/ResilientAtomicFile.java
+++ b/services/core/java/com/android/server/pm/ResilientAtomicFile.java
@@ -23,6 +23,7 @@
 import android.util.Log;
 import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.security.FileIntegrity;
 
 import libcore.io.IoUtils;
@@ -121,6 +122,11 @@
     }
 
     public void finishWrite(FileOutputStream str) throws IOException {
+        finishWrite(str, true /* doFsVerity */);
+    }
+
+    @VisibleForTesting
+    public void finishWrite(FileOutputStream str, final boolean doFsVerity) throws IOException {
         if (mMainOutStream != str) {
             throw new IllegalStateException("Invalid incoming stream.");
         }
@@ -145,13 +151,15 @@
                 finalizeOutStream(reserveOutStream);
             }
 
-            // Protect both main and reserve using fs-verity.
-            try (ParcelFileDescriptor mainPfd = ParcelFileDescriptor.dup(mainInStream.getFD());
-                 ParcelFileDescriptor copyPfd = ParcelFileDescriptor.dup(reserveInStream.getFD())) {
-                FileIntegrity.setUpFsVerity(mainPfd);
-                FileIntegrity.setUpFsVerity(copyPfd);
-            } catch (IOException e) {
-                Slog.e(LOG_TAG, "Failed to verity-protect " + mDebugName, e);
+            if (doFsVerity) {
+                // Protect both main and reserve using fs-verity.
+                try (ParcelFileDescriptor mainPfd = ParcelFileDescriptor.dup(mainInStream.getFD());
+                     ParcelFileDescriptor copyPfd = ParcelFileDescriptor.dup(reserveInStream.getFD())) {
+                    FileIntegrity.setUpFsVerity(mainPfd);
+                    FileIntegrity.setUpFsVerity(copyPfd);
+                } catch (IOException e) {
+                    Slog.e(LOG_TAG, "Failed to verity-protect " + mDebugName, e);
+                }
             }
         } catch (IOException e) {
             Slog.e(LOG_TAG, "Failed to write reserve copy " + mDebugName + ": " + mReserveCopy, e);
diff --git a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java
index 17d7a14..e1b7622 100644
--- a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java
+++ b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java
@@ -612,7 +612,7 @@
                 final PackageSetting staticLibPkgSetting =
                         mPm.getPackageSettingForMutation(sharedLibraryInfo.getPackageName());
                 if (staticLibPkgSetting == null) {
-                    Slog.wtf(TAG, "Shared lib without setting: " + sharedLibraryInfo);
+                    Slog.w(TAG, "Shared lib without setting: " + sharedLibraryInfo);
                     continue;
                 }
                 for (int u = 0; u < installedUserCount; u++) {
diff --git a/services/core/java/com/android/server/pm/ShortcutPackageItem.java b/services/core/java/com/android/server/pm/ShortcutPackageItem.java
index 44789e4..027da49 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackageItem.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackageItem.java
@@ -179,7 +179,7 @@
                 itemOut.endDocument();
 
                 os.flush();
-                file.finishWrite(os);
+                mShortcutUser.mService.injectFinishWrite(file, os);
             } catch (XmlPullParserException | IOException e) {
                 Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e);
                 file.failWrite(os);
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 2785da5..373c1ed 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -1008,7 +1008,7 @@
                 out.endDocument();
 
                 // Close.
-                file.finishWrite(outs);
+                injectFinishWrite(file, outs);
             } catch (IOException e) {
                 Slog.w(TAG, "Failed to write to file " + file.getBaseFile(), e);
                 file.failWrite(outs);
@@ -1096,7 +1096,7 @@
                     saveUserInternalLocked(userId, os, /* forBackup= */ false);
                 }
 
-                file.finishWrite(os);
+                injectFinishWrite(file, os);
 
                 // Remove all dangling bitmap files.
                 cleanupDanglingBitmapDirectoriesLocked(userId);
@@ -5067,6 +5067,12 @@
         return Build.FINGERPRINT;
     }
 
+    // Injection point.
+    void injectFinishWrite(@NonNull final ResilientAtomicFile file,
+            @NonNull final FileOutputStream os) throws IOException {
+        file.finishWrite(os);
+    }
+
     final void wtf(String message) {
         wtf(message, /* exception= */ null);
     }
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 8249d65..81956fb 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -6661,7 +6661,7 @@
                                                 + userId);
                             }
                             new Thread(() -> {
-                                getActivityManagerInternal().onUserRemoved(userId);
+                                getActivityManagerInternal().onUserRemoving(userId);
                                 removeUserState(userId);
                             }).start();
                         }
@@ -6701,6 +6701,7 @@
         synchronized (mUsersLock) {
             removeUserDataLU(userId);
             mIsUserManaged.delete(userId);
+            getActivityManagerInternal().onUserRemoved(userId);
         }
         synchronized (mUserStates) {
             mUserStates.delete(userId);
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index 672eb4c..9d840d0 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -1681,8 +1681,8 @@
 
             // handle overflow
             if (attributionChainId < 0) {
-                attributionChainId = 0;
                 sAttributionChainIds.set(0);
+                attributionChainId = sAttributionChainIds.incrementAndGet();
             }
             return attributionChainId;
         }
diff --git a/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java b/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java
index 7a5a14d..b329437 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java
@@ -293,8 +293,8 @@
         if (mOverlayPaths == null && mSharedLibraryOverlayPaths == null) {
             return null;
         }
-        final OverlayPaths.Builder newPaths = new OverlayPaths.Builder();
-        newPaths.addAll(mOverlayPaths);
+        final OverlayPaths.Builder newPaths = mOverlayPaths == null
+                ? new OverlayPaths.Builder() : new OverlayPaths.Builder(mOverlayPaths);
         if (mSharedLibraryOverlayPaths != null) {
             for (final OverlayPaths libOverlayPaths : mSharedLibraryOverlayPaths.values()) {
                 newPaths.addAll(libOverlayPaths);
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 5ab5965..7f511e1 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -85,15 +85,16 @@
 
 import static com.android.hardware.input.Flags.enableNew25q2Keycodes;
 import static com.android.hardware.input.Flags.enableTalkbackAndMagnifierKeyGestures;
+import static com.android.hardware.input.Flags.enableVoiceAccessKeyGestures;
 import static com.android.hardware.input.Flags.inputManagerLifecycleSupport;
 import static com.android.hardware.input.Flags.keyboardA11yShortcutControl;
 import static com.android.hardware.input.Flags.modifierShortcutDump;
 import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow;
 import static com.android.hardware.input.Flags.useKeyGestureEventHandler;
+import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY;
 import static com.android.server.GestureLauncherService.DOUBLE_POWER_TAP_COUNT_THRESHOLD;
 import static com.android.server.flags.Flags.modifierShortcutManagerMultiuser;
 import static com.android.server.flags.Flags.newBugreportKeyboardShortcut;
-import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY;
 import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVERED;
 import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVER_ABSENT;
 import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_UNCOVERED;
@@ -502,6 +503,8 @@
 
     private TalkbackShortcutController mTalkbackShortcutController;
 
+    private VoiceAccessShortcutController mVoiceAccessShortcutController;
+
     private WindowWakeUpPolicy mWindowWakeUpPolicy;
 
     /**
@@ -562,8 +565,8 @@
     volatile boolean mPowerKeyHandled;
     volatile boolean mBackKeyHandled;
     volatile boolean mEndCallKeyHandled;
-    volatile boolean mCameraGestureTriggered;
-    volatile boolean mCameraGestureTriggeredDuringGoingToSleep;
+    volatile boolean mPowerButtonLaunchGestureTriggered;
+    volatile boolean mPowerButtonLaunchGestureTriggeredDuringGoingToSleep;
 
     /**
      * {@code true} if the device is entering a low-power state; {@code false otherwise}.
@@ -2265,6 +2268,10 @@
             return new TalkbackShortcutController(mContext);
         }
 
+        VoiceAccessShortcutController getVoiceAccessShortcutController() {
+            return new VoiceAccessShortcutController(mContext);
+        }
+
         WindowWakeUpPolicy getWindowWakeUpPolicy() {
             return new WindowWakeUpPolicy(mContext);
         }
@@ -2512,6 +2519,7 @@
                 com.android.internal.R.integer.config_keyguardDrawnTimeout);
         mKeyguardDelegate = injector.getKeyguardServiceDelegate();
         mTalkbackShortcutController = injector.getTalkbackShortcutController();
+        mVoiceAccessShortcutController = injector.getVoiceAccessShortcutController();
         mWindowWakeUpPolicy = injector.getWindowWakeUpPolicy();
         initKeyCombinationRules();
         initSingleKeyGestureRules(injector.getLooper());
@@ -4262,6 +4270,8 @@
                                 .isAccessibilityShortcutAvailable(false);
                     case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK:
                         return enableTalkbackAndMagnifierKeyGestures();
+                    case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS:
+                        return enableVoiceAccessKeyGestures();
                     default:
                         return false;
                 }
@@ -4492,6 +4502,14 @@
                     return true;
                 }
                 break;
+            case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS:
+                if (enableVoiceAccessKeyGestures()) {
+                    if (complete) {
+                        mVoiceAccessShortcutController.toggleVoiceAccess(mCurrentUserId);
+                    }
+                    return true;
+                }
+                break;
             case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION:
                 AppLaunchData data = event.getAppLaunchData();
                 if (complete && canLaunchApp && data != null
@@ -5893,7 +5911,7 @@
         if (mGestureLauncherService == null) {
             return false;
         }
-        mCameraGestureTriggered = false;
+        mPowerButtonLaunchGestureTriggered = false;
         final MutableBoolean outLaunched = new MutableBoolean(false);
         final boolean intercept =
                 mGestureLauncherService.interceptPowerKeyDown(event, interactive, outLaunched);
@@ -5903,9 +5921,9 @@
             // detector from processing the power key later on.
             return intercept;
         }
-        mCameraGestureTriggered = true;
+        mPowerButtonLaunchGestureTriggered = true;
         if (mRequestedOrSleepingDefaultDisplay) {
-            mCameraGestureTriggeredDuringGoingToSleep = true;
+            mPowerButtonLaunchGestureTriggeredDuringGoingToSleep = true;
             // Wake device up early to prevent display doing redundant turning off/on stuff.
             mWindowWakeUpPolicy.wakeUpFromPowerKeyCameraGesture();
         }
@@ -6282,13 +6300,13 @@
 
         if (mKeyguardDelegate != null) {
             mKeyguardDelegate.onFinishedGoingToSleep(pmSleepReason,
-                    mCameraGestureTriggeredDuringGoingToSleep);
+                    mPowerButtonLaunchGestureTriggeredDuringGoingToSleep);
         }
         if (mDisplayFoldController != null) {
             mDisplayFoldController.finishedGoingToSleep();
         }
-        mCameraGestureTriggeredDuringGoingToSleep = false;
-        mCameraGestureTriggered = false;
+        mPowerButtonLaunchGestureTriggeredDuringGoingToSleep = false;
+        mPowerButtonLaunchGestureTriggered = false;
     }
 
     // Called on the PowerManager's Notifier thread.
@@ -6319,10 +6337,10 @@
         mDefaultDisplayRotation.updateOrientationListener();
 
         if (mKeyguardDelegate != null) {
-            mKeyguardDelegate.onStartedWakingUp(pmWakeReason, mCameraGestureTriggered);
+            mKeyguardDelegate.onStartedWakingUp(pmWakeReason, mPowerButtonLaunchGestureTriggered);
         }
 
-        mCameraGestureTriggered = false;
+        mPowerButtonLaunchGestureTriggered = false;
     }
 
     // Called on the PowerManager's Notifier thread.
diff --git a/services/core/java/com/android/server/policy/TalkbackShortcutController.java b/services/core/java/com/android/server/policy/TalkbackShortcutController.java
index 9e16a7d..efda337 100644
--- a/services/core/java/com/android/server/policy/TalkbackShortcutController.java
+++ b/services/core/java/com/android/server/policy/TalkbackShortcutController.java
@@ -18,20 +18,15 @@
 
 import static com.android.internal.util.FrameworkStatsLog.ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_WEAR_TRIPLE_PRESS_GESTURE;
 
-import android.accessibilityservice.AccessibilityServiceInfo;
 import android.content.ComponentName;
 import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.pm.ServiceInfo;
 import android.os.UserHandle;
 import android.provider.Settings;
-import android.view.accessibility.AccessibilityManager;
 
 import com.android.internal.accessibility.util.AccessibilityStatsLogUtils;
 import com.android.internal.accessibility.util.AccessibilityUtils;
 import com.android.internal.annotations.VisibleForTesting;
 
-import java.util.List;
 import java.util.Set;
 
 /**
@@ -42,7 +37,6 @@
 class TalkbackShortcutController {
     private static final String TALKBACK_LABEL = "TalkBack";
     private final Context mContext;
-    private final PackageManager mPackageManager;
 
     public enum ShortcutSource {
         GESTURE,
@@ -51,7 +45,6 @@
 
     TalkbackShortcutController(Context context) {
         mContext = context;
-        mPackageManager = mContext.getPackageManager();
     }
 
     /**
@@ -63,7 +56,10 @@
     boolean toggleTalkback(int userId, ShortcutSource source) {
         final Set<ComponentName> enabledServices =
                 AccessibilityUtils.getEnabledServicesFromSettings(mContext, userId);
-        ComponentName componentName = getTalkbackComponent();
+        ComponentName componentName =
+                AccessibilityUtils.getInstalledAccessibilityServiceComponentNameByLabel(
+                        mContext, TALKBACK_LABEL);
+        ;
         if (componentName == null) {
             return false;
         }
@@ -83,21 +79,6 @@
         return isTalkbackAlreadyEnabled;
     }
 
-    private ComponentName getTalkbackComponent() {
-        AccessibilityManager accessibilityManager = mContext.getSystemService(
-                AccessibilityManager.class);
-        List<AccessibilityServiceInfo> serviceInfos =
-                accessibilityManager.getInstalledAccessibilityServiceList();
-
-        for (AccessibilityServiceInfo service : serviceInfos) {
-            final ServiceInfo serviceInfo = service.getResolveInfo().serviceInfo;
-            if (isTalkback(serviceInfo)) {
-                return new ComponentName(serviceInfo.packageName, serviceInfo.name);
-            }
-        }
-        return null;
-    }
-
     boolean isTalkBackShortcutGestureEnabled() {
         return Settings.System.getIntForUser(mContext.getContentResolver(),
                 Settings.System.WEAR_ACCESSIBILITY_GESTURE_ENABLED,
@@ -120,9 +101,4 @@
                 ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_WEAR_TRIPLE_PRESS_GESTURE,
                 /* serviceEnabled= */ true);
     }
-
-    private boolean isTalkback(ServiceInfo info) {
-        return TALKBACK_LABEL.equals(info.loadLabel(mPackageManager).toString())
-            && (info.applicationInfo.isSystemApp() || info.applicationInfo.isUpdatedSystemApp());
-    }
 }
diff --git a/services/core/java/com/android/server/policy/VoiceAccessShortcutController.java b/services/core/java/com/android/server/policy/VoiceAccessShortcutController.java
new file mode 100644
index 0000000..a37fb11
--- /dev/null
+++ b/services/core/java/com/android/server/policy/VoiceAccessShortcutController.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.policy;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.util.Slog;
+
+import com.android.internal.accessibility.util.AccessibilityUtils;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.util.Set;
+
+/** This class controls voice access shortcut related operations such as toggling, querying. */
+class VoiceAccessShortcutController {
+    private static final String TAG = VoiceAccessShortcutController.class.getSimpleName();
+    private static final String VOICE_ACCESS_LABEL = "Voice Access";
+
+    private final Context mContext;
+
+    VoiceAccessShortcutController(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * A function that toggles voice access service.
+     *
+     * @return whether voice access is enabled after being toggled.
+     */
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    boolean toggleVoiceAccess(int userId) {
+        final Set<ComponentName> enabledServices =
+                AccessibilityUtils.getEnabledServicesFromSettings(mContext, userId);
+        ComponentName componentName =
+                AccessibilityUtils.getInstalledAccessibilityServiceComponentNameByLabel(
+                        mContext, VOICE_ACCESS_LABEL);
+        if (componentName == null) {
+            Slog.e(TAG, "Toggle Voice Access failed due to componentName being null");
+            return false;
+        }
+
+        boolean newState = !enabledServices.contains(componentName);
+        AccessibilityUtils.setAccessibilityServiceState(mContext, componentName, newState, userId);
+
+        return newState;
+    }
+}
diff --git a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java
index da8b01a..587447b 100644
--- a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java
+++ b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java
@@ -198,7 +198,7 @@
                 if (mKeyguardState.interactiveState == INTERACTIVE_STATE_AWAKE
                         || mKeyguardState.interactiveState == INTERACTIVE_STATE_WAKING) {
                     mKeyguardService.onStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN,
-                            false /* cameraGestureTriggered */);
+                            false /* powerButtonLaunchGestureTriggered */);
                 }
                 if (mKeyguardState.interactiveState == INTERACTIVE_STATE_AWAKE) {
                     mKeyguardService.onFinishedWakingUp();
@@ -319,10 +319,10 @@
     }
 
     public void onStartedWakingUp(
-            @PowerManager.WakeReason int pmWakeReason, boolean cameraGestureTriggered) {
+            @PowerManager.WakeReason int pmWakeReason, boolean powerButtonLaunchGestureTriggered) {
         if (mKeyguardService != null) {
             if (DEBUG) Log.v(TAG, "onStartedWakingUp()");
-            mKeyguardService.onStartedWakingUp(pmWakeReason, cameraGestureTriggered);
+            mKeyguardService.onStartedWakingUp(pmWakeReason, powerButtonLaunchGestureTriggered);
         }
         mKeyguardState.interactiveState = INTERACTIVE_STATE_WAKING;
     }
@@ -383,9 +383,11 @@
     }
 
     public void onFinishedGoingToSleep(
-            @PowerManager.GoToSleepReason int pmSleepReason, boolean cameraGestureTriggered) {
+            @PowerManager.GoToSleepReason int pmSleepReason,
+            boolean powerButtonLaunchGestureTriggered) {
         if (mKeyguardService != null) {
-            mKeyguardService.onFinishedGoingToSleep(pmSleepReason, cameraGestureTriggered);
+            mKeyguardService.onFinishedGoingToSleep(pmSleepReason,
+                    powerButtonLaunchGestureTriggered);
         }
         mKeyguardState.interactiveState = INTERACTIVE_STATE_SLEEP;
     }
diff --git a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java
index cd789ea..f2342e0 100644
--- a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java
+++ b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java
@@ -113,9 +113,10 @@
 
     @Override
     public void onFinishedGoingToSleep(
-            @PowerManager.GoToSleepReason int pmSleepReason, boolean cameraGestureTriggered) {
+            @PowerManager.GoToSleepReason int pmSleepReason,
+            boolean powerButtonLaunchGestureTriggered) {
         try {
-            mService.onFinishedGoingToSleep(pmSleepReason, cameraGestureTriggered);
+            mService.onFinishedGoingToSleep(pmSleepReason, powerButtonLaunchGestureTriggered);
         } catch (RemoteException e) {
             Slog.w(TAG , "Remote Exception", e);
         }
@@ -123,9 +124,9 @@
 
     @Override
     public void onStartedWakingUp(
-            @PowerManager.WakeReason int pmWakeReason, boolean cameraGestureTriggered) {
+            @PowerManager.WakeReason int pmWakeReason, boolean powerButtonLaunchGestureTriggered) {
         try {
-            mService.onStartedWakingUp(pmWakeReason, cameraGestureTriggered);
+            mService.onStartedWakingUp(pmWakeReason, powerButtonLaunchGestureTriggered);
         } catch (RemoteException e) {
             Slog.w(TAG , "Remote Exception", e);
         }
diff --git a/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java b/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java
index 7808c4e..e09ab60 100644
--- a/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java
+++ b/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java
@@ -28,7 +28,6 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.ResolveInfo;
-import android.content.pm.Signature;
 import android.os.Environment;
 import android.permission.flags.Flags;
 import android.provider.Settings;
@@ -312,17 +311,10 @@
         DataOutputStream dataOutputStream = new DataOutputStream(new BufferedOutputStream(mdos));
         packageManagerInternal.forEachInstalledPackage(pkg -> {
             try {
-                dataOutputStream.writeUTF(pkg.getPackageName());
-                dataOutputStream.writeLong(pkg.getLongVersionCode());
+                dataOutputStream.writeUTF(pkg.getPath());
                 dataOutputStream.writeInt(packageManagerInternal.getApplicationEnabledState(
                         pkg.getPackageName(), userId));
 
-                final Set<String> requestedPermissions = pkg.getRequestedPermissions();
-                dataOutputStream.writeInt(requestedPermissions.size());
-                for (String permissionName : requestedPermissions) {
-                    dataOutputStream.writeUTF(permissionName);
-                }
-
                 final ArraySet<String> enabledComponents =
                         packageManagerInternal.getEnabledComponents(pkg.getPackageName(), userId);
                 final int enabledComponentsSize = CollectionUtils.size(enabledComponents);
@@ -337,10 +329,6 @@
                 for (int i = 0; i < disabledComponentsSize; i++) {
                     dataOutputStream.writeUTF(disabledComponents.valueAt(i));
                 }
-
-                for (final Signature signature : pkg.getSigningDetails().getSignatures()) {
-                    dataOutputStream.write(signature.toByteArray());
-                }
             } catch (IOException e) {
                 // Never happens for MessageDigestOutputStream and DataOutputStream.
                 throw new AssertionError(e);
diff --git a/services/core/java/com/android/server/power/ThermalManagerService.java b/services/core/java/com/android/server/power/ThermalManagerService.java
index 42dbb79..f46fa44 100644
--- a/services/core/java/com/android/server/power/ThermalManagerService.java
+++ b/services/core/java/com/android/server/power/ThermalManagerService.java
@@ -155,6 +155,9 @@
     @VisibleForTesting
     final TemperatureWatcher mTemperatureWatcher;
 
+    @VisibleForTesting
+    final AtomicBoolean mIsHalSkinForecastSupported = new AtomicBoolean(false);
+
     private final ThermalHalWrapper.WrapperThermalChangedCallback mWrapperCallback =
             new ThermalHalWrapper.WrapperThermalChangedCallback() {
                 @Override
@@ -254,6 +257,18 @@
             }
             onTemperatureMapChangedLocked();
             mTemperatureWatcher.getAndUpdateThresholds();
+            // we only check forecast if a single SKIN sensor threshold is reported
+            synchronized (mTemperatureWatcher.mSamples) {
+                if (mTemperatureWatcher.mSevereThresholds.size() == 1) {
+                    try {
+                        mIsHalSkinForecastSupported.set(
+                                Flags.allowThermalHalSkinForecast()
+                                        && !Float.isNaN(mHalWrapper.forecastSkinTemperature(10)));
+                    } catch (UnsupportedOperationException e) {
+                        Slog.i(TAG, "Thermal HAL does not support forecastSkinTemperature");
+                    }
+                }
+            }
             mHalReady.set(true);
         }
     }
@@ -1092,6 +1107,8 @@
         protected abstract List<TemperatureThreshold> getTemperatureThresholds(boolean shouldFilter,
                 int type);
 
+        protected abstract float forecastSkinTemperature(int forecastSeconds);
+
         protected abstract boolean connectToHal();
 
         protected abstract void dump(PrintWriter pw, String prefix);
@@ -1124,8 +1141,16 @@
     @VisibleForTesting
     static class ThermalHalAidlWrapper extends ThermalHalWrapper implements IBinder.DeathRecipient {
         /* Proxy object for the Thermal HAL AIDL service. */
+
+        @GuardedBy("mHalLock")
         private IThermal mInstance = null;
 
+        private IThermal getHalInstance() {
+            synchronized (mHalLock) {
+                return mInstance;
+            }
+        }
+
         /** Callback for Thermal HAL AIDL. */
         private final IThermalChangedCallback mThermalCallbackAidl =
                 new IThermalChangedCallback.Stub() {
@@ -1169,154 +1194,183 @@
         @Override
         protected List<Temperature> getCurrentTemperatures(boolean shouldFilter,
                 int type) {
-            synchronized (mHalLock) {
-                final List<Temperature> ret = new ArrayList<>();
-                if (mInstance == null) {
-                    return ret;
-                }
-                try {
-                    final android.hardware.thermal.Temperature[] halRet =
-                            shouldFilter ? mInstance.getTemperaturesWithType(type)
-                                    : mInstance.getTemperatures();
-                    if (halRet == null) {
-                        return ret;
-                    }
-                    for (android.hardware.thermal.Temperature t : halRet) {
-                        if (!Temperature.isValidStatus(t.throttlingStatus)) {
-                            Slog.e(TAG, "Invalid temperature status " + t.throttlingStatus
-                                    + " received from AIDL HAL");
-                            t.throttlingStatus = Temperature.THROTTLING_NONE;
-                        }
-                        if (shouldFilter && t.type != type) {
-                            continue;
-                        }
-                        ret.add(new Temperature(t.value, t.type, t.name, t.throttlingStatus));
-                    }
-                } catch (IllegalArgumentException | IllegalStateException e) {
-                    Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e);
-                } catch (RemoteException e) {
-                    Slog.e(TAG, "Couldn't getCurrentTemperatures, reconnecting", e);
-                    connectToHal();
-                }
+            final IThermal instance = getHalInstance();
+            final List<Temperature> ret = new ArrayList<>();
+            if (instance == null) {
                 return ret;
             }
+            try {
+                final android.hardware.thermal.Temperature[] halRet =
+                        shouldFilter ? instance.getTemperaturesWithType(type)
+                                : instance.getTemperatures();
+                if (halRet == null) {
+                    return ret;
+                }
+                for (android.hardware.thermal.Temperature t : halRet) {
+                    if (!Temperature.isValidStatus(t.throttlingStatus)) {
+                        Slog.e(TAG, "Invalid temperature status " + t.throttlingStatus
+                                + " received from AIDL HAL");
+                        t.throttlingStatus = Temperature.THROTTLING_NONE;
+                    }
+                    if (shouldFilter && t.type != type) {
+                        continue;
+                    }
+                    ret.add(new Temperature(t.value, t.type, t.name, t.throttlingStatus));
+                }
+            } catch (IllegalArgumentException | IllegalStateException e) {
+                Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Couldn't getCurrentTemperatures, reconnecting", e);
+                synchronized (mHalLock) {
+                    connectToHalIfNeededLocked(instance);
+                }
+            }
+            return ret;
         }
 
         @Override
         protected List<CoolingDevice> getCurrentCoolingDevices(boolean shouldFilter,
                 int type) {
-            synchronized (mHalLock) {
-                final List<CoolingDevice> ret = new ArrayList<>();
-                if (mInstance == null) {
-                    return ret;
-                }
-                try {
-                    final android.hardware.thermal.CoolingDevice[] halRet = shouldFilter
-                            ? mInstance.getCoolingDevicesWithType(type)
-                            : mInstance.getCoolingDevices();
-                    if (halRet == null) {
-                        return ret;
-                    }
-                    for (android.hardware.thermal.CoolingDevice t : halRet) {
-                        if (!CoolingDevice.isValidType(t.type)) {
-                            Slog.e(TAG, "Invalid cooling device type " + t.type + " from AIDL HAL");
-                            continue;
-                        }
-                        if (shouldFilter && t.type != type) {
-                            continue;
-                        }
-                        ret.add(new CoolingDevice(t.value, t.type, t.name));
-                    }
-                } catch (IllegalArgumentException | IllegalStateException e) {
-                    Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e);
-                } catch (RemoteException e) {
-                    Slog.e(TAG, "Couldn't getCurrentCoolingDevices, reconnecting", e);
-                    connectToHal();
-                }
+            final IThermal instance = getHalInstance();
+            final List<CoolingDevice> ret = new ArrayList<>();
+            if (instance == null) {
                 return ret;
             }
+            try {
+                final android.hardware.thermal.CoolingDevice[] halRet = shouldFilter
+                        ? instance.getCoolingDevicesWithType(type)
+                        : instance.getCoolingDevices();
+                if (halRet == null) {
+                    return ret;
+                }
+                for (android.hardware.thermal.CoolingDevice t : halRet) {
+                    if (!CoolingDevice.isValidType(t.type)) {
+                        Slog.e(TAG, "Invalid cooling device type " + t.type + " from AIDL HAL");
+                        continue;
+                    }
+                    if (shouldFilter && t.type != type) {
+                        continue;
+                    }
+                    ret.add(new CoolingDevice(t.value, t.type, t.name));
+                }
+            } catch (IllegalArgumentException | IllegalStateException e) {
+                Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Couldn't getCurrentCoolingDevices, reconnecting", e);
+                synchronized (mHalLock) {
+                    connectToHalIfNeededLocked(instance);
+                }
+            }
+            return ret;
         }
 
         @Override
         @NonNull
         protected List<TemperatureThreshold> getTemperatureThresholds(
                 boolean shouldFilter, int type) {
-            synchronized (mHalLock) {
-                final List<TemperatureThreshold> ret = new ArrayList<>();
-                if (mInstance == null) {
-                    return ret;
-                }
-                try {
-                    final TemperatureThreshold[] halRet =
-                            shouldFilter ? mInstance.getTemperatureThresholdsWithType(type)
-                                    : mInstance.getTemperatureThresholds();
-                    if (halRet == null) {
-                        return ret;
-                    }
-                    if (shouldFilter) {
-                        return Arrays.stream(halRet).filter(t -> t.type == type).collect(
-                                Collectors.toList());
-                    }
-                    return Arrays.asList(halRet);
-                } catch (IllegalArgumentException | IllegalStateException e) {
-                    Slog.e(TAG, "Couldn't getTemperatureThresholds due to invalid status", e);
-                } catch (RemoteException e) {
-                    Slog.e(TAG, "Couldn't getTemperatureThresholds, reconnecting...", e);
-                    connectToHal();
-                }
+            final IThermal instance = getHalInstance();
+            final List<TemperatureThreshold> ret = new ArrayList<>();
+            if (instance == null) {
                 return ret;
             }
+            try {
+                final TemperatureThreshold[] halRet =
+                        shouldFilter ? instance.getTemperatureThresholdsWithType(type)
+                                : instance.getTemperatureThresholds();
+                if (halRet == null) {
+                    return ret;
+                }
+                if (shouldFilter) {
+                    return Arrays.stream(halRet).filter(t -> t.type == type).collect(
+                            Collectors.toList());
+                }
+                return Arrays.asList(halRet);
+            } catch (IllegalArgumentException | IllegalStateException e) {
+                Slog.e(TAG, "Couldn't getTemperatureThresholds due to invalid status", e);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Couldn't getTemperatureThresholds, reconnecting...", e);
+                synchronized (mHalLock) {
+                    connectToHalIfNeededLocked(instance);
+                }
+            }
+            return ret;
+        }
+
+        @Override
+        protected float forecastSkinTemperature(int forecastSeconds) {
+            final IThermal instance = getHalInstance();
+            if (instance == null) {
+                return Float.NaN;
+            }
+            try {
+                return instance.forecastSkinTemperature(forecastSeconds);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Couldn't forecastSkinTemperature, reconnecting...", e);
+                synchronized (mHalLock) {
+                    connectToHalIfNeededLocked(instance);
+                }
+            }
+            return Float.NaN;
         }
 
         @Override
         protected boolean connectToHal() {
             synchronized (mHalLock) {
-                IBinder binder = Binder.allowBlocking(ServiceManager.waitForDeclaredService(
-                        IThermal.DESCRIPTOR + "/default"));
-                initProxyAndRegisterCallback(binder);
+                return connectToHalIfNeededLocked(mInstance);
             }
+        }
+
+        @GuardedBy("mHalLock")
+        protected boolean connectToHalIfNeededLocked(IThermal instance) {
+            if (instance != mInstance) {
+                // instance has been updated since last used
+                return true;
+            }
+            IBinder binder = Binder.allowBlocking(ServiceManager.waitForDeclaredService(
+                    IThermal.DESCRIPTOR + "/default"));
+            initProxyAndRegisterCallbackLocked(binder);
             return mInstance != null;
         }
 
         @VisibleForTesting
         void initProxyAndRegisterCallback(IBinder binder) {
             synchronized (mHalLock) {
-                if (binder != null) {
-                    mInstance = IThermal.Stub.asInterface(binder);
+                initProxyAndRegisterCallbackLocked(binder);
+            }
+        }
+
+        @GuardedBy("mHalLock")
+        protected void initProxyAndRegisterCallbackLocked(IBinder binder) {
+            if (binder != null) {
+                mInstance = IThermal.Stub.asInterface(binder);
+                try {
+                    binder.linkToDeath(this, 0);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Unable to connect IThermal AIDL instance", e);
+                    connectToHal();
+                }
+                if (mInstance != null) {
                     try {
-                        binder.linkToDeath(this, 0);
+                        Slog.i(TAG, "Thermal HAL AIDL service connected with version "
+                                + mInstance.getInterfaceVersion());
+                    } catch (RemoteException e) {
+                        Slog.e(TAG, "Unable to read interface version from Thermal HAL", e);
+                        connectToHal();
+                        return;
+                    }
+                    try {
+                        mInstance.registerThermalChangedCallback(mThermalCallbackAidl);
+                    } catch (IllegalArgumentException | IllegalStateException e) {
+                        Slog.e(TAG, "Couldn't registerThermalChangedCallback due to invalid status",
+                                e);
                     } catch (RemoteException e) {
                         Slog.e(TAG, "Unable to connect IThermal AIDL instance", e);
                         connectToHal();
                     }
-                    if (mInstance != null) {
-                        try {
-                            Slog.i(TAG, "Thermal HAL AIDL service connected with version "
-                                    + mInstance.getInterfaceVersion());
-                        } catch (RemoteException e) {
-                            Slog.e(TAG, "Unable to read interface version from Thermal HAL", e);
-                            connectToHal();
-                            return;
-                        }
-                        registerThermalChangedCallback();
-                    }
                 }
             }
         }
 
-        @VisibleForTesting
-        void registerThermalChangedCallback() {
-            try {
-                mInstance.registerThermalChangedCallback(mThermalCallbackAidl);
-            } catch (IllegalArgumentException | IllegalStateException e) {
-                Slog.e(TAG, "Couldn't registerThermalChangedCallback due to invalid status",
-                        e);
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Unable to connect IThermal AIDL instance", e);
-                connectToHal();
-            }
-        }
-
         @Override
         protected void dump(PrintWriter pw, String prefix) {
             synchronized (mHalLock) {
@@ -1445,6 +1499,11 @@
         }
 
         @Override
+        protected float forecastSkinTemperature(int forecastSeconds) {
+            throw new UnsupportedOperationException("Not supported in Thermal HAL 1.0");
+        }
+
+        @Override
         protected void dump(PrintWriter pw, String prefix) {
             synchronized (mHalLock) {
                 pw.print(prefix);
@@ -1583,6 +1642,11 @@
         }
 
         @Override
+        protected float forecastSkinTemperature(int forecastSeconds) {
+            throw new UnsupportedOperationException("Not supported in Thermal HAL 1.1");
+        }
+
+        @Override
         protected void dump(PrintWriter pw, String prefix) {
             synchronized (mHalLock) {
                 pw.print(prefix);
@@ -1749,6 +1813,11 @@
         }
 
         @Override
+        protected float forecastSkinTemperature(int forecastSeconds) {
+            throw new UnsupportedOperationException("Not supported in Thermal HAL 2.0");
+        }
+
+        @Override
         protected void dump(PrintWriter pw, String prefix) {
             synchronized (mHalLock) {
                 pw.print(prefix);
@@ -1977,6 +2046,39 @@
 
         float getForecast(int forecastSeconds) {
             synchronized (mSamples) {
+                // If we don't have any thresholds, we can't normalize the temperatures,
+                // so return early
+                if (mSevereThresholds.isEmpty()) {
+                    Slog.e(TAG, "No temperature thresholds found");
+                    FrameworkStatsLog.write(FrameworkStatsLog.THERMAL_HEADROOM_CALLED,
+                            Binder.getCallingUid(),
+                            THERMAL_HEADROOM_CALLED__API_STATUS__NO_TEMPERATURE_THRESHOLD,
+                            Float.NaN, forecastSeconds);
+                    return Float.NaN;
+                }
+            }
+            if (mIsHalSkinForecastSupported.get()) {
+                float threshold = -1f;
+                synchronized (mSamples) {
+                    // we only do forecast if a single SKIN sensor threshold is reported
+                    if (mSevereThresholds.size() == 1) {
+                        threshold = mSevereThresholds.valueAt(0);
+                    }
+                }
+                if (threshold > 0) {
+                    try {
+                        final float forecastTemperature =
+                                mHalWrapper.forecastSkinTemperature(forecastSeconds);
+                        return normalizeTemperature(forecastTemperature, threshold);
+                    } catch (UnsupportedOperationException e) {
+                        Slog.wtf(TAG, "forecastSkinTemperature returns unsupported");
+                    } catch (Exception e) {
+                        Slog.e(TAG, "forecastSkinTemperature fails");
+                    }
+                    return Float.NaN;
+                }
+            }
+            synchronized (mSamples) {
                 mLastForecastCallTimeMillis = SystemClock.elapsedRealtime();
                 if (mSamples.isEmpty()) {
                     getAndUpdateTemperatureSamples();
@@ -1993,17 +2095,6 @@
                     return Float.NaN;
                 }
 
-                // If we don't have any thresholds, we can't normalize the temperatures,
-                // so return early
-                if (mSevereThresholds.isEmpty()) {
-                    Slog.e(TAG, "No temperature thresholds found");
-                    FrameworkStatsLog.write(FrameworkStatsLog.THERMAL_HEADROOM_CALLED,
-                            Binder.getCallingUid(),
-                            THERMAL_HEADROOM_CALLED__API_STATUS__NO_TEMPERATURE_THRESHOLD,
-                            Float.NaN, forecastSeconds);
-                    return Float.NaN;
-                }
-
                 if (mCachedHeadrooms.contains(forecastSeconds)) {
                     // TODO(b/360486877): replace with metrics
                     Slog.d(TAG,
diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java
index 1726f0d..1cf24fc 100644
--- a/services/core/java/com/android/server/power/hint/HintManagerService.java
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -20,7 +20,6 @@
 
 import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
 import static com.android.server.power.hint.Flags.adpfSessionTag;
-import static com.android.server.power.hint.Flags.cpuHeadroomAffinityCheck;
 import static com.android.server.power.hint.Flags.powerhintThreadCleanup;
 import static com.android.server.power.hint.Flags.resetOnForkEnabled;
 
@@ -121,6 +120,8 @@
     @VisibleForTesting final long mHintSessionPreferredRate;
 
     @VisibleForTesting static final int MAX_GRAPHICS_PIPELINE_THREADS_COUNT = 5;
+    private static final int DEFAULT_MAX_CPU_HEADROOM_THREADS_COUNT = 5;
+    private static final int DEFAULT_CHECK_HEADROOM_PROC_STAT_MIN_MILLIS = 50;
 
     // Multi-level map storing all active AppHintSessions.
     // First level is keyed by the UID of the client process creating the session.
@@ -206,12 +207,17 @@
             "persist.hms.check_headroom_affinity";
     private static final String PROPERTY_CHECK_HEADROOM_PROC_STAT_MIN_MILLIS =
             "persist.hms.check_headroom_proc_stat_min_millis";
+    private static final String PROPERTY_CPU_HEADROOM_TID_MAX_CNT =
+            "persist.hms.cpu_headroom_tid_max_cnt";
     private Boolean mFMQUsesIntegratedEventFlag = false;
 
     private final Object mCpuHeadroomLock = new Object();
     @VisibleForTesting
     final float mJiffyMillis;
+    private final boolean mCheckHeadroomTid;
+    private final boolean mCheckHeadroomAffinity;
     private final int mCheckHeadroomProcStatMinMillis;
+    private final int mCpuHeadroomMaxTidCnt;
     @GuardedBy("mCpuHeadroomLock")
     private long mLastCpuUserModeTimeCheckedMillis = 0;
     @GuardedBy("mCpuHeadroomLock")
@@ -339,13 +345,23 @@
             mUidToLastUserModeJiffies = new ArrayMap<>();
             long jiffyHz = Os.sysconf(OsConstants._SC_CLK_TCK);
             mJiffyMillis = 1000.0f / jiffyHz;
+            mCheckHeadroomTid = SystemProperties.getBoolean(PROPERTY_CHECK_HEADROOM_TID, true);
+            mCheckHeadroomAffinity = SystemProperties.getBoolean(PROPERTY_CHECK_HEADROOM_AFFINITY,
+                    true);
             mCheckHeadroomProcStatMinMillis = SystemProperties.getInt(
-                    PROPERTY_CHECK_HEADROOM_PROC_STAT_MIN_MILLIS, 50);
+                    PROPERTY_CHECK_HEADROOM_PROC_STAT_MIN_MILLIS,
+                    DEFAULT_CHECK_HEADROOM_PROC_STAT_MIN_MILLIS);
+            mCpuHeadroomMaxTidCnt = Math.min(SystemProperties.getInt(
+                    PROPERTY_CPU_HEADROOM_TID_MAX_CNT, DEFAULT_MAX_CPU_HEADROOM_THREADS_COUNT),
+                    mSupportInfo.headroom.cpuMaxTidCount);
         } else {
             mCpuHeadroomCache = null;
             mUidToLastUserModeJiffies = null;
             mJiffyMillis = 0.0f;
+            mCheckHeadroomTid = true;
+            mCheckHeadroomAffinity = true;
             mCheckHeadroomProcStatMinMillis = 0;
+            mCpuHeadroomMaxTidCnt = 0;
         }
         if (mSupportInfo.headroom.isGpuSupported) {
             mGpuHeadroomCache = new HeadroomCache<>(2, mSupportInfo.headroom.gpuMinIntervalMillis);
@@ -1577,8 +1593,7 @@
             if (params.usesDeviceHeadroom) {
                 halParams.tids = new int[]{};
             } else if (params.tids != null && params.tids.length > 0) {
-                if (UserHandle.getAppId(uid) != Process.SYSTEM_UID && SystemProperties.getBoolean(
-                        PROPERTY_CHECK_HEADROOM_TID, true)) {
+                if (UserHandle.getAppId(uid) != Process.SYSTEM_UID && mCheckHeadroomTid) {
                     final int tgid = Process.getThreadGroupLeader(Binder.getCallingPid());
                     for (int tid : params.tids) {
                         if (Process.getThreadGroupLeader(tid) != tgid) {
@@ -1588,8 +1603,7 @@
                         }
                     }
                 }
-                if (cpuHeadroomAffinityCheck() && params.tids.length > 1
-                        && SystemProperties.getBoolean(PROPERTY_CHECK_HEADROOM_AFFINITY, true)) {
+                if (mCheckHeadroomAffinity && params.tids.length > 1) {
                     checkThreadAffinityForTids(params.tids);
                 }
                 halParams.tids = params.tids;
@@ -1709,15 +1723,22 @@
                 throw new IllegalArgumentException(
                         "Unknown CPU headroom calculation type " + (int) params.calculationType);
             }
-            if (params.calculationWindowMillis < 50 || params.calculationWindowMillis > 10000) {
+            if (params.calculationWindowMillis < mSupportInfo.headroom.cpuMinCalculationWindowMillis
+                    || params.calculationWindowMillis
+                    > mSupportInfo.headroom.cpuMaxCalculationWindowMillis) {
                 throw new IllegalArgumentException(
-                        "Invalid CPU headroom calculation window, expected [50, 10000] but got "
+                        "Invalid CPU headroom calculation window, expected ["
+                                + mSupportInfo.headroom.cpuMinCalculationWindowMillis
+                                + ", "
+                                + mSupportInfo.headroom.cpuMaxCalculationWindowMillis
+                                + "] but got "
                                 + params.calculationWindowMillis);
             }
             if (!params.usesDeviceHeadroom) {
-                if (params.tids != null && params.tids.length > 5) {
+                if (params.tids != null && params.tids.length > mCpuHeadroomMaxTidCnt) {
                     throw new IllegalArgumentException(
-                            "More than 5 TIDs requested: " + params.tids.length);
+                            "More than " + mCpuHeadroomMaxTidCnt + " TIDs requested: "
+                                    + params.tids.length);
                 }
             }
         }
@@ -1772,9 +1793,13 @@
                 throw new IllegalArgumentException(
                         "Unknown GPU headroom calculation type " + (int) params.calculationType);
             }
-            if (params.calculationWindowMillis < 50 || params.calculationWindowMillis > 10000) {
+            if (params.calculationWindowMillis < mSupportInfo.headroom.gpuMinCalculationWindowMillis
+                    || params.calculationWindowMillis
+                    > mSupportInfo.headroom.gpuMaxCalculationWindowMillis) {
                 throw new IllegalArgumentException(
-                        "Invalid GPU headroom calculation window, expected [50, 10000] but got "
+                        "Invalid GPU headroom calculation window, expected ["
+                                + mSupportInfo.headroom.gpuMinCalculationWindowMillis + ", "
+                                + mSupportInfo.headroom.gpuMaxCalculationWindowMillis + "] but got "
                                 + params.calculationWindowMillis);
             }
         }
@@ -1807,9 +1832,15 @@
         @Override
         public IHintManager.HintManagerClientData
                 registerClient(@NonNull IHintManager.IHintManagerClient clientBinder) {
+            return getClientData();
+        }
+
+        @Override
+        public IHintManager.HintManagerClientData getClientData() {
             IHintManager.HintManagerClientData out = new IHintManager.HintManagerClientData();
             out.preferredRateNanos = mHintSessionPreferredRate;
             out.maxGraphicsPipelineThreads = getMaxGraphicsPipelineThreadsCount();
+            out.maxCpuHeadroomThreads = DEFAULT_MAX_CPU_HEADROOM_THREADS_COUNT;
             out.powerHalVersion = mPowerHalVersion;
             out.supportInfo = mSupportInfo;
             return out;
@@ -1838,23 +1869,40 @@
                     }
                 }
             }
-            pw.println("CPU Headroom Interval: " + mSupportInfo.headroom.cpuMinIntervalMillis);
-            pw.println("GPU Headroom Interval: " + mSupportInfo.headroom.gpuMinIntervalMillis);
-            try {
-                CpuHeadroomParamsInternal params = new CpuHeadroomParamsInternal();
-                params.usesDeviceHeadroom = true;
-                CpuHeadroomResult ret = getCpuHeadroom(params);
-                pw.println("CPU headroom: " + (ret == null ? "N/A" : ret.getGlobalHeadroom()));
-            } catch (Exception e) {
-                Slog.d(TAG, "Failed to dump CPU headroom", e);
-                pw.println("CPU headroom: N/A");
+            pw.println("CPU Headroom Supported: " + mSupportInfo.headroom.isCpuSupported);
+            if (mSupportInfo.headroom.isCpuSupported) {
+                pw.println("CPU Headroom Interval: " + mSupportInfo.headroom.cpuMinIntervalMillis);
+                pw.println("CPU Headroom TID Max Count: " + mCpuHeadroomMaxTidCnt);
+                pw.println("CPU Headroom TID Max Count From HAL: "
+                        + mSupportInfo.headroom.cpuMaxTidCount);
+                pw.println("CPU Headroom Calculation Window Range: ["
+                        + mSupportInfo.headroom.cpuMinCalculationWindowMillis + ", "
+                        + mSupportInfo.headroom.cpuMaxCalculationWindowMillis + "]");
+                try {
+                    CpuHeadroomParamsInternal params = new CpuHeadroomParamsInternal();
+                    params.usesDeviceHeadroom = true;
+                    CpuHeadroomResult ret = getCpuHeadroom(params);
+                    pw.println("CPU headroom: " + (ret == null ? "N/A" : ret.getGlobalHeadroom()));
+                } catch (Exception e) {
+                    Slog.d(TAG, "Failed to dump CPU headroom", e);
+                    pw.println("CPU headroom: N/A");
+                }
             }
-            try {
-                GpuHeadroomResult ret = getGpuHeadroom(new GpuHeadroomParamsInternal());
-                pw.println("GPU headroom: " + (ret == null ? "N/A" : ret.getGlobalHeadroom()));
-            } catch (Exception e) {
-                Slog.d(TAG, "Failed to dump GPU headroom", e);
-                pw.println("GPU headroom: N/A");
+            pw.println("GPU Headroom Supported: " + mSupportInfo.headroom.isGpuSupported);
+            if (mSupportInfo.headroom.isGpuSupported) {
+                pw.println("GPU Headroom Interval: " + mSupportInfo.headroom.gpuMinIntervalMillis);
+                pw.println("GPU Headroom Calculation Window Range: ["
+                        + mSupportInfo.headroom.gpuMinCalculationWindowMillis + ", "
+                        + mSupportInfo.headroom.gpuMaxCalculationWindowMillis + "]");
+                try {
+                    GpuHeadroomParamsInternal params = new GpuHeadroomParamsInternal();
+                    params.calculationWindowMillis = mDefaultGpuHeadroomCalculationWindowMillis;
+                    GpuHeadroomResult ret = getGpuHeadroom(params);
+                    pw.println("GPU headroom: " + (ret == null ? "N/A" : ret.getGlobalHeadroom()));
+                } catch (Exception e) {
+                    Slog.d(TAG, "Failed to dump GPU headroom", e);
+                    pw.println("GPU headroom: N/A");
+                }
             }
         }
 
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 caaf5a2..9206cce 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -2205,6 +2205,11 @@
                 getWakelockDurationRetriever() {
             return mWakelockDurationRetriever;
         }
+
+        @Override
+        public NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats) {
+            return BatteryStatsImpl.this.networkStatsDelta(stats, oldStats);
+        }
     }
 
     private final PowerStatsCollectorInjector mPowerStatsCollectorInjector =
@@ -12392,83 +12397,13 @@
         return networkStatsManager.getWifiUidStats();
     }
 
-    static class NetworkStatsDelta {
-        int mUid;
-        int mSet;
-        long mRxBytes;
-        long mRxPackets;
-        long mTxBytes;
-        long mTxPackets;
-
-        public int getUid() {
-            return mUid;
+    @VisibleForTesting
+    protected NetworkStats networkStatsDelta(@NonNull NetworkStats stats,
+            @Nullable NetworkStats oldStats) {
+        if (oldStats == null) {
+            return stats;
         }
-
-
-        public int getSet() {
-            return mSet;
-        }
-
-        public long getRxBytes() {
-            return mRxBytes;
-        }
-
-        public long getRxPackets() {
-            return mRxPackets;
-        }
-
-        public long getTxBytes() {
-            return mTxBytes;
-        }
-
-        public long getTxPackets() {
-            return mTxPackets;
-        }
-
-        @Override
-        public String toString() {
-            return "NetworkStatsDelta{mUid=" + mUid + ", mSet=" + mSet + ", mRxBytes=" + mRxBytes
-                    + ", mRxPackets=" + mRxPackets + ", mTxBytes=" + mTxBytes + ", mTxPackets="
-                    + mTxPackets + '}';
-        }
-    }
-
-    static List<NetworkStatsDelta> computeDelta(NetworkStats currentStats,
-            NetworkStats lastStats) {
-        List<NetworkStatsDelta> deltaList = new ArrayList<>();
-        for (NetworkStats.Entry entry : currentStats) {
-            NetworkStatsDelta delta = new NetworkStatsDelta();
-            delta.mUid = entry.getUid();
-            delta.mSet = entry.getSet();
-            NetworkStats.Entry lastEntry = null;
-            if (lastStats != null) {
-                for (NetworkStats.Entry e : lastStats) {
-                    if (e.getUid() == entry.getUid() && e.getSet() == entry.getSet()
-                            && e.getTag() == entry.getTag()
-                            && e.getMetered() == entry.getMetered()
-                            && e.getRoaming() == entry.getRoaming()
-                            && e.getDefaultNetwork() == entry.getDefaultNetwork()
-                            /*&& Objects.equals(e.getIface(), entry.getIface())*/) {
-                        lastEntry = e;
-                        break;
-                    }
-                }
-            }
-            if (lastEntry != null) {
-                delta.mRxBytes = Math.max(0, entry.getRxBytes() - lastEntry.getRxBytes());
-                delta.mRxPackets = Math.max(0, entry.getRxPackets() - lastEntry.getRxPackets());
-                delta.mTxBytes = Math.max(0, entry.getTxBytes() - lastEntry.getTxBytes());
-                delta.mTxPackets = Math.max(0, entry.getTxPackets() - lastEntry.getTxPackets());
-            } else {
-                delta.mRxBytes = entry.getRxBytes();
-                delta.mRxPackets = entry.getRxPackets();
-                delta.mTxBytes = entry.getTxBytes();
-                delta.mTxPackets = entry.getTxPackets();
-            }
-            deltaList.add(delta);
-        }
-
-        return deltaList;
+        return stats.subtract(oldStats);
     }
 
     /**
@@ -12486,12 +12421,12 @@
             }
         }
 
+        NetworkStats delta;
         // Grab a separate lock to acquire the network stats, which may do I/O.
-        List<NetworkStatsDelta> delta;
         synchronized (mWifiNetworkLock) {
             final NetworkStats latestStats = readWifiNetworkStatsLocked(networkStatsManager);
             if (latestStats != null) {
-                delta = computeDelta(latestStats, mLastWifiNetworkStats);
+                delta = networkStatsDelta(latestStats, mLastWifiNetworkStats);
                 mLastWifiNetworkStats = latestStats;
             } else {
                 delta = null;
@@ -12501,15 +12436,15 @@
     }
 
     private void onWifiPowerStatsRetrieved(WifiActivityEnergyInfo wifiActivityEnergyInfo,
-            List<NetworkStatsDelta> networkStatsDeltas, long elapsedRealtimeMs, long uptimeMs) {
+            NetworkStats networkStatsDelta, long elapsedRealtimeMs, long uptimeMs) {
         // Do not populate consumed energy, because energy attribution is done by
         // WifiPowerStatsProcessor.
-        updateWifiBatteryStats(wifiActivityEnergyInfo, networkStatsDeltas, POWER_DATA_UNAVAILABLE,
+        updateWifiBatteryStats(wifiActivityEnergyInfo, networkStatsDelta, POWER_DATA_UNAVAILABLE,
                 elapsedRealtimeMs, uptimeMs);
     }
 
     private void updateWifiBatteryStats(WifiActivityEnergyInfo info,
-            List<NetworkStatsDelta> delta, long consumedChargeUC, long elapsedRealtimeMs,
+            NetworkStats delta, long consumedChargeUC, long elapsedRealtimeMs,
             long uptimeMs) {
         synchronized (this) {
             if (!mOnBatteryInternal || mIgnoreNextExternalStats) {
@@ -12535,7 +12470,7 @@
             long totalTxPackets = 0;
             long totalRxPackets = 0;
             if (delta != null) {
-                for (NetworkStatsDelta entry : delta) {
+                for (NetworkStats.Entry entry : delta) {
                     if (DEBUG_ENERGY) {
                         Slog.d(TAG, "Wifi uid " + entry.getUid()
                                 + ": delta rx=" + entry.getRxBytes()
@@ -12879,11 +12814,11 @@
         mLastModemActivityInfo = activityInfo;
 
         // Grab a separate lock to acquire the network stats, which may do I/O.
-        List<NetworkStatsDelta> delta = null;
+        NetworkStats delta = null;
         synchronized (mModemNetworkLock) {
             final NetworkStats latestStats = readMobileNetworkStatsLocked(networkStatsManager);
             if (latestStats != null) {
-                delta = computeDelta(latestStats, mLastModemNetworkStats);
+                delta = networkStatsDelta(latestStats, mLastModemNetworkStats);
                 mLastModemNetworkStats = latestStats;
             }
         }
@@ -12892,15 +12827,15 @@
     }
 
     private void onMobileRadioPowerStatsRetrieved(ModemActivityInfo modemActivityInfo,
-            List<NetworkStatsDelta> networkStatsDeltas, long elapsedRealtimeMs, long uptimeMs) {
+            NetworkStats networkStatsDelta, long elapsedRealtimeMs, long uptimeMs) {
         // Do not populate consumed energy, because energy attribution is done by
         // MobileRadioPowerStatsProcessor.
-        updateCellularBatteryStats(modemActivityInfo, networkStatsDeltas, POWER_DATA_UNAVAILABLE,
+        updateCellularBatteryStats(modemActivityInfo, networkStatsDelta, POWER_DATA_UNAVAILABLE,
                 elapsedRealtimeMs, uptimeMs);
     }
 
     private void updateCellularBatteryStats(@Nullable ModemActivityInfo deltaInfo,
-            @Nullable List<NetworkStatsDelta> delta, long consumedChargeUC, long elapsedRealtimeMs,
+            @Nullable NetworkStats delta, long consumedChargeUC, long elapsedRealtimeMs,
             long uptimeMs) {
         // Add modem tx power to history.
         addModemTxPowerToHistory(deltaInfo, elapsedRealtimeMs, uptimeMs);
@@ -13003,7 +12938,7 @@
             long totalRxPackets = 0;
             long totalTxPackets = 0;
             if (delta != null) {
-                for (NetworkStatsDelta entry : delta) {
+                for (NetworkStats.Entry entry : delta) {
                     if (entry.getRxPackets() == 0 && entry.getTxPackets() == 0) {
                         continue;
                     }
@@ -13044,7 +12979,7 @@
                 // Now distribute proportional blame to the apps that did networking.
                 long totalPackets = totalRxPackets + totalTxPackets;
                 if (totalPackets > 0) {
-                    for (NetworkStatsDelta entry : delta) {
+                    for (NetworkStats.Entry entry : delta) {
                         if (entry.getRxPackets() == 0 && entry.getTxPackets() == 0) {
                             continue;
                         }
diff --git a/services/core/java/com/android/server/power/stats/MobileRadioPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/MobileRadioPowerStatsCollector.java
index cbd6fab..f971e2e 100644
--- a/services/core/java/com/android/server/power/stats/MobileRadioPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/MobileRadioPowerStatsCollector.java
@@ -38,7 +38,6 @@
 import com.android.server.power.stats.format.MobileRadioPowerStatsLayout;
 
 import java.util.Arrays;
-import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.function.LongSupplier;
@@ -71,7 +70,7 @@
     interface Observer {
         void onMobileRadioPowerStatsRetrieved(
                 @Nullable ModemActivityInfo modemActivityDelta,
-                @Nullable List<BatteryStatsImpl.NetworkStatsDelta> networkStatsDeltas,
+                @Nullable NetworkStats networkStatsDeltas,
                 long elapsedRealtimeMs, long uptimeMs);
     }
 
@@ -86,6 +85,8 @@
         TelephonyManager getTelephonyManager();
         LongSupplier getCallDurationSupplier();
         LongSupplier getPhoneSignalScanDurationSupplier();
+
+        NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats);
     }
 
     private final Injector mInjector;
@@ -190,7 +191,7 @@
         mPowerStats.uidStats.clear();
 
         ModemActivityInfo modemActivityDelta = collectModemActivityInfo();
-        List<BatteryStatsImpl.NetworkStatsDelta> networkStatsDeltas = collectNetworkStats();
+        NetworkStats networkStatsDeltas = collectNetworkStats();
 
         mConsumedEnergyHelper.collectConsumedEnergy(mPowerStats, mLayout);
 
@@ -288,17 +289,15 @@
         return deltaInfo;
     }
 
-    private List<BatteryStatsImpl.NetworkStatsDelta> collectNetworkStats() {
+    private NetworkStats collectNetworkStats() {
         NetworkStats networkStats = mNetworkStatsSupplier.get();
         if (networkStats == null) {
             return null;
         }
 
-        List<BatteryStatsImpl.NetworkStatsDelta> delta =
-                BatteryStatsImpl.computeDelta(networkStats, mLastNetworkStats);
+        NetworkStats delta = mInjector.networkStatsDelta(networkStats, mLastNetworkStats);
         mLastNetworkStats = networkStats;
-        for (int i = delta.size() - 1; i >= 0; i--) {
-            BatteryStatsImpl.NetworkStatsDelta uidDelta = delta.get(i);
+        for (NetworkStats.Entry uidDelta : delta) {
             long rxBytes = uidDelta.getRxBytes();
             long txBytes = uidDelta.getTxBytes();
             long rxPackets = uidDelta.getRxPackets();
diff --git a/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java
index 1fdeac9..5440bcf 100644
--- a/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java
@@ -31,7 +31,6 @@
 import com.android.server.power.stats.format.WifiPowerStatsLayout;
 
 import java.util.Arrays;
-import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Supplier;
@@ -43,7 +42,7 @@
 
     interface Observer {
         void onWifiPowerStatsRetrieved(WifiActivityEnergyInfo info,
-                List<BatteryStatsImpl.NetworkStatsDelta> delta, long elapsedRealtimeMs,
+                NetworkStats delta, long elapsedRealtimeMs,
                 long uptimeMs);
     }
 
@@ -66,6 +65,8 @@
         Supplier<NetworkStats> getWifiNetworkStatsSupplier();
         WifiManager getWifiManager();
         WifiStatsRetriever getWifiStatsRetriever();
+
+        NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats);
     }
 
     private final Injector mInjector;
@@ -161,7 +162,7 @@
         } else {
             collectWifiActivityStats();
         }
-        List<BatteryStatsImpl.NetworkStatsDelta> networkStatsDeltas = collectNetworkStats();
+        NetworkStats networkStatsDeltas = collectNetworkStats();
         collectWifiScanTime();
 
         mConsumedEnergyHelper.collectConsumedEnergy(mPowerStats, mLayout);
@@ -227,17 +228,15 @@
         mPowerStats.durationMs = duration;
     }
 
-    private List<BatteryStatsImpl.NetworkStatsDelta> collectNetworkStats() {
+    private NetworkStats collectNetworkStats() {
         NetworkStats networkStats = mNetworkStatsSupplier.get();
         if (networkStats == null) {
             return null;
         }
 
-        List<BatteryStatsImpl.NetworkStatsDelta> delta =
-                BatteryStatsImpl.computeDelta(networkStats, mLastNetworkStats);
+        NetworkStats delta = mInjector.networkStatsDelta(networkStats, mLastNetworkStats);
         mLastNetworkStats = networkStats;
-        for (int i = delta.size() - 1; i >= 0; i--) {
-            BatteryStatsImpl.NetworkStatsDelta uidDelta = delta.get(i);
+        for (NetworkStats.Entry uidDelta : delta) {
             long rxBytes = uidDelta.getRxBytes();
             long txBytes = uidDelta.getTxBytes();
             long rxPackets = uidDelta.getRxPackets();
diff --git a/services/core/java/com/android/server/rollback/RollbackStore.java b/services/core/java/com/android/server/rollback/RollbackStore.java
index 14539d5..50db1e4 100644
--- a/services/core/java/com/android/server/rollback/RollbackStore.java
+++ b/services/core/java/com/android/server/rollback/RollbackStore.java
@@ -84,8 +84,12 @@
      */
     private static List<Rollback> loadRollbacks(File rollbackDataDir) {
         List<Rollback> rollbacks = new ArrayList<>();
-        rollbackDataDir.mkdirs();
-        for (File rollbackDir : rollbackDataDir.listFiles()) {
+        File[] rollbackDirs = rollbackDataDir.listFiles();
+        if (rollbackDirs == null) {
+            Slog.e(TAG, "Folder doesn't exist: " + rollbackDataDir);
+            return rollbacks;
+        }
+        for (File rollbackDir : rollbackDirs) {
             if (rollbackDir.isDirectory()) {
                 try {
                     rollbacks.add(loadRollback(rollbackDir));
diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java
index d69150d..a1f72be 100644
--- a/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java
+++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java
@@ -15,7 +15,6 @@
  */
 package com.android.server.selinux;
 
-import android.provider.DeviceConfig;
 import android.text.TextUtils;
 import android.util.Slog;
 
@@ -34,10 +33,6 @@
 
     private static final String TAG = "SelinuxAuditLogs";
 
-    // This config indicates which Selinux logs for source domains to collect. The string will be
-    // inserted into a regex, so it must follow the regex syntax. For example, a valid value would
-    // be "system_server|untrusted_app".
-    @VisibleForTesting static final String CONFIG_SELINUX_AUDIT_DOMAIN = "selinux_audit_domain";
     private static final Matcher NO_OP_MATCHER = Pattern.compile("no-op^").matcher("");
     private static final String TCONTEXT_PATTERN =
             "u:object_r:(?<ttype>\\w+):s0(:c)?(?<tcategories>((,c)?\\d+)+)*";
@@ -50,7 +45,7 @@
     private Iterator<String> mTokens;
     private final SelinuxAuditLog mAuditLog = new SelinuxAuditLog();
 
-    SelinuxAuditLogBuilder() {
+    SelinuxAuditLogBuilder(String auditDomain) {
         Matcher scontextMatcher = NO_OP_MATCHER;
         Matcher tcontextMatcher = NO_OP_MATCHER;
         Matcher pathMatcher = NO_OP_MATCHER;
@@ -59,10 +54,7 @@
                     Pattern.compile(
                                     TextUtils.formatSimple(
                                             "u:r:(?<stype>%s):s0(:c)?(?<scategories>((,c)?\\d+)+)*",
-                                            DeviceConfig.getString(
-                                                    DeviceConfig.NAMESPACE_ADSERVICES,
-                                                    CONFIG_SELINUX_AUDIT_DOMAIN,
-                                                    "no_match^")))
+                                            auditDomain))
                             .matcher("");
             tcontextMatcher = Pattern.compile(TCONTEXT_PATTERN).matcher("");
             pathMatcher = Pattern.compile(PATH_PATTERN).matcher("");
diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java
index c655d46..0aa7058 100644
--- a/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java
+++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java
@@ -15,6 +15,7 @@
  */
 package com.android.server.selinux;
 
+import android.provider.DeviceConfig;
 import android.util.EventLog;
 import android.util.EventLog.Event;
 import android.util.Log;
@@ -32,6 +33,7 @@
 import java.util.List;
 import java.util.Queue;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -43,9 +45,16 @@
 
     private static final String SELINUX_PATTERN = "^.*\\bavc:\\s+(?<denial>.*)$";
 
+    // This config indicates which Selinux logs for source domains to collect. The string will be
+    // inserted into a regex, so it must follow the regex syntax. For example, a valid value would
+    // be "system_server|untrusted_app".
+    @VisibleForTesting static final String CONFIG_SELINUX_AUDIT_DOMAIN = "selinux_audit_domain";
+    @VisibleForTesting static final String DEFAULT_SELINUX_AUDIT_DOMAIN = "no_match^";
+
     @VisibleForTesting
     static final Matcher SELINUX_MATCHER = Pattern.compile(SELINUX_PATTERN).matcher("");
 
+    private final Supplier<String> mAuditDomainSupplier;
     private final RateLimiter mRateLimiter;
     private final QuotaLimiter mQuotaLimiter;
 
@@ -53,11 +62,26 @@
 
     AtomicBoolean mStopRequested = new AtomicBoolean(false);
 
-    SelinuxAuditLogsCollector(RateLimiter rateLimiter, QuotaLimiter quotaLimiter) {
+    SelinuxAuditLogsCollector(
+            Supplier<String> auditDomainSupplier,
+            RateLimiter rateLimiter,
+            QuotaLimiter quotaLimiter) {
+        mAuditDomainSupplier = auditDomainSupplier;
         mRateLimiter = rateLimiter;
         mQuotaLimiter = quotaLimiter;
     }
 
+    SelinuxAuditLogsCollector(RateLimiter rateLimiter, QuotaLimiter quotaLimiter) {
+        this(
+                () ->
+                        DeviceConfig.getString(
+                                DeviceConfig.NAMESPACE_ADSERVICES,
+                                CONFIG_SELINUX_AUDIT_DOMAIN,
+                                DEFAULT_SELINUX_AUDIT_DOMAIN),
+                rateLimiter,
+                quotaLimiter);
+    }
+
     public void setStopRequested(boolean stopRequested) {
         mStopRequested.set(stopRequested);
     }
@@ -108,7 +132,8 @@
     }
 
     private boolean writeAuditLogs(Queue<Event> logLines) {
-        final SelinuxAuditLogBuilder auditLogBuilder = new SelinuxAuditLogBuilder();
+        final SelinuxAuditLogBuilder auditLogBuilder =
+                new SelinuxAuditLogBuilder(mAuditDomainSupplier.get());
         int auditsWritten = 0;
 
         while (!mStopRequested.get() && !logLines.isEmpty()) {
diff --git a/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java b/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java
index 2088e41..3831352 100644
--- a/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java
+++ b/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java
@@ -142,11 +142,8 @@
     private final RateLimiter mRateLimiter;
 
     AggregatedMobileDataStatsPuller(@NonNull NetworkStatsManager networkStatsManager) {
-        if (DEBUG) {
-            if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) {
-                Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER,
-                        TAG + "-AggregatedMobileDataStatsPullerInit");
-            }
+        if (DEBUG && Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) {
+            Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, TAG + "-Init");
         }
 
         mRateLimiter = new RateLimiter(/* window= */ Duration.ofSeconds(1));
@@ -173,10 +170,16 @@
 
     public void noteUidProcessState(int uid, int state, long unusedElapsedRealtime,
                                     long unusedUptime) {
-        mMobileDataStatsHandler.post(
+        if (mRateLimiter.tryAcquire()) {
+            mMobileDataStatsHandler.post(
                 () -> {
                     noteUidProcessStateImpl(uid, state);
                 });
+        } else {
+            synchronized (mLock) {
+                mUidPreviousState.put(uid, state);
+            }
+        }
     }
 
     public int pullDataBytesTransfer(List<StatsEvent> data) {
@@ -209,29 +212,27 @@
     }
 
     private void noteUidProcessStateImpl(int uid, int state) {
-        if (mRateLimiter.tryAcquire()) {
-            // noteUidProcessStateImpl can be called back to back several times while
-            // the updateNetworkStats loops over several stats for multiple uids
-            // and during the first call in a batch of proc state change event it can
-            // contain info for uid with unknown previous state yet which can happen due to a few
-            // reasons:
-            // - app was just started
-            // - app was started before the ActivityManagerService
-            // as result stats would be created with state == ActivityManager.PROCESS_STATE_UNKNOWN
-            if (mNetworkStatsManager != null) {
-                updateNetworkStats(mNetworkStatsManager);
-            } else {
-                Slog.w(TAG, "noteUidProcessStateLocked() can not get mNetworkStatsManager");
-            }
+        // noteUidProcessStateImpl can be called back to back several times while
+        // the updateNetworkStats loops over several stats for multiple uids
+        // and during the first call in a batch of proc state change event it can
+        // contain info for uid with unknown previous state yet which can happen due to a few
+        // reasons:
+        // - app was just started
+        // - app was started before the ActivityManagerService
+        // as result stats would be created with state == ActivityManager.PROCESS_STATE_UNKNOWN
+        if (mNetworkStatsManager != null) {
+            updateNetworkStats(mNetworkStatsManager);
+        } else {
+            Slog.w(TAG, "noteUidProcessStateLocked() can not get mNetworkStatsManager");
         }
-        mUidPreviousState.put(uid, state);
+        synchronized (mLock) {
+            mUidPreviousState.put(uid, state);
+        }
     }
 
     private void updateNetworkStats(NetworkStatsManager networkStatsManager) {
-        if (DEBUG) {
-            if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) {
-                Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, TAG + "-updateNetworkStats");
-            }
+        if (DEBUG && Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) {
+            Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, TAG + "-updateNetworkStats");
         }
 
         final NetworkStats latestStats = networkStatsManager.getMobileUidStats();
@@ -256,20 +257,25 @@
     }
 
     private void updateNetworkStatsDelta(NetworkStats delta) {
+        if (DEBUG && Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) {
+            Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, TAG + "-updateNetworkStatsDelta");
+        }
         synchronized (mLock) {
             for (NetworkStats.Entry entry : delta) {
-                if (entry.getRxPackets() == 0 && entry.getTxPackets() == 0) {
-                    continue;
-                }
-                MobileDataStats stats = getUidStatsForPreviousStateLocked(entry.getUid());
-                if (stats != null) {
-                    stats.addTxBytes(entry.getTxBytes());
-                    stats.addRxBytes(entry.getRxBytes());
-                    stats.addTxPackets(entry.getTxPackets());
-                    stats.addRxPackets(entry.getRxPackets());
+                if (entry.getRxPackets() != 0 || entry.getTxPackets() != 0) {
+                    MobileDataStats stats = getUidStatsForPreviousStateLocked(entry.getUid());
+                    if (stats != null) {
+                        stats.addTxBytes(entry.getTxBytes());
+                        stats.addRxBytes(entry.getRxBytes());
+                        stats.addTxPackets(entry.getTxPackets());
+                        stats.addRxPackets(entry.getRxPackets());
+                    }
                 }
             }
         }
+        if (DEBUG) {
+            Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
+        }
     }
 
     @GuardedBy("mLock")
@@ -298,18 +304,12 @@
     }
 
     private static boolean isEmpty(NetworkStats stats) {
-        long totalRxPackets = 0;
-        long totalTxPackets = 0;
         for (NetworkStats.Entry entry : stats) {
-            if (entry.getRxPackets() == 0 && entry.getTxPackets() == 0) {
-                continue;
+            if (entry.getRxPackets() != 0 || entry.getTxPackets() != 0) {
+                // at least one non empty entry located
+                return false;
             }
-            totalRxPackets += entry.getRxPackets();
-            totalTxPackets += entry.getTxPackets();
-            // at least one non empty entry located
-            break;
         }
-        final long totalPackets = totalRxPackets + totalTxPackets;
-        return totalPackets == 0;
+        return true;
     }
 }
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 4ed5f90..a19a342 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -2199,6 +2199,19 @@
         });
     }
 
+    /**
+     *  Called when the notification should be rebundled.
+     * @param key the notification key
+     */
+    @Override
+    public void rebundleNotification(String key) {
+        enforceStatusBarService();
+        enforceValidCallingUser();
+        Binder.withCleanCallingIdentity(() -> {
+            mNotificationDelegate.rebundleNotification(key);
+        });
+    }
+
 
     @Override
     public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
diff --git a/services/core/java/com/android/server/timedetector/ServerFlags.java b/services/core/java/com/android/server/timedetector/ServerFlags.java
index 2049a02..b651c7b 100644
--- a/services/core/java/com/android/server/timedetector/ServerFlags.java
+++ b/services/core/java/com/android/server/timedetector/ServerFlags.java
@@ -72,8 +72,12 @@
             KEY_TIME_ZONE_DETECTOR_AUTO_DETECTION_ENABLED_DEFAULT,
             KEY_TIME_ZONE_DETECTOR_TELEPHONY_FALLBACK_SUPPORTED,
             KEY_ENHANCED_METRICS_COLLECTION_ENABLED,
+            KEY_TIME_ZONE_NOTIFICATIONS_SUPPORTED,
+            KEY_TIME_ZONE_NOTIFICATIONS_ENABLED_DEFAULT,
+            KEY_TIME_ZONE_NOTIFICATIONS_TRACKING_SUPPORTED,
+            KEY_TIME_ZONE_MANUAL_CHANGE_TRACKING_SUPPORTED
     })
-    @Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER })
+    @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
     @Retention(RetentionPolicy.SOURCE)
     @interface DeviceConfigKey {}
 
@@ -192,6 +196,31 @@
             "enhanced_metrics_collection_enabled";
 
     /**
+     * The key to control support for time zone notifications under certain circumstances.
+     */
+    public static final @DeviceConfigKey String KEY_TIME_ZONE_NOTIFICATIONS_SUPPORTED =
+            "time_zone_notifications_supported";
+
+    /**
+     * The key for the default value used to determine whether time zone notifications is enabled
+     * when the user hasn't explicitly set it yet.
+     */
+    public static final @DeviceConfigKey String KEY_TIME_ZONE_NOTIFICATIONS_ENABLED_DEFAULT =
+            "time_zone_notifications_enabled_default";
+
+    /**
+     * The key to control support for time zone notifications tracking under certain circumstances.
+     */
+    public static final @DeviceConfigKey String KEY_TIME_ZONE_NOTIFICATIONS_TRACKING_SUPPORTED =
+            "time_zone_notifications_tracking_supported";
+
+    /**
+     * The key to control support for time zone manual change tracking under certain circumstances.
+     */
+    public static final @DeviceConfigKey String KEY_TIME_ZONE_MANUAL_CHANGE_TRACKING_SUPPORTED =
+            "time_zone_manual_change_tracking_supported";
+
+    /**
      * The registered listeners and the keys to trigger on. The value is explicitly a HashSet to
      * ensure O(1) lookup performance when working out whether a listener should trigger.
      */
diff --git a/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java b/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java
index 3579246..0495f54 100644
--- a/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java
+++ b/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java
@@ -286,7 +286,8 @@
         // This check is racey, but the whole settings update process is racey. This check prevents
         // a ConfigurationChangeListener callback triggering due to ContentObserver's still
         // triggering *sometimes* for no-op updates. Because callbacks are async this is necessary
-        // for stable behavior during tests.
+        // for stable behavior during tests. This behavior is copied from
+        // setAutoDetectionEnabledIfRequired and assumed to be the correct way.
         if (getAutoDetectionEnabledSetting() != enabled) {
             Settings.Global.putInt(mCr, Settings.Global.AUTO_TIME, enabled ? 1 : 0);
         }
diff --git a/services/core/java/com/android/server/timezonedetector/ConfigurationInternal.java b/services/core/java/com/android/server/timezonedetector/ConfigurationInternal.java
index fc659c5..c4c86a42 100644
--- a/services/core/java/com/android/server/timezonedetector/ConfigurationInternal.java
+++ b/services/core/java/com/android/server/timezonedetector/ConfigurationInternal.java
@@ -65,6 +65,10 @@
     private final boolean mUserConfigAllowed;
     private final boolean mLocationEnabledSetting;
     private final boolean mGeoDetectionEnabledSetting;
+    private final boolean mNotificationsSupported;
+    private final boolean mNotificationsEnabledSetting;
+    private final boolean mNotificationTrackingSupported;
+    private final boolean mManualChangeTrackingSupported;
 
     private ConfigurationInternal(Builder builder) {
         mTelephonyDetectionSupported = builder.mTelephonyDetectionSupported;
@@ -78,6 +82,10 @@
         mUserConfigAllowed = builder.mUserConfigAllowed;
         mLocationEnabledSetting = builder.mLocationEnabledSetting;
         mGeoDetectionEnabledSetting = builder.mGeoDetectionEnabledSetting;
+        mNotificationsSupported = builder.mNotificationsSupported;
+        mNotificationsEnabledSetting = builder.mNotificationsEnabledSetting;
+        mNotificationTrackingSupported = builder.mNotificationsTrackingSupported;
+        mManualChangeTrackingSupported = builder.mManualChangeTrackingSupported;
     }
 
     /** Returns true if the device supports any form of auto time zone detection. */
@@ -104,6 +112,27 @@
     }
 
     /**
+     * Returns true if the device supports time-related notifications.
+     */
+    public boolean areNotificationsSupported() {
+        return mNotificationsSupported;
+    }
+
+    /**
+     * Returns true if the device supports tracking of time-related notifications.
+     */
+    public boolean isNotificationTrackingSupported() {
+        return areNotificationsSupported() && mNotificationTrackingSupported;
+    }
+
+    /**
+     * Returns true if the device supports tracking of time zone manual changes.
+     */
+    public boolean isManualChangeTrackingSupported() {
+        return mManualChangeTrackingSupported;
+    }
+
+    /**
      * Returns {@code true} if location time zone detection should run when auto time zone detection
      * is enabled on supported devices, even when the user has not enabled the algorithm explicitly
      * in settings. Enabled for internal testing only. See {@link #isGeoDetectionExecutionEnabled()}
@@ -223,6 +252,15 @@
                 && getGeoDetectionRunInBackgroundEnabledSetting();
     }
 
+    /** Returns true if time-related notifications can be shown on this device. */
+    public boolean getNotificationsEnabledBehavior() {
+        return areNotificationsSupported() && getNotificationsEnabledSetting();
+    }
+
+    private boolean getNotificationsEnabledSetting() {
+        return mNotificationsEnabledSetting;
+    }
+
     @NonNull
     public TimeZoneCapabilities asCapabilities(boolean bypassUserPolicyChecks) {
         UserHandle userHandle = UserHandle.of(mUserId);
@@ -283,6 +321,14 @@
         }
         builder.setSetManualTimeZoneCapability(suggestManualTimeZoneCapability);
 
+        final @CapabilityState int configureNotificationsEnabledCapability;
+        if (areNotificationsSupported()) {
+            configureNotificationsEnabledCapability = CAPABILITY_POSSESSED;
+        } else {
+            configureNotificationsEnabledCapability = CAPABILITY_NOT_SUPPORTED;
+        }
+        builder.setConfigureNotificationsEnabledCapability(configureNotificationsEnabledCapability);
+
         return builder.build();
     }
 
@@ -291,6 +337,7 @@
         return new TimeZoneConfiguration.Builder()
                 .setAutoDetectionEnabled(getAutoDetectionEnabledSetting())
                 .setGeoDetectionEnabled(getGeoDetectionEnabledSetting())
+                .setNotificationsEnabled(getNotificationsEnabledSetting())
                 .build();
     }
 
@@ -307,6 +354,9 @@
         if (newConfiguration.hasIsGeoDetectionEnabled()) {
             builder.setGeoDetectionEnabledSetting(newConfiguration.isGeoDetectionEnabled());
         }
+        if (newConfiguration.hasIsNotificationsEnabled()) {
+            builder.setNotificationsEnabledSetting(newConfiguration.areNotificationsEnabled());
+        }
         return builder.build();
     }
 
@@ -328,7 +378,11 @@
                 && mEnhancedMetricsCollectionEnabled == that.mEnhancedMetricsCollectionEnabled
                 && mAutoDetectionEnabledSetting == that.mAutoDetectionEnabledSetting
                 && mLocationEnabledSetting == that.mLocationEnabledSetting
-                && mGeoDetectionEnabledSetting == that.mGeoDetectionEnabledSetting;
+                && mGeoDetectionEnabledSetting == that.mGeoDetectionEnabledSetting
+                && mNotificationsSupported == that.mNotificationsSupported
+                && mNotificationsEnabledSetting == that.mNotificationsEnabledSetting
+                && mNotificationTrackingSupported == that.mNotificationTrackingSupported
+                && mManualChangeTrackingSupported == that.mManualChangeTrackingSupported;
     }
 
     @Override
@@ -336,7 +390,9 @@
         return Objects.hash(mUserId, mUserConfigAllowed, mTelephonyDetectionSupported,
                 mGeoDetectionSupported, mTelephonyFallbackSupported,
                 mGeoDetectionRunInBackgroundEnabled, mEnhancedMetricsCollectionEnabled,
-                mAutoDetectionEnabledSetting, mLocationEnabledSetting, mGeoDetectionEnabledSetting);
+                mAutoDetectionEnabledSetting, mLocationEnabledSetting, mGeoDetectionEnabledSetting,
+                mNotificationsSupported, mNotificationsEnabledSetting,
+                mNotificationTrackingSupported, mManualChangeTrackingSupported);
     }
 
     @Override
@@ -352,6 +408,10 @@
                 + ", mAutoDetectionEnabledSetting=" + mAutoDetectionEnabledSetting
                 + ", mLocationEnabledSetting=" + mLocationEnabledSetting
                 + ", mGeoDetectionEnabledSetting=" + mGeoDetectionEnabledSetting
+                + ", mNotificationsSupported=" + mNotificationsSupported
+                + ", mNotificationsEnabledSetting=" + mNotificationsEnabledSetting
+                + ", mNotificationTrackingSupported=" + mNotificationTrackingSupported
+                + ", mManualChangeTrackingSupported=" + mManualChangeTrackingSupported
                 + '}';
     }
 
@@ -370,6 +430,10 @@
         private boolean mAutoDetectionEnabledSetting;
         private boolean mLocationEnabledSetting;
         private boolean mGeoDetectionEnabledSetting;
+        private boolean mNotificationsSupported;
+        private boolean mNotificationsEnabledSetting;
+        private boolean mNotificationsTrackingSupported;
+        private boolean mManualChangeTrackingSupported;
 
         /**
          * Creates a new Builder.
@@ -390,6 +454,10 @@
             this.mAutoDetectionEnabledSetting = toCopy.mAutoDetectionEnabledSetting;
             this.mLocationEnabledSetting = toCopy.mLocationEnabledSetting;
             this.mGeoDetectionEnabledSetting = toCopy.mGeoDetectionEnabledSetting;
+            this.mNotificationsSupported = toCopy.mNotificationsSupported;
+            this.mNotificationsEnabledSetting = toCopy.mNotificationsEnabledSetting;
+            this.mNotificationsTrackingSupported = toCopy.mNotificationTrackingSupported;
+            this.mManualChangeTrackingSupported = toCopy.mManualChangeTrackingSupported;
         }
 
         /**
@@ -475,6 +543,38 @@
             return this;
         }
 
+        /**
+         * Sets the value of the time notification setting for this user.
+         */
+        public Builder setNotificationsEnabledSetting(boolean enabled) {
+            mNotificationsEnabledSetting = enabled;
+            return this;
+        }
+
+        /**
+         * Sets whether time zone notifications are supported on this device.
+         */
+        public Builder setNotificationsSupported(boolean enabled) {
+            mNotificationsSupported = enabled;
+            return this;
+        }
+
+        /**
+         * Sets whether time zone notification tracking is supported on this device.
+         */
+        public Builder setNotificationsTrackingSupported(boolean supported) {
+            mNotificationsTrackingSupported = supported;
+            return this;
+        }
+
+        /**
+         * Sets whether time zone manual change tracking are supported on this device.
+         */
+        public Builder setManualChangeTrackingSupported(boolean supported) {
+            mManualChangeTrackingSupported = supported;
+            return this;
+        }
+
         /** Returns a new {@link ConfigurationInternal}. */
         @NonNull
         public ConfigurationInternal build() {
diff --git a/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java b/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java
index f1248a3..d809fc6 100644
--- a/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java
+++ b/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java
@@ -68,7 +68,11 @@
             ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_SETTING_ENABLED_DEFAULT,
             ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_SETTING_ENABLED_OVERRIDE,
             ServerFlags.KEY_TIME_ZONE_DETECTOR_AUTO_DETECTION_ENABLED_DEFAULT,
-            ServerFlags.KEY_TIME_ZONE_DETECTOR_TELEPHONY_FALLBACK_SUPPORTED
+            ServerFlags.KEY_TIME_ZONE_DETECTOR_TELEPHONY_FALLBACK_SUPPORTED,
+            ServerFlags.KEY_TIME_ZONE_NOTIFICATIONS_SUPPORTED,
+            ServerFlags.KEY_TIME_ZONE_NOTIFICATIONS_ENABLED_DEFAULT,
+            ServerFlags.KEY_TIME_ZONE_NOTIFICATIONS_TRACKING_SUPPORTED,
+            ServerFlags.KEY_TIME_ZONE_MANUAL_CHANGE_TRACKING_SUPPORTED
     );
 
     /**
@@ -100,11 +104,16 @@
     @Nullable
     private static ServiceConfigAccessor sInstance;
 
-    @NonNull private final Context mContext;
-    @NonNull private final ServerFlags mServerFlags;
-    @NonNull private final ContentResolver mCr;
-    @NonNull private final UserManager mUserManager;
-    @NonNull private final LocationManager mLocationManager;
+    @NonNull
+    private final Context mContext;
+    @NonNull
+    private final ServerFlags mServerFlags;
+    @NonNull
+    private final ContentResolver mCr;
+    @NonNull
+    private final UserManager mUserManager;
+    @NonNull
+    private final LocationManager mLocationManager;
 
     @GuardedBy("this")
     @NonNull
@@ -193,6 +202,9 @@
         contentResolver.registerContentObserver(
                 Settings.Global.getUriFor(Settings.Global.AUTO_TIME_ZONE_EXPLICIT), true,
                 contentObserver);
+        contentResolver.registerContentObserver(
+                Settings.Global.getUriFor(Settings.Global.TIME_ZONE_NOTIFICATIONS), true,
+                contentObserver);
 
         // Add async callbacks for user scoped location settings being changed.
         contentResolver.registerContentObserver(
@@ -331,6 +343,14 @@
                 setGeoDetectionEnabledSettingIfRequired(userId, geoDetectionEnabledSetting);
             }
         }
+
+        if (areNotificationsSupported()) {
+            if (requestedConfigurationUpdates.hasIsNotificationsEnabled()) {
+                setNotificationsEnabledSetting(
+                        requestedConfigurationUpdates.areNotificationsEnabled());
+            }
+            setNotificationsEnabledIfRequired(newConfiguration.areNotificationsEnabled());
+        }
     }
 
     @Override
@@ -348,6 +368,10 @@
                 .setUserConfigAllowed(isUserConfigAllowed(userId))
                 .setLocationEnabledSetting(getLocationEnabledSetting(userId))
                 .setGeoDetectionEnabledSetting(getGeoDetectionEnabledSetting(userId))
+                .setNotificationsSupported(areNotificationsSupported())
+                .setNotificationsEnabledSetting(getNotificationsEnabledSetting())
+                .setNotificationsTrackingSupported(isNotificationTrackingSupported())
+                .setManualChangeTrackingSupported(isManualChangeTrackingSupported())
                 .build();
     }
 
@@ -421,6 +445,49 @@
         }
     }
 
+    private boolean areNotificationsSupported() {
+        return mServerFlags.getBoolean(
+                ServerFlags.KEY_TIME_ZONE_NOTIFICATIONS_SUPPORTED,
+                getConfigBoolean(R.bool.config_enableTimeZoneNotificationsSupported));
+    }
+
+    private boolean isNotificationTrackingSupported() {
+        return mServerFlags.getBoolean(
+                ServerFlags.KEY_TIME_ZONE_NOTIFICATIONS_TRACKING_SUPPORTED,
+                getConfigBoolean(R.bool.config_enableTimeZoneNotificationsTrackingSupported));
+    }
+
+    private boolean isManualChangeTrackingSupported() {
+        return mServerFlags.getBoolean(
+                ServerFlags.KEY_TIME_ZONE_MANUAL_CHANGE_TRACKING_SUPPORTED,
+                getConfigBoolean(R.bool.config_enableTimeZoneManualChangeTrackingSupported));
+    }
+
+    private boolean getNotificationsEnabledSetting() {
+        final boolean notificationsEnabledByDefault = areNotificationsEnabledByDefault();
+        return Settings.Global.getInt(mCr, Settings.Global.TIME_ZONE_NOTIFICATIONS,
+                (notificationsEnabledByDefault ? 1 : 0) /* defaultValue */) != 0;
+    }
+
+    private boolean areNotificationsEnabledByDefault() {
+        return mServerFlags.getBoolean(
+                ServerFlags.KEY_TIME_ZONE_NOTIFICATIONS_ENABLED_DEFAULT, true);
+    }
+
+    private void setNotificationsEnabledSetting(boolean enabled) {
+        Settings.Global.putInt(mCr, Settings.Global.TIME_ZONE_NOTIFICATIONS, enabled ? 1 : 0);
+    }
+
+    private void setNotificationsEnabledIfRequired(boolean enabled) {
+        // This check is racey, but the whole settings update process is racey. This check prevents
+        // a ConfigurationChangeListener callback triggering due to ContentObserver's still
+        // triggering *sometimes* for no-op updates. Because callbacks are async this is necessary
+        // for stable behavior during tests.
+        if (getNotificationsEnabledSetting() != enabled) {
+            Settings.Global.putInt(mCr, Settings.Global.TIME_ZONE_NOTIFICATIONS, enabled ? 1 : 0);
+        }
+    }
+
     @Override
     public void addLocationTimeZoneManagerConfigListener(
             @NonNull StateChangeListener listener) {
@@ -441,8 +508,7 @@
 
     @Override
     public boolean isGeoTimeZoneDetectionFeatureSupportedInConfig() {
-        return mContext.getResources().getBoolean(
-                com.android.internal.R.bool.config_enableGeolocationTimeZoneDetection);
+        return getConfigBoolean(R.bool.config_enableGeolocationTimeZoneDetection);
     }
 
     @Override
@@ -660,8 +726,7 @@
     private boolean isTelephonyFallbackSupported() {
         return mServerFlags.getBoolean(
                 ServerFlags.KEY_TIME_ZONE_DETECTOR_TELEPHONY_FALLBACK_SUPPORTED,
-                getConfigBoolean(
-                        com.android.internal.R.bool.config_supportTelephonyTimeZoneFallback));
+                getConfigBoolean(R.bool.config_supportTelephonyTimeZoneFallback));
     }
 
     private boolean getConfigBoolean(int providerEnabledConfigId) {
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java b/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java
new file mode 100644
index 0000000..e14326c
--- /dev/null
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.timezonedetector;
+
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.ElapsedRealtimeLong;
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.util.IndentingPrintWriter;
+
+import com.android.server.SystemTimeZone.TimeZoneConfidence;
+import com.android.server.timezonedetector.TimeZoneDetectorStrategy.Origin;
+
+import java.util.Objects;
+
+public interface TimeZoneChangeListener {
+
+    /** Record a time zone change. */
+    void process(TimeZoneChangeEvent event);
+
+    /** Dump internal state. */
+    void dump(IndentingPrintWriter ipw);
+
+    class TimeZoneChangeEvent {
+
+        private final @ElapsedRealtimeLong long mElapsedRealtimeMillis;
+        private final @CurrentTimeMillisLong long mUnixEpochTimeMillis;
+        private final @Origin int mOrigin;
+        private final @UserIdInt int mUserId;
+        private final String mOldZoneId;
+        private final String mNewZoneId;
+        private final @TimeZoneConfidence int mNewConfidence;
+        private final String mCause;
+
+        public TimeZoneChangeEvent(@ElapsedRealtimeLong long elapsedRealtimeMillis,
+                @CurrentTimeMillisLong long unixEpochTimeMillis,
+                @Origin int origin, @UserIdInt int userId, @NonNull String oldZoneId,
+                @NonNull String newZoneId, int newConfidence, @NonNull String cause) {
+            mElapsedRealtimeMillis = elapsedRealtimeMillis;
+            mUnixEpochTimeMillis = unixEpochTimeMillis;
+            mOrigin = origin;
+            mUserId = userId;
+            mOldZoneId = Objects.requireNonNull(oldZoneId);
+            mNewZoneId = Objects.requireNonNull(newZoneId);
+            mNewConfidence = newConfidence;
+            mCause = Objects.requireNonNull(cause);
+        }
+
+        public @ElapsedRealtimeLong long getElapsedRealtimeMillis() {
+            return mElapsedRealtimeMillis;
+        }
+
+        public @CurrentTimeMillisLong long getUnixEpochTimeMillis() {
+            return mUnixEpochTimeMillis;
+        }
+
+        public @Origin int getOrigin() {
+            return mOrigin;
+        }
+
+        /**
+         * The ID of the user that triggered the change.
+         *
+         * <p>If automatic time zone is turned on, the user ID returned is the system's user id.
+         */
+        public @UserIdInt int getUserId() {
+            return mUserId;
+        }
+
+        public String getOldZoneId() {
+            return mOldZoneId;
+        }
+
+        public String getNewZoneId() {
+            return mNewZoneId;
+        }
+
+        @Override
+        public String toString() {
+            return "TimeZoneChangeEvent{"
+                    + "mElapsedRealtimeMillis=" + mElapsedRealtimeMillis
+                    + ", mUnixEpochTimeMillis=" + mUnixEpochTimeMillis
+                    + ", mOrigin=" + mOrigin
+                    + ", mUserId=" + mUserId
+                    + ", mOldZoneId='" + mOldZoneId + '\''
+                    + ", mNewZoneId='" + mNewZoneId + '\''
+                    + ", mNewConfidence=" + mNewConfidence
+                    + ", mCause='" + mCause + '\''
+                    + '}';
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java
index d914544..af02ad8 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.annotation.UserIdInt;
 import android.app.time.ITimeZoneDetectorListener;
 import android.app.time.TimeZoneCapabilitiesAndConfig;
@@ -73,6 +74,7 @@
         }
 
         @Override
+        @RequiresPermission("android.permission.INTERACT_ACROSS_USERS_FULL")
         public void onStart() {
             // Obtain / create the shared dependencies.
             Context context = getContext();
@@ -81,7 +83,7 @@
             ServiceConfigAccessor serviceConfigAccessor =
                     ServiceConfigAccessorImpl.getInstance(context);
             TimeZoneDetectorStrategy timeZoneDetectorStrategy =
-                    TimeZoneDetectorStrategyImpl.create(handler, serviceConfigAccessor);
+                    TimeZoneDetectorStrategyImpl.create(context, handler, serviceConfigAccessor);
             DeviceActivityMonitor deviceActivityMonitor =
                     DeviceActivityMonitorImpl.create(context, handler);
 
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java
index 37e67c9..8cfbe9d 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java
@@ -15,6 +15,7 @@
  */
 package com.android.server.timezonedetector;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.UserIdInt;
 import android.app.time.TimeZoneCapabilitiesAndConfig;
@@ -24,6 +25,11 @@
 import android.app.timezonedetector.TelephonyTimeZoneSuggestion;
 import android.util.IndentingPrintWriter;
 
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
 /**
  * The interface for the class that is responsible for setting the time zone on a device, used by
  * {@link TimeZoneDetectorService} and {@link TimeZoneDetectorInternal}.
@@ -97,6 +103,22 @@
  * @hide
  */
 public interface TimeZoneDetectorStrategy extends Dumpable {
+    @IntDef({ ORIGIN_UNKNOWN, ORIGIN_MANUAL, ORIGIN_TELEPHONY, ORIGIN_LOCATION })
+    @Retention(RetentionPolicy.SOURCE)
+    @Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER })
+    @interface Origin {}
+
+    /** Used when the origin of the time zone value cannot be inferred. */
+    @Origin int ORIGIN_UNKNOWN = 0;
+
+    /** Used when a time zone value originated from a user / manual settings. */
+    @Origin int ORIGIN_MANUAL = 1;
+
+    /** Used when a time zone value originated from a telephony signal. */
+    @Origin int ORIGIN_TELEPHONY = 2;
+
+    /** Used when a time zone value originated from a location signal. */
+    @Origin int ORIGIN_LOCATION = 3;
 
     /**
      * Adds a listener that will be triggered when something changes that could affect the result
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java
index dddb46f..19a28dd 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java
@@ -28,6 +28,7 @@
 import android.annotation.ElapsedRealtimeLong;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.annotation.UserIdInt;
 import android.app.time.DetectorStatusTypes;
 import android.app.time.LocationTimeZoneAlgorithmStatus;
@@ -39,8 +40,10 @@
 import android.app.time.TimeZoneState;
 import android.app.timezonedetector.ManualTimeZoneSuggestion;
 import android.app.timezonedetector.TelephonyTimeZoneSuggestion;
+import android.content.Context;
 import android.os.Handler;
 import android.os.TimestampedValue;
+import android.os.UserHandle;
 import android.util.IndentingPrintWriter;
 import android.util.Slog;
 
@@ -72,12 +75,14 @@
         /**
          * Returns the device's currently configured time zone. May return an empty string.
          */
-        @NonNull String getDeviceTimeZone();
+        @NonNull
+        String getDeviceTimeZone();
 
         /**
          * Returns the confidence of the device's current time zone.
          */
-        @TimeZoneConfidence int getDeviceTimeZoneConfidence();
+        @TimeZoneConfidence
+        int getDeviceTimeZoneConfidence();
 
         /**
          * Sets the device's time zone, associated confidence, and records a debug log entry.
@@ -115,7 +120,7 @@
     /**
      * The abstract score for an empty or invalid telephony suggestion.
      *
-     * Used to score telephony suggestions where there is no zone.
+     * <p>Used to score telephony suggestions where there is no zone.
      */
     @VisibleForTesting
     public static final int TELEPHONY_SCORE_NONE = 0;
@@ -123,11 +128,11 @@
     /**
      * The abstract score for a low quality telephony suggestion.
      *
-     * Used to score suggestions where:
-     * The suggested zone ID is one of several possibilities, and the possibilities have different
-     * offsets.
+     * <p>Used to score suggestions where:
+     * The suggested zone ID is one of several possibilities,
+     * and the possibilities have different offsets.
      *
-     * You would have to be quite desperate to want to use this choice.
+     * <p>You would have to be quite desperate to want to use this choice.
      */
     @VisibleForTesting
     public static final int TELEPHONY_SCORE_LOW = 1;
@@ -135,7 +140,7 @@
     /**
      * The abstract score for a medium quality telephony suggestion.
      *
-     * Used for:
+     * <p>Used for:
      * The suggested zone ID is one of several possibilities but at least the possibilities have the
      * same offset. Users would get the correct time but for the wrong reason. i.e. their device may
      * switch to DST at the wrong time and (for example) their calendar events.
@@ -146,7 +151,7 @@
     /**
      * The abstract score for a high quality telephony suggestion.
      *
-     * Used for:
+     * <p>Used for:
      * The suggestion was for one zone ID and the answer was unambiguous and likely correct given
      * the info available.
      */
@@ -156,7 +161,7 @@
     /**
      * The abstract score for a highest quality telephony suggestion.
      *
-     * Used for:
+     * <p>Used for:
      * Suggestions that must "win" because they constitute test or emulator zone ID.
      */
     @VisibleForTesting
@@ -206,7 +211,8 @@
     private final ServiceConfigAccessor mServiceConfigAccessor;
 
     @GuardedBy("this")
-    @NonNull private final List<StateChangeListener> mStateChangeListeners = new ArrayList<>();
+    @NonNull
+    private final List<StateChangeListener> mStateChangeListeners = new ArrayList<>();
 
     /**
      * A snapshot of the current detector status. A local copy is cached because it is relatively
@@ -244,8 +250,10 @@
     /**
      * Creates a new instance of {@link TimeZoneDetectorStrategyImpl}.
      */
+    @RequiresPermission("android.permission.INTERACT_ACROSS_USERS_FULL")
     public static TimeZoneDetectorStrategyImpl create(
-            @NonNull Handler handler, @NonNull ServiceConfigAccessor serviceConfigAccessor) {
+            @NonNull Context context, @NonNull Handler handler,
+            @NonNull ServiceConfigAccessor serviceConfigAccessor) {
 
         Environment environment = new EnvironmentImpl(handler);
         return new TimeZoneDetectorStrategyImpl(serviceConfigAccessor, environment);
@@ -468,7 +476,7 @@
         // later disables automatic time zone detection.
         mLatestManualSuggestion.set(suggestion);
 
-        setDeviceTimeZoneIfRequired(timeZoneId, cause);
+        setDeviceTimeZoneIfRequired(timeZoneId, ORIGIN_MANUAL, userId, cause);
         return true;
     }
 
@@ -685,7 +693,7 @@
 
         // GeolocationTimeZoneSuggestion has no measure of quality. We assume all suggestions are
         // reliable.
-        String zoneId;
+        String timeZoneId;
 
         // Introduce bias towards the device's current zone when there are multiple zone suggested.
         String deviceTimeZone = mEnvironment.getDeviceTimeZone();
@@ -694,11 +702,12 @@
                 Slog.d(LOG_TAG,
                         "Geo tz suggestion contains current device time zone. Applying bias.");
             }
-            zoneId = deviceTimeZone;
+            timeZoneId = deviceTimeZone;
         } else {
-            zoneId = zoneIds.get(0);
+            timeZoneId = zoneIds.get(0);
         }
-        setDeviceTimeZoneIfRequired(zoneId, detectionReason);
+        setDeviceTimeZoneIfRequired(timeZoneId, ORIGIN_LOCATION, UserHandle.USER_SYSTEM,
+                detectionReason);
         return true;
     }
 
@@ -779,8 +788,8 @@
 
         // Paranoia: Every suggestion above the SCORE_USAGE_THRESHOLD should have a non-null time
         // zone ID.
-        String zoneId = bestTelephonySuggestion.suggestion.getZoneId();
-        if (zoneId == null) {
+        String timeZoneId = bestTelephonySuggestion.suggestion.getZoneId();
+        if (timeZoneId == null) {
             Slog.w(LOG_TAG, "Empty zone suggestion scored higher than expected. This is an error:"
                     + " bestTelephonySuggestion=" + bestTelephonySuggestion
                     + ", detectionReason=" + detectionReason);
@@ -790,11 +799,12 @@
         String cause = "Found good suggestion:"
                 + " bestTelephonySuggestion=" + bestTelephonySuggestion
                 + ", detectionReason=" + detectionReason;
-        setDeviceTimeZoneIfRequired(zoneId, cause);
+        setDeviceTimeZoneIfRequired(timeZoneId, ORIGIN_TELEPHONY, UserHandle.USER_SYSTEM, cause);
     }
 
     @GuardedBy("this")
-    private void setDeviceTimeZoneIfRequired(@NonNull String newZoneId, @NonNull String cause) {
+    private void setDeviceTimeZoneIfRequired(@NonNull String newZoneId, @Origin int origin,
+            @UserIdInt int userId, @NonNull String cause) {
         String currentZoneId = mEnvironment.getDeviceTimeZone();
         // All manual and automatic suggestions are considered high confidence as low-quality
         // suggestions are not currently passed on.
diff --git a/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java b/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java
index 54ae047..0b676ff 100644
--- a/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java
+++ b/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java
@@ -100,6 +100,11 @@
         }
 
         VibratorInfo.FrequencyProfile frequencyProfile = info.getFrequencyProfile();
+        if (frequencyProfile.isEmpty()) {
+            // The frequency profile has an invalid frequency range, so keep the segments unchanged.
+            return repeatIndex;
+        }
+
         float[] frequenciesHz = frequencyProfile.getFrequenciesHz();
         float[] accelerationsGs = frequencyProfile.getOutputAccelerationsGs();
 
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index 89c7a3d..6f308aa 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -1631,7 +1631,7 @@
 
         int positionToLog = APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__NOT_LETTERBOXED_POSITION;
         if (isAppCompateStateChangedToLetterboxed(state)) {
-            positionToLog = activity.mAppCompatController.getAppCompatReachabilityOverrides()
+            positionToLog = activity.mAppCompatController.getReachabilityOverrides()
                     .getLetterboxPositionForLogging();
         }
         FrameworkStatsLog.write(FrameworkStatsLog.APP_COMPAT_STATE_CHANGED,
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 5dbdeff..1fe6159 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -232,6 +232,7 @@
 import static com.android.server.wm.StartingData.AFTER_TRANSACTION_COPY_TO_CLIENT;
 import static com.android.server.wm.StartingData.AFTER_TRANSACTION_IDLE;
 import static com.android.server.wm.StartingData.AFTER_TRANSACTION_REMOVE_DIRECTLY;
+import static com.android.server.wm.StartingData.AFTER_TRANSITION_FINISH;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION;
@@ -367,7 +368,6 @@
 import com.android.internal.os.TimeoutRecord;
 import com.android.internal.os.TransferPipe;
 import com.android.internal.policy.AttributeCache;
-import com.android.internal.policy.PhoneWindow;
 import com.android.internal.protolog.ProtoLog;
 import com.android.internal.util.XmlUtils;
 import com.android.modules.utils.TypedXmlPullParser;
@@ -2027,8 +2027,8 @@
                     || ent.array.getBoolean(R.styleable.Window_windowShowWallpaper, false);
             mStyleFillsParent = mOccludesParent;
             mNoDisplay = ent.array.getBoolean(R.styleable.Window_windowNoDisplay, false);
-            mOptOutEdgeToEdge = PhoneWindow.isOptingOutEdgeToEdgeEnforcement(
-                    aInfo.applicationInfo, false /* local */, ent.array);
+            mOptOutEdgeToEdge = ent.array.getBoolean(
+                    R.styleable.Window_windowOptOutEdgeToEdgeEnforcement, false);
         } else {
             mStyleFillsParent = mOccludesParent = true;
             mNoDisplay = false;
@@ -2815,9 +2815,27 @@
         attachStartingSurfaceToAssociatedTask();
     }
 
+    /**
+     * If the device is locked and the app does not request showWhenLocked,
+     * defer removing the starting window until the transition is complete.
+     * This prevents briefly appearing the app context and causing secure concern.
+     */
+    void deferStartingWindowRemovalForKeyguardUnoccluding() {
+        if (mStartingData.mRemoveAfterTransaction != AFTER_TRANSITION_FINISH
+                && isKeyguardLocked() && !canShowWhenLockedInner(this) && !isVisibleRequested()
+                && mTransitionController.inTransition(this)) {
+            mStartingData.mRemoveAfterTransaction = AFTER_TRANSITION_FINISH;
+        }
+    }
+
     void removeStartingWindow() {
         boolean prevEligibleForLetterboxEducation = isEligibleForLetterboxEducation();
 
+        if (mStartingData != null
+                && mStartingData.mRemoveAfterTransaction == AFTER_TRANSITION_FINISH) {
+            return;
+        }
+
         if (transferSplashScreenIfNeeded()) {
             return;
         }
@@ -3210,7 +3228,7 @@
                 true /* forActivity */)) {
             return false;
         }
-        if (mAppCompatController.mAllowRestrictedResizability.getAsBoolean()) {
+        if (mAppCompatController.getResizeOverrides().allowRestrictedResizability()) {
             return false;
         }
         // If the user preference respects aspect ratio, then it becomes non-resizable.
@@ -3241,8 +3259,8 @@
             // The caller will check both application and activity level property.
             return true;
         }
-        return !AppCompatController.allowRestrictedResizability(wms.mContext.getPackageManager(),
-                appInfo.packageName);
+        return !AppCompatResizeOverrides.allowRestrictedResizability(
+                wms.mContext.getPackageManager(), appInfo.packageName);
     }
 
     boolean isResizeable() {
@@ -4262,7 +4280,7 @@
     }
 
     void finishRelaunching() {
-        mAppCompatController.getAppCompatOrientationOverrides()
+        mAppCompatController.getOrientationOverrides()
                 .setRelaunchingAfterRequestedOrientationChanged(false);
         mTaskSupervisor.getActivityMetricsLogger().notifyActivityRelaunched(this);
 
@@ -4656,6 +4674,9 @@
                 tStartingWindow.mToken = this;
                 tStartingWindow.mActivityRecord = this;
 
+                if (mStartingData.mRemoveAfterTransaction == AFTER_TRANSITION_FINISH) {
+                    mStartingData.mRemoveAfterTransaction = AFTER_TRANSACTION_IDLE;
+                }
                 if (mStartingData.mRemoveAfterTransaction == AFTER_TRANSACTION_REMOVE_DIRECTLY) {
                     // The removal of starting window should wait for window drawn of current
                     // activity.
@@ -8126,10 +8147,7 @@
         if (task != null && requestedOrientation == SCREEN_ORIENTATION_BEHIND) {
             // We use Task here because we want to be consistent with what happens in
             // multi-window mode where other tasks orientations are ignored.
-            final ActivityRecord belowCandidate = task.getActivity(
-                    a -> a.canDefineOrientationForActivitiesAbove() /* callback */,
-                    this /* boundary */, false /* includeBoundary */,
-                    true /* traverseTopToBottom */);
+            final ActivityRecord belowCandidate = task.getActivityBelowForDefiningOrientation(this);
             if (belowCandidate != null) {
                 return belowCandidate.getRequestedConfigurationOrientation(forDisplay);
             }
@@ -8223,7 +8241,7 @@
                 mLastReportedConfiguration.getMergedConfiguration())) {
             ensureActivityConfiguration(false /* ignoreVisibility */);
             if (mPendingRelaunchCount > originalRelaunchingCount) {
-                mAppCompatController.getAppCompatOrientationOverrides()
+                mAppCompatController.getOrientationOverrides()
                         .setRelaunchingAfterRequestedOrientationChanged(true);
             }
             if (mTransitionController.inPlayingTransition(this)) {
@@ -8436,8 +8454,8 @@
      */
     @ActivityInfo.SizeChangesSupportMode
     private int supportsSizeChanges() {
-        if (mAppCompatController.getAppCompatResizeOverrides()
-                .shouldOverrideForceNonResizeApp()) {
+        final AppCompatResizeOverrides resizeOverrides = mAppCompatController.getResizeOverrides();
+        if (resizeOverrides.shouldOverrideForceNonResizeApp()) {
             return SIZE_CHANGES_UNSUPPORTED_OVERRIDE;
         }
 
@@ -8445,8 +8463,7 @@
             return SIZE_CHANGES_SUPPORTED_METADATA;
         }
 
-        if (mAppCompatController.getAppCompatResizeOverrides()
-                .shouldOverrideForceResizeApp()) {
+        if (resizeOverrides.shouldOverrideForceResizeApp()) {
             return SIZE_CHANGES_SUPPORTED_OVERRIDE;
         }
 
@@ -8746,7 +8763,7 @@
             navBarInsets = Insets.NONE;
         }
         final AppCompatReachabilityOverrides reachabilityOverrides =
-                mAppCompatController.getAppCompatReachabilityOverrides();
+                mAppCompatController.getReachabilityOverrides();
         // Horizontal position
         int offsetX = 0;
         if (parentBounds.width() != screenResolvedBoundsWidth) {
@@ -10219,10 +10236,10 @@
                 mAppCompatController.getAppCompatAspectRatioOverrides()
                         .shouldOverrideMinAspectRatio());
         proto.write(SHOULD_IGNORE_ORIENTATION_REQUEST_LOOP,
-                mAppCompatController.getAppCompatOrientationOverrides()
+                mAppCompatController.getOrientationOverrides()
                         .shouldIgnoreOrientationRequestLoop());
         proto.write(SHOULD_OVERRIDE_FORCE_RESIZE_APP,
-                mAppCompatController.getAppCompatResizeOverrides().shouldOverrideForceResizeApp());
+                mAppCompatController.getResizeOverrides().shouldOverrideForceResizeApp());
         proto.write(SHOULD_ENABLE_USER_ASPECT_RATIO_SETTINGS,
                 mAppCompatController.getAppCompatAspectRatioOverrides()
                         .shouldEnableUserAspectRatioSettings());
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index ef6f923..12c8f9c 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -2538,7 +2538,7 @@
 
     void wakeUp(int displayId, String reason) {
         mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_APPLICATION,
-                "android.server.am:TURN_ON:" + reason, displayId);
+                "android.server.wm:TURN_ON:" + reason, displayId);
     }
 
     /** Starts a batch of visibility updates. */
diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java
index 4433d64..6d0e8ea 100644
--- a/services/core/java/com/android/server/wm/AppCompatController.java
+++ b/services/core/java/com/android/server/wm/AppCompatController.java
@@ -15,23 +15,17 @@
  */
 package com.android.server.wm;
 
-import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY;
-
 import android.annotation.NonNull;
 import android.content.pm.PackageManager;
 
 import com.android.server.wm.utils.OptPropFactory;
 
 import java.io.PrintWriter;
-import java.util.function.BooleanSupplier;
 
 /**
  * Allows the interaction with all the app compat policies and configurations
  */
 class AppCompatController {
-
-    @NonNull
-    private final ActivityRecord mActivityRecord;
     @NonNull
     private final TransparentPolicy mTransparentPolicy;
     @NonNull
@@ -39,7 +33,7 @@
     @NonNull
     private final AppCompatAspectRatioPolicy mAppCompatAspectRatioPolicy;
     @NonNull
-    private final AppCompatReachabilityPolicy mAppCompatReachabilityPolicy;
+    private final AppCompatReachabilityPolicy mReachabilityPolicy;
     @NonNull
     private final DesktopAppCompatAspectRatioPolicy mDesktopAppCompatAspectRatioPolicy;
     @NonNull
@@ -50,56 +44,28 @@
     private final AppCompatLetterboxPolicy mAppCompatLetterboxPolicy;
     @NonNull
     private final AppCompatSizeCompatModePolicy mAppCompatSizeCompatModePolicy;
-    @NonNull
-    final BooleanSupplier mAllowRestrictedResizability;
 
     AppCompatController(@NonNull WindowManagerService wmService,
                         @NonNull ActivityRecord activityRecord) {
-        mActivityRecord = activityRecord;
         final PackageManager packageManager = wmService.mContext.getPackageManager();
         final OptPropFactory optPropBuilder = new OptPropFactory(packageManager,
                 activityRecord.packageName);
         mAppCompatDeviceStateQuery = new AppCompatDeviceStateQuery(activityRecord);
         mTransparentPolicy = new TransparentPolicy(activityRecord,
                 wmService.mAppCompatConfiguration);
-        mAppCompatOverrides = new AppCompatOverrides(activityRecord,
+        mAppCompatOverrides = new AppCompatOverrides(activityRecord, packageManager,
                 wmService.mAppCompatConfiguration, optPropBuilder, mAppCompatDeviceStateQuery);
         mOrientationPolicy = new AppCompatOrientationPolicy(activityRecord, mAppCompatOverrides);
         mAppCompatAspectRatioPolicy = new AppCompatAspectRatioPolicy(activityRecord,
                 mTransparentPolicy, mAppCompatOverrides);
-        mAppCompatReachabilityPolicy = new AppCompatReachabilityPolicy(mActivityRecord,
+        mReachabilityPolicy = new AppCompatReachabilityPolicy(activityRecord,
                 wmService.mAppCompatConfiguration);
-        mAppCompatLetterboxPolicy = new AppCompatLetterboxPolicy(mActivityRecord,
+        mAppCompatLetterboxPolicy = new AppCompatLetterboxPolicy(activityRecord,
                 wmService.mAppCompatConfiguration);
         mDesktopAppCompatAspectRatioPolicy = new DesktopAppCompatAspectRatioPolicy(activityRecord,
                 mAppCompatOverrides, mTransparentPolicy, wmService.mAppCompatConfiguration);
-        mAppCompatSizeCompatModePolicy = new AppCompatSizeCompatModePolicy(mActivityRecord,
+        mAppCompatSizeCompatModePolicy = new AppCompatSizeCompatModePolicy(activityRecord,
                 mAppCompatOverrides);
-        mAllowRestrictedResizability = AppCompatUtils.asLazy(() -> {
-            // Application level.
-            if (allowRestrictedResizability(packageManager, mActivityRecord.packageName)) {
-                return true;
-            }
-            // Activity level.
-            try {
-                return packageManager.getPropertyAsUser(
-                        PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY,
-                        mActivityRecord.mActivityComponent.getPackageName(),
-                        mActivityRecord.mActivityComponent.getClassName(),
-                        mActivityRecord.mUserId).getBoolean();
-            } catch (PackageManager.NameNotFoundException e) {
-                return false;
-            }
-        });
-    }
-
-    static boolean allowRestrictedResizability(PackageManager pm, String packageName) {
-        try {
-            return pm.getProperty(PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY, packageName)
-                    .getBoolean();
-        } catch (PackageManager.NameNotFoundException e) {
-            return false;
-        }
     }
 
     @NonNull
@@ -123,8 +89,8 @@
     }
 
     @NonNull
-    AppCompatOrientationOverrides getAppCompatOrientationOverrides() {
-        return mAppCompatOverrides.getAppCompatOrientationOverrides();
+    AppCompatOrientationOverrides getOrientationOverrides() {
+        return mAppCompatOverrides.getOrientationOverrides();
     }
 
     @NonNull
@@ -138,13 +104,13 @@
     }
 
     @NonNull
-    AppCompatResizeOverrides getAppCompatResizeOverrides() {
-        return mAppCompatOverrides.getAppCompatResizeOverrides();
+    AppCompatResizeOverrides getResizeOverrides() {
+        return mAppCompatOverrides.getResizeOverrides();
     }
 
     @NonNull
-    AppCompatReachabilityPolicy getAppCompatReachabilityPolicy() {
-        return mAppCompatReachabilityPolicy;
+    AppCompatReachabilityPolicy getReachabilityPolicy() {
+        return mReachabilityPolicy;
     }
 
     @NonNull
@@ -158,8 +124,8 @@
     }
 
     @NonNull
-    AppCompatReachabilityOverrides getAppCompatReachabilityOverrides() {
-        return mAppCompatOverrides.getAppCompatReachabilityOverrides();
+    AppCompatReachabilityOverrides getReachabilityOverrides() {
+        return mAppCompatOverrides.getReachabilityOverrides();
     }
 
     @NonNull
diff --git a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java
index e929fb4..4494586 100644
--- a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java
@@ -154,7 +154,7 @@
 
     @VisibleForTesting
     boolean shouldShowLetterboxUi(@NonNull WindowState mainWindow) {
-        if (mActivityRecord.mAppCompatController.getAppCompatOrientationOverrides()
+        if (mActivityRecord.mAppCompatController.getOrientationOverrides()
                 .getIsRelaunchingAfterRequestedOrientationChanged()) {
             return mLastShouldShowLetterboxUi;
         }
@@ -205,7 +205,7 @@
         }
         pw.println(prefix + "  letterboxReason="
                 + AppCompatUtils.getLetterboxReasonString(mActivityRecord, mainWin));
-        mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy().dump(pw, prefix);
+        mActivityRecord.mAppCompatController.getReachabilityPolicy().dump(pw, prefix);
         final AppCompatLetterboxOverrides letterboxOverride = mActivityRecord.mAppCompatController
                 .getAppCompatLetterboxOverrides();
         pw.println(prefix + "  letterboxBackgroundColor=" + Integer.toHexString(
@@ -276,12 +276,12 @@
                 final AppCompatLetterboxOverrides letterboxOverrides = mActivityRecord
                         .mAppCompatController.getAppCompatLetterboxOverrides();
                 final AppCompatReachabilityPolicy reachabilityPolicy = mActivityRecord
-                        .mAppCompatController.getAppCompatReachabilityPolicy();
+                        .mAppCompatController.getReachabilityPolicy();
                 mLetterbox = new Letterbox(() -> mActivityRecord.makeChildSurface(null),
                         mActivityRecord.mWmService.mTransactionFactory,
                         reachabilityPolicy, letterboxOverrides,
                         this::getLetterboxParentSurface);
-                mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy()
+                mActivityRecord.mAppCompatController.getReachabilityPolicy()
                         .setLetterboxInnerBoundsSupplier(mLetterbox::getInnerFrame);
             }
             final Point letterboxPosition = new Point();
@@ -291,7 +291,7 @@
             final Rect innerFrame = new Rect();
             calculateLetterboxInnerBounds(mActivityRecord, w, innerFrame);
             mLetterbox.layout(spaceToFill, innerFrame, letterboxPosition);
-            if (mActivityRecord.mAppCompatController.getAppCompatReachabilityOverrides()
+            if (mActivityRecord.mAppCompatController.getReachabilityOverrides()
                     .isDoubleTapEvent()) {
                 // We need to notify Shell that letterbox position has changed.
                 mActivityRecord.getTask().dispatchTaskInfoChangedIfNeeded(true /* force */);
@@ -321,7 +321,7 @@
                 mLetterbox.destroy();
                 mLetterbox = null;
             }
-            mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy()
+            mActivityRecord.mAppCompatController.getReachabilityPolicy()
                     .setLetterboxInnerBoundsSupplier(null);
         }
 
@@ -415,7 +415,7 @@
             calculateLetterboxPosition(mActivityRecord, mLetterboxPosition);
             calculateLetterboxOuterBounds(mActivityRecord, mOuterBounds);
             calculateLetterboxInnerBounds(mActivityRecord, w, mInnerBounds);
-            mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy()
+            mActivityRecord.mAppCompatController.getReachabilityPolicy()
                     .setLetterboxInnerBoundsSupplier(() -> mInnerBounds);
         }
 
@@ -438,7 +438,7 @@
             mLetterboxPosition.set(0, 0);
             mInnerBounds.setEmpty();
             mOuterBounds.setEmpty();
-            mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy()
+            mActivityRecord.mAppCompatController.getReachabilityPolicy()
                     .setLetterboxInnerBoundsSupplier(null);
         }
 
diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java b/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java
index c84711d..af83668 100644
--- a/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java
@@ -113,7 +113,7 @@
         // Task to ensure that Activity Embedding is excluded.
         return mActivityRecord.isVisibleRequested() && mActivityRecord.getTaskFragment() != null
                 && mActivityRecord.getTaskFragment().getWindowingMode() == WINDOWING_MODE_FULLSCREEN
-                && mActivityRecord.mAppCompatController.getAppCompatOrientationOverrides()
+                && mActivityRecord.mAppCompatController.getOrientationOverrides()
                     .isOverrideRespectRequestedOrientationEnabled();
     }
 
diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
index 16e2029..fc758ef 100644
--- a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
@@ -94,7 +94,7 @@
             return SCREEN_ORIENTATION_PORTRAIT;
         }
 
-        if (mAppCompatOverrides.getAppCompatOrientationOverrides()
+        if (mAppCompatOverrides.getOrientationOverrides()
                 .isAllowOrientationOverrideOptOut()) {
             return candidate;
         }
@@ -108,7 +108,7 @@
         }
 
         final AppCompatOrientationOverrides.OrientationOverridesState capabilityState =
-                mAppCompatOverrides.getAppCompatOrientationOverrides()
+                mAppCompatOverrides.getOrientationOverrides()
                         .mOrientationOverridesState;
 
         if (capabilityState.mIsOverrideToReverseLandscapeOrientationEnabled
@@ -170,7 +170,7 @@
     boolean shouldIgnoreRequestedOrientation(
             @ActivityInfo.ScreenOrientation int requestedOrientation) {
         final AppCompatOrientationOverrides orientationOverrides =
-                mAppCompatOverrides.getAppCompatOrientationOverrides();
+                mAppCompatOverrides.getOrientationOverrides();
         if (orientationOverrides.shouldEnableIgnoreOrientationRequest()) {
             if (orientationOverrides.getIsRelaunchingAfterRequestedOrientationChanged()) {
                 Slog.w(TAG, "Ignoring orientation update to "
diff --git a/services/core/java/com/android/server/wm/AppCompatOverrides.java b/services/core/java/com/android/server/wm/AppCompatOverrides.java
index 2f03105..9fb54db 100644
--- a/services/core/java/com/android/server/wm/AppCompatOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatOverrides.java
@@ -17,6 +17,7 @@
 package com.android.server.wm;
 
 import android.annotation.NonNull;
+import android.content.pm.PackageManager;
 
 import com.android.server.wm.utils.OptPropFactory;
 
@@ -26,7 +27,7 @@
 public class AppCompatOverrides {
 
     @NonNull
-    private final AppCompatOrientationOverrides mAppCompatOrientationOverrides;
+    private final AppCompatOrientationOverrides mOrientationOverrides;
     @NonNull
     private final AppCompatCameraOverrides mAppCompatCameraOverrides;
     @NonNull
@@ -34,35 +35,37 @@
     @NonNull
     private final AppCompatFocusOverrides mAppCompatFocusOverrides;
     @NonNull
-    private final AppCompatResizeOverrides mAppCompatResizeOverrides;
+    private final AppCompatResizeOverrides mResizeOverrides;
     @NonNull
-    private final AppCompatReachabilityOverrides mAppCompatReachabilityOverrides;
+    private final AppCompatReachabilityOverrides mReachabilityOverrides;
     @NonNull
     private final AppCompatLetterboxOverrides mAppCompatLetterboxOverrides;
 
     AppCompatOverrides(@NonNull ActivityRecord activityRecord,
+            @NonNull PackageManager packageManager,
             @NonNull AppCompatConfiguration appCompatConfiguration,
             @NonNull OptPropFactory optPropBuilder,
             @NonNull AppCompatDeviceStateQuery appCompatDeviceStateQuery) {
         mAppCompatCameraOverrides = new AppCompatCameraOverrides(activityRecord,
                 appCompatConfiguration, optPropBuilder);
-        mAppCompatOrientationOverrides = new AppCompatOrientationOverrides(activityRecord,
+        mOrientationOverrides = new AppCompatOrientationOverrides(activityRecord,
                 appCompatConfiguration, optPropBuilder, mAppCompatCameraOverrides);
-        mAppCompatReachabilityOverrides = new AppCompatReachabilityOverrides(activityRecord,
+        mReachabilityOverrides = new AppCompatReachabilityOverrides(activityRecord,
                 appCompatConfiguration, appCompatDeviceStateQuery);
         mAppCompatAspectRatioOverrides = new AppCompatAspectRatioOverrides(activityRecord,
                 appCompatConfiguration, optPropBuilder, appCompatDeviceStateQuery,
-                mAppCompatReachabilityOverrides);
+                mReachabilityOverrides);
         mAppCompatFocusOverrides = new AppCompatFocusOverrides(activityRecord,
                 appCompatConfiguration, optPropBuilder);
-        mAppCompatResizeOverrides = new AppCompatResizeOverrides(activityRecord, optPropBuilder);
+        mResizeOverrides = new AppCompatResizeOverrides(activityRecord, packageManager,
+                optPropBuilder);
         mAppCompatLetterboxOverrides = new AppCompatLetterboxOverrides(activityRecord,
                 appCompatConfiguration);
     }
 
     @NonNull
-    AppCompatOrientationOverrides getAppCompatOrientationOverrides() {
-        return mAppCompatOrientationOverrides;
+    AppCompatOrientationOverrides getOrientationOverrides() {
+        return mOrientationOverrides;
     }
 
     @NonNull
@@ -81,13 +84,13 @@
     }
 
     @NonNull
-    AppCompatResizeOverrides getAppCompatResizeOverrides() {
-        return mAppCompatResizeOverrides;
+    AppCompatResizeOverrides getResizeOverrides() {
+        return mResizeOverrides;
     }
 
     @NonNull
-    AppCompatReachabilityOverrides getAppCompatReachabilityOverrides() {
-        return mAppCompatReachabilityOverrides;
+    AppCompatReachabilityOverrides getReachabilityOverrides() {
+        return mReachabilityOverrides;
     }
 
     @NonNull
diff --git a/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java b/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java
index d03a803..087edc1 100644
--- a/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java
@@ -77,7 +77,7 @@
 
     void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
         final AppCompatReachabilityOverrides reachabilityOverrides =
-                mActivityRecord.mAppCompatController.getAppCompatReachabilityOverrides();
+                mActivityRecord.mAppCompatController.getReachabilityOverrides();
         pw.println(prefix + "  isVerticalThinLetterboxed=" + reachabilityOverrides
                 .isVerticalThinLetterboxed());
         pw.println(prefix + "  isHorizontalThinLetterboxed=" + reachabilityOverrides
@@ -96,7 +96,7 @@
 
     private void handleHorizontalDoubleTap(int x) {
         final AppCompatReachabilityOverrides reachabilityOverrides =
-                mActivityRecord.mAppCompatController.getAppCompatReachabilityOverrides();
+                mActivityRecord.mAppCompatController.getReachabilityOverrides();
         if (!reachabilityOverrides.isHorizontalReachabilityEnabled()
                 || mActivityRecord.isInTransition()) {
             return;
@@ -142,7 +142,7 @@
 
     private void handleVerticalDoubleTap(int y) {
         final AppCompatReachabilityOverrides reachabilityOverrides =
-                mActivityRecord.mAppCompatController.getAppCompatReachabilityOverrides();
+                mActivityRecord.mAppCompatController.getReachabilityOverrides();
         if (!reachabilityOverrides.isVerticalReachabilityEnabled()
                 || mActivityRecord.isInTransition()) {
             return;
diff --git a/services/core/java/com/android/server/wm/AppCompatResizeOverrides.java b/services/core/java/com/android/server/wm/AppCompatResizeOverrides.java
index 60c1825..fa53153 100644
--- a/services/core/java/com/android/server/wm/AppCompatResizeOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatResizeOverrides.java
@@ -19,13 +19,17 @@
 import static android.content.pm.ActivityInfo.FORCE_NON_RESIZE_APP;
 import static android.content.pm.ActivityInfo.FORCE_RESIZE_APP;
 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY;
 
 import static com.android.server.wm.AppCompatUtils.isChangeEnabled;
 
 import android.annotation.NonNull;
+import android.content.pm.PackageManager;
 
 import com.android.server.wm.utils.OptPropFactory;
 
+import java.util.function.BooleanSupplier;
+
 /**
  * Encapsulate app compat logic about resizability.
  */
@@ -37,11 +41,40 @@
     @NonNull
     private final OptPropFactory.OptProp mAllowForceResizeOverrideOptProp;
 
+    @NonNull
+    private final BooleanSupplier mAllowRestrictedResizability;
+
     AppCompatResizeOverrides(@NonNull ActivityRecord activityRecord,
+            @NonNull PackageManager packageManager,
             @NonNull OptPropFactory optPropBuilder) {
         mActivityRecord = activityRecord;
         mAllowForceResizeOverrideOptProp = optPropBuilder.create(
                 PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES);
+        mAllowRestrictedResizability = AppCompatUtils.asLazy(() -> {
+            // Application level.
+            if (allowRestrictedResizability(packageManager, mActivityRecord.packageName)) {
+                return true;
+            }
+            // Activity level.
+            try {
+                return packageManager.getPropertyAsUser(
+                        PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY,
+                        mActivityRecord.mActivityComponent.getPackageName(),
+                        mActivityRecord.mActivityComponent.getClassName(),
+                        mActivityRecord.mUserId).getBoolean();
+            } catch (PackageManager.NameNotFoundException e) {
+                return false;
+            }
+        });
+    }
+
+    static boolean allowRestrictedResizability(PackageManager pm, String packageName) {
+        try {
+            return pm.getProperty(PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY, packageName)
+                    .getBoolean();
+        } catch (PackageManager.NameNotFoundException e) {
+            return false;
+        }
     }
 
     /**
@@ -75,4 +108,9 @@
         return mAllowForceResizeOverrideOptProp.shouldEnableWithOptInOverrideAndOptOutProperty(
                 isChangeEnabled(mActivityRecord, FORCE_NON_RESIZE_APP));
     }
+
+    /** @see android.view.WindowManager#PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY */
+    boolean allowRestrictedResizability() {
+        return mAllowRestrictedResizability.getAsBoolean();
+    }
 }
diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java
index 9f88bc9..e28dddc 100644
--- a/services/core/java/com/android/server/wm/AppCompatUtils.java
+++ b/services/core/java/com/android/server/wm/AppCompatUtils.java
@@ -138,7 +138,7 @@
             return;
         }
         final AppCompatReachabilityOverrides reachabilityOverrides = top.mAppCompatController
-                .getAppCompatReachabilityOverrides();
+                .getReachabilityOverrides();
         final boolean isTopActivityResumed = top.getOrganizedTask() == task && top.isState(RESUMED);
         final boolean isTopActivityVisible = top.getOrganizedTask() == task && top.isVisible();
         // Whether the direct top activity is in size compat mode.
diff --git a/services/core/java/com/android/server/wm/ContentRecorder.java b/services/core/java/com/android/server/wm/ContentRecorder.java
index a4e58ef..d6ae651 100644
--- a/services/core/java/com/android/server/wm/ContentRecorder.java
+++ b/services/core/java/com/android/server/wm/ContentRecorder.java
@@ -108,9 +108,7 @@
 
     ContentRecorder(@NonNull DisplayContent displayContent) {
         this(displayContent, new RemoteMediaProjectionManagerWrapper(displayContent.mDisplayId),
-                new DisplayManagerFlags().isConnectedDisplayManagementEnabled()
-                        && !new DisplayManagerFlags()
-                                    .isPixelAnisotropyCorrectionInLogicalDisplayEnabled()
+                !new DisplayManagerFlags().isPixelAnisotropyCorrectionInLogicalDisplayEnabled()
                         && displayContent.getDisplayInfo().type == Display.TYPE_EXTERNAL);
     }
 
diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java
index f40d636..b932ef3 100644
--- a/services/core/java/com/android/server/wm/DisplayArea.java
+++ b/services/core/java/com/android/server/wm/DisplayArea.java
@@ -264,7 +264,7 @@
         // that should be respected, Check all activities in display to make sure any eligible
         // activity should be respected.
         final ActivityRecord activity = mDisplayContent.getActivity((r) ->
-                r.mAppCompatController.getAppCompatOrientationOverrides()
+                r.mAppCompatController.getOrientationOverrides()
                     .shouldRespectRequestedOrientationDueToOverride());
         return activity != null;
     }
diff --git a/services/core/java/com/android/server/wm/DisplayAreaPolicy.java b/services/core/java/com/android/server/wm/DisplayAreaPolicy.java
index d49a507..5bec442 100644
--- a/services/core/java/com/android/server/wm/DisplayAreaPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayAreaPolicy.java
@@ -25,6 +25,8 @@
 import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE;
 import static android.view.WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
+import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
+import static android.window.DisplayAreaOrganizer.FEATURE_APP_ZOOM_OUT;
 import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER;
 import static android.window.DisplayAreaOrganizer.FEATURE_FULLSCREEN_MAGNIFICATION;
 import static android.window.DisplayAreaOrganizer.FEATURE_HIDE_DISPLAY_CUTOUT;
@@ -151,6 +153,12 @@
                                 .all()
                                 .except(TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL,
                                         TYPE_SECURE_SYSTEM_OVERLAY)
+                                .build())
+                        .addFeature(new Feature.Builder(wmService.mPolicy, "AppZoomOut",
+                                FEATURE_APP_ZOOM_OUT)
+                                .all()
+                                .except(TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL,
+                                        TYPE_STATUS_BAR, TYPE_NOTIFICATION_SHADE, TYPE_WALLPAPER)
                                 .build());
             }
             rootHierarchy
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 145c7b3..d32c31f 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -1822,7 +1822,13 @@
      */
     private void applyFixedRotationForNonTopVisibleActivityIfNeeded(@NonNull ActivityRecord ar,
             @ActivityInfo.ScreenOrientation int topOrientation) {
-        final int orientation = ar.getRequestedOrientation();
+        int orientation = ar.getRequestedOrientation();
+        if (orientation == ActivityInfo.SCREEN_ORIENTATION_BEHIND) {
+            final ActivityRecord nextCandidate = getActivityBelowForDefiningOrientation(ar);
+            if (nextCandidate != null) {
+                orientation = nextCandidate.getRequestedOrientation();
+            }
+        }
         if (orientation == topOrientation || ar.inMultiWindowMode()
                 || ar.getRequestedConfigurationOrientation() == ORIENTATION_UNDEFINED) {
             return;
@@ -1864,9 +1870,7 @@
             return ROTATION_UNDEFINED;
         }
         if (activityOrientation == ActivityInfo.SCREEN_ORIENTATION_BEHIND) {
-            final ActivityRecord nextCandidate = getActivity(
-                    a -> a.canDefineOrientationForActivitiesAbove() /* callback */,
-                    r /* boundary */, false /* includeBoundary */, true /* traverseTopToBottom */);
+            final ActivityRecord nextCandidate = getActivityBelowForDefiningOrientation(r);
             if (nextCandidate != null) {
                 r = nextCandidate;
                 activityOrientation = r.getOverrideOrientation();
@@ -2964,7 +2968,7 @@
         if (!handlesOrientationChangeFromDescendant(orientation)) {
             ActivityRecord topActivity = topRunningActivity(/* considerKeyguardState= */ true);
             if (topActivity != null && topActivity.mAppCompatController
-                    .getAppCompatOrientationOverrides()
+                    .getOrientationOverrides()
                         .shouldUseDisplayLandscapeNaturalOrientation()) {
                 ProtoLog.v(WM_DEBUG_ORIENTATION,
                         "Display id=%d is ignoring orientation request for %d, return %d"
@@ -4291,7 +4295,7 @@
             return target;
         }
         if (android.view.inputmethod.Flags.refactorInsetsController()) {
-            final DisplayContent defaultDc = mWmService.getDefaultDisplayContentLocked();
+            final DisplayContent defaultDc = getUserMainDisplayContent();
             return defaultDc.mRemoteInsetsControlTarget;
         } else {
             return getImeFallback();
@@ -4301,11 +4305,26 @@
     InsetsControlTarget getImeFallback() {
         // host is in non-default display that doesn't support system decor, default to
         // default display's StatusBar to control IME (when available), else let system control it.
-        final DisplayContent defaultDc = mWmService.getDefaultDisplayContentLocked();
-        WindowState statusBar = defaultDc.getDisplayPolicy().getStatusBar();
+        final DisplayContent defaultDc = getUserMainDisplayContent();
+        final WindowState statusBar = defaultDc.getDisplayPolicy().getStatusBar();
         return statusBar != null ? statusBar : defaultDc.mRemoteInsetsControlTarget;
     }
 
+    private DisplayContent getUserMainDisplayContent() {
+        final DisplayContent defaultDc;
+        if (android.view.inputmethod.Flags.fallbackDisplayForSecondaryUserOnSecondaryDisplay()) {
+            final int userId = mWmService.mUmInternal.getUserAssignedToDisplay(mDisplayId);
+            defaultDc = mWmService.getUserMainDisplayContentLocked(userId);
+            if (defaultDc == null) {
+                throw new IllegalStateException(
+                        "No default display was assigned to user " + userId);
+            }
+        } else {
+            defaultDc = mWmService.getDefaultDisplayContentLocked();
+        }
+        return defaultDc;
+    }
+
     /**
      * Returns the corresponding IME insets control target according the IME target type.
      *
@@ -4841,8 +4860,15 @@
                 // The control target could be the RemoteInsetsControlTarget (if the focussed
                 // view is on a virtual display that can not show the IME (and therefore it will
                 // be shown on the default display)
-                if (isDefaultDisplay && mRemoteInsetsControlTarget != null) {
-                    return mRemoteInsetsControlTarget;
+                if (android.view.inputmethod.Flags
+                        .fallbackDisplayForSecondaryUserOnSecondaryDisplay()) {
+                    if (isUserMainDisplay() && mRemoteInsetsControlTarget != null) {
+                        return mRemoteInsetsControlTarget;
+                    }
+                } else {
+                    if (isDefaultDisplay && mRemoteInsetsControlTarget != null) {
+                        return mRemoteInsetsControlTarget;
+                    }
                 }
             }
             return null;
@@ -4858,6 +4884,16 @@
     }
 
     /**
+     * Returns {@code true} if {@link #mDisplayId} corresponds to the user's main display.
+     *
+     * <p>Visible background users may have other than DEFAULT_DISPLAY marked as their main display.
+     */
+    private boolean isUserMainDisplay() {
+        final int userId = mWmService.mUmInternal.getUserAssignedToDisplay(mDisplayId);
+        return mDisplayId == mWmService.mUmInternal.getMainDisplayAssignedToUser(userId);
+    }
+
+    /**
      * Computes the window the IME should be attached to.
      */
     @VisibleForTesting
diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java
index 3c60d82..db058ca 100644
--- a/services/core/java/com/android/server/wm/DragDropController.java
+++ b/services/core/java/com/android/server/wm/DragDropController.java
@@ -215,7 +215,8 @@
                     mDragState.mOriginalAlpha = alpha;
                     mDragState.mAnimatedScale = callingWin.mGlobalScale;
                     mDragState.mToken = dragToken;
-                    mDragState.mDisplayContent = displayContent;
+                    mDragState.mStartDragDisplayContent = displayContent;
+                    mDragState.mCurrentDisplayContent = displayContent;
                     mDragState.mData = data;
                     mDragState.mCallingTaskIdToHide = shouldMoveCallingTaskToBack(callingWin,
                             flags);
@@ -273,7 +274,7 @@
                     InputManagerGlobal.getInstance().setPointerIcon(
                             PointerIcon.getSystemIcon(
                                     mService.mContext, PointerIcon.TYPE_GRABBING),
-                            mDragState.mDisplayContent.getDisplayId(), touchDeviceId,
+                            mDragState.mCurrentDisplayContent.getDisplayId(), touchDeviceId,
                             touchPointerId, mDragState.getInputToken());
                 }
                 // remember the thumb offsets for later
diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java
index 3a0e41a..d48b9b4 100644
--- a/services/core/java/com/android/server/wm/DragState.java
+++ b/services/core/java/com/android/server/wm/DragState.java
@@ -45,6 +45,7 @@
 import android.content.ClipData;
 import android.content.ClipDescription;
 import android.graphics.Point;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.os.Binder;
 import android.os.Build;
@@ -70,6 +71,7 @@
 import com.android.internal.view.IDragAndDropPermissions;
 import com.android.server.LocalServices;
 import com.android.server.pm.UserManagerInternal;
+import com.android.window.flags.Flags;
 
 import java.util.ArrayList;
 import java.util.concurrent.CompletableFuture;
@@ -127,10 +129,17 @@
      */
     volatile boolean mAnimationCompleted = false;
     /**
+     * The display on which the drag originally started. Note that it's possible for either/both
+     * mStartDragDisplayContent and mCurrentDisplayContent to be invalid if DisplayTopology was
+     * changed or removed in the middle of the drag. In this case, drag will also be cancelled as
+     * soon as listener is notified.
+     */
+    DisplayContent mStartDragDisplayContent;
+    /**
      * The display on which the drag is happening. If it goes into a different display this will
      * be updated.
      */
-    DisplayContent mDisplayContent;
+    DisplayContent mCurrentDisplayContent;
 
     @Nullable private ValueAnimator mAnimator;
     private final Interpolator mCubicEaseOutInterpolator = new DecelerateInterpolator(1.5f);
@@ -179,7 +188,7 @@
                     .setContainerLayer()
                     .setName("Drag and Drop Input Consumer")
                     .setCallsite("DragState.showInputSurface")
-                    .setParent(mDisplayContent.getOverlayLayer())
+                    .setParent(mCurrentDisplayContent.getOverlayLayer())
                     .build();
         }
         final InputWindowHandle h = getInputWindowHandle();
@@ -244,7 +253,8 @@
                     }
                 }
                 DragEvent event = DragEvent.obtain(DragEvent.ACTION_DRAG_ENDED, inWindowX,
-                        inWindowY, mThumbOffsetX, mThumbOffsetY, mFlags, null, null, null,
+                        inWindowY, mThumbOffsetX, mThumbOffsetY,
+                        mCurrentDisplayContent.getDisplayId(), mFlags, null, null, null,
                         dragSurface, null, mDragResult);
                 try {
                     if (DEBUG_DRAG) Slog.d(TAG_WM, "Sending DRAG_ENDED to " + ws);
@@ -542,10 +552,26 @@
                 }
             }
             ClipDescription description = data != null ? data.getDescription() : mDataDescription;
+
+            // Note this can be negative numbers if touch coords are left or top of the window.
+            PointF relativeToWindowCoords = new PointF(newWin.translateToWindowX(touchX),
+                    newWin.translateToWindowY(touchY));
+            if (Flags.enableConnectedDisplaysDnd()
+                    && mCurrentDisplayContent.getDisplayId() != newWin.getDisplayId()) {
+                // Currently DRAG_STARTED coords are sent relative to the window target in **px**
+                // coordinates. However, this cannot be extended to connected displays scenario,
+                // as there's only global **dp** coordinates and no global **px** coordinates.
+                // Hence, the coords sent here will only try to indicate that drag started outside
+                // this window display, but relative distance should not be calculated or depended
+                // on.
+                relativeToWindowCoords = new PointF(-newWin.getBounds().left - 1,
+                        -newWin.getBounds().top - 1);
+            }
+
             DragEvent event = obtainDragEvent(DragEvent.ACTION_DRAG_STARTED,
-                    newWin.translateToWindowX(touchX), newWin.translateToWindowY(touchY),
-                    description, data, false /* includeDragSurface */,
-                    true /* includeDragFlags */, null /* dragAndDropPermission */);
+                    relativeToWindowCoords.x, relativeToWindowCoords.y, description, data,
+                    false /* includeDragSurface */, true /* includeDragFlags */,
+                    null /* dragAndDropPermission */);
             try {
                 newWin.mClient.dispatchDragEvent(event);
                 // track each window that we've notified that the drag is starting
@@ -702,6 +728,20 @@
         mCurrentDisplayX = displayX;
         mCurrentDisplayY = displayY;
 
+        final DisplayContent lastSetDisplayContent = mCurrentDisplayContent;
+        boolean cursorMovedToDifferentDisplay = false;
+        // Keep latest display up-to-date even when drag has stopped.
+        if (Flags.enableConnectedDisplaysDnd() && mCurrentDisplayContent.mDisplayId != displayId) {
+            final DisplayContent newDisplay = mService.mRoot.getDisplayContent(displayId);
+            if (newDisplay == null) {
+                Slog.e(TAG_WM, "Target displayId=" + displayId + " was not found, ending drag.");
+                endDragLocked(false /* dropConsumed */,
+                        false /* relinquishDragSurfaceToDropTarget */);
+                return;
+            }
+            cursorMovedToDifferentDisplay = true;
+            mCurrentDisplayContent = newDisplay;
+        }
         if (!keepHandling) {
             return;
         }
@@ -710,6 +750,24 @@
         if (SHOW_LIGHT_TRANSACTIONS) {
             Slog.i(TAG_WM, ">>> OPEN TRANSACTION notifyMoveLocked");
         }
+        if (cursorMovedToDifferentDisplay) {
+            mAnimatedScale = mAnimatedScale * mCurrentDisplayContent.mBaseDisplayDensity
+                    / lastSetDisplayContent.mBaseDisplayDensity;
+            mThumbOffsetX = mThumbOffsetX * mCurrentDisplayContent.mBaseDisplayDensity
+                    / lastSetDisplayContent.mBaseDisplayDensity;
+            mThumbOffsetY = mThumbOffsetY * mCurrentDisplayContent.mBaseDisplayDensity
+                    / lastSetDisplayContent.mBaseDisplayDensity;
+            mTransaction.reparent(mSurfaceControl, mCurrentDisplayContent.getSurfaceControl());
+            mTransaction.setScale(mSurfaceControl, mAnimatedScale, mAnimatedScale);
+
+            final InputWindowHandle inputWindowHandle = getInputWindowHandle();
+            if (inputWindowHandle == null) {
+                Slog.w(TAG_WM, "Drag is in progress but there is no drag window handle.");
+                return;
+            }
+            inputWindowHandle.displayId = displayId;
+            mTransaction.setInputWindowInfo(mInputSurface, inputWindowHandle);
+        }
         mTransaction.setPosition(mSurfaceControl, displayX - mThumbOffsetX,
                 displayY - mThumbOffsetY).apply();
         ProtoLog.i(WM_SHOW_TRANSACTIONS, "DRAG %s: displayId=%d, pos=(%d,%d)", mSurfaceControl,
@@ -734,10 +792,10 @@
             ClipData data, boolean includeDragSurface, boolean includeDragFlags,
             IDragAndDropPermissions dragAndDropPermissions) {
         return DragEvent.obtain(action, x, y, mThumbOffsetX, mThumbOffsetY,
-                includeDragFlags ? mFlags : 0,
+                mCurrentDisplayContent.getDisplayId(), includeDragFlags ? mFlags : 0,
                 null  /* localState */, description, data,
-                includeDragSurface ? mSurfaceControl : null,
-                dragAndDropPermissions, false /* result */);
+                includeDragSurface ? mSurfaceControl : null, dragAndDropPermissions,
+                false /* result */);
     }
 
     private ValueAnimator createReturnAnimationLocked() {
diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
index 98ed6f7..54ae80c 100644
--- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
@@ -103,6 +103,8 @@
             // again, so that the control with leash can be eventually dispatched
             if (!mGivenInsetsReady && isServerVisible() && !givenInsetsPending
                     && mControlTarget != null) {
+                ProtoLog.d(WM_DEBUG_IME,
+                        "onPostLayout: IME control ready to be dispatched, ws=%s", ws);
                 mGivenInsetsReady = true;
                 ImeTracker.forLogging().onProgress(mStatsToken,
                         ImeTracker.PHASE_WM_POST_LAYOUT_NOTIFY_CONTROLS_CHANGED);
@@ -118,6 +120,8 @@
                         ImeTracker.PHASE_WM_POST_LAYOUT_NOTIFY_CONTROLS_CHANGED);
                 mStatsToken = null;
             } else if (wasServerVisible && !isServerVisible()) {
+                ProtoLog.d(WM_DEBUG_IME, "onPostLayout: setImeShowing(false) was: %s, ws=%s",
+                        isImeShowing(), ws);
                 setImeShowing(false);
             }
         }
@@ -621,6 +625,7 @@
             // request (cancelling the initial show) or hide request (aborting the initial show).
             logIsScheduledAndReadyToShowIme(!visible /* aborted */);
         }
+        ProtoLog.d(WM_DEBUG_IME, "receiveImeStatsToken: visible=%s", visible);
         if (visible) {
             ImeTracker.forLogging().onCancelled(
                     mStatsToken, ImeTracker.PHASE_WM_ABORT_SHOW_IME_POST_LAYOUT);
diff --git a/services/core/java/com/android/server/wm/InputConfigAdapter.java b/services/core/java/com/android/server/wm/InputConfigAdapter.java
index ae6e724..e3ffe71 100644
--- a/services/core/java/com/android/server/wm/InputConfigAdapter.java
+++ b/services/core/java/com/android/server/wm/InputConfigAdapter.java
@@ -76,9 +76,6 @@
                     LayoutParams.FLAG_NOT_TOUCHABLE,
                     InputConfig.NOT_TOUCHABLE, false /* inverted */),
             new FlagMapping(
-                    LayoutParams.FLAG_SPLIT_TOUCH,
-                    InputConfig.PREVENT_SPLITTING, true /* inverted */),
-            new FlagMapping(
                     LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
                     InputConfig.WATCH_OUTSIDE_TOUCH, false /* inverted */),
             new FlagMapping(
diff --git a/services/core/java/com/android/server/wm/PageSizeMismatchDialog.java b/services/core/java/com/android/server/wm/PageSizeMismatchDialog.java
index 8c50913..29922f0 100644
--- a/services/core/java/com/android/server/wm/PageSizeMismatchDialog.java
+++ b/services/core/java/com/android/server/wm/PageSizeMismatchDialog.java
@@ -57,9 +57,11 @@
 
         final AlertDialog.Builder builder =
                 new AlertDialog.Builder(context)
-                        .setPositiveButton(
-                                R.string.ok,
-                                (dialog, which) -> {/* Do nothing */})
+                        .setPositiveButton(R.string.ok, (dialog, which) ->
+                                        manager.setPackageFlag(
+                                                mUserId, mPackageName,
+                                                AppWarnings.FLAG_HIDE_PAGE_SIZE_MISMATCH,
+                                                true))
                         .setMessage(Html.fromHtml(warning, FROM_HTML_MODE_COMPACT))
                         .setTitle(label);
 
diff --git a/services/core/java/com/android/server/wm/PersisterQueue.java b/services/core/java/com/android/server/wm/PersisterQueue.java
index 9dc3d6a..bc16a56 100644
--- a/services/core/java/com/android/server/wm/PersisterQueue.java
+++ b/services/core/java/com/android/server/wm/PersisterQueue.java
@@ -86,6 +86,34 @@
         mLazyTaskWriterThread = new LazyTaskWriterThread("LazyTaskWriterThread");
     }
 
+    /**
+     * Busy wait until {@link #mLazyTaskWriterThread} is in {@link Thread.State#WAITING}, or
+     * times out. This indicates the thread is waiting for new tasks to appear. If the wait
+     * succeeds, this queue waits at least {@link #mPreTaskDelayMs} milliseconds before running the
+     * next task.
+     *
+     * <p>This is for testing purposes only.
+     *
+     * @param timeoutMillis the maximum time of waiting in milliseconds
+     * @return {@code true} if the thread is in {@link Thread.State#WAITING} at return
+     */
+    @VisibleForTesting
+    boolean waitUntilWritingThreadIsWaiting(long timeoutMillis) {
+        final long timeoutTime = SystemClock.uptimeMillis() + timeoutMillis;
+        do {
+            Thread.State state;
+            synchronized (this) {
+                state = mLazyTaskWriterThread.getState();
+            }
+            if (state == Thread.State.WAITING) {
+                return true;
+            }
+            Thread.yield();
+        } while (SystemClock.uptimeMillis() < timeoutTime);
+
+        return false;
+    }
+
     synchronized void startPersisting() {
         if (!mLazyTaskWriterThread.isAlive()) {
             mLazyTaskWriterThread.start();
diff --git a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
index a545454..3eb13c5 100644
--- a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
+++ b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
@@ -407,10 +407,8 @@
             bitmap.recycle();
 
             final File file = mPersistInfoProvider.getHighResolutionBitmapFile(mId, mUserId);
-            try {
-                FileOutputStream fos = new FileOutputStream(file);
+            try (FileOutputStream fos = new FileOutputStream(file)) {
                 swBitmap.compress(JPEG, COMPRESS_QUALITY, fos);
-                fos.close();
             } catch (IOException e) {
                 Slog.e(TAG, "Unable to open " + file + " for persisting.", e);
                 return false;
@@ -428,10 +426,8 @@
             swBitmap.recycle();
 
             final File lowResFile = mPersistInfoProvider.getLowResolutionBitmapFile(mId, mUserId);
-            try {
-                FileOutputStream lowResFos = new FileOutputStream(lowResFile);
+            try (FileOutputStream lowResFos = new FileOutputStream(lowResFile)) {
                 lowResBitmap.compress(JPEG, COMPRESS_QUALITY, lowResFos);
-                lowResFos.close();
             } catch (IOException e) {
                 Slog.e(TAG, "Unable to open " + lowResFile + " for persisting.", e);
                 return false;
diff --git a/services/core/java/com/android/server/wm/StartingData.java b/services/core/java/com/android/server/wm/StartingData.java
index 7349224..1a7a619 100644
--- a/services/core/java/com/android/server/wm/StartingData.java
+++ b/services/core/java/com/android/server/wm/StartingData.java
@@ -31,11 +31,18 @@
     static final int AFTER_TRANSACTION_REMOVE_DIRECTLY = 1;
     /** Do copy splash screen to client after transaction done. */
     static final int AFTER_TRANSACTION_COPY_TO_CLIENT = 2;
+    /**
+     * Remove the starting window after transition finish.
+     * Used when activity doesn't request show when locked, so the app window should never show to
+     * the user if device is locked.
+     **/
+    static final int AFTER_TRANSITION_FINISH = 3;
 
     @IntDef(prefix = { "AFTER_TRANSACTION" }, value = {
             AFTER_TRANSACTION_IDLE,
             AFTER_TRANSACTION_REMOVE_DIRECTLY,
             AFTER_TRANSACTION_COPY_TO_CLIENT,
+            AFTER_TRANSITION_FINISH,
     })
     @interface AfterTransaction {}
 
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index d92301b..fe478c60 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -5230,9 +5230,15 @@
             // to ensure any necessary pause logic occurs. In the case where the Activity will be
             // shown regardless of the lock screen, the call to
             // {@link ActivityTaskSupervisor#checkReadyForSleepLocked} is skipped.
-            final ActivityRecord next = topRunningActivity(true /* focusableOnly */);
-            if (next == null || !next.canTurnScreenOn()) {
-                checkReadyForSleep();
+            if (shouldSleepActivities()) {
+                final ActivityRecord next = topRunningActivity(true /* focusableOnly */);
+                if (next != null && next.canTurnScreenOn()
+                        && !mWmService.mPowerManager.isInteractive()) {
+                    mTaskSupervisor.wakeUp(getDisplayId(), "resumeTop-turnScreenOnFlag");
+                    next.setCurrentLaunchCanTurnScreenOn(false);
+                } else {
+                    checkReadyForSleep();
+                }
             }
         } finally {
             mInResumeTopActivity = false;
@@ -5255,6 +5261,10 @@
             return false;
         }
 
+        if (!mTaskSupervisor.readyToResume()) {
+            return false;
+        }
+
         final ActivityRecord topActivity = topRunningActivity(true /* focusableOnly */);
         if (topActivity == null) {
             // There are no activities left in this task, let's look somewhere else.
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index f0faa8e46..f4a455a 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -70,6 +70,8 @@
 import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_RECENTS_ANIM;
 import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_SPLASH_SCREEN;
 import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_WINDOWS_DRAWN;
+import static com.android.server.wm.StartingData.AFTER_TRANSACTION_IDLE;
+import static com.android.server.wm.StartingData.AFTER_TRANSITION_FINISH;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK;
 import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
 import static com.android.server.wm.WindowState.BLAST_TIMEOUT_DURATION;
@@ -1374,6 +1376,13 @@
                         enterAutoPip = true;
                     }
                 }
+
+                if (ar.mStartingData != null && ar.mStartingData.mRemoveAfterTransaction
+                        == AFTER_TRANSITION_FINISH
+                        && (!ar.isVisible() || !ar.mTransitionController.inTransition(ar))) {
+                    ar.mStartingData.mRemoveAfterTransaction = AFTER_TRANSACTION_IDLE;
+                    ar.removeStartingWindow();
+                }
                 final ChangeInfo changeInfo = mChanges.get(ar);
                 // Due to transient-hide, there may be some activities here which weren't in the
                 // transition.
@@ -1412,6 +1421,7 @@
                         if (!tr.isAttached() || !tr.isVisibleRequested()
                                 || !tr.inPinnedWindowingMode()) return;
                         final ActivityRecord currTop = tr.getTopNonFinishingActivity();
+                        if (currTop == null) return;
                         if (currTop.inPinnedWindowingMode()) return;
                         Slog.e(TAG, "Enter-PIP was started but not completed, this is a Shell/SysUI"
                                 + " bug. This state breaks gesture-nav, so attempting clean-up.");
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index aa60f93..54a3d41 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -1659,6 +1659,12 @@
         return ORIENTATION_UNDEFINED;
     }
 
+    @Nullable
+    ActivityRecord getActivityBelowForDefiningOrientation(ActivityRecord from) {
+        return getActivity(ActivityRecord::canDefineOrientationForActivitiesAbove,
+                from /* boundary */, false /* includeBoundary */, true /* traverseTopToBottom */);
+    }
+
     /**
      * Calls {@link #setOrientation(int, WindowContainer)} with {@code null} to the last 2
      * parameters.
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 3c6778e..e4ef3d1 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -7473,6 +7473,23 @@
         return mRoot.getDisplayContent(DEFAULT_DISPLAY);
     }
 
+    /**
+     * Returns the main display content for the user passed as parameter.
+     *
+     * <p>Visible background users may have their own designated main display, distinct from the
+     * system default display (DEFAULT_DISPLAY). Visible background users operate independently
+     * with their own main displays. These secondary user main displays host the secondary home
+     * activities.
+     */
+    @Nullable
+    DisplayContent getUserMainDisplayContentLocked(@UserIdInt int userId) {
+        final int userMainDisplayId = mUmInternal.getMainDisplayAssignedToUser(userId);
+        if (userMainDisplayId == -1) {
+            return null;
+        }
+        return mRoot.getDisplayContent(userMainDisplayId);
+    }
+
     public void onOverlayChanged() {
         // Post to display thread so it can get the latest display info.
         mH.post(() -> {
@@ -10177,9 +10194,10 @@
             throw new SecurityException("Access denied to process: " + pid
                     + ", must have permission " + Manifest.permission.ACCESS_FPS_COUNTER);
         }
-
-        if (mRoot.anyTaskForId(taskId) == null) {
-            throw new IllegalArgumentException("no task with taskId: " + taskId);
+        synchronized (mGlobalLock) {
+            if (mRoot.anyTaskForId(taskId) == null) {
+                throw new IllegalArgumentException("no task with taskId: " + taskId);
+            }
         }
 
         mTaskFpsCallbackController.registerListener(taskId, callback);
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index b43e334..d69b06a 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -126,6 +126,7 @@
 import static com.android.server.wm.MoveAnimationSpecProto.DURATION_MS;
 import static com.android.server.wm.MoveAnimationSpecProto.FROM;
 import static com.android.server.wm.MoveAnimationSpecProto.TO;
+import static com.android.server.wm.StartingData.AFTER_TRANSITION_FINISH;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_ALL;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_STARTING_REVEAL;
@@ -1920,6 +1921,13 @@
         }
         final ActivityRecord atoken = mActivityRecord;
         if (atoken != null) {
+            if (atoken.mStartingData != null && mAttrs.type != TYPE_APPLICATION_STARTING
+                    && atoken.mStartingData.mRemoveAfterTransaction
+                    == AFTER_TRANSITION_FINISH) {
+                // Preventing app window from visible during un-occluding animation playing due to
+                // alpha blending.
+                return false;
+            }
             final boolean isVisible = isStartingWindowAssociatedToTask()
                     ? mStartingData.mAssociatedTask.isVisible() : atoken.isVisible();
             return ((!isParentWindowHidden() && isVisible)
@@ -2925,7 +2933,14 @@
             final int mask = FLAG_SHOW_WHEN_LOCKED | FLAG_DISMISS_KEYGUARD
                     | FLAG_ALLOW_LOCK_WHILE_SCREEN_ON;
             WindowManager.LayoutParams sa = mActivityRecord.mStartingWindow.mAttrs;
+            final boolean wasShowWhenLocked = (sa.flags & FLAG_SHOW_WHEN_LOCKED) != 0;
+            final boolean removeShowWhenLocked = (mAttrs.flags & FLAG_SHOW_WHEN_LOCKED) == 0;
             sa.flags = (sa.flags & ~mask) | (mAttrs.flags & mask);
+            if (Flags.keepAppWindowHideWhileLocked() && wasShowWhenLocked && removeShowWhenLocked) {
+                // Trigger unoccluding animation if needed.
+                mActivityRecord.checkKeyguardFlagsChanged();
+                mActivityRecord.deferStartingWindowRemovalForKeyguardUnoccluding();
+            }
         }
     }
 
@@ -5424,7 +5439,7 @@
             // change then delay the position update until it has redrawn to avoid any flickers.
             final boolean isLetterboxedAndRelaunching = activityRecord != null
                     && activityRecord.areBoundsLetterboxed()
-                    && activityRecord.mAppCompatController.getAppCompatOrientationOverrides()
+                    && activityRecord.mAppCompatController.getOrientationOverrides()
                         .getIsRelaunchingAfterRequestedOrientationChanged();
             if (surfaceResizedWithoutMoveAnimation || isLetterboxedAndRelaunching) {
                 applyWithNextDraw(mSetSurfacePositionConsumer);
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 65cf4ee..911c686 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -343,9 +343,10 @@
     void setPointerDisplayId(ui::LogicalDisplayId displayId);
     int32_t getMousePointerSpeed();
     void setPointerSpeed(int32_t speed);
-    void setMousePointerAccelerationEnabled(ui::LogicalDisplayId displayId, bool enabled);
+    void setMouseScalingEnabled(ui::LogicalDisplayId displayId, bool enabled);
     void setMouseReverseVerticalScrollingEnabled(bool enabled);
     void setMouseScrollingAccelerationEnabled(bool enabled);
+    void setMouseScrollingSpeed(int32_t speed);
     void setMouseSwapPrimaryButtonEnabled(bool enabled);
     void setMouseAccelerationEnabled(bool enabled);
     void setTouchpadPointerSpeed(int32_t speed);
@@ -473,8 +474,8 @@
         // Pointer speed.
         int32_t pointerSpeed{0};
 
-        // Displays on which its associated mice will have pointer acceleration disabled.
-        std::set<ui::LogicalDisplayId> displaysWithMousePointerAccelerationDisabled{};
+        // Displays on which its associated mice will have all scaling disabled.
+        std::set<ui::LogicalDisplayId> displaysWithMouseScalingDisabled{};
 
         // True if pointer gestures are enabled.
         bool pointerGesturesEnabled{true};
@@ -500,6 +501,9 @@
         // True if mouse scrolling acceleration is enabled.
         bool mouseScrollingAccelerationEnabled{true};
 
+        // The mouse scrolling speed, as a number from -7 (slowest) to 7 (fastest).
+        int32_t mouseScrollingSpeed{0};
+
         // True if mouse vertical scrolling is reversed.
         bool mouseReverseVerticalScrollingEnabled{false};
 
@@ -599,9 +603,8 @@
         dump += StringPrintf(INDENT "System UI Lights Out: %s\n",
                              toString(mLocked.systemUiLightsOut));
         dump += StringPrintf(INDENT "Pointer Speed: %" PRId32 "\n", mLocked.pointerSpeed);
-        dump += StringPrintf(INDENT "Display with Mouse Pointer Acceleration Disabled: %s\n",
-                             dumpSet(mLocked.displaysWithMousePointerAccelerationDisabled,
-                                     streamableToString)
+        dump += StringPrintf(INDENT "Display with Mouse Scaling Disabled: %s\n",
+                             dumpSet(mLocked.displaysWithMouseScalingDisabled, streamableToString)
                                      .c_str());
         dump += StringPrintf(INDENT "Pointer Gestures Enabled: %s\n",
                              toString(mLocked.pointerGesturesEnabled));
@@ -830,19 +833,20 @@
         std::scoped_lock _l(mLock);
 
         outConfig->mousePointerSpeed = mLocked.pointerSpeed;
-        outConfig->displaysWithMousePointerAccelerationDisabled =
-                mLocked.displaysWithMousePointerAccelerationDisabled;
+        outConfig->displaysWithMouseScalingDisabled = mLocked.displaysWithMouseScalingDisabled;
         outConfig->pointerVelocityControlParameters.scale =
                 exp2f(mLocked.pointerSpeed * POINTER_SPEED_EXPONENT);
         outConfig->pointerVelocityControlParameters.acceleration =
-                mLocked.displaysWithMousePointerAccelerationDisabled.count(
-                        mLocked.pointerDisplayId) == 0
+                mLocked.displaysWithMouseScalingDisabled.count(mLocked.pointerDisplayId) == 0
                 ? android::os::IInputConstants::DEFAULT_POINTER_ACCELERATION
                 : 1;
         outConfig->wheelVelocityControlParameters.acceleration =
                 mLocked.mouseScrollingAccelerationEnabled
                 ? android::os::IInputConstants::DEFAULT_MOUSE_WHEEL_ACCELERATION
                 : 1;
+        outConfig->wheelVelocityControlParameters.scale = mLocked.mouseScrollingAccelerationEnabled
+                ? 1
+                : exp2f(mLocked.mouseScrollingSpeed * POINTER_SPEED_EXPONENT);
         outConfig->pointerGesturesEnabled = mLocked.pointerGesturesEnabled;
 
         outConfig->pointerCaptureRequest = mLocked.pointerCaptureRequest;
@@ -1451,6 +1455,21 @@
             InputReaderConfiguration::Change::POINTER_SPEED);
 }
 
+void NativeInputManager::setMouseScrollingSpeed(int32_t speed) {
+    { // acquire lock
+        std::scoped_lock _l(mLock);
+
+        if (mLocked.mouseScrollingSpeed == speed) {
+            return;
+        }
+
+        mLocked.mouseScrollingSpeed = speed;
+    } // release lock
+
+    mInputManager->getReader().requestRefreshConfiguration(
+            InputReaderConfiguration::Change::POINTER_SPEED);
+}
+
 void NativeInputManager::setMouseSwapPrimaryButtonEnabled(bool enabled) {
     { // acquire lock
         std::scoped_lock _l(mLock);
@@ -1497,23 +1516,21 @@
             InputReaderConfiguration::Change::POINTER_SPEED);
 }
 
-void NativeInputManager::setMousePointerAccelerationEnabled(ui::LogicalDisplayId displayId,
-                                                            bool enabled) {
+void NativeInputManager::setMouseScalingEnabled(ui::LogicalDisplayId displayId, bool enabled) {
     { // acquire lock
         std::scoped_lock _l(mLock);
 
-        const bool oldEnabled =
-                mLocked.displaysWithMousePointerAccelerationDisabled.count(displayId) == 0;
+        const bool oldEnabled = mLocked.displaysWithMouseScalingDisabled.count(displayId) == 0;
         if (oldEnabled == enabled) {
             return;
         }
 
-        ALOGI("Setting mouse pointer acceleration to %s on display %s", toString(enabled),
+        ALOGI("Setting mouse pointer scaling to %s on display %s", toString(enabled),
               displayId.toString().c_str());
         if (enabled) {
-            mLocked.displaysWithMousePointerAccelerationDisabled.erase(displayId);
+            mLocked.displaysWithMouseScalingDisabled.erase(displayId);
         } else {
-            mLocked.displaysWithMousePointerAccelerationDisabled.emplace(displayId);
+            mLocked.displaysWithMouseScalingDisabled.emplace(displayId);
         }
     } // release lock
 
@@ -2567,11 +2584,11 @@
     im->setPointerSpeed(speed);
 }
 
-static void nativeSetMousePointerAccelerationEnabled(JNIEnv* env, jobject nativeImplObj,
-                                                     jint displayId, jboolean enabled) {
+static void nativeSetMouseScalingEnabled(JNIEnv* env, jobject nativeImplObj, jint displayId,
+                                         jboolean enabled) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
-    im->setMousePointerAccelerationEnabled(ui::LogicalDisplayId{displayId}, enabled);
+    im->setMouseScalingEnabled(ui::LogicalDisplayId{displayId}, enabled);
 }
 
 static void nativeSetTouchpadPointerSpeed(JNIEnv* env, jobject nativeImplObj, jint speed) {
@@ -3243,6 +3260,11 @@
     im->setMouseScrollingAccelerationEnabled(enabled);
 }
 
+static void nativeSetMouseScrollingSpeed(JNIEnv* env, jobject nativeImplObj, jint speed) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+    im->setMouseScrollingSpeed(speed);
+}
+
 static void nativeSetMouseReverseVerticalScrollingEnabled(JNIEnv* env, jobject nativeImplObj,
                                                           bool enabled) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
@@ -3313,12 +3335,12 @@
         {"transferTouch", "(Landroid/os/IBinder;I)Z", (void*)nativeTransferTouchOnDisplay},
         {"getMousePointerSpeed", "()I", (void*)nativeGetMousePointerSpeed},
         {"setPointerSpeed", "(I)V", (void*)nativeSetPointerSpeed},
-        {"setMousePointerAccelerationEnabled", "(IZ)V",
-         (void*)nativeSetMousePointerAccelerationEnabled},
+        {"setMouseScalingEnabled", "(IZ)V", (void*)nativeSetMouseScalingEnabled},
         {"setMouseReverseVerticalScrollingEnabled", "(Z)V",
          (void*)nativeSetMouseReverseVerticalScrollingEnabled},
         {"setMouseScrollingAccelerationEnabled", "(Z)V",
          (void*)nativeSetMouseScrollingAccelerationEnabled},
+        {"setMouseScrollingSpeed", "(I)V", (void*)nativeSetMouseScrollingSpeed},
         {"setMouseSwapPrimaryButtonEnabled", "(Z)V", (void*)nativeSetMouseSwapPrimaryButtonEnabled},
         {"setMouseAccelerationEnabled", "(Z)V", (void*)nativeSetMouseAccelerationEnabled},
         {"setTouchpadPointerSpeed", "(I)V", (void*)nativeSetTouchpadPointerSpeed},
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 2627895..d2d3884 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -8909,11 +8909,15 @@
 
         if (parent) {
             Preconditions.checkCallAuthorization(
-                    isProfileOwnerOfOrganizationOwnedDevice(getCallerIdentity().getUserId()));
+                    isProfileOwnerOfOrganizationOwnedDevice(caller.getUserId()));
+            // If a DPC is querying on the parent instance, make sure it's only querying the parent
+            // user of itself. Querying any other user is not allowed.
+            Preconditions.checkArgument(caller.getUserId() == userHandle);
         }
+        int affectedUserId = parent ? getProfileParentId(userHandle) : userHandle;
         Boolean disallowed = mDevicePolicyEngine.getResolvedPolicy(
                 PolicyDefinition.SCREEN_CAPTURE_DISABLED,
-                userHandle);
+                affectedUserId);
         return disallowed != null && disallowed;
     }
 
@@ -14669,7 +14673,7 @@
     @Override
     public void setSecondaryLockscreenEnabled(ComponentName who, boolean enabled,
             PersistableBundle options) {
-        if (Flags.secondaryLockscreenApiEnabled()) {
+        if (Flags.secondaryLockscreenApiEnabled() && mSupervisionManagerInternal != null) {
             final CallerIdentity caller = getCallerIdentity();
             final boolean isRoleHolder = isCallerSystemSupervisionRoleHolder(caller);
             synchronized (getLockObject()) {
@@ -14680,16 +14684,8 @@
                         caller.getUserId());
             }
 
-            if (mSupervisionManagerInternal != null) {
-                mSupervisionManagerInternal.setSupervisionLockscreenEnabledForUser(
-                        caller.getUserId(), enabled, options);
-            } else {
-                synchronized (getLockObject()) {
-                    DevicePolicyData policy = getUserData(caller.getUserId());
-                    policy.mSecondaryLockscreenEnabled = enabled;
-                    saveSettingsLocked(caller.getUserId());
-                }
-            }
+            mSupervisionManagerInternal.setSupervisionLockscreenEnabledForUser(
+                    caller.getUserId(), enabled, options);
         } else {
             Objects.requireNonNull(who, "ComponentName is null");
 
@@ -21907,7 +21903,7 @@
                     accountToMigrate,
                     sourceUser,
                     targetUser,
-                    /* callback= */ null, /* handler= */ null)
+                    /* handler= */ null, /* callback= */ null)
                     .getResult(60 * 3, TimeUnit.SECONDS);
             if (copySucceeded) {
                 logCopyAccountStatus(COPY_ACCOUNT_SUCCEEDED, callerPackage);
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 9ab9a8f..8e06ed8 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1787,9 +1787,11 @@
             SignedConfigService.registerUpdateReceiver(mSystemContext);
             t.traceEnd();
 
-            t.traceBegin("AppIntegrityService");
-            mSystemServiceManager.startService(AppIntegrityManagerService.class);
-            t.traceEnd();
+            if (!android.server.Flags.removeAppIntegrityManagerService()) {
+                t.traceBegin("AppIntegrityService");
+                mSystemServiceManager.startService(AppIntegrityManagerService.class);
+                t.traceEnd();
+            }
 
             t.traceBegin("StartLogcatManager");
             mSystemServiceManager.startService(LogcatManagerService.class);
@@ -2712,16 +2714,18 @@
             mSystemServiceManager.startService(AuthService.class);
             t.traceEnd();
 
-            if (android.security.Flags.secureLockdown()) {
-                t.traceBegin("StartSecureLockDeviceService.Lifecycle");
-                mSystemServiceManager.startService(SecureLockDeviceService.Lifecycle.class);
-                t.traceEnd();
-            }
+            if (!isWatch && !isTv && !isAutomotive) {
+                if (android.security.Flags.secureLockdown()) {
+                    t.traceBegin("StartSecureLockDeviceService.Lifecycle");
+                    mSystemServiceManager.startService(SecureLockDeviceService.Lifecycle.class);
+                    t.traceEnd();
+                }
 
-            if (android.adaptiveauth.Flags.enableAdaptiveAuth()) {
-                t.traceBegin("StartAuthenticationPolicyService");
-                mSystemServiceManager.startService(AuthenticationPolicyService.class);
-                t.traceEnd();
+                if (android.adaptiveauth.Flags.enableAdaptiveAuth()) {
+                    t.traceBegin("StartAuthenticationPolicyService");
+                    mSystemServiceManager.startService(AuthenticationPolicyService.class);
+                    t.traceEnd();
+                }
             }
 
             if (!isWatch) {
diff --git a/services/java/com/android/server/flags.aconfig b/services/java/com/android/server/flags.aconfig
index 0d222fb..4d021ec 100644
--- a/services/java/com/android/server/flags.aconfig
+++ b/services/java/com/android/server/flags.aconfig
@@ -51,4 +51,11 @@
      description: "Remove GameManagerService from Wear"
      bug: "340929737"
      is_fixed_read_only: true
+}
+
+flag {
+     name: "remove_app_integrity_manager_service"
+     namespace: "package_manager_service"
+     description: "Remove AppIntegrityManagerService"
+     bug: "364200023"
 }
\ No newline at end of file
diff --git a/services/print/Android.bp b/services/print/Android.bp
index 0dfceaa..b77cf16 100644
--- a/services/print/Android.bp
+++ b/services/print/Android.bp
@@ -18,8 +18,21 @@
     name: "services.print",
     defaults: ["platform_service_defaults"],
     srcs: [":services.print-sources"],
+    static_libs: ["print_flags_lib"],
     libs: ["services.core"],
     lint: {
         baseline_filename: "lint-baseline.xml",
     },
 }
+
+aconfig_declarations {
+    name: "print_flags",
+    package: "com.android.server.print",
+    container: "system",
+    srcs: ["**/flags.aconfig"],
+}
+
+java_aconfig_library {
+    name: "print_flags_lib",
+    aconfig_declarations: "print_flags",
+}
diff --git a/services/print/java/com/android/server/print/RemotePrintService.java b/services/print/java/com/android/server/print/RemotePrintService.java
index 502cd2c..b856715 100644
--- a/services/print/java/com/android/server/print/RemotePrintService.java
+++ b/services/print/java/com/android/server/print/RemotePrintService.java
@@ -572,7 +572,8 @@
 
         boolean wasBound = mContext.bindServiceAsUser(mIntent, mServiceConnection,
                 Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE
-                        | Context.BIND_INCLUDE_CAPABILITIES | Context.BIND_ALLOW_INSTANT,
+                        | (Flags.doNotIncludeCapabilities() ? 0 : Context.BIND_INCLUDE_CAPABILITIES)
+                        | Context.BIND_ALLOW_INSTANT,
                 new UserHandle(mUserId));
 
         if (!wasBound) {
diff --git a/services/print/java/com/android/server/print/flags.aconfig b/services/print/java/com/android/server/print/flags.aconfig
new file mode 100644
index 0000000..42d1425
--- /dev/null
+++ b/services/print/java/com/android/server/print/flags.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.server.print"
+container: "system"
+
+flag {
+    name: "do_not_include_capabilities"
+    namespace: "printing"
+    description: "Do not use the flag Context.BIND_INCLUDE_CAPABILITIES when binding to the service"
+    bug: "291281543"
+}
diff --git a/services/robotests/Android.bp b/services/robotests/Android.bp
index 6c4158e..8e0eb6b 100644
--- a/services/robotests/Android.bp
+++ b/services/robotests/Android.bp
@@ -63,7 +63,6 @@
 
     instrumentation_for: "FrameworksServicesLib",
 
-    upstream: true,
 
     strict_mode: false,
 }
diff --git a/services/robotests/backup/Android.bp b/services/robotests/backup/Android.bp
index 3ace3fb..95b38e5 100644
--- a/services/robotests/backup/Android.bp
+++ b/services/robotests/backup/Android.bp
@@ -66,7 +66,6 @@
 
     instrumentation_for: "BackupFrameworksServicesLib",
 
-    upstream: true,
 
     strict_mode: false,
 
diff --git a/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java b/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java
index 4e9fff2..b80d68d 100644
--- a/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java
@@ -37,6 +37,7 @@
 
 import android.annotation.UserIdInt;
 import android.app.Application;
+import android.app.backup.BackupManagerInternal;
 import android.app.backup.IBackupManagerMonitor;
 import android.app.backup.IBackupObserver;
 import android.app.backup.IFullBackupRestoreObserver;
@@ -52,6 +53,7 @@
 import android.platform.test.annotations.Presubmit;
 import android.util.SparseArray;
 
+import com.android.server.LocalServices;
 import com.android.server.SystemService.TargetUser;
 import com.android.server.backup.testing.TransportData;
 import com.android.server.testing.shadows.ShadowApplicationPackageManager;
@@ -229,7 +231,7 @@
         setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false);
         IBinder agentBinder = mock(IBinder.class);
 
-        backupManagerService.agentConnected(mUserOneId, TEST_PACKAGE, agentBinder);
+        backupManagerService.agentConnectedForUser(TEST_PACKAGE, mUserOneId, agentBinder);
 
         verify(mUserOneBackupAgentConnectionManager).agentConnected(TEST_PACKAGE, agentBinder);
     }
@@ -242,7 +244,7 @@
         setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false);
         IBinder agentBinder = mock(IBinder.class);
 
-        backupManagerService.agentConnected(mUserTwoId, TEST_PACKAGE, agentBinder);
+        backupManagerService.agentConnectedForUser(TEST_PACKAGE, mUserTwoId, agentBinder);
 
         verify(mUserOneBackupAgentConnectionManager, never()).agentConnected(TEST_PACKAGE,
                 agentBinder);
@@ -1549,6 +1551,7 @@
     @Test
     public void testOnStart_publishesService() {
         BackupManagerService backupManagerService = mock(BackupManagerService.class);
+        LocalServices.removeServiceForTest(BackupManagerInternal.class);
         BackupManagerService.Lifecycle lifecycle =
                 spy(new BackupManagerService.Lifecycle(mContext, backupManagerService));
         doNothing().when(lifecycle).publishService(anyString(), any());
diff --git a/services/tests/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp
index e6ff506..da58aa1 100644
--- a/services/tests/InputMethodSystemServerTests/Android.bp
+++ b/services/tests/InputMethodSystemServerTests/Android.bp
@@ -86,6 +86,7 @@
         "src/com/android/server/inputmethod/**/ClientControllerTest.java",
     ],
     auto_gen_config: true,
+    team: "trendy_team_ravenwood",
 }
 
 android_test {
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java
index 7277fd7..66aaa562 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java
@@ -45,6 +45,7 @@
 import android.platform.test.annotations.Presubmit;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.Pair;
 import android.util.SparseArray;
 
 import androidx.annotation.NonNull;
@@ -78,10 +79,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
 
 @Presubmit
 @RunWith(JUnit4.class)
@@ -885,18 +883,15 @@
                         return null;
                     }
 
-                    @NonNull
+                    @Nullable
                     @Override
-                    public Map<String, Set<String>> getTargetToOverlayables(
+                    public Pair<String, String> getTargetToOverlayables(
                             @NonNull AndroidPackage pkg) {
                         if (overlay.getPackageName().equals(pkg.getPackageName())) {
-                            Map<String, Set<String>> map = new ArrayMap<>();
-                            Set<String> set = new ArraySet<>();
-                            set.add(overlay.getOverlayTargetOverlayableName());
-                            map.put(overlay.getOverlayTarget(), set);
-                            return map;
+                            return Pair.create(overlay.getOverlayTarget(),
+                                    overlay.getOverlayTargetOverlayableName());
                         }
-                        return Collections.emptyMap();
+                        return null;
                     }
                 },
                 mMockHandler);
@@ -977,18 +972,15 @@
                         return null;
                     }
 
-                    @NonNull
+                    @Nullable
                     @Override
-                    public Map<String, Set<String>> getTargetToOverlayables(
+                    public Pair<String, String> getTargetToOverlayables(
                             @NonNull AndroidPackage pkg) {
                         if (overlay.getPackageName().equals(pkg.getPackageName())) {
-                            Map<String, Set<String>> map = new ArrayMap<>();
-                            Set<String> set = new ArraySet<>();
-                            set.add(overlay.getOverlayTargetOverlayableName());
-                            map.put(overlay.getOverlayTarget(), set);
-                            return map;
+                            return Pair.create(overlay.getOverlayTarget(),
+                                    overlay.getOverlayTargetOverlayableName());
                         }
-                        return Collections.emptyMap();
+                        return null;
                     }
                 },
                 mMockHandler);
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 a9ad435..02e5470 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -415,7 +415,6 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(false);
 
         mLocalServiceKeeperRule.overrideLocalService(
                 InputManagerInternal.class, mMockInputManagerInternal);
@@ -2797,30 +2796,7 @@
     }
 
     @Test
-    public void testConnectExternalDisplay_withoutDisplayManagement_shouldAddDisplay() {
-        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(false);
-        manageDisplaysPermission(/* granted= */ true);
-        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
-        DisplayManagerService.BinderService bs = displayManager.new BinderService();
-        LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
-        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
-        bs.registerCallbackWithEventMask(callback, STANDARD_AND_CONNECTION_DISPLAY_EVENTS);
-        callback.expectsEvent(EVENT_DISPLAY_ADDED);
-
-        FakeDisplayDevice displayDevice =
-                createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_EXTERNAL);
-        callback.waitForExpectedEvent();
-
-        LogicalDisplay display =
-                logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true);
-        assertThat(display.isEnabledLocked()).isTrue();
-        assertThat(callback.receivedEvents()).containsExactly(EVENT_DISPLAY_ADDED);
-
-    }
-
-    @Test
-    public void testConnectExternalDisplay_withDisplayManagement_shouldDisableDisplay() {
-        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
+    public void testConnectExternalDisplay_shouldDisableDisplay() {
         manageDisplaysPermission(/* granted= */ true);
         DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
         displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
@@ -2849,9 +2825,8 @@
     }
 
     @Test
-    public void testConnectExternalDisplay_withDisplayManagementAndSysprop_shouldEnableDisplay() {
+    public void testConnectExternalDisplay_withSysprop_shouldEnableDisplay() {
         Assume.assumeTrue(Build.IS_ENG || Build.IS_USERDEBUG);
-        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
         doAnswer((Answer<Boolean>) invocationOnMock -> true)
                 .when(() -> SystemProperties.getBoolean(ENABLE_ON_CONNECT, false));
         manageDisplaysPermission(/* granted= */ true);
@@ -2883,8 +2858,7 @@
     }
 
     @Test
-    public void testConnectExternalDisplay_withDisplayManagement_allowsEnableAndDisableDisplay() {
-        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
+    public void testConnectExternalDisplay_allowsEnableAndDisableDisplay() {
         when(mMockFlags.isApplyDisplayChangedDuringDisplayAddedEnabled()).thenReturn(true);
         manageDisplaysPermission(/* granted= */ true);
         LocalServices.addService(WindowManagerPolicy.class, mMockedWindowManagerPolicy);
@@ -2955,8 +2929,7 @@
     }
 
     @Test
-    public void testConnectInternalDisplay_withDisplayManagement_shouldConnectAndAddDisplay() {
-        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
+    public void testConnectInternalDisplay_shouldConnectAndAddDisplay() {
         manageDisplaysPermission(/* granted= */ true);
         DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
         DisplayManagerService.BinderService bs = displayManager.new BinderService();
@@ -3011,7 +2984,7 @@
         DisplayManagerService.BinderService bs = displayManager.new BinderService();
         LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
         FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
-        bs.registerCallbackWithEventMask(callback, STANDARD_AND_CONNECTION_DISPLAY_EVENTS);
+        bs.registerCallbackWithEventMask(callback, STANDARD_DISPLAY_EVENTS);
 
         callback.expectsEvent(EVENT_DISPLAY_ADDED);
         FakeDisplayDevice displayDevice =
@@ -3032,8 +3005,7 @@
     }
 
     @Test
-    public void testEnableExternalDisplay_withDisplayManagement_shouldSignalDisplayAdded() {
-        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
+    public void testEnableExternalDisplay_shouldSignalDisplayAdded() {
         manageDisplaysPermission(/* granted= */ true);
         DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
         displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
@@ -3062,8 +3034,7 @@
     }
 
     @Test
-    public void testEnableExternalDisplay_withoutPermission_shouldThrowException() {
-        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
+    public void testEnableExternalDisplay_shouldThrowException() {
         DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
         DisplayManagerService.BinderService bs = displayManager.new BinderService();
         LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
@@ -3087,8 +3058,7 @@
     }
 
     @Test
-    public void testEnableInternalDisplay_withManageDisplays_shouldSignalAdded() {
-        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
+    public void testEnableInternalDisplay_shouldSignalAdded() {
         DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
         DisplayManagerService.BinderService bs = displayManager.new BinderService();
         LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
@@ -3115,8 +3085,7 @@
     }
 
     @Test
-    public void testDisableInternalDisplay_withDisplayManagement_shouldSignalRemove() {
-        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
+    public void testDisableInternalDisplay_shouldSignalRemove() {
         DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
         DisplayManagerService.BinderService bs = displayManager.new BinderService();
         LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
@@ -3140,7 +3109,6 @@
 
     @Test
     public void testDisableExternalDisplay_shouldSignalDisplayRemoved() {
-        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
         DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
         DisplayManagerService.BinderService bs = displayManager.new BinderService();
         LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
@@ -3181,7 +3149,6 @@
 
     @Test
     public void testDisableExternalDisplay_withoutPermission_shouldThrowException() {
-        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
         DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
         DisplayManagerService.BinderService bs = displayManager.new BinderService();
         LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
@@ -3207,7 +3174,6 @@
 
     @Test
     public void testRemoveExternalDisplay_whenDisabled_shouldSignalDisconnected() {
-        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
         manageDisplaysPermission(/* granted= */ true);
         DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
         displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
@@ -3244,7 +3210,6 @@
 
     @Test
     public void testRegisterCallback_withoutPermission_shouldThrow() {
-        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
         DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
         DisplayManagerService.BinderService bs = displayManager.new BinderService();
         FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
@@ -3255,7 +3220,6 @@
 
     @Test
     public void testRemoveExternalDisplay_whenEnabled_shouldSignalRemovedAndDisconnected() {
-        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
         manageDisplaysPermission(/* granted= */ true);
         DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
         displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
@@ -3288,7 +3252,6 @@
 
     @Test
     public void testRemoveInternalDisplay_whenEnabled_shouldSignalRemovedAndDisconnected() {
-        when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
         manageDisplaysPermission(/* granted= */ true);
         DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
         DisplayManagerService.BinderService bs = displayManager.new BinderService();
diff --git a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java
index 782262d..a48a88c 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java
@@ -22,7 +22,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.junit.Assume.assumeFalse;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -47,7 +46,6 @@
 import com.android.server.display.notifications.DisplayNotificationManager;
 import com.android.server.testutils.TestHandler;
 
-import com.google.testing.junit.testparameterinjector.TestParameter;
 import com.google.testing.junit.testparameterinjector.TestParameterInjector;
 
 import org.junit.Before;
@@ -124,7 +122,6 @@
     public void setup() throws Exception {
         MockitoAnnotations.initMocks(this);
         mHandler = new TestHandler(/*callback=*/ null);
-        when(mMockedFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
         when(mMockedFlags.isConnectedDisplayErrorHandlingEnabled()).thenReturn(true);
         when(mMockedInjector.getFlags()).thenReturn(mMockedFlags);
         when(mMockedInjector.getLogicalDisplayMapper()).thenReturn(mMockedLogicalDisplayMapper);
@@ -173,16 +170,6 @@
     }
 
     @Test
-    public void testTryEnableExternalDisplay_featureDisabled(@TestParameter final boolean enable) {
-        when(mMockedFlags.isConnectedDisplayManagementEnabled()).thenReturn(false);
-        mExternalDisplayPolicy.setExternalDisplayEnabledLocked(mMockedLogicalDisplay, enable);
-        mHandler.flush();
-        verify(mMockedLogicalDisplayMapper, never()).setDisplayEnabledLocked(any(), anyBoolean());
-        verify(mMockedDisplayNotificationManager, never())
-                .onHighTemperatureExternalDisplayNotAllowed();
-    }
-
-    @Test
     public void testTryDisableExternalDisplay_criticalThermalCondition() throws RemoteException {
         // Disallow external displays due to thermals.
         setTemperature(registerThermalListener(), List.of(CRITICAL_TEMPERATURE));
@@ -278,21 +265,6 @@
     }
 
     @Test
-    public void testNoThermalListenerRegistered_featureDisabled(
-            @TestParameter final boolean isConnectedDisplayManagementEnabled,
-            @TestParameter final boolean isErrorHandlingEnabled) throws RemoteException {
-        assumeFalse(isConnectedDisplayManagementEnabled && isErrorHandlingEnabled);
-        when(mMockedFlags.isConnectedDisplayManagementEnabled()).thenReturn(
-                isConnectedDisplayManagementEnabled);
-        when(mMockedFlags.isConnectedDisplayErrorHandlingEnabled()).thenReturn(
-                isErrorHandlingEnabled);
-
-        mExternalDisplayPolicy.onBootCompleted();
-        verify(mMockedThermalService, never()).registerThermalEventListenerWithType(
-                any(), anyInt());
-    }
-
-    @Test
     public void testOnCriticalTemperature_disallowAndAllowExternalDisplay() throws RemoteException {
         final var thermalListener = registerThermalListener();
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
index 0dbb6ba..7d3cd8a 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
@@ -222,7 +222,6 @@
         when(mSyntheticModeManagerMock.createAppSupportedModes(any(), any(), anyBoolean()))
                 .thenAnswer(AdditionalAnswers.returnsSecondArg());
 
-        when(mFlagsMock.isConnectedDisplayManagementEnabled()).thenReturn(false);
         mLooper = new TestLooper();
         mHandler = new Handler(mLooper.getLooper());
         mLogicalDisplayMapper = new LogicalDisplayMapper(mContextMock, mFoldSettingProviderMock,
@@ -351,8 +350,7 @@
     }
 
     @Test
-    public void testDisplayDeviceAddAndRemove_withDisplayManagement() {
-        when(mFlagsMock.isConnectedDisplayManagementEnabled()).thenReturn(true);
+    public void testDisplayDeviceAddAndRemove() {
         DisplayDevice device = createDisplayDevice(TYPE_INTERNAL, 600, 800,
                 FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
 
@@ -390,8 +388,7 @@
     }
 
     @Test
-    public void testDisplayDisableEnable_withDisplayManagement() {
-        when(mFlagsMock.isConnectedDisplayManagementEnabled()).thenReturn(true);
+    public void testDisplayDisableEnable() {
         DisplayDevice device = createDisplayDevice(TYPE_INTERNAL, 600, 800,
                 FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
         LogicalDisplay displayAdded = add(device);
@@ -1350,9 +1347,14 @@
         ArgumentCaptor<LogicalDisplay> displayCaptor =
                 ArgumentCaptor.forClass(LogicalDisplay.class);
         verify(mListenerMock).onLogicalDisplayEventLocked(
-                displayCaptor.capture(), eq(LOGICAL_DISPLAY_EVENT_ADDED));
+                displayCaptor.capture(), eq(LOGICAL_DISPLAY_EVENT_CONNECTED));
+        LogicalDisplay display = displayCaptor.getValue();
+        if (display.isEnabledLocked()) {
+            verify(mListenerMock).onLogicalDisplayEventLocked(
+                    eq(display), eq(LOGICAL_DISPLAY_EVENT_ADDED));
+        }
         clearInvocations(mListenerMock);
-        return displayCaptor.getValue();
+        return display;
     }
 
     private void testDisplayDeviceAddAndRemove_NonInternal(int type) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java
index 6defadf..35ab2d2 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java
@@ -1454,6 +1454,18 @@
         assertThat(tokenForFullIntent.getKeyFields()).isEqualTo(tokenForCloneIntent.getKeyFields());
     }
 
+    @Test
+    public void testCanLaunchClipDataIntent() {
+        ClipData clipData = ClipData.newIntent("test", new Intent("test"));
+        clipData.prepareToLeaveProcess(true);
+        // skip mimicking sending clipData to another app because it will just be parceled and
+        // un-parceled.
+        Intent intent = clipData.getItemAt(0).getIntent();
+        // default intent redirect protection won't block an intent nested in a top level ClipData.
+        assertThat(intent.getExtendedFlags()
+                & Intent.EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN).isEqualTo(0);
+    }
+
     private void verifyWaitingForNetworkStateUpdate(long curProcStateSeq,
             long lastNetworkUpdatedProcStateSeq,
             final long procStateSeqToWait, boolean expectWait) throws Exception {
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 9e96800..fe7cc92 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -258,7 +258,6 @@
         mService.mOomAdjuster = mService.mProcessStateController.getOomAdjuster();
         mService.mOomAdjuster.mAdjSeq = 10000;
         mService.mWakefulness = new AtomicInteger(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        mSetFlagsRule.enableFlags(Flags.FLAG_NEW_FGS_RESTRICTION_LOGIC);
 
         mUiTierSize = mService.mConstants.TIERED_CACHED_ADJ_UI_TIER_SIZE;
         mFirstNonUiCachedAdj = sFirstUiCachedAdj + mUiTierSize;
@@ -744,6 +743,43 @@
 
     @SuppressWarnings("GuardedBy")
     @Test
+    @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY)
+    public void testUpdateOomAdjFreezeState_receivers() {
+        final ProcessRecord app = makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
+                MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true);
+
+        updateOomAdj(app);
+        assertNoCpuTime(app);
+
+        app.mReceivers.incrementCurReceivers();
+        updateOomAdj(app);
+        assertCpuTime(app);
+
+        app.mReceivers.decrementCurReceivers();
+        updateOomAdj(app);
+        assertNoCpuTime(app);
+    }
+
+    @SuppressWarnings("GuardedBy")
+    @Test
+    @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY)
+    public void testUpdateOomAdjFreezeState_activeInstrumentation() {
+        ProcessRecord app = makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, MOCKAPP_PROCESSNAME,
+                MOCKAPP_PACKAGENAME, true);
+        updateOomAdj(app);
+        assertNoCpuTime(app);
+
+        mProcessStateController.setActiveInstrumentation(app, mock(ActiveInstrumentation.class));
+        updateOomAdj(app);
+        assertCpuTime(app);
+
+        mProcessStateController.setActiveInstrumentation(app, null);
+        updateOomAdj(app);
+        assertNoCpuTime(app);
+    }
+
+    @SuppressWarnings("GuardedBy")
+    @Test
     public void testUpdateOomAdj_DoOne_OverlayUi() {
         ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
                 MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java
index 89b48ba..27eada0 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.am;
 
+import static android.os.PowerWhitelistManager.REASON_NOTIFICATION_SERVICE;
+import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
 import static android.os.Process.INVALID_UID;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -27,9 +29,11 @@
 import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_OWNER_FORCE_STOPPED;
 import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_SUPERSEDED;
 import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_USER_STOPPED;
+import static com.android.server.am.PendingIntentRecord.FLAG_ACTIVITY_SENDER;
 import static com.android.server.am.PendingIntentRecord.cancelReasonToString;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
@@ -39,9 +43,11 @@
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
 import android.app.AppGlobals;
+import android.app.BackgroundStartPrivileges;
 import android.app.PendingIntent;
 import android.content.Intent;
 import android.content.pm.IPackageManager;
+import android.os.Binder;
 import android.os.Looper;
 import android.os.UserHandle;
 
@@ -179,6 +185,34 @@
         }
     }
 
+    @Test
+    public void testClearAllowBgActivityStartsClearsToken() {
+        final PendingIntentRecord pir = createPendingIntentRecord(0);
+        Binder token = new Binder();
+        pir.setAllowBgActivityStarts(token, FLAG_ACTIVITY_SENDER);
+        assertEquals(BackgroundStartPrivileges.allowBackgroundActivityStarts(token),
+                pir.getBackgroundStartPrivilegesForActivitySender(token));
+        pir.clearAllowBgActivityStarts(token);
+        assertEquals(BackgroundStartPrivileges.NONE,
+                pir.getBackgroundStartPrivilegesForActivitySender(token));
+    }
+
+    @Test
+    public void testClearAllowBgActivityStartsClearsDuration() {
+        final PendingIntentRecord pir = createPendingIntentRecord(0);
+        Binder token = new Binder();
+        pir.setAllowlistDurationLocked(token, 1000,
+                TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, REASON_NOTIFICATION_SERVICE,
+                "NotificationManagerService");
+        PendingIntentRecord.TempAllowListDuration allowlistDurationLocked =
+                pir.getAllowlistDurationLocked(token);
+        assertEquals(1000, allowlistDurationLocked.duration);
+        pir.clearAllowBgActivityStarts(token);
+        PendingIntentRecord.TempAllowListDuration allowlistDurationLockedAfterClear =
+                pir.getAllowlistDurationLocked(token);
+        assertNull(allowlistDurationLockedAfterClear);
+    }
+
     private void assertCancelReason(int expectedReason, int actualReason) {
         final String errMsg = "Expected: " + cancelReasonToString(expectedReason)
                 + "; Actual: " + cancelReasonToString(actualReason);
diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java
index c4a0423..f1f4a0e 100644
--- a/services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java
@@ -39,6 +39,7 @@
 
 import android.Manifest;
 import android.app.backup.BackupManager;
+import android.app.backup.BackupManagerInternal;
 import android.app.backup.ISelectBackupTransportCallback;
 import android.app.job.JobScheduler;
 import android.content.ComponentName;
@@ -59,6 +60,7 @@
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.internal.util.DumpUtils;
+import com.android.server.LocalServices;
 import com.android.server.SystemService;
 import com.android.server.backup.utils.RandomAccessFileUtils;
 
@@ -716,7 +718,7 @@
         // Create BMS *before* setting a main user to simulate the main user being created after
         // BMS, which can happen for the first ever boot of a new device.
         mService = new BackupManagerServiceTestable(mContextMock);
-        mServiceLifecycle = new BackupManagerService.Lifecycle(mContextMock, mService);
+        createBackupServiceLifecycle(mContextMock, mService);
         when(mUserManagerMock.getMainUser()).thenReturn(UserHandle.of(NON_SYSTEM_USER));
         assertFalse(mService.isBackupServiceActive(NON_SYSTEM_USER));
 
@@ -730,7 +732,7 @@
         // Create BMS *before* setting a main user to simulate the main user being created after
         // BMS, which can happen for the first ever boot of a new device.
         mService = new BackupManagerServiceTestable(mContextMock);
-        mServiceLifecycle = new BackupManagerService.Lifecycle(mContextMock, mService);
+        createBackupServiceLifecycle(mContextMock, mService);
         when(mUserManagerMock.getMainUser()).thenReturn(UserHandle.of(NON_SYSTEM_USER));
         assertFalse(mService.isBackupServiceActive(NON_SYSTEM_USER));
 
@@ -754,7 +756,7 @@
 
     private void createBackupManagerServiceAndUnlockSystemUser() {
         mService = new BackupManagerServiceTestable(mContextMock);
-        mServiceLifecycle = new BackupManagerService.Lifecycle(mContextMock, mService);
+        createBackupServiceLifecycle(mContextMock, mService);
         simulateUserUnlocked(UserHandle.USER_SYSTEM);
     }
 
@@ -765,7 +767,15 @@
     private void setMockMainUserAndCreateBackupManagerService(int userId) {
         when(mUserManagerMock.getMainUser()).thenReturn(UserHandle.of(userId));
         mService = new BackupManagerServiceTestable(mContextMock);
-        mServiceLifecycle = new BackupManagerService.Lifecycle(mContextMock, mService);
+        createBackupServiceLifecycle(mContextMock, mService);
+    }
+
+    private void createBackupServiceLifecycle(Context context, BackupManagerService service) {
+        // Anytime we manually create the Lifecycle, we need to remove the internal BMS because
+        // it would've been added already at boot time and LocalServices does not allow
+        // overriding an existing service.
+        LocalServices.removeServiceForTest(BackupManagerInternal.class);
+        mServiceLifecycle = new BackupManagerService.Lifecycle(context, service);
     }
 
     private void simulateUserUnlocked(int userId) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java b/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java
index f1072da..6d91bee 100644
--- a/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java
@@ -573,4 +573,14 @@
         assertNotNull(ret);
         assertEquals(0, ret.size());
     }
+
+    @Test
+    public void forecastSkinTemperature() throws RemoteException {
+        Mockito.when(mAidlHalMock.forecastSkinTemperature(Mockito.anyInt())).thenReturn(
+                0.55f
+        );
+        float forecast = mAidlWrapper.forecastSkinTemperature(10);
+        Mockito.verify(mAidlHalMock, Mockito.times(1)).forecastSkinTemperature(10);
+        assertEquals(0.55f, forecast, 0.01f);
+    }
 }
diff --git a/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
index cd94c0f..e615712 100644
--- a/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
+++ b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java
@@ -70,8 +70,6 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.SessionCreationConfig;
-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;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -1388,7 +1386,6 @@
 
 
     @Test
-    @EnableFlags({Flags.FLAG_CPU_HEADROOM_AFFINITY_CHECK})
     public void testCpuHeadroomCache() throws Exception {
         CpuHeadroomParamsInternal params1 = new CpuHeadroomParamsInternal();
         CpuHeadroomParams halParams1 = new CpuHeadroomParams();
@@ -1476,8 +1473,7 @@
     }
 
     @Test
-    @EnableFlags({Flags.FLAG_CPU_HEADROOM_AFFINITY_CHECK})
-    public void testGetCpuHeadroomDifferentAffinity_flagOn() throws Exception {
+    public void testGetCpuHeadroomDifferentAffinity() throws Exception {
         CountDownLatch latch = new CountDownLatch(2);
         int[] tids = createThreads(2, latch);
         CpuHeadroomParamsInternal params = new CpuHeadroomParamsInternal();
@@ -1497,28 +1493,6 @@
         verify(mIPowerMock, times(0)).getCpuHeadroom(any());
     }
 
-    @Test
-    @DisableFlags({Flags.FLAG_CPU_HEADROOM_AFFINITY_CHECK})
-    public void testGetCpuHeadroomDifferentAffinity_flagOff() throws Exception {
-        CountDownLatch latch = new CountDownLatch(2);
-        int[] tids = createThreads(2, latch);
-        CpuHeadroomParamsInternal params = new CpuHeadroomParamsInternal();
-        params.tids = tids;
-        CpuHeadroomParams halParams = new CpuHeadroomParams();
-        halParams.tids = tids;
-        float headroom = 0.1f;
-        CpuHeadroomResult halRet = CpuHeadroomResult.globalHeadroom(headroom);
-        String ret1 = runAndWaitForCommand("taskset -p 1 " + tids[0]);
-        String ret2 = runAndWaitForCommand("taskset -p 3 " + tids[1]);
-
-        HintManagerService service = createService();
-        clearInvocations(mIPowerMock);
-        when(mIPowerMock.getCpuHeadroom(eq(halParams))).thenReturn(halRet);
-        assertEquals("taskset cmd return: " + ret1 + "\n" + ret2, halRet,
-                service.getBinderServiceInstance().getCpuHeadroom(params));
-        verify(mIPowerMock, times(1)).getCpuHeadroom(any());
-    }
-
     private String runAndWaitForCommand(String command) throws Exception {
         java.lang.Process process = Runtime.getRuntime().exec(command);
         BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
diff --git a/services/tests/powerstatstests/Android.bp b/services/tests/powerstatstests/Android.bp
index d6ca10a..07b18db 100644
--- a/services/tests/powerstatstests/Android.bp
+++ b/services/tests/powerstatstests/Android.bp
@@ -27,6 +27,7 @@
         "servicestests-utils",
         "platform-test-annotations",
         "flag-junit",
+        "apct-perftests-utils",
     ],
 
     libs: [
@@ -64,10 +65,12 @@
         "ravenwood-junit",
         "truth",
         "androidx.annotation_annotation",
+        "androidx.test.ext.junit",
         "androidx.test.rules",
         "androidx.test.uiautomator_uiautomator",
         "modules-utils-binary-xml",
         "flag-junit",
+        "apct-perftests-utils",
     ],
     srcs: [
         "src/com/android/server/power/stats/*.java",
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTraceTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTraceTest.java
new file mode 100644
index 0000000..cc75e9e
--- /dev/null
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTraceTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2025 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.power.stats;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.BatteryStatsManager;
+import android.os.BatteryUsageStats;
+import android.os.BatteryUsageStatsQuery;
+import android.os.ParcelFileDescriptor;
+import android.perftests.utils.TraceMarkParser;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.uiautomator.UiDevice;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+@android.platform.test.annotations.DisabledOnRavenwood(reason = "Atrace event test")
+public class BatteryStatsHistoryTraceTest {
+    private static final String ATRACE_START = "atrace --async_start -b 1024 -c ss";
+    private static final String ATRACE_STOP = "atrace --async_stop";
+    private static final String ATRACE_DUMP = "atrace --async_dump";
+
+    @Before
+    public void before() throws Exception {
+        runShellCommand(ATRACE_START);
+    }
+
+    @After
+    public void after() throws Exception {
+        runShellCommand(ATRACE_STOP);
+    }
+
+    @Test
+    public void dumpsys() throws Exception {
+        runShellCommand("dumpsys batterystats --history");
+
+        Set<String> slices = readAtraceSlices();
+        assertThat(slices).contains("BatteryStatsHistory.copy");
+        assertThat(slices).contains("BatteryStatsHistory.iterate");
+    }
+
+    @Test
+    public void getBatteryUsageStats() throws Exception {
+        BatteryStatsManager batteryStatsManager =
+                getInstrumentation().getTargetContext().getSystemService(BatteryStatsManager.class);
+        BatteryUsageStatsQuery query = new BatteryUsageStatsQuery.Builder()
+                .includeBatteryHistory().build();
+        BatteryUsageStats batteryUsageStats = batteryStatsManager.getBatteryUsageStats(query);
+        assertThat(batteryUsageStats).isNotNull();
+
+        Set<String> slices = readAtraceSlices();
+        assertThat(slices).contains("BatteryStatsHistory.copy");
+        assertThat(slices).contains("BatteryStatsHistory.iterate");
+        assertThat(slices).contains("BatteryStatsHistory.writeToParcel");
+    }
+
+    private String runShellCommand(String cmd) throws Exception {
+        return UiDevice.getInstance(getInstrumentation()).executeShellCommand(cmd);
+    }
+
+    private Set<String> readAtraceSlices() throws Exception {
+        Set<String> keys = new HashSet<>();
+
+        TraceMarkParser parser = new TraceMarkParser(
+                line -> line.name.startsWith("BatteryStatsHistory."));
+        ParcelFileDescriptor pfd =
+                getInstrumentation().getUiAutomation().executeShellCommand(ATRACE_DUMP);
+        try (BufferedReader reader = new BufferedReader(
+                new InputStreamReader(new ParcelFileDescriptor.AutoCloseInputStream(pfd)))) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                parser.visit(line);
+            }
+        }
+        parser.forAllSlices((key, slices) -> keys.add(key));
+        return keys;
+    }
+}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsCollectorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsCollectorTest.java
index 00b911b..cd3683b 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsCollectorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsCollectorTest.java
@@ -158,6 +158,11 @@
         public LongSupplier getPhoneSignalScanDurationSupplier() {
             return mScanDurationSupplier;
         }
+
+        @Override
+        public NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats) {
+            return NetworkStatsTestUtils.networkStatsDelta(stats, oldStats);
+        }
     };
 
     @Before
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
index 4b6fcc3..8a081f8 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
@@ -283,6 +283,11 @@
     protected void updateBatteryPropertiesLocked() {
     }
 
+    @Override
+    protected NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats) {
+        return NetworkStatsTestUtils.networkStatsDelta(stats, oldStats);
+    }
+
     public static class DummyExternalStatsSync implements ExternalStatsSync {
         public int flags = 0;
 
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/NetworkStatsTestUtils.java b/services/tests/powerstatstests/src/com/android/server/power/stats/NetworkStatsTestUtils.java
new file mode 100644
index 0000000..21be654
--- /dev/null
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/NetworkStatsTestUtils.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2025 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.power.stats;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.NetworkStats;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class NetworkStatsTestUtils {
+    /**
+     * Equivalent to NetworkStats.subtract, reimplementing the method for Ravenwood tests.
+     */
+    @NonNull
+    public static NetworkStats networkStatsDelta(@NonNull NetworkStats currentStats,
+            @Nullable NetworkStats lastStats) {
+        if (!RavenwoodRule.isOnRavenwood()) {
+            if (lastStats == null) {
+                return currentStats;
+            }
+            return currentStats.subtract(lastStats);
+        }
+
+        List<NetworkStats.Entry> entries = new ArrayList<>();
+        for (NetworkStats.Entry entry : currentStats) {
+            NetworkStats.Entry lastEntry = null;
+            int uid = entry.getUid();
+            if (lastStats != null) {
+                for (NetworkStats.Entry e : lastStats) {
+                    if (e.getUid() == uid && e.getSet() == entry.getSet()
+                            && e.getTag() == entry.getTag()
+                            && e.getMetered() == entry.getMetered()
+                            && e.getRoaming() == entry.getRoaming()
+                            && e.getDefaultNetwork() == entry.getDefaultNetwork()
+                        /*&& Objects.equals(e.getIface(), entry.getIface())*/) {
+                        lastEntry = e;
+                        break;
+                    }
+                }
+            }
+            long rxBytes, rxPackets, txBytes, txPackets;
+            if (lastEntry != null) {
+                rxBytes = Math.max(0, entry.getRxBytes() - lastEntry.getRxBytes());
+                rxPackets = Math.max(0, entry.getRxPackets() - lastEntry.getRxPackets());
+                txBytes = Math.max(0, entry.getTxBytes() - lastEntry.getTxBytes());
+                txPackets = Math.max(0, entry.getTxPackets() - lastEntry.getTxPackets());
+            } else {
+                rxBytes = entry.getRxBytes();
+                rxPackets = entry.getRxPackets();
+                txBytes = entry.getTxBytes();
+                txPackets = entry.getTxPackets();
+            }
+
+            NetworkStats.Entry uidEntry = mock(NetworkStats.Entry.class);
+            when(uidEntry.getUid()).thenReturn(uid);
+            when(uidEntry.getRxBytes()).thenReturn(rxBytes);
+            when(uidEntry.getRxPackets()).thenReturn(rxPackets);
+            when(uidEntry.getTxBytes()).thenReturn(txBytes);
+            when(uidEntry.getTxPackets()).thenReturn(txPackets);
+
+            entries.add(uidEntry);
+        }
+        NetworkStats delta = mock(NetworkStats.class);
+        when(delta.iterator()).thenAnswer(inv -> entries.iterator());
+        return delta;
+    }
+}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java
index 8b5e6ee..a26b2c9 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java
@@ -168,6 +168,11 @@
         public WifiManager getWifiManager() {
             return mWifiManager;
         }
+
+        @Override
+        public NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats) {
+            return NetworkStatsTestUtils.networkStatsDelta(stats, oldStats);
+        }
     };
 
     @Before
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessorTest.java
index 4ed44a0..6acd368 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessorTest.java
@@ -56,6 +56,7 @@
 import com.android.internal.os.PowerStats;
 import com.android.server.power.stats.BatteryUsageStatsRule;
 import com.android.server.power.stats.MobileRadioPowerStatsCollector;
+import com.android.server.power.stats.NetworkStatsTestUtils;
 import com.android.server.power.stats.PowerStatsCollector;
 import com.android.server.power.stats.PowerStatsUidResolver;
 import com.android.server.power.stats.format.MobileRadioPowerStatsLayout;
@@ -152,6 +153,11 @@
                 public LongSupplier getPhoneSignalScanDurationSupplier() {
                     return mScanDurationSupplier;
                 }
+
+                @Override
+                public NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats) {
+                    return NetworkStatsTestUtils.networkStatsDelta(stats, oldStats);
+                }
             };
 
     @Before
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PhoneCallPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PhoneCallPowerStatsProcessorTest.java
index 535f2da..a20274f 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PhoneCallPowerStatsProcessorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PhoneCallPowerStatsProcessorTest.java
@@ -43,6 +43,7 @@
 import com.android.internal.os.Clock;
 import com.android.server.power.stats.BatteryUsageStatsRule;
 import com.android.server.power.stats.MobileRadioPowerStatsCollector;
+import com.android.server.power.stats.NetworkStatsTestUtils;
 import com.android.server.power.stats.PowerStatsCollector;
 import com.android.server.power.stats.PowerStatsUidResolver;
 import com.android.server.power.stats.format.PowerStatsLayout;
@@ -135,6 +136,11 @@
                 public LongSupplier getPhoneSignalScanDurationSupplier() {
                     return mScanDurationSupplier;
                 }
+
+                @Override
+                public NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats) {
+                    return NetworkStatsTestUtils.networkStatsDelta(stats, oldStats);
+                }
             };
 
     @Before
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WifiPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WifiPowerStatsProcessorTest.java
index 1e09769..bd92a84 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WifiPowerStatsProcessorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WifiPowerStatsProcessorTest.java
@@ -56,6 +56,7 @@
 import com.android.internal.os.PowerProfile;
 import com.android.server.power.stats.BatteryUsageStatsRule;
 import com.android.server.power.stats.MockBatteryStatsImpl;
+import com.android.server.power.stats.NetworkStatsTestUtils;
 import com.android.server.power.stats.PowerStatsCollector;
 import com.android.server.power.stats.PowerStatsUidResolver;
 import com.android.server.power.stats.WifiPowerStatsCollector;
@@ -178,6 +179,11 @@
                 public WifiStatsRetriever getWifiStatsRetriever() {
                     return mWifiStatsRetriever;
                 }
+
+                @Override
+                public NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats) {
+                    return NetworkStatsTestUtils.networkStatsDelta(stats, oldStats);
+                }
             };
 
     @Before
diff --git a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java
index e86108d..ede61a5 100644
--- a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java
+++ b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java
@@ -15,18 +15,14 @@
  */
 package com.android.server.selinux;
 
-import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
 import static com.android.server.selinux.SelinuxAuditLogBuilder.toCategories;
 
 import static com.google.common.truth.Truth.assertThat;
 
-import android.provider.DeviceConfig;
-
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import com.android.server.selinux.SelinuxAuditLogBuilder.SelinuxAuditLog;
 
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -45,24 +41,12 @@
 
     @Before
     public void setUp() {
-        runWithShellPermissionIdentity(
-                () ->
-                        DeviceConfig.setLocalOverride(
-                                DeviceConfig.NAMESPACE_ADSERVICES,
-                                SelinuxAuditLogBuilder.CONFIG_SELINUX_AUDIT_DOMAIN,
-                                TEST_DOMAIN));
-
-        mAuditLogBuilder = new SelinuxAuditLogBuilder();
+        mAuditLogBuilder = new SelinuxAuditLogBuilder(TEST_DOMAIN);
         mScontextMatcher = mAuditLogBuilder.mScontextMatcher;
         mTcontextMatcher = mAuditLogBuilder.mTcontextMatcher;
         mPathMatcher = mAuditLogBuilder.mPathMatcher;
     }
 
-    @After
-    public void tearDown() {
-        runWithShellPermissionIdentity(() -> DeviceConfig.clearAllLocalOverrides());
-    }
-
     @Test
     public void testMatcher_scontext() {
         assertThat(mScontextMatcher.reset("u:r:" + TEST_DOMAIN + ":s0").matches()).isTrue();
@@ -109,13 +93,9 @@
 
     @Test
     public void testMatcher_scontextDefaultConfig() {
-        runWithShellPermissionIdentity(
-                () ->
-                        DeviceConfig.clearLocalOverride(
-                                DeviceConfig.NAMESPACE_ADSERVICES,
-                                SelinuxAuditLogBuilder.CONFIG_SELINUX_AUDIT_DOMAIN));
-
-        Matcher scontexMatcher = new SelinuxAuditLogBuilder().mScontextMatcher;
+        Matcher scontexMatcher =
+                new SelinuxAuditLogBuilder(SelinuxAuditLogsCollector.DEFAULT_SELINUX_AUDIT_DOMAIN)
+                        .mScontextMatcher;
 
         assertThat(scontexMatcher.reset("u:r:" + TEST_DOMAIN + ":s0").matches()).isFalse();
         assertThat(scontexMatcher.reset("u:r:" + TEST_DOMAIN + ":s0:c123,c456").matches())
@@ -221,13 +201,7 @@
     @Test
     public void testSelinuxAuditLogsBuilder_wrongConfig() {
         String notARegexDomain = "not]a[regex";
-        runWithShellPermissionIdentity(
-                () ->
-                        DeviceConfig.setLocalOverride(
-                                DeviceConfig.NAMESPACE_ADSERVICES,
-                                SelinuxAuditLogBuilder.CONFIG_SELINUX_AUDIT_DOMAIN,
-                                notARegexDomain));
-        SelinuxAuditLogBuilder noOpBuilder = new SelinuxAuditLogBuilder();
+        SelinuxAuditLogBuilder noOpBuilder = new SelinuxAuditLogBuilder(notARegexDomain);
 
         noOpBuilder.reset(
                 "granted { p } scontext=u:r:"
diff --git a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java
index b6ccf5e..db58c74 100644
--- a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java
+++ b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java
@@ -15,7 +15,6 @@
  */
 package com.android.server.selinux;
 
-import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 
@@ -28,7 +27,6 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 
-import android.provider.DeviceConfig;
 import android.util.EventLog;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -59,6 +57,7 @@
     private final SelinuxAuditLogsCollector mSelinuxAutidLogsCollector =
             // Ignore rate limiting for tests
             new SelinuxAuditLogsCollector(
+                    () -> TEST_DOMAIN,
                     new RateLimiter(mClock, /* window= */ Duration.ofMillis(0)),
                     new QuotaLimiter(
                             mClock, /* windowSize= */ Duration.ofHours(1), /* maxPermits= */ 5));
@@ -67,13 +66,6 @@
 
     @Before
     public void setUp() {
-        runWithShellPermissionIdentity(
-                () ->
-                        DeviceConfig.setLocalOverride(
-                                DeviceConfig.NAMESPACE_ADSERVICES,
-                                SelinuxAuditLogBuilder.CONFIG_SELINUX_AUDIT_DOMAIN,
-                                TEST_DOMAIN));
-
         mSelinuxAutidLogsCollector.setStopRequested(false);
         // move the clock forward for the limiters.
         mClock.currentTimeMillis += Duration.ofHours(1).toMillis();
@@ -85,7 +77,6 @@
 
     @After
     public void tearDown() {
-        runWithShellPermissionIdentity(() -> DeviceConfig.clearAllLocalOverrides());
         mMockitoSession.finishMocking();
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java
index 82efae4..92c6db5 100644
--- a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java
@@ -21,6 +21,9 @@
 import static android.service.quickaccesswallet.Flags.launchWalletOptionOnPowerDoubleTap;
 import static android.service.quickaccesswallet.Flags.launchWalletViaSysuiCallbacks;
 
+import static com.android.server.GestureLauncherService.DOUBLE_TAP_POWER_DISABLED_MODE;
+import static com.android.server.GestureLauncherService.DOUBLE_TAP_POWER_LAUNCH_CAMERA_MODE;
+import static com.android.server.GestureLauncherService.DOUBLE_TAP_POWER_MULTI_TARGET_MODE;
 import static com.android.server.GestureLauncherService.LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER;
 import static com.android.server.GestureLauncherService.LAUNCH_WALLET_ON_DOUBLE_TAP_POWER;
 import static com.android.server.GestureLauncherService.POWER_DOUBLE_TAP_MAX_TIME_MS;
@@ -163,7 +166,7 @@
                 new GestureLauncherService(
                         mContext, mMetricsLogger, mQuickAccessWalletClient, mUiEventLogger);
 
-        withDoubleTapPowerGestureEnableSettingValue(true);
+        withMultiTargetDoubleTapPowerGestureEnableSettingValue(true);
         withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER);
     }
 
@@ -215,68 +218,117 @@
     }
 
     @Test
-    public void testIsCameraDoubleTapPowerSettingEnabled_configFalseSettingDisabled() {
-        if (launchWalletOptionOnPowerDoubleTap()) {
-            withDoubleTapPowerEnabledConfigValue(false);
-            withDoubleTapPowerGestureEnableSettingValue(false);
-            withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER);
-        } else {
-            withCameraDoubleTapPowerEnableConfigValue(false);
-            withCameraDoubleTapPowerDisableSettingValue(1);
-        }
+    @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
+    public void testIsCameraDoubleTapPowerSettingEnabled_flagEnabled_configFalseSettingDisabled() {
+        withDoubleTapPowerModeConfigValue(
+                DOUBLE_TAP_POWER_DISABLED_MODE);
+        withMultiTargetDoubleTapPowerGestureEnableSettingValue(false);
+        withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER);
+
         assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled(
                 mContext, FAKE_USER_ID));
     }
 
     @Test
-    public void testIsCameraDoubleTapPowerSettingEnabled_configFalseSettingEnabled() {
-        if (launchWalletOptionOnPowerDoubleTap()) {
-            withDoubleTapPowerEnabledConfigValue(false);
-            withDoubleTapPowerGestureEnableSettingValue(true);
-            withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER);
-            assertTrue(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled(
-                    mContext, FAKE_USER_ID));
-        } else {
-            withCameraDoubleTapPowerEnableConfigValue(false);
-            withCameraDoubleTapPowerDisableSettingValue(0);
-            assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled(
-                    mContext, FAKE_USER_ID));
-        }
-    }
+    @RequiresFlagsDisabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
+    public void testIsCameraDoubleTapPowerSettingEnabled_flagDisabled_configFalseSettingDisabled() {
+        withCameraDoubleTapPowerEnableConfigValue(false);
+        withCameraDoubleTapPowerDisableSettingValue(1);
 
-    @Test
-    public void testIsCameraDoubleTapPowerSettingEnabled_configTrueSettingDisabled() {
-        if (launchWalletOptionOnPowerDoubleTap()) {
-            withDoubleTapPowerEnabledConfigValue(true);
-            withDoubleTapPowerGestureEnableSettingValue(false);
-            withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER);
-        } else {
-            withCameraDoubleTapPowerEnableConfigValue(true);
-            withCameraDoubleTapPowerDisableSettingValue(1);
-        }
         assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled(
                 mContext, FAKE_USER_ID));
     }
 
     @Test
-    public void testIsCameraDoubleTapPowerSettingEnabled_configTrueSettingEnabled() {
-        if (launchWalletOptionOnPowerDoubleTap()) {
-            withDoubleTapPowerEnabledConfigValue(true);
-            withDoubleTapPowerGestureEnableSettingValue(true);
-            withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER);
-        } else {
-            withCameraDoubleTapPowerEnableConfigValue(true);
-            withCameraDoubleTapPowerDisableSettingValue(0);
-        }
+    @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
+    public void testIsCameraDoubleTapPowerSettingEnabled_flagEnabled_configFalseSettingEnabled() {
+        withDoubleTapPowerModeConfigValue(DOUBLE_TAP_POWER_DISABLED_MODE);
+        withMultiTargetDoubleTapPowerGestureEnableSettingValue(true);
+        withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER);
+
+        assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled(
+                mContext, FAKE_USER_ID));
+    }
+
+    @Test
+    @RequiresFlagsDisabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
+    public void testIsCameraDoubleTapPowerSettingEnabled_flagDisabled_configFalseSettingEnabled() {
+        withCameraDoubleTapPowerEnableConfigValue(false);
+        withCameraDoubleTapPowerDisableSettingValue(0);
+
+        assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled(
+                mContext, FAKE_USER_ID));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
+    public void testIsCameraDoubleTapPowerSettingEnabled_flagEnabled_configTrueSettingDisabled() {
+        withDoubleTapPowerModeConfigValue(DOUBLE_TAP_POWER_MULTI_TARGET_MODE);
+        withMultiTargetDoubleTapPowerGestureEnableSettingValue(false);
+        withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER);
+
+        assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled(
+                mContext, FAKE_USER_ID));
+    }
+
+    @Test
+    @RequiresFlagsDisabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
+    public void testIsCameraDoubleTapPowerSettingEnabled_flagDisabled_configTrueSettingDisabled() {
+        withCameraDoubleTapPowerEnableConfigValue(true);
+        withCameraDoubleTapPowerDisableSettingValue(1);
+
+        assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled(
+                mContext, FAKE_USER_ID));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
+    public void testIsCameraDoubleTapPowerSettingEnabled_flagEnabled_configTrueSettingEnabled() {
+        withDoubleTapPowerModeConfigValue(DOUBLE_TAP_POWER_MULTI_TARGET_MODE);
+        withMultiTargetDoubleTapPowerGestureEnableSettingValue(true);
+        withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER);
+
+        assertTrue(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled(
+                mContext, FAKE_USER_ID));
+    }
+
+    @Test
+    @RequiresFlagsDisabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
+    public void testIsCameraDoubleTapPowerSettingEnabled_flagDisabled_configTrueSettingEnabled() {
+        withCameraDoubleTapPowerEnableConfigValue(true);
+        withCameraDoubleTapPowerDisableSettingValue(0);
+
         assertTrue(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled(
                 mContext, FAKE_USER_ID));
     }
 
     @Test
     @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
+    public void testIsCameraDoubleTapPowerSettingEnabled_launchCameraMode_settingEnabled() {
+        withDoubleTapPowerModeConfigValue(DOUBLE_TAP_POWER_LAUNCH_CAMERA_MODE);
+        withCameraDoubleTapPowerDisableSettingValue(0);
+
+        assertTrue(
+                mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled(
+                        mContext, FAKE_USER_ID));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
+    public void testIsCameraDoubleTapPowerSettingEnabled_launchCameraMode_settingDisabled() {
+        withDoubleTapPowerModeConfigValue(DOUBLE_TAP_POWER_LAUNCH_CAMERA_MODE);
+        withCameraDoubleTapPowerDisableSettingValue(1);
+
+        assertFalse(
+                mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled(
+                        mContext, FAKE_USER_ID));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
     public void testIsCameraDoubleTapPowerSettingEnabled_actionWallet() {
-        withDoubleTapPowerEnabledConfigValue(true);
-        withDoubleTapPowerGestureEnableSettingValue(true);
+        withDoubleTapPowerModeConfigValue(DOUBLE_TAP_POWER_MULTI_TARGET_MODE);
+        withMultiTargetDoubleTapPowerGestureEnableSettingValue(true);
         withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER);
 
         assertFalse(
@@ -287,8 +339,8 @@
     @Test
     @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
     public void testIsWalletDoubleTapPowerSettingEnabled() {
-        withDoubleTapPowerEnabledConfigValue(true);
-        withDoubleTapPowerGestureEnableSettingValue(true);
+        withDoubleTapPowerModeConfigValue(DOUBLE_TAP_POWER_MULTI_TARGET_MODE);
+        withMultiTargetDoubleTapPowerGestureEnableSettingValue(true);
         withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER);
 
         assertTrue(
@@ -299,11 +351,11 @@
     @Test
     @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
     public void testIsWalletDoubleTapPowerSettingEnabled_configDisabled() {
-        withDoubleTapPowerEnabledConfigValue(false);
-        withDoubleTapPowerGestureEnableSettingValue(true);
+        withDoubleTapPowerModeConfigValue(DOUBLE_TAP_POWER_DISABLED_MODE);
+        withMultiTargetDoubleTapPowerGestureEnableSettingValue(true);
         withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER);
 
-        assertTrue(
+        assertFalse(
                 mGestureLauncherService.isWalletDoubleTapPowerSettingEnabled(
                         mContext, FAKE_USER_ID));
     }
@@ -311,8 +363,8 @@
     @Test
     @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
     public void testIsWalletDoubleTapPowerSettingEnabled_settingDisabled() {
-        withDoubleTapPowerEnabledConfigValue(true);
-        withDoubleTapPowerGestureEnableSettingValue(false);
+        withDoubleTapPowerModeConfigValue(DOUBLE_TAP_POWER_MULTI_TARGET_MODE);
+        withMultiTargetDoubleTapPowerGestureEnableSettingValue(false);
         withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER);
 
         assertFalse(
@@ -323,8 +375,8 @@
     @Test
     @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
     public void testIsWalletDoubleTapPowerSettingEnabled_actionCamera() {
-        withDoubleTapPowerEnabledConfigValue(true);
-        withDoubleTapPowerGestureEnableSettingValue(true);
+        withDoubleTapPowerModeConfigValue(DOUBLE_TAP_POWER_MULTI_TARGET_MODE);
+        withMultiTargetDoubleTapPowerGestureEnableSettingValue(true);
         withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER);
 
         assertFalse(
@@ -449,13 +501,7 @@
 
     @Test
     public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOffInteractive() {
-        if (launchWalletOptionOnPowerDoubleTap()) {
-            withDoubleTapPowerGestureEnableSettingValue(false);
-        } else {
-            withCameraDoubleTapPowerEnableConfigValue(false);
-            withCameraDoubleTapPowerDisableSettingValue(1);
-        }
-        mGestureLauncherService.updateCameraDoubleTapPowerEnabled();
+        disableDoubleTapPowerGesture();
 
         long eventTime = INITIAL_EVENT_TIME_MILLIS;
         KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
@@ -498,13 +544,7 @@
 
     @Test
     public void testInterceptPowerKeyDown_intervalMidBoundsCameraPowerGestureOffInteractive() {
-        if (launchWalletOptionOnPowerDoubleTap()) {
-            withDoubleTapPowerGestureEnableSettingValue(false);
-        } else {
-            withCameraDoubleTapPowerEnableConfigValue(false);
-            withCameraDoubleTapPowerDisableSettingValue(1);
-        }
-        mGestureLauncherService.updateCameraDoubleTapPowerEnabled();
+        disableDoubleTapPowerGesture();
 
         long eventTime = INITIAL_EVENT_TIME_MILLIS;
         KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
@@ -549,9 +589,7 @@
 
     @Test
     public void testInterceptPowerKeyDown_intervalOutOfBoundsCameraPowerGestureOffInteractive() {
-        withCameraDoubleTapPowerEnableConfigValue(false);
-        withCameraDoubleTapPowerDisableSettingValue(1);
-        mGestureLauncherService.updateCameraDoubleTapPowerEnabled();
+        disableDoubleTapPowerGesture();
 
         long eventTime = INITIAL_EVENT_TIME_MILLIS;
         KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
@@ -1031,9 +1069,7 @@
     public void
     testInterceptPowerKeyDown_triggerEmergency_cameraGestureEnabled_doubleTap_cooldownTriggered() {
         // Enable camera double tap gesture
-        withCameraDoubleTapPowerEnableConfigValue(true);
-        withCameraDoubleTapPowerDisableSettingValue(0);
-        mGestureLauncherService.updateCameraDoubleTapPowerEnabled();
+        enableCameraGesture();
 
         // Enable power button cooldown
         withEmergencyGesturePowerButtonCooldownPeriodMsValue(3000);
@@ -1220,10 +1256,7 @@
 
     @Test
     public void testInterceptPowerKeyDown_longpress() {
-        withCameraDoubleTapPowerEnableConfigValue(true);
-        withCameraDoubleTapPowerDisableSettingValue(0);
-        mGestureLauncherService.updateCameraDoubleTapPowerEnabled();
-        withUserSetupCompleteValue(true);
+        enableCameraGesture();
 
         long eventTime = INITIAL_EVENT_TIME_MILLIS;
         KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
@@ -1400,13 +1433,7 @@
 
     @Test
     public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOffNotInteractive() {
-        if (launchWalletOptionOnPowerDoubleTap()) {
-            withDoubleTapPowerGestureEnableSettingValue(false);
-        } else {
-            withCameraDoubleTapPowerEnableConfigValue(false);
-            withCameraDoubleTapPowerDisableSettingValue(1);
-        }
-        mGestureLauncherService.updateCameraDoubleTapPowerEnabled();
+        disableDoubleTapPowerGesture();
 
         long eventTime = INITIAL_EVENT_TIME_MILLIS;
         KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
@@ -1449,9 +1476,7 @@
 
     @Test
     public void testInterceptPowerKeyDown_intervalMidBoundsCameraPowerGestureOffNotInteractive() {
-        withCameraDoubleTapPowerEnableConfigValue(false);
-        withCameraDoubleTapPowerDisableSettingValue(1);
-        mGestureLauncherService.updateCameraDoubleTapPowerEnabled();
+        disableDoubleTapPowerGesture();
 
         long eventTime = INITIAL_EVENT_TIME_MILLIS;
         KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
@@ -1495,9 +1520,7 @@
 
     @Test
     public void testInterceptPowerKeyDown_intervalOutOfBoundsCameraPowerGestureOffNotInteractive() {
-        withCameraDoubleTapPowerEnableConfigValue(false);
-        withCameraDoubleTapPowerDisableSettingValue(1);
-        mGestureLauncherService.updateCameraDoubleTapPowerEnabled();
+        disableDoubleTapPowerGesture();
 
         long eventTime = INITIAL_EVENT_TIME_MILLIS;
         KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
@@ -1630,9 +1653,7 @@
 
     @Test
     public void testInterceptPowerKeyDown_intervalMidBoundsCameraPowerGestureOnNotInteractive() {
-        withCameraDoubleTapPowerEnableConfigValue(true);
-        withCameraDoubleTapPowerDisableSettingValue(0);
-        mGestureLauncherService.updateCameraDoubleTapPowerEnabled();
+        enableCameraGesture();
 
         long eventTime = INITIAL_EVENT_TIME_MILLIS;
         KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE,
@@ -1823,12 +1844,13 @@
                 .thenReturn(enableConfigValue);
     }
 
-    private void withDoubleTapPowerEnabledConfigValue(boolean enable) {
-        when(mResources.getBoolean(com.android.internal.R.bool.config_doubleTapPowerGestureEnabled))
-                .thenReturn(enable);
+    private void withDoubleTapPowerModeConfigValue(
+            int modeConfigValue) {
+        when(mResources.getInteger(com.android.internal.R.integer.config_doubleTapPowerGestureMode))
+                .thenReturn(modeConfigValue);
     }
 
-    private void withDoubleTapPowerGestureEnableSettingValue(boolean enable) {
+    private void withMultiTargetDoubleTapPowerGestureEnableSettingValue(boolean enable) {
         Settings.Secure.putIntForUser(
                 mContentResolver,
                 Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE_ENABLED,
@@ -1910,8 +1932,8 @@
 
     private void enableWalletGesture() {
         withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER);
-        withDoubleTapPowerGestureEnableSettingValue(true);
-        withDoubleTapPowerEnabledConfigValue(true);
+        withMultiTargetDoubleTapPowerGestureEnableSettingValue(true);
+        withDoubleTapPowerModeConfigValue(DOUBLE_TAP_POWER_MULTI_TARGET_MODE);
 
         mGestureLauncherService.updateWalletDoubleTapPowerEnabled();
         withUserSetupCompleteValue(true);
@@ -1926,8 +1948,9 @@
 
     private void enableCameraGesture() {
         if (launchWalletOptionOnPowerDoubleTap()) {
-            withDoubleTapPowerEnabledConfigValue(true);
-            withDoubleTapPowerGestureEnableSettingValue(true);
+            withDoubleTapPowerModeConfigValue(
+                    DOUBLE_TAP_POWER_MULTI_TARGET_MODE);
+            withMultiTargetDoubleTapPowerGestureEnableSettingValue(true);
             withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER);
         } else {
             withCameraDoubleTapPowerEnableConfigValue(true);
@@ -1937,6 +1960,18 @@
         withUserSetupCompleteValue(true);
     }
 
+    private void disableDoubleTapPowerGesture() {
+        if (launchWalletOptionOnPowerDoubleTap()) {
+            withDoubleTapPowerModeConfigValue(DOUBLE_TAP_POWER_DISABLED_MODE);
+            withMultiTargetDoubleTapPowerGestureEnableSettingValue(false);
+        } else {
+            withCameraDoubleTapPowerEnableConfigValue(false);
+            withCameraDoubleTapPowerDisableSettingValue(1);
+        }
+        mGestureLauncherService.updateWalletDoubleTapPowerEnabled();
+        withUserSetupCompleteValue(true);
+    }
+
     private void sendPowerKeyDownToGestureLauncherServiceAndAssertValues(
             long eventTime, boolean expectedIntercept, boolean expectedOutLaunchedValue) {
         KeyEvent keyEvent =
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index fa78dfc..dafe482 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -220,6 +220,9 @@
     @Mock private ProxyManager mProxyManager;
     @Mock private StatusBarManagerInternal mStatusBarManagerInternal;
     @Mock private DevicePolicyManager mDevicePolicyManager;
+    @Mock
+    private HearingDevicePhoneCallNotificationController
+            mMockHearingDevicePhoneCallNotificationController;
     @Spy private IUserInitializationCompleteCallback mUserInitializationCompleteCallback;
     @Captor private ArgumentCaptor<Intent> mIntentArgumentCaptor;
     private IAccessibilityManager mA11yManagerServiceOnDevice;
@@ -289,7 +292,8 @@
                 mMockMagnificationController,
                 mInputFilter,
                 mProxyManager,
-                mFakePermissionEnforcer);
+                mFakePermissionEnforcer,
+                mMockHearingDevicePhoneCallNotificationController);
         mA11yms.switchUser(mTestableContext.getUserId());
         mTestableLooper.processAllMessages();
 
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java
new file mode 100644
index 0000000..efea214
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java
@@ -0,0 +1,187 @@
+/*
+ * 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.accessibility;
+
+import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Application;
+import android.app.Instrumentation;
+import android.app.NotificationManager;
+import android.bluetooth.BluetoothAdapter;
+import android.content.Context;
+import android.media.AudioDeviceInfo;
+import android.media.AudioDevicePort;
+import android.media.AudioManager;
+import android.telephony.TelephonyCallback;
+import android.telephony.TelephonyManager;
+
+import androidx.annotation.NonNull;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.messages.nano.SystemMessageProto;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Tests for the {@link HearingDevicePhoneCallNotificationController}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class HearingDevicePhoneCallNotificationControllerTest {
+    @Rule
+    public MockitoRule mockito = MockitoJUnit.rule();
+
+    private static final String TEST_ADDRESS = "55:66:77:88:99:AA";
+
+    private final Application mApplication = ApplicationProvider.getApplicationContext();
+    @Spy
+    private final Context mContext = mApplication.getApplicationContext();
+    private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
+
+    @Mock
+    private TelephonyManager mTelephonyManager;
+    @Mock
+    private NotificationManager mNotificationManager;
+    @Mock
+    private AudioManager mAudioManager;
+    private HearingDevicePhoneCallNotificationController mController;
+    private TestCallStateListener mTestCallStateListener;
+
+    @Before
+    public void setUp() {
+        mInstrumentation.getUiAutomation().adoptShellPermissionIdentity(BLUETOOTH_PRIVILEGED);
+        when(mContext.getSystemService(TelephonyManager.class)).thenReturn(mTelephonyManager);
+        when(mContext.getSystemService(NotificationManager.class)).thenReturn(mNotificationManager);
+        when(mContext.getSystemService(AudioManager.class)).thenReturn(mAudioManager);
+
+        mTestCallStateListener = new TestCallStateListener(mContext);
+        mController = new HearingDevicePhoneCallNotificationController(mContext,
+                mTestCallStateListener);
+        mController.startListenForCallState();
+    }
+
+    @Test
+    public void startListenForCallState_callbackNotNull() {
+        Mockito.reset(mTelephonyManager);
+        mController = new HearingDevicePhoneCallNotificationController(mContext);
+        ArgumentCaptor<TelephonyCallback> listenerCaptor = ArgumentCaptor.forClass(
+                TelephonyCallback.class);
+
+        mController.startListenForCallState();
+
+        verify(mTelephonyManager).registerTelephonyCallback(any(Executor.class),
+                listenerCaptor.capture());
+        TelephonyCallback callback = listenerCaptor.getValue();
+        assertThat(callback).isNotNull();
+    }
+
+    @Test
+    public void onCallStateChanged_stateOffHook_hapDevice_showNotification() {
+        AudioDeviceInfo hapDeviceInfo = createAudioDeviceInfo(TEST_ADDRESS,
+                AudioManager.DEVICE_OUT_BLE_HEADSET);
+        when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(
+                new AudioDeviceInfo[]{hapDeviceInfo});
+        when(mAudioManager.getAvailableCommunicationDevices()).thenReturn(List.of(hapDeviceInfo));
+
+        mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK);
+
+        verify(mNotificationManager).notify(
+                eq(SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH), any());
+    }
+
+    @Test
+    public void onCallStateChanged_stateOffHook_a2dpDevice_noNotification() {
+        AudioDeviceInfo a2dpDeviceInfo = createAudioDeviceInfo(TEST_ADDRESS,
+                AudioManager.DEVICE_OUT_BLUETOOTH_A2DP);
+        when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(
+                new AudioDeviceInfo[]{a2dpDeviceInfo});
+        when(mAudioManager.getAvailableCommunicationDevices()).thenReturn(List.of(a2dpDeviceInfo));
+
+        mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK);
+
+        verify(mNotificationManager, never()).notify(
+                eq(SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH), any());
+    }
+
+    @Test
+    public void onCallStateChanged_stateOffHookThenIdle_hapDeviceInfo_cancelNotification() {
+        AudioDeviceInfo hapDeviceInfo = createAudioDeviceInfo(TEST_ADDRESS,
+                AudioManager.DEVICE_OUT_BLE_HEADSET);
+        when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(
+                new AudioDeviceInfo[]{hapDeviceInfo});
+        when(mAudioManager.getAvailableCommunicationDevices()).thenReturn(List.of(hapDeviceInfo));
+
+        mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK);
+        mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_IDLE);
+
+        verify(mNotificationManager).cancel(
+                eq(SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH));
+    }
+
+    private AudioDeviceInfo createAudioDeviceInfo(String address, int type) {
+        AudioDevicePort audioDevicePort = mock(AudioDevicePort.class);
+        doReturn(type).when(audioDevicePort).type();
+        doReturn(address).when(audioDevicePort).address();
+        doReturn("testDevice").when(audioDevicePort).name();
+
+        return new AudioDeviceInfo(audioDevicePort);
+    }
+
+    /**
+     * For easier testing for CallStateListener, override methods that contain final object.
+     */
+    private static class TestCallStateListener extends
+            HearingDevicePhoneCallNotificationController.CallStateListener {
+
+        TestCallStateListener(@NonNull Context context) {
+            super(context);
+        }
+
+        @Override
+        boolean isHapClientSupported() {
+            return true;
+        }
+
+        @Override
+        boolean isHapClientDevice(BluetoothAdapter bluetoothAdapter, AudioDeviceInfo info) {
+            return TEST_ADDRESS.equals(info.getAddress());
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/OWNERS b/services/tests/servicestests/src/com/android/server/accessibility/OWNERS
index b74281e..c824c39 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/OWNERS
+++ b/services/tests/servicestests/src/com/android/server/accessibility/OWNERS
@@ -1 +1,3 @@
+# Bug component: 44215
+
 include /core/java/android/view/accessibility/OWNERS
diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java b/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java
index 0bf419e..998c1d1 100644
--- a/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java
@@ -20,6 +20,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -35,6 +36,7 @@
 import android.content.IntentFilter;
 import android.content.ServiceConnection;
 import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.DropBoxManager;
@@ -48,6 +50,7 @@
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.platform.test.annotations.Presubmit;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
@@ -68,6 +71,7 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -150,6 +154,25 @@
     }
 
     @Test
+    public void testRemovedUserShouldNotBeRunning() throws Exception {
+        final UserManager userManager = mContext.getSystemService(UserManager.class);
+        assertNotNull("UserManager should not be null", userManager);
+        final UserInfo user = userManager.createUser(
+                "TestUser", UserManager.USER_TYPE_FULL_SECONDARY, 0);
+
+        mService.startUserInBackground(user.id);
+        assertTrue("User should be running", mService.isUserRunning(user.id, 0));
+        assertTrue("User should be in running users",
+                Arrays.stream(mService.getRunningUserIds()).anyMatch(x -> x == user.id));
+
+        userManager.removeUser(user.id);
+        mService.startUserInBackground(user.id);
+        assertFalse("Removed user should not be running", mService.isUserRunning(user.id, 0));
+        assertFalse("Removed user should not be in running users",
+                Arrays.stream(mService.getRunningUserIds()).anyMatch(x -> x == user.id));
+    }
+
+    @Test
     public void testServiceUnbindAndKilling() {
         for (int i = TEST_LOOPS; i > 0; i--) {
             runOnce(i);
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index 2fe6918..06958b8 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -94,6 +94,7 @@
 import android.os.Message;
 import android.os.PowerManagerInternal;
 import android.os.RemoteException;
+import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.os.storage.IStorageManager;
@@ -181,14 +182,12 @@
             Intent.ACTION_USER_STARTING);
 
     private static final Set<Integer> START_FOREGROUND_USER_MESSAGE_CODES = newHashSet(
-            0, // for startUserInternalOnHandler
             REPORT_USER_SWITCH_MSG,
             USER_SWITCH_TIMEOUT_MSG,
             USER_START_MSG,
             USER_CURRENT_MSG);
 
     private static final Set<Integer> START_BACKGROUND_USER_MESSAGE_CODES = newHashSet(
-            0, // for startUserInternalOnHandler
             USER_START_MSG,
             REPORT_LOCKED_BOOT_COMPLETE_MSG);
 
@@ -376,7 +375,7 @@
         // and the cascade effect goes on...). In fact, a better approach would to not assert the
         // binder calls, but their side effects (in this case, that the user is stopped right away)
         assertWithMessage("wrong binder message calls").that(mInjector.mHandler.getMessageCodes())
-                .containsExactly(/* for startUserInternalOnHandler */ 0, USER_START_MSG);
+                .containsExactly(USER_START_MSG);
     }
 
     private void startUserAssertions(
@@ -419,17 +418,12 @@
     @Test
     public void testDispatchUserSwitch() throws RemoteException {
         // Prepare mock observer and register it
-        IUserSwitchObserver observer = mock(IUserSwitchObserver.class);
-        when(observer.asBinder()).thenReturn(new Binder());
-        doAnswer(invocation -> {
-            IRemoteCallback callback = (IRemoteCallback) invocation.getArguments()[1];
-            callback.sendResult(null);
-            return null;
-        }).when(observer).onUserSwitching(anyInt(), any());
-        mUserController.registerUserSwitchObserver(observer, "mock");
+        IUserSwitchObserver observer = registerUserSwitchObserver(
+                /* replyToOnBeforeUserSwitchingCallback= */ true,
+                /* replyToOnUserSwitchingCallback= */ true);
         // Start user -- this will update state of mUserController
         mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND);
-        verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID));
+        verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID), any());
         Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG);
         assertNotNull(reportMsg);
         UserState userState = (UserState) reportMsg.obj;
@@ -454,13 +448,13 @@
 
     @Test
     public void testDispatchUserSwitchBadReceiver() throws RemoteException {
-        // Prepare mock observer which doesn't notify the callback and register it
-        IUserSwitchObserver observer = mock(IUserSwitchObserver.class);
-        when(observer.asBinder()).thenReturn(new Binder());
-        mUserController.registerUserSwitchObserver(observer, "mock");
+        // Prepare mock observer which doesn't notify the onUserSwitching callback and register it
+        IUserSwitchObserver observer = registerUserSwitchObserver(
+                /* replyToOnBeforeUserSwitchingCallback= */ true,
+                /* replyToOnUserSwitchingCallback= */ false);
         // Start user -- this will update state of mUserController
         mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND);
-        verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID));
+        verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID), any());
         Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG);
         assertNotNull(reportMsg);
         UserState userState = (UserState) reportMsg.obj;
@@ -496,29 +490,6 @@
         mInjector.mHandler.clearAllRecordedMessages();
         // Verify that continueUserSwitch worked as expected
         continueAndCompleteUserSwitch(userState, oldUserId, newUserId);
-        verify(mInjector, times(0)).dismissKeyguard(any());
-        verify(mInjector, times(1)).dismissUserSwitchingDialog(any());
-        continueUserSwitchAssertions(oldUserId, TEST_USER_ID, false, false);
-        verifySystemUserVisibilityChangesNeverNotified();
-    }
-
-    @Test
-    public void testContinueUserSwitchDismissKeyguard() {
-        when(mInjector.mKeyguardManagerMock.isDeviceSecure(anyInt())).thenReturn(false);
-        mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true,
-                /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false,
-                /* backgroundUserScheduledStopTimeSecs= */ -1);
-        // Start user -- this will update state of mUserController
-        mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND);
-        Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG);
-        assertNotNull(reportMsg);
-        UserState userState = (UserState) reportMsg.obj;
-        int oldUserId = reportMsg.arg1;
-        int newUserId = reportMsg.arg2;
-        mInjector.mHandler.clearAllRecordedMessages();
-        // Verify that continueUserSwitch worked as expected
-        continueAndCompleteUserSwitch(userState, oldUserId, newUserId);
-        verify(mInjector, times(1)).dismissKeyguard(any());
         verify(mInjector, times(1)).dismissUserSwitchingDialog(any());
         continueUserSwitchAssertions(oldUserId, TEST_USER_ID, false, false);
         verifySystemUserVisibilityChangesNeverNotified();
@@ -551,7 +522,6 @@
         expectedCodes.add(REPORT_USER_SWITCH_COMPLETE_MSG);
         if (backgroundUserStopping) {
             expectedCodes.add(CLEAR_USER_JOURNEY_SESSION_MSG);
-            expectedCodes.add(0); // this is for directly posting in stopping.
         }
         if (expectScheduleBackgroundUserStopping) {
             expectedCodes.add(SCHEDULED_STOP_BACKGROUND_USER_MSG);
@@ -567,9 +537,9 @@
     @Test
     public void testDispatchUserSwitchComplete() throws RemoteException {
         // Prepare mock observer and register it
-        IUserSwitchObserver observer = mock(IUserSwitchObserver.class);
-        when(observer.asBinder()).thenReturn(new Binder());
-        mUserController.registerUserSwitchObserver(observer, "mock");
+        IUserSwitchObserver observer = registerUserSwitchObserver(
+                /* replyToOnBeforeUserSwitchingCallback= */ true,
+                /* replyToOnUserSwitchingCallback= */ true);
         // Start user -- this will update state of mUserController
         mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND);
         Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG);
@@ -1752,6 +1722,29 @@
         verify(mInjector, never()).onSystemUserVisibilityChanged(anyBoolean());
     }
 
+    private IUserSwitchObserver registerUserSwitchObserver(
+            boolean replyToOnBeforeUserSwitchingCallback, boolean replyToOnUserSwitchingCallback)
+            throws RemoteException {
+        IUserSwitchObserver observer = mock(IUserSwitchObserver.class);
+        when(observer.asBinder()).thenReturn(new Binder());
+        if (replyToOnBeforeUserSwitchingCallback) {
+            doAnswer(invocation -> {
+                IRemoteCallback callback = (IRemoteCallback) invocation.getArguments()[1];
+                callback.sendResult(null);
+                return null;
+            }).when(observer).onBeforeUserSwitching(anyInt(), any());
+        }
+        if (replyToOnUserSwitchingCallback) {
+            doAnswer(invocation -> {
+                IRemoteCallback callback = (IRemoteCallback) invocation.getArguments()[1];
+                callback.sendResult(null);
+                return null;
+            }).when(observer).onUserSwitching(anyInt(), any());
+        }
+        mUserController.registerUserSwitchObserver(observer, "mock");
+        return observer;
+    }
+
     // Should be public to allow mocking
     private static class TestInjector extends UserController.Injector {
         public final TestHandler mHandler;
@@ -1907,11 +1900,6 @@
         }
 
         @Override
-        protected void dismissKeyguard(Runnable runnable) {
-            runnable.run();
-        }
-
-        @Override
         void showUserSwitchingDialog(UserInfo fromUser, UserInfo toUser,
                 String switchingFromSystemUserMessage, String switchingToSystemUserMessage,
                 Runnable onShown) {
@@ -1957,6 +1945,7 @@
          * fix this, but in the meantime, this is your warning.
          */
         private final List<Message> mMessages = new ArrayList<>();
+        private final List<Runnable> mPendingCallbacks = new ArrayList<>();
 
         TestHandler(Looper looper) {
             super(looper);
@@ -1989,14 +1978,24 @@
 
         @Override
         public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
-            Message copy = new Message();
-            copy.copyFrom(msg);
-            mMessages.add(copy);
-            if (msg.getCallback() != null) {
-                msg.getCallback().run();
+            if (msg.getCallback() == null) {
+                Message copy = new Message();
+                copy.copyFrom(msg);
+                mMessages.add(copy);
+            } else {
+                if (SystemClock.uptimeMillis() >= uptimeMillis) {
+                    msg.getCallback().run();
+                } else {
+                    mPendingCallbacks.add(msg.getCallback());
+                }
                 msg.setCallback(null);
             }
             return super.sendMessageAtTime(msg, uptimeMillis);
         }
+
+        private void runPendingCallbacks() {
+            mPendingCallbacks.forEach(Runnable::run);
+            mPendingCallbacks.clear();
+        }
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpSqlPersistenceTest.java b/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpSqlPersistenceTest.java
new file mode 100644
index 0000000..8471307
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpSqlPersistenceTest.java
@@ -0,0 +1,309 @@
+/*
+ * 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.appop;
+
+import static android.app.AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR;
+import static android.app.AppOpsManager.ATTRIBUTION_FLAG_RECEIVER;
+import static android.app.AppOpsManager.ATTRIBUTION_FLAG_TRUSTED;
+import static android.app.AppOpsManager.UID_STATE_FOREGROUND;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.os.Process;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.LongSparseArray;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.appop.DiscreteOpsSqlRegistry.DiscreteOp;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class DiscreteAppOpSqlPersistenceTest {
+    private static final String DATABASE_NAME = "test_app_ops.db";
+    private DiscreteOpsSqlRegistry mDiscreteRegistry;
+    private final Context mContext =
+            InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+    @Before
+    public void setUp() {
+        mDiscreteRegistry = new DiscreteOpsSqlRegistry(mContext,
+                mContext.getDatabasePath(DATABASE_NAME));
+        mDiscreteRegistry.systemReady();
+    }
+
+    @After
+    public void cleanUp() {
+        mContext.deleteDatabase(DATABASE_NAME);
+    }
+
+    @Test
+    public void discreteOpEventIsRecorded() {
+        DiscreteOp opEvent = new DiscreteOpBuilder(mContext).build();
+        mDiscreteRegistry.recordDiscreteAccess(opEvent);
+        List<DiscreteOp> discreteOps = mDiscreteRegistry.getCachedDiscreteOps();
+        assertThat(discreteOps.size()).isEqualTo(1);
+        assertThat(discreteOps).contains(opEvent);
+    }
+
+    @Test
+    public void discreteOpEventIsPersistedToDisk() {
+        DiscreteOp opEvent = new DiscreteOpBuilder(mContext).build();
+        mDiscreteRegistry.recordDiscreteAccess(opEvent);
+        flushDiscreteOpsToDatabase();
+        assertThat(mDiscreteRegistry.getCachedDiscreteOps()).isEmpty();
+        List<DiscreteOp> discreteOps = mDiscreteRegistry.getAllDiscreteOps();
+        assertThat(discreteOps.size()).isEqualTo(1);
+        assertThat(discreteOps).contains(opEvent);
+    }
+
+    @Test
+    public void discreteOpEventInSameMinuteIsNotRecorded() {
+        long oneMinuteMillis = Duration.ofMinutes(1).toMillis();
+        // round timestamp at minute level and add 5 seconds
+        long accessTime = System.currentTimeMillis() / oneMinuteMillis * oneMinuteMillis + 5000;
+        DiscreteOp opEvent = new DiscreteOpBuilder(mContext).setAccessTime(accessTime).build();
+        mDiscreteRegistry.recordDiscreteAccess(opEvent);
+        // create duplicate event in same minute, with added 30 seconds
+        DiscreteOp opEvent2 =
+                new DiscreteOpBuilder(mContext).setAccessTime(accessTime + 30000).build();
+        mDiscreteRegistry.recordDiscreteAccess(opEvent2);
+        List<DiscreteOp> discreteOps = mDiscreteRegistry.getAllDiscreteOps();
+
+        assertThat(discreteOps.size()).isEqualTo(1);
+        assertThat(discreteOps).contains(opEvent);
+    }
+
+    @Test
+    public void multipleDiscreteOpEventAreRecorded() {
+        DiscreteOp opEvent = new DiscreteOpBuilder(mContext).build();
+        DiscreteOp opEvent2 = new DiscreteOpBuilder(mContext).setPackageName(
+                "test.package").build();
+        mDiscreteRegistry.recordDiscreteAccess(opEvent);
+        mDiscreteRegistry.recordDiscreteAccess(opEvent2);
+
+        List<DiscreteOp> discreteOps = mDiscreteRegistry.getAllDiscreteOps();
+        assertThat(discreteOps).contains(opEvent);
+        assertThat(discreteOps).contains(opEvent2);
+        assertThat(discreteOps.size()).isEqualTo(2);
+    }
+
+    @Test
+    public void clearDiscreteOps() {
+        DiscreteOp opEvent = new DiscreteOpBuilder(mContext).build();
+        mDiscreteRegistry.recordDiscreteAccess(opEvent);
+        flushDiscreteOpsToDatabase();
+        DiscreteOp opEvent2 = new DiscreteOpBuilder(mContext).setUid(12345).setPackageName(
+                "abc").build();
+        mDiscreteRegistry.recordDiscreteAccess(opEvent2);
+        mDiscreteRegistry.clearHistory();
+        assertThat(mDiscreteRegistry.getAllDiscreteOps()).isEmpty();
+    }
+
+    @Test
+    public void clearDiscreteOpsForPackage() {
+        DiscreteOp opEvent = new DiscreteOpBuilder(mContext).build();
+        mDiscreteRegistry.recordDiscreteAccess(opEvent);
+        flushDiscreteOpsToDatabase();
+        mDiscreteRegistry.recordDiscreteAccess(new DiscreteOpBuilder(mContext).build());
+        mDiscreteRegistry.clearHistory(Process.myUid(), mContext.getPackageName());
+
+        assertThat(mDiscreteRegistry.getAllDiscreteOps()).isEmpty();
+    }
+
+    @Test
+    public void offsetDiscreteOps() {
+        DiscreteOp opEvent = new DiscreteOpBuilder(mContext).build();
+        long event2AccessTime = System.currentTimeMillis() - 300000;
+        DiscreteOp opEvent2 = new DiscreteOpBuilder(mContext).setAccessTime(
+                event2AccessTime).build();
+        mDiscreteRegistry.recordDiscreteAccess(opEvent);
+        flushDiscreteOpsToDatabase();
+        mDiscreteRegistry.recordDiscreteAccess(opEvent2);
+        long offset = Duration.ofMinutes(2).toMillis();
+
+        mDiscreteRegistry.offsetHistory(offset);
+
+        // adjust input for assertion
+        DiscreteOp e1 = new DiscreteOpBuilder(opEvent)
+                .setAccessTime(opEvent.getAccessTime() - offset).build();
+        DiscreteOp e2 = new DiscreteOpBuilder(opEvent2)
+                .setAccessTime(event2AccessTime - offset).build();
+
+        List<DiscreteOp> results = mDiscreteRegistry.getAllDiscreteOps();
+        assertThat(results.size()).isEqualTo(2);
+        assertThat(results).contains(e1);
+        assertThat(results).contains(e2);
+    }
+
+    @Test
+    public void completeAttributionChain() {
+        long chainId = 100;
+        DiscreteOp event1 = new DiscreteOpBuilder(mContext)
+                .setChainId(chainId)
+                .setAttributionFlags(ATTRIBUTION_FLAG_RECEIVER | ATTRIBUTION_FLAG_TRUSTED)
+                .build();
+        DiscreteOp event2 = new DiscreteOpBuilder(mContext)
+                .setChainId(chainId)
+                .setAttributionFlags(ATTRIBUTION_FLAG_ACCESSOR | ATTRIBUTION_FLAG_TRUSTED)
+                .build();
+        List<DiscreteOp> events = new ArrayList<>();
+        events.add(event1);
+        events.add(event2);
+
+        LongSparseArray<DiscreteOpsSqlRegistry.AttributionChain> chains =
+                mDiscreteRegistry.createAttributionChains(events, new ArraySet<>());
+
+        assertThat(chains.size()).isGreaterThan(0);
+        DiscreteOpsSqlRegistry.AttributionChain chain = chains.get(chainId);
+        assertThat(chain).isNotNull();
+        assertThat(chain.isComplete()).isTrue();
+        assertThat(chain.getStart()).isEqualTo(event1);
+        assertThat(chain.getLastVisible()).isEqualTo(event2);
+    }
+
+    @Test
+    public void addToHistoricalOps() {
+        long beginTimeMillis = System.currentTimeMillis();
+        DiscreteOp event1 = new DiscreteOpBuilder(mContext)
+                .build();
+        DiscreteOp event2 = new DiscreteOpBuilder(mContext)
+                .setUid(123457)
+                .build();
+        mDiscreteRegistry.recordDiscreteAccess(event1);
+        flushDiscreteOpsToDatabase();
+        mDiscreteRegistry.recordDiscreteAccess(event2);
+
+        long endTimeMillis = System.currentTimeMillis() + 500;
+        AppOpsManager.HistoricalOps results = new AppOpsManager.HistoricalOps(beginTimeMillis,
+                endTimeMillis);
+
+        mDiscreteRegistry.addFilteredDiscreteOpsToHistoricalOps(results, beginTimeMillis,
+                endTimeMillis, 0, 0, null, null, null, 0, new ArraySet<>());
+        Log.i("Manjeet", "TEST read " + results);
+        assertWithMessage("results shouldn't be empty").that(results.isEmpty()).isFalse();
+    }
+
+    @Test
+    public void dump() {
+        DiscreteOp event1 = new DiscreteOpBuilder(mContext)
+                .setAccessTime(1732221340628L)
+                .setUid(12345)
+                .build();
+        DiscreteOp event2 = new DiscreteOpBuilder(mContext)
+                .setAccessTime(1732227340628L)
+                .setUid(123457)
+                .build();
+        mDiscreteRegistry.recordDiscreteAccess(event1);
+        flushDiscreteOpsToDatabase();
+        mDiscreteRegistry.recordDiscreteAccess(event2);
+    }
+
+    /** This clears in-memory cache and push records into the database. */
+    private void flushDiscreteOpsToDatabase() {
+        mDiscreteRegistry.writeAndClearOldAccessHistory();
+    }
+
+    /**
+     * Creates default op event for CAMERA app op with current time as access time
+     * and 1 minute duration
+     */
+    private static class DiscreteOpBuilder {
+        private int mUid;
+        private String mPackageName;
+        private String mAttributionTag;
+        private String mDeviceId;
+        private int mOpCode;
+        private int mOpFlags;
+        private int mAttributionFlags;
+        private int mUidState;
+        private long mChainId;
+        private long mAccessTime;
+        private long mDuration;
+
+        DiscreteOpBuilder(Context context) {
+            mUid = Process.myUid();
+            mPackageName = context.getPackageName();
+            mAttributionTag = null;
+            mDeviceId = String.valueOf(context.getDeviceId());
+            mOpCode = AppOpsManager.OP_CAMERA;
+            mOpFlags = AppOpsManager.OP_FLAG_SELF;
+            mAttributionFlags = ATTRIBUTION_FLAG_ACCESSOR;
+            mUidState = UID_STATE_FOREGROUND;
+            mChainId = AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE;
+            mAccessTime = System.currentTimeMillis();
+            mDuration = Duration.ofMinutes(1).toMillis();
+        }
+
+        DiscreteOpBuilder(DiscreteOp discreteOp) {
+            this.mUid = discreteOp.getUid();
+            this.mPackageName = discreteOp.getPackageName();
+            this.mAttributionTag = discreteOp.getAttributionTag();
+            this.mDeviceId = discreteOp.getDeviceId();
+            this.mOpCode = discreteOp.getOpCode();
+            this.mOpFlags = discreteOp.getOpFlags();
+            this.mAttributionFlags = discreteOp.getAttributionFlags();
+            this.mUidState = discreteOp.getUidState();
+            this.mChainId = discreteOp.getChainId();
+            this.mAccessTime = discreteOp.getAccessTime();
+            this.mDuration = discreteOp.getDuration();
+        }
+
+        public DiscreteOpBuilder setUid(int uid) {
+            this.mUid = uid;
+            return this;
+        }
+
+        public DiscreteOpBuilder setPackageName(String packageName) {
+            this.mPackageName = packageName;
+            return this;
+        }
+
+        public DiscreteOpBuilder setAttributionFlags(int attributionFlags) {
+            this.mAttributionFlags = attributionFlags;
+            return this;
+        }
+
+        public DiscreteOpBuilder setChainId(long chainId) {
+            this.mChainId = chainId;
+            return this;
+        }
+
+        public DiscreteOpBuilder setAccessTime(long accessTime) {
+            this.mAccessTime = accessTime;
+            return this;
+        }
+
+        public DiscreteOp build() {
+            return new DiscreteOp(mUid, mPackageName, mAttributionTag, mDeviceId, mOpCode, mOpFlags,
+                    mAttributionFlags, mUidState, mChainId, mAccessTime, mDuration);
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpPersistenceTest.java b/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpXmlPersistenceTest.java
similarity index 84%
rename from services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpPersistenceTest.java
rename to services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpXmlPersistenceTest.java
index 2ff0c62..ae973be 100644
--- a/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpPersistenceTest.java
+++ b/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpXmlPersistenceTest.java
@@ -47,9 +47,12 @@
 import java.io.File;
 import java.util.List;
 
+/**
+ * Test xml persistence implementation for discrete ops.
+ */
 @RunWith(AndroidJUnit4.class)
-public class DiscreteAppOpPersistenceTest {
-    private DiscreteRegistry mDiscreteRegistry;
+public class DiscreteAppOpXmlPersistenceTest {
+    private DiscreteOpsXmlRegistry mDiscreteRegistry;
     private final Object mLock = new Object();
     private File mMockDataDirectory;
     private final Context mContext =
@@ -61,13 +64,13 @@
     @Before
     public void setUp() {
         mMockDataDirectory = mContext.getDir("mock_data", Context.MODE_PRIVATE);
-        mDiscreteRegistry = new DiscreteRegistry(mLock, mMockDataDirectory);
+        mDiscreteRegistry = new DiscreteOpsXmlRegistry(mLock, mMockDataDirectory);
         mDiscreteRegistry.systemReady();
     }
 
     @After
     public void cleanUp() {
-        mDiscreteRegistry.writeAndClearAccessHistory();
+        mDiscreteRegistry.writeAndClearOldAccessHistory();
         FileUtils.deleteContents(mMockDataDirectory);
     }
 
@@ -87,14 +90,14 @@
 
         mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, null, opFlags,
                 uidState, accessTime, duration, attributionFlags, attributionChainId,
-                DiscreteRegistry.ACCESS_TYPE_FINISH_OP);
+                DiscreteOpsXmlRegistry.ACCESS_TYPE_FINISH_OP);
 
         // Verify in-memory object is correct
         fetchDiscreteOpsAndValidate(uid, packageName, op, deviceId, null, accessTime,
                 duration, uidState, opFlags, attributionFlags, attributionChainId);
 
         // Write to disk and clear the in-memory object
-        mDiscreteRegistry.writeAndClearAccessHistory();
+        mDiscreteRegistry.writeAndClearOldAccessHistory();
 
         // Verify the storage file is created and then verify its content is correct
         File[] files = FileUtils.listFilesOrEmpty(mMockDataDirectory);
@@ -119,12 +122,12 @@
 
         mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, null, opFlags,
                 uidState, accessTime, duration, attributionFlags, attributionChainId,
-                DiscreteRegistry.ACCESS_TYPE_START_OP);
+                DiscreteOpsXmlRegistry.ACCESS_TYPE_START_OP);
 
         fetchDiscreteOpsAndValidate(uid, packageName, op, deviceId, null, accessTime,
                 duration, uidState, opFlags, attributionFlags, attributionChainId);
 
-        mDiscreteRegistry.writeAndClearAccessHistory();
+        mDiscreteRegistry.writeAndClearOldAccessHistory();
 
         File[] files = FileUtils.listFilesOrEmpty(mMockDataDirectory);
         assertThat(files.length).isEqualTo(1);
@@ -136,30 +139,31 @@
             int expectedOp, String expectedDeviceId, String expectedAttrTag,
             long expectedAccessTime, long expectedAccessDuration, int expectedUidState,
             int expectedOpFlags, int expectedAttrFlags, int expectedAttrChainId) {
-        DiscreteRegistry.DiscreteOps discreteOps = mDiscreteRegistry.getAllDiscreteOps();
+        DiscreteOpsXmlRegistry.DiscreteOps discreteOps = mDiscreteRegistry.getAllDiscreteOps();
 
         assertThat(discreteOps.isEmpty()).isFalse();
         assertThat(discreteOps.mUids.size()).isEqualTo(1);
 
-        DiscreteRegistry.DiscreteUidOps discreteUidOps = discreteOps.mUids.get(expectedUid);
+        DiscreteOpsXmlRegistry.DiscreteUidOps discreteUidOps = discreteOps.mUids.get(expectedUid);
         assertThat(discreteUidOps.mPackages.size()).isEqualTo(1);
 
-        DiscreteRegistry.DiscretePackageOps discretePackageOps =
+        DiscreteOpsXmlRegistry.DiscretePackageOps discretePackageOps =
                 discreteUidOps.mPackages.get(expectedPackageName);
         assertThat(discretePackageOps.mPackageOps.size()).isEqualTo(1);
 
-        DiscreteRegistry.DiscreteOp discreteOp = discretePackageOps.mPackageOps.get(expectedOp);
+        DiscreteOpsXmlRegistry.DiscreteOp discreteOp =
+                discretePackageOps.mPackageOps.get(expectedOp);
         assertThat(discreteOp.mDeviceAttributedOps.size()).isEqualTo(1);
 
-        DiscreteRegistry.DiscreteDeviceOp discreteDeviceOp =
+        DiscreteOpsXmlRegistry.DiscreteDeviceOp discreteDeviceOp =
                 discreteOp.mDeviceAttributedOps.get(expectedDeviceId);
         assertThat(discreteDeviceOp.mAttributedOps.size()).isEqualTo(1);
 
-        List<DiscreteRegistry.DiscreteOpEvent> discreteOpEvents =
+        List<DiscreteOpsXmlRegistry.DiscreteOpEvent> discreteOpEvents =
                 discreteDeviceOp.mAttributedOps.get(expectedAttrTag);
         assertThat(discreteOpEvents.size()).isEqualTo(1);
 
-        DiscreteRegistry.DiscreteOpEvent discreteOpEvent = discreteOpEvents.get(0);
+        DiscreteOpsXmlRegistry.DiscreteOpEvent discreteOpEvent = discreteOpEvents.get(0);
         assertThat(discreteOpEvent.mNoteTime).isEqualTo(expectedAccessTime);
         assertThat(discreteOpEvent.mNoteDuration).isEqualTo(expectedAccessDuration);
         assertThat(discreteOpEvent.mUidState).isEqualTo(expectedUidState);
diff --git a/services/tests/servicestests/src/com/android/server/appop/DiscreteOpsMigrationAndRollbackTest.java b/services/tests/servicestests/src/com/android/server/appop/DiscreteOpsMigrationAndRollbackTest.java
new file mode 100644
index 0000000..21cc3ba
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/appop/DiscreteOpsMigrationAndRollbackTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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.appop;
+
+import static android.app.AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR;
+import static android.app.AppOpsManager.UID_STATE_FOREGROUND;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.AppOpsManager;
+import android.companion.virtual.VirtualDeviceManager;
+import android.content.Context;
+import android.os.FileUtils;
+import android.os.Process;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.time.Duration;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class DiscreteOpsMigrationAndRollbackTest {
+    private final Context mContext =
+            InstrumentationRegistry.getInstrumentation().getTargetContext();
+    private static final String DATABASE_NAME = "test_app_ops.db";
+    private static final int RECORD_COUNT = 500;
+    private final File mMockDataDirectory = mContext.getDir("mock_data", Context.MODE_PRIVATE);
+    final Object mLock = new Object();
+
+    @After
+    @Before
+    public void clean() {
+        mContext.deleteDatabase(DATABASE_NAME);
+        FileUtils.deleteContents(mMockDataDirectory);
+    }
+
+    @Test
+    public void migrateFromXmlToSqlite() {
+        // write records to xml registry
+        DiscreteOpsXmlRegistry xmlRegistry = new DiscreteOpsXmlRegistry(mLock, mMockDataDirectory);
+        xmlRegistry.systemReady();
+        for (int i = 1; i <= RECORD_COUNT; i++) {
+            DiscreteOpsSqlRegistry.DiscreteOp opEvent =
+                    new DiscreteOpBuilder(mContext)
+                            .setChainId(i)
+                            .setUid(10000 + i) // make all records unique
+                            .build();
+            xmlRegistry.recordDiscreteAccess(opEvent.getUid(), opEvent.getPackageName(),
+                    opEvent.getDeviceId(), opEvent.getOpCode(), opEvent.getAttributionTag(),
+                    opEvent.getOpFlags(), opEvent.getUidState(), opEvent.getAccessTime(),
+                    opEvent.getDuration(), opEvent.getAttributionFlags(),
+                    (int) opEvent.getChainId(), DiscreteOpsRegistry.ACCESS_TYPE_NOTE_OP);
+        }
+        xmlRegistry.writeAndClearOldAccessHistory();
+        assertThat(xmlRegistry.readLargestChainIdFromDiskLocked()).isEqualTo(RECORD_COUNT);
+        assertThat(xmlRegistry.getAllDiscreteOps().mUids.size()).isEqualTo(RECORD_COUNT);
+
+        // migration to sql registry
+        DiscreteOpsSqlRegistry sqlRegistry = new DiscreteOpsSqlRegistry(mContext,
+                mContext.getDatabasePath(DATABASE_NAME));
+        sqlRegistry.systemReady();
+        DiscreteOpsMigrationHelper.migrateDiscreteOpsToSqlite(xmlRegistry, sqlRegistry);
+        List<DiscreteOpsSqlRegistry.DiscreteOp> sqlOps = sqlRegistry.getAllDiscreteOps();
+
+        assertThat(xmlRegistry.getAllDiscreteOps().mUids).isEmpty();
+        assertThat(sqlOps.size()).isEqualTo(RECORD_COUNT);
+        assertThat(sqlRegistry.getLargestAttributionChainId()).isEqualTo(RECORD_COUNT);
+    }
+
+    @Test
+    public void migrateFromSqliteToXml() {
+        // write to sql registry
+        DiscreteOpsSqlRegistry sqlRegistry = new DiscreteOpsSqlRegistry(mContext,
+                mContext.getDatabasePath(DATABASE_NAME));
+        sqlRegistry.systemReady();
+        for (int i = 1; i <= RECORD_COUNT; i++) {
+            DiscreteOpsSqlRegistry.DiscreteOp opEvent =
+                    new DiscreteOpBuilder(mContext)
+                            .setChainId(i)
+                            .setUid(RECORD_COUNT + i) // make all records unique
+                            .build();
+            sqlRegistry.recordDiscreteAccess(opEvent.getUid(), opEvent.getPackageName(),
+                    opEvent.getDeviceId(), opEvent.getOpCode(), opEvent.getAttributionTag(),
+                    opEvent.getOpFlags(), opEvent.getUidState(), opEvent.getAccessTime(),
+                    opEvent.getDuration(), opEvent.getAttributionFlags(),
+                    (int) opEvent.getChainId(), DiscreteOpsRegistry.ACCESS_TYPE_NOTE_OP);
+        }
+        sqlRegistry.writeAndClearOldAccessHistory();
+        assertThat(sqlRegistry.getAllDiscreteOps().size()).isEqualTo(RECORD_COUNT);
+        assertThat(sqlRegistry.getLargestAttributionChainId()).isEqualTo(RECORD_COUNT);
+
+        // migration to xml registry
+        DiscreteOpsXmlRegistry xmlRegistry = new DiscreteOpsXmlRegistry(mLock, mMockDataDirectory);
+        xmlRegistry.systemReady();
+        DiscreteOpsMigrationHelper.migrateDiscreteOpsToXml(sqlRegistry, xmlRegistry);
+        DiscreteOpsXmlRegistry.DiscreteOps xmlOps = xmlRegistry.getAllDiscreteOps();
+
+        assertThat(sqlRegistry.getAllDiscreteOps()).isEmpty();
+        assertThat(xmlOps.mLargestChainId).isEqualTo(RECORD_COUNT);
+        assertThat(xmlOps.mUids.size()).isEqualTo(RECORD_COUNT);
+    }
+
+    private static class DiscreteOpBuilder {
+        private int mUid;
+        private String mPackageName;
+        private String mAttributionTag;
+        private String mDeviceId;
+        private int mOpCode;
+        private int mOpFlags;
+        private int mAttributionFlags;
+        private int mUidState;
+        private int mChainId;
+        private long mAccessTime;
+        private long mDuration;
+
+        DiscreteOpBuilder(Context context) {
+            mUid = Process.myUid();
+            mPackageName = context.getPackageName();
+            mAttributionTag = null;
+            mDeviceId = VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT;
+            mOpCode = AppOpsManager.OP_CAMERA;
+            mOpFlags = AppOpsManager.OP_FLAG_SELF;
+            mAttributionFlags = ATTRIBUTION_FLAG_ACCESSOR;
+            mUidState = UID_STATE_FOREGROUND;
+            mChainId = AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE;
+            mAccessTime = System.currentTimeMillis();
+            mDuration = Duration.ofMinutes(1).toMillis();
+        }
+
+        public DiscreteOpBuilder setUid(int uid) {
+            this.mUid = uid;
+            return this;
+        }
+
+        public DiscreteOpBuilder setChainId(int chainId) {
+            this.mChainId = chainId;
+            return this;
+        }
+
+        public DiscreteOpsSqlRegistry.DiscreteOp build() {
+            return new DiscreteOpsSqlRegistry.DiscreteOp(mUid, mPackageName, mAttributionTag,
+                    mDeviceId,
+                    mOpCode, mOpFlags, mAttributionFlags, mUidState, mChainId, mAccessTime,
+                    mDuration);
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index 32578a7..bdbb495 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -340,8 +340,7 @@
         LocalServices.removeServiceForTest(DisplayManagerInternal.class);
         LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock);
 
-        doNothing().when(mInputManagerInternalMock)
-                .setMousePointerAccelerationEnabled(anyBoolean(), anyInt());
+        doNothing().when(mInputManagerInternalMock).setMouseScalingEnabled(anyBoolean(), anyInt());
         doNothing().when(mInputManagerInternalMock).setPointerIconVisible(anyBoolean(), anyInt());
         LocalServices.removeServiceForTest(InputManagerInternal.class);
         LocalServices.addService(InputManagerInternal.class, mInputManagerInternalMock);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
index 0244164..4f55111 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
@@ -887,6 +887,21 @@
                 systemAudioModeRequest_fromAudioSystem);
     }
 
+    @Test
+    public void addAndStartAction_remove() throws Exception {
+        // utilize callback test to test if addAndStartAction(action, remove)
+        TestCallback callback = new TestCallback();
+
+        mHdmiCecLocalDeviceAudioSystem.setArcStatus(true);
+        mHdmiCecLocalDeviceAudioSystem.addAndStartAction(
+                new ArcTerminationActionFromAvr(mHdmiCecLocalDeviceAudioSystem, callback),
+                true);
+
+        mTestLooper.dispatchAll();
+        assertThat(mHdmiCecLocalDeviceAudioSystem.getActions(
+                ArcTerminationActionFromAvr.class).size()).isEqualTo(1);
+    }
+
     private static class TestCallback extends IHdmiControlCallback.Stub {
         private final ArrayList<Integer> mCallbackResult = new ArrayList<Integer>();
 
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt b/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt
index 72fa949..085ef53b 100644
--- a/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt
@@ -196,6 +196,8 @@
                 },
                 ActorState.INVALID_OVERLAYABLE_ACTOR_NAME withCases {
                     fun TestState.mockActor(actorUri: String) {
+                        namedActorsMap = mapOf(VALID_NAMESPACE to
+                                mapOf(VALID_ACTOR_NAME to VALID_ACTOR_PKG))
                         targetOverlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, actorUri)
                     }
                     failure("wrongScheme") {
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayReferenceMapperTests.kt b/services/tests/servicestests/src/com/android/server/om/OverlayReferenceMapperTests.kt
index 1352ade..ad6e467 100644
--- a/services/tests/servicestests/src/com/android/server/om/OverlayReferenceMapperTests.kt
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayReferenceMapperTests.kt
@@ -76,12 +76,10 @@
         val overlay1 = mockOverlay(1)
         mapper = mapper(
                 overlayToTargetToOverlayables = mapOf(
-                        overlay0.packageName to mapOf(
-                                target.packageName to target.overlayables.keys
-                        ),
-                        overlay1.packageName to mapOf(
-                                target.packageName to target.overlayables.keys
-                        )
+                        overlay0.packageName to android.util.Pair(target.packageName,
+                            target.overlayables.keys.first()),
+                        overlay1.packageName to android.util.Pair(target.packageName,
+                            target.overlayables.keys.first())
                 )
         )
         val existing = mapper.addInOrder(overlay0, overlay1) {
@@ -134,33 +132,6 @@
     }
 
     @Test
-    fun overlayWithMultipleTargets() {
-        val target0 = mockTarget(0)
-        val target1 = mockTarget(1)
-        val overlay = mockOverlay()
-        mapper = mapper(
-                overlayToTargetToOverlayables = mapOf(
-                        overlay.packageName to mapOf(
-                                target0.packageName to target0.overlayables.keys,
-                                target1.packageName to target1.overlayables.keys
-                        )
-                )
-        )
-        mapper.addInOrder(target0, target1, overlay) {
-            assertThat(it).containsExactly(ACTOR_PACKAGE_NAME)
-        }
-        assertMapping(ACTOR_PACKAGE_NAME to setOf(target0, target1, overlay))
-        mapper.remove(target0) {
-            assertThat(it).containsExactly(ACTOR_PACKAGE_NAME)
-        }
-        assertMapping(ACTOR_PACKAGE_NAME to setOf(target1, overlay))
-        mapper.remove(target1) {
-            assertThat(it).containsExactly(ACTOR_PACKAGE_NAME)
-        }
-        assertEmpty()
-    }
-
-    @Test
     fun overlayWithoutTarget() {
         val overlay = mockOverlay()
         mapper.addInOrder(overlay) {
@@ -174,6 +145,29 @@
         assertEmpty()
     }
 
+    @Test
+    fun targetWithNullOverlayable() {
+        val target = mockTarget()
+        val overlay = mockOverlay()
+        mapper = mapper(
+            overlayToTargetToOverlayables = mapOf(
+                overlay.packageName to android.util.Pair(target.packageName, null)
+            )
+        )
+        val existing = mapper.addInOrder(overlay) {
+            assertThat(it).isEmpty()
+        }
+        assertEmpty()
+        mapper.addInOrder(target, existing = existing) {
+            assertThat(it).containsExactly(ACTOR_PACKAGE_NAME)
+        }
+        assertMapping(ACTOR_PACKAGE_NAME to setOf(target))
+        mapper.remove(target) {
+            assertThat(it).containsExactly(ACTOR_PACKAGE_NAME)
+        }
+        assertEmpty()
+    }
+
     private fun OverlayReferenceMapper.addInOrder(
         vararg pkgs: AndroidPackage,
         existing: MutableMap<String, AndroidPackage> = mutableMapOf(),
@@ -219,17 +213,15 @@
         namedActors: Map<String, Map<String, String>> = Uri.parse(ACTOR_NAME).run {
             mapOf(authority!! to mapOf(pathSegments.first() to ACTOR_PACKAGE_NAME))
         },
-        overlayToTargetToOverlayables: Map<String, Map<String, Set<String>>> = mapOf(
-                mockOverlay().packageName to mapOf(
-                        mockTarget().run { packageName to overlayables.keys }
-                )
-        )
+        overlayToTargetToOverlayables: Map<String, android.util.Pair<String, String>> = mapOf(
+                mockOverlay().packageName to mockTarget().run { android.util.Pair(packageName!!,
+                    overlayables.keys.first()) })
     ) = OverlayReferenceMapper(deferRebuild, object : OverlayReferenceMapper.Provider {
         override fun getActorPkg(actor: String) =
                 OverlayActorEnforcer.getPackageNameForActor(actor, namedActors).first
 
         override fun getTargetToOverlayables(pkg: AndroidPackage) =
-                overlayToTargetToOverlayables[pkg.packageName] ?: emptyMap()
+                overlayToTargetToOverlayables[pkg.packageName]
     })
 
     private fun mockTarget(increment: Int = 0) = mockThrowOnUnmocked<AndroidPackage> {
diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
index 4e030d4..3ef360a 100644
--- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
@@ -112,6 +112,7 @@
 import java.io.BufferedReader;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.FileOutputStream;
 import java.io.FileReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
@@ -556,6 +557,12 @@
         }
 
         @Override
+        void injectFinishWrite(@NonNull ResilientAtomicFile file,
+                @NonNull FileOutputStream os) throws IOException {
+            file.finishWrite(os, false /* doFsVerity */);
+        }
+
+        @Override
         void wtf(String message, Throwable th) {
             // During tests, WTF is fatal.
             fail(message + "  exception: " + th + "\n" + Log.getStackTraceString(th));
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
index c01283a..776f05d 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
@@ -159,7 +159,7 @@
     /**
      * Test for the first launch path, no settings file available.
      */
-    public void FirstInitialize() {
+    public void testFirstInitialize() {
         assertResetTimes(START_TIME, START_TIME + INTERVAL);
     }
 
@@ -167,7 +167,7 @@
      * Test for {@link ShortcutService#getLastResetTimeLocked()} and
      * {@link ShortcutService#getNextResetTimeLocked()}.
      */
-    public void UpdateAndGetNextResetTimeLocked() {
+    public void testUpdateAndGetNextResetTimeLocked() {
         assertResetTimes(START_TIME, START_TIME + INTERVAL);
 
         // Advance clock.
@@ -196,7 +196,7 @@
     /**
      * Test for the restoration from saved file.
      */
-    public void InitializeFromSavedFile() {
+    public void testInitializeFromSavedFile() {
 
         mInjectedCurrentTimeMillis = START_TIME + 4 * INTERVAL + 50;
         assertResetTimes(START_TIME + 4 * INTERVAL, START_TIME + 5 * INTERVAL);
@@ -220,7 +220,7 @@
         // TODO Add various broken cases.
     }
 
-    public void LoadConfig() {
+    public void testLoadConfig() {
         mService.updateConfigurationLocked(
                 ConfigConstants.KEY_RESET_INTERVAL_SEC + "=123,"
                         + ConfigConstants.KEY_MAX_SHORTCUTS + "=4,"
@@ -261,22 +261,22 @@
     // === Test for app side APIs ===
 
     /** Test for {@link android.content.pm.ShortcutManager#getMaxShortcutCountForActivity()} */
-    public void GetMaxDynamicShortcutCount() {
+    public void testGetMaxDynamicShortcutCount() {
         assertEquals(MAX_SHORTCUTS, mManager.getMaxShortcutCountForActivity());
     }
 
     /** Test for {@link android.content.pm.ShortcutManager#getRemainingCallCount()} */
-    public void GetRemainingCallCount() {
+    public void testGetRemainingCallCount() {
         assertEquals(MAX_UPDATES_PER_INTERVAL, mManager.getRemainingCallCount());
     }
 
-    public void GetIconMaxDimensions() {
+    public void testGetIconMaxDimensions() {
         assertEquals(MAX_ICON_DIMENSION, mManager.getIconMaxWidth());
         assertEquals(MAX_ICON_DIMENSION, mManager.getIconMaxHeight());
     }
 
     /** Test for {@link android.content.pm.ShortcutManager#getRateLimitResetTime()} */
-    public void GetRateLimitResetTime() {
+    public void testGetRateLimitResetTime() {
         assertEquals(START_TIME + INTERVAL, mManager.getRateLimitResetTime());
 
         mInjectedCurrentTimeMillis = START_TIME + 4 * INTERVAL + 50;
@@ -284,7 +284,7 @@
         assertEquals(START_TIME + 5 * INTERVAL, mManager.getRateLimitResetTime());
     }
 
-    public void SetDynamicShortcuts() {
+    public void testSetDynamicShortcuts() {
         setCaller(CALLING_PACKAGE_1, USER_10);
 
         final Icon icon1 = Icon.createWithResource(getTestContext(), R.drawable.icon1);
@@ -354,7 +354,7 @@
         });
     }
 
-    public void AddDynamicShortcuts() {
+    public void testAddDynamicShortcuts() {
         setCaller(CALLING_PACKAGE_1, USER_10);
 
         final ShortcutInfo si1 = makeShortcut("shortcut1");
@@ -402,7 +402,7 @@
         });
     }
 
-    public void PushDynamicShortcut() {
+    public void disabled_testPushDynamicShortcut() {
         // Change the max number of shortcuts.
         mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=5,"
                 + ShortcutService.ConfigConstants.KEY_SAVE_DELAY_MILLIS + "=1");
@@ -544,7 +544,7 @@
                 eq(CALLING_PACKAGE_1), eq("s9"), eq(USER_10));
     }
 
-    public void PushDynamicShortcut_CallsToUsageStatsManagerAreThrottled()
+    public void disabled_testPushDynamicShortcut_CallsToUsageStatsManagerAreThrottled()
             throws InterruptedException {
         mService.updateConfigurationLocked(
                 ShortcutService.ConfigConstants.KEY_SAVE_DELAY_MILLIS + "=500");
@@ -576,6 +576,7 @@
         Mockito.reset(mMockUsageStatsManagerInternal);
         for (int i = 2; i <= 10; i++) {
             final ShortcutInfo si = makeShortcut("s" + i);
+            setCaller(CALLING_PACKAGE_2, USER_10);
             mManager.pushDynamicShortcut(si);
         }
         verify(mMockUsageStatsManagerInternal, times(0)).reportShortcutUsage(
@@ -595,7 +596,7 @@
                 eq(CALLING_PACKAGE_2), any(), eq(USER_10));
     }
 
-    public void UnlimitedCalls() {
+    public void testUnlimitedCalls() {
         setCaller(CALLING_PACKAGE_1, USER_10);
 
         final ShortcutInfo si1 = makeShortcut("shortcut1");
@@ -626,7 +627,7 @@
         assertEquals(3, mManager.getRemainingCallCount());
     }
 
-    public void PublishWithNoActivity() {
+    public void disbabledTestPublishWithNoActivity() {
         // If activity is not explicitly set, use the default one.
 
         mRunningUsers.put(USER_11, true);
@@ -732,7 +733,7 @@
         });
     }
 
-    public void PublishWithNoActivity_noMainActivityInPackage() {
+    public void disabled_testPublishWithNoActivity_noMainActivityInPackage() {
         mRunningUsers.put(USER_11, true);
 
         runWithCaller(CALLING_PACKAGE_2, USER_11, () -> {
@@ -751,7 +752,7 @@
         });
     }
 
-    public void DeleteDynamicShortcuts() {
+    public void testDeleteDynamicShortcuts() {
         final ShortcutInfo si1 = makeShortcut("shortcut1");
         final ShortcutInfo si2 = makeShortcut("shortcut2");
         final ShortcutInfo si3 = makeShortcut("shortcut3");
@@ -792,7 +793,7 @@
         assertEquals(2, mManager.getRemainingCallCount());
     }
 
-    public void DeleteAllDynamicShortcuts() {
+    public void testDeleteAllDynamicShortcuts() {
         final ShortcutInfo si1 = makeShortcut("shortcut1");
         final ShortcutInfo si2 = makeShortcut("shortcut2");
         final ShortcutInfo si3 = makeShortcut("shortcut3");
@@ -821,7 +822,7 @@
         assertEquals(1, mManager.getRemainingCallCount());
     }
 
-    public void Icons() throws IOException {
+    public void testIcons() throws IOException {
         final Icon res32x32 = Icon.createWithResource(getTestContext(), R.drawable.black_32x32);
         final Icon res64x64 = Icon.createWithResource(getTestContext(), R.drawable.black_64x64);
         final Icon res512x512 = Icon.createWithResource(getTestContext(), R.drawable.black_512x512);
@@ -1035,7 +1036,7 @@
 */
     }
 
-    public void CleanupDanglingBitmaps() throws Exception {
+    public void testCleanupDanglingBitmaps() throws Exception {
         assertBitmapDirectories(USER_10, EMPTY_STRINGS);
         assertBitmapDirectories(USER_11, EMPTY_STRINGS);
 
@@ -1204,7 +1205,7 @@
                         maxSize));
     }
 
-    public void ShrinkBitmap() {
+    public void testShrinkBitmap() {
         checkShrinkBitmap(32, 32, R.drawable.black_512x512, 32);
         checkShrinkBitmap(511, 511, R.drawable.black_512x512, 511);
         checkShrinkBitmap(512, 512, R.drawable.black_512x512, 512);
@@ -1227,7 +1228,7 @@
         return out.getFile();
     }
 
-    public void OpenIconFileForWrite() throws IOException {
+    public void testOpenIconFileForWrite() throws IOException {
         mInjectedCurrentTimeMillis = 1000;
 
         final File p10_1_1 = openIconFileForWriteAndGetPath(10, CALLING_PACKAGE_1);
@@ -1301,7 +1302,7 @@
         assertFalse(p11_1_3.getName().contains("_"));
     }
 
-    public void UpdateShortcuts() {
+    public void testUpdateShortcuts() {
         runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
             assertTrue(mManager.setDynamicShortcuts(list(
                     makeShortcut("s1"),
@@ -1432,7 +1433,7 @@
         });
     }
 
-    public void UpdateShortcuts_icons() {
+    public void testUpdateShortcuts_icons() {
         runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
             assertTrue(mManager.setDynamicShortcuts(list(
                     makeShortcut("s1")
@@ -1526,7 +1527,7 @@
         });
     }
 
-    public void ShortcutManagerGetShortcuts_shortcutTypes() {
+    public void testShortcutManagerGetShortcuts_shortcutTypes() {
 
         // Create 3 manifest and 3 dynamic shortcuts
         addManifestShortcutResource(
@@ -1617,7 +1618,7 @@
         assertShortcutIds(mManager.getShortcuts(ShortcutManager.FLAG_MATCH_CACHED), "s1", "s2");
     }
 
-    public void CachedShortcuts() {
+    public void testCachedShortcuts() {
         runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
             assertTrue(mManager.setDynamicShortcuts(list(makeShortcut("s1"),
                     makeLongLivedShortcut("s2"), makeLongLivedShortcut("s3"),
@@ -1701,7 +1702,7 @@
                 "s2");
     }
 
-    public void CachedShortcuts_accessShortcutsPermission() {
+    public void testCachedShortcuts_accessShortcutsPermission() {
         runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
             assertTrue(mManager.setDynamicShortcuts(list(makeShortcut("s1"),
                     makeLongLivedShortcut("s2"), makeLongLivedShortcut("s3"),
@@ -1743,7 +1744,7 @@
         assertShortcutIds(mManager.getShortcuts(ShortcutManager.FLAG_MATCH_CACHED), "s3");
     }
 
-    public void CachedShortcuts_canPassShortcutLimit() {
+    public void testCachedShortcuts_canPassShortcutLimit() {
         // Change the max number of shortcuts.
         mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=4");
 
@@ -1781,7 +1782,7 @@
 
     // === Test for launcher side APIs ===
 
-    public void GetShortcuts() {
+    public void testGetShortcuts() {
 
         // Set up shortcuts.
 
@@ -1998,7 +1999,7 @@
                 "s1", "s3");
     }
 
-    public void GetShortcuts_shortcutKinds() throws Exception {
+    public void testGetShortcuts_shortcutKinds() throws Exception {
         // Create 3 manifest and 3 dynamic shortcuts
         addManifestShortcutResource(
                 new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
@@ -2109,7 +2110,7 @@
         });
     }
 
-    public void GetShortcuts_resolveStrings() throws Exception {
+    public void testGetShortcuts_resolveStrings() throws Exception {
         runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
             ShortcutInfo si = new ShortcutInfo.Builder(mClientContext)
                     .setId("id")
@@ -2157,7 +2158,7 @@
         });
     }
 
-    public void GetShortcuts_personsFlag() {
+    public void testGetShortcuts_personsFlag() {
         ShortcutInfo s = new ShortcutInfo.Builder(mClientContext, "id")
                 .setShortLabel("label")
                 .setActivity(new ComponentName(mClientContext, ShortcutActivity2.class))
@@ -2205,7 +2206,7 @@
     }
 
     // TODO resource
-    public void GetShortcutInfo() {
+    public void testGetShortcutInfo() {
         // Create shortcuts.
         setCaller(CALLING_PACKAGE_1);
         final ShortcutInfo s1_1 = makeShortcut(
@@ -2280,7 +2281,7 @@
         assertEquals("ABC", findById(list, "s1").getTitle());
     }
 
-    public void PinShortcutAndGetPinnedShortcuts() {
+    public void testPinShortcutAndGetPinnedShortcuts() {
         runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
             final ShortcutInfo s1_1 = makeShortcutWithTimestamp("s1", 1000);
             final ShortcutInfo s1_2 = makeShortcutWithTimestamp("s2", 2000);
@@ -2361,7 +2362,7 @@
      * This is similar to the above test, except it used "disable" instead of "remove".  It also
      * does "enable".
      */
-    public void DisableAndEnableShortcuts() {
+    public void testDisableAndEnableShortcuts() {
         runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
             final ShortcutInfo s1_1 = makeShortcutWithTimestamp("s1", 1000);
             final ShortcutInfo s1_2 = makeShortcutWithTimestamp("s2", 2000);
@@ -2486,7 +2487,7 @@
         });
     }
 
-    public void DisableShortcuts_thenRepublish() {
+    public void testDisableShortcuts_thenRepublish() {
         runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
             assertTrue(mManager.setDynamicShortcuts(list(
                     makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
@@ -2556,7 +2557,7 @@
         });
     }
 
-    public void PinShortcutAndGetPinnedShortcuts_multi() {
+    public void disabled_testPinShortcutAndGetPinnedShortcuts_multi() {
         // Create some shortcuts.
         runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
             assertTrue(mManager.setDynamicShortcuts(list(
@@ -2832,7 +2833,7 @@
         });
     }
 
-    public void PinShortcutAndGetPinnedShortcuts_assistant() {
+    public void testPinShortcutAndGetPinnedShortcuts_assistant() {
         // Create some shortcuts.
         runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
             assertTrue(mManager.setDynamicShortcuts(list(
@@ -2888,7 +2889,7 @@
         });
     }
 
-    public void PinShortcutAndGetPinnedShortcuts_crossProfile_plusLaunch() {
+    public void disabled_testPinShortcutAndGetPinnedShortcuts_crossProfile_plusLaunch() {
         // Create some shortcuts.
         runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
             assertTrue(mManager.setDynamicShortcuts(list(
@@ -3477,7 +3478,7 @@
         });
     }
 
-    public void StartShortcut() {
+    public void testStartShortcut() {
         // Create some shortcuts.
         runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
             final ShortcutInfo s1_1 = makeShortcut(
@@ -3612,7 +3613,7 @@
         // TODO Check extra, etc
     }
 
-    public void LauncherCallback() throws Throwable {
+    public void testLauncherCallback() throws Throwable {
         // Disable throttling for this test.
         mService.updateConfigurationLocked(
                 ConfigConstants.KEY_MAX_UPDATES_PER_INTERVAL + "=99999999,"
@@ -3778,7 +3779,7 @@
                 .isEmpty();
     }
 
-    public void LauncherCallback_crossProfile() throws Throwable {
+    public void testLauncherCallback_crossProfile() throws Throwable {
         prepareCrossProfileDataSet();
 
         final Handler h = new Handler(Looper.getMainLooper());
@@ -3901,7 +3902,7 @@
 
     // === Test for persisting ===
 
-    public void SaveAndLoadUser_empty() {
+    public void testSaveAndLoadUser_empty() {
         assertTrue(mManager.setDynamicShortcuts(list()));
 
         Log.i(TAG, "Saved state");
@@ -3918,7 +3919,7 @@
     /**
      * Try save and load, also stop/start the user.
      */
-    public void SaveAndLoadUser() {
+    public void disabled_testSaveAndLoadUser() {
         // First, create some shortcuts and save.
         runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
             final Icon icon1 = Icon.createWithResource(getTestContext(), R.drawable.black_64x16);
@@ -4059,7 +4060,7 @@
         // TODO Check all other fields
     }
 
-    public void LoadCorruptedShortcuts() throws Exception {
+    public void testLoadCorruptedShortcuts() throws Exception {
         initService();
 
         addPackage("com.android.chrome", 0, 0);
@@ -4073,7 +4074,7 @@
         assertNull(ShortcutPackage.loadFromFile(mService, user, corruptedShortcutPackage, false));
     }
 
-    public void SaveCorruptAndLoadUser() throws Exception {
+    public void testSaveCorruptAndLoadUser() throws Exception {
         // First, create some shortcuts and save.
         runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
             final Icon icon1 = Icon.createWithResource(getTestContext(), R.drawable.black_64x16);
@@ -4229,7 +4230,7 @@
         // TODO Check all other fields
     }
 
-    public void CleanupPackage() {
+    public void testCleanupPackage() {
         runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
             assertTrue(mManager.setDynamicShortcuts(list(
                     makeShortcut("s0_1"))));
@@ -4506,7 +4507,7 @@
         mService.saveDirtyInfo();
     }
 
-    public void CleanupPackage_republishManifests() {
+    public void testCleanupPackage_republishManifests() {
         addManifestShortcutResource(
                 new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
                 R.xml.shortcut_2);
@@ -4574,7 +4575,7 @@
         });
     }
 
-    public void HandleGonePackage_crossProfile() {
+    public void testHandleGonePackage_crossProfile() {
         // Create some shortcuts.
         runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
             assertTrue(mManager.setDynamicShortcuts(list(
@@ -4846,7 +4847,7 @@
         assertEquals(expected, spi.canRestoreTo(mService, pi, true));
     }
 
-    public void CanRestoreTo() {
+    public void testCanRestoreTo() {
         addPackage(CALLING_PACKAGE_1, CALLING_UID_1, 10, "sig1");
         addPackage(CALLING_PACKAGE_2, CALLING_UID_2, 10, "sig1", "sig2");
         addPackage(CALLING_PACKAGE_3, CALLING_UID_3, 10, "sig1");
@@ -4909,7 +4910,7 @@
         checkCanRestoreTo(DISABLED_REASON_BACKUP_NOT_SUPPORTED, spi3, true, 10, true, "sig1");
     }
 
-    public void HandlePackageDelete() {
+    public void testHandlePackageDelete() {
         checkHandlePackageDeleteInner((userId, packageName) -> {
             uninstallPackage(userId, packageName);
             mService.mPackageMonitor.onReceive(getTestContext(),
@@ -4917,7 +4918,7 @@
         });
     }
 
-    public void HandlePackageDisable() {
+    public void testHandlePackageDisable() {
         checkHandlePackageDeleteInner((userId, packageName) -> {
             disablePackage(userId, packageName);
             mService.mPackageMonitor.onReceive(getTestContext(),
@@ -5049,7 +5050,7 @@
     }
 
     /** Almost ame as testHandlePackageDelete, except it doesn't uninstall packages. */
-    public void HandlePackageClearData() {
+    public void testHandlePackageClearData() {
         final Icon bmp32x32 = Icon.createWithBitmap(BitmapFactory.decodeResource(
                 getTestContext().getResources(), R.drawable.black_32x32));
         setCaller(CALLING_PACKAGE_1, USER_10);
@@ -5125,7 +5126,7 @@
         assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_3, USER_11));
     }
 
-    public void HandlePackageClearData_manifestRepublished() {
+    public void testHandlePackageClearData_manifestRepublished() {
 
         mRunningUsers.put(USER_11, true);
 
@@ -5167,7 +5168,7 @@
         });
     }
 
-    public void HandlePackageUpdate() throws Throwable {
+    public void testHandlePackageUpdate() throws Throwable {
         // Set up shortcuts and launchers.
 
         final Icon res32x32 = Icon.createWithResource(getTestContext(), R.drawable.black_32x32);
@@ -5341,7 +5342,7 @@
     /**
      * Test the case where an updated app has resource IDs changed.
      */
-    public void HandlePackageUpdate_resIdChanged() throws Exception {
+    public void testHandlePackageUpdate_resIdChanged() throws Exception {
         final Icon icon1 = Icon.createWithResource(getTestContext(), /* res ID */ 1000);
         final Icon icon2 = Icon.createWithResource(getTestContext(), /* res ID */ 1001);
 
@@ -5416,7 +5417,7 @@
         });
     }
 
-    public void HandlePackageUpdate_systemAppUpdate() {
+    public void testHandlePackageUpdate_systemAppUpdate() {
 
         // Package1 is a system app.  Package 2 is not a system app, so it's not scanned
         // in this test at all.
@@ -5522,7 +5523,7 @@
                 mService.getUserShortcutsLocked(USER_10).getLastAppScanOsFingerprint());
     }
 
-    public void HandlePackageChanged() {
+    public void testHandlePackageChanged() {
         final ComponentName ACTIVITY1 = new ComponentName(CALLING_PACKAGE_1, "act1");
         final ComponentName ACTIVITY2 = new ComponentName(CALLING_PACKAGE_1, "act2");
 
@@ -5652,7 +5653,7 @@
         });
     }
 
-    public void HandlePackageUpdate_activityNoLongerMain() throws Throwable {
+    public void testHandlePackageUpdate_activityNoLongerMain() throws Throwable {
         runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
             assertTrue(mManager.setDynamicShortcuts(list(
                     makeShortcutWithActivity("s1a",
@@ -5738,7 +5739,7 @@
      * - Unpinned dynamic shortcuts
      * - Bitmaps
      */
-    public void BackupAndRestore() {
+    public void testBackupAndRestore() {
 
         assertFileNotExists("user-0/shortcut_dump/restore-0-start.txt");
         assertFileNotExists("user-0/shortcut_dump/restore-1-payload.xml");
@@ -5759,7 +5760,7 @@
         checkBackupAndRestore_success(/*firstRestore=*/ true);
     }
 
-    public void BackupAndRestore_backupRestoreTwice() {
+    public void testBackupAndRestore_backupRestoreTwice() {
         prepareForBackupTest();
 
         checkBackupAndRestore_success(/*firstRestore=*/ true);
@@ -5775,7 +5776,7 @@
         checkBackupAndRestore_success(/*firstRestore=*/ false);
     }
 
-    public void BackupAndRestore_restoreToNewVersion() {
+    public void testBackupAndRestore_restoreToNewVersion() {
         prepareForBackupTest();
 
         addPackage(CALLING_PACKAGE_1, CALLING_UID_1, 2);
@@ -5784,7 +5785,7 @@
         checkBackupAndRestore_success(/*firstRestore=*/ true);
     }
 
-    public void BackupAndRestore_restoreToSuperSetSignatures() {
+    public void testBackupAndRestore_restoreToSuperSetSignatures() {
         prepareForBackupTest();
 
         // Change package signatures.
@@ -5981,7 +5982,7 @@
         });
     }
 
-    public void BackupAndRestore_publisherWrongSignature() {
+    public void testBackupAndRestore_publisherWrongSignature() {
         prepareForBackupTest();
 
         addPackage(CALLING_PACKAGE_1, CALLING_UID_1, 10, "sigx"); // different signature
@@ -5989,7 +5990,7 @@
         checkBackupAndRestore_publisherNotRestored(ShortcutInfo.DISABLED_REASON_SIGNATURE_MISMATCH);
     }
 
-    public void BackupAndRestore_publisherNoLongerBackupTarget() {
+    public void testBackupAndRestore_publisherNoLongerBackupTarget() {
         prepareForBackupTest();
 
         updatePackageInfo(CALLING_PACKAGE_1,
@@ -6118,7 +6119,7 @@
         });
     }
 
-    public void BackupAndRestore_launcherLowerVersion() {
+    public void testBackupAndRestore_launcherLowerVersion() {
         prepareForBackupTest();
 
         addPackage(LAUNCHER_1, LAUNCHER_UID_1, 0); // Lower version
@@ -6127,7 +6128,7 @@
         checkBackupAndRestore_success(/*firstRestore=*/ true);
     }
 
-    public void BackupAndRestore_launcherWrongSignature() {
+    public void testBackupAndRestore_launcherWrongSignature() {
         prepareForBackupTest();
 
         addPackage(LAUNCHER_1, LAUNCHER_UID_1, 10, "sigx"); // different signature
@@ -6135,7 +6136,7 @@
         checkBackupAndRestore_launcherNotRestored(true);
     }
 
-    public void BackupAndRestore_launcherNoLongerBackupTarget() {
+    public void testBackupAndRestore_launcherNoLongerBackupTarget() {
         prepareForBackupTest();
 
         updatePackageInfo(LAUNCHER_1,
@@ -6240,7 +6241,7 @@
         });
     }
 
-    public void BackupAndRestore_launcherAndPackageNoLongerBackupTarget() {
+    public void testBackupAndRestore_launcherAndPackageNoLongerBackupTarget() {
         prepareForBackupTest();
 
         updatePackageInfo(CALLING_PACKAGE_1,
@@ -6338,7 +6339,7 @@
         });
     }
 
-    public void BackupAndRestore_disabled() {
+    public void testBackupAndRestore_disabled() {
         prepareCrossProfileDataSet();
 
         // Before doing backup & restore, disable s1.
@@ -6403,7 +6404,7 @@
     }
 
 
-    public void BackupAndRestore_manifestRePublished() {
+    public void testBackupAndRestore_manifestRePublished() {
         // Publish two manifest shortcuts.
         addManifestShortcutResource(
                 new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
@@ -6494,7 +6495,7 @@
      * logcat.
      * - if it has allowBackup=false, we don't touch any of the existing shortcuts.
      */
-    public void BackupAndRestore_appAlreadyInstalledWhenRestored() {
+    public void testBackupAndRestore_appAlreadyInstalledWhenRestored() {
         // Pre-backup.  Same as testBackupAndRestore_manifestRePublished().
 
         // Publish two manifest shortcuts.
@@ -6619,7 +6620,7 @@
     /**
      * Test for restoring the pre-P backup format.
      */
-    public void BackupAndRestore_api27format() throws Exception {
+    public void testBackupAndRestore_api27format() throws Exception {
         final byte[] payload = readTestAsset("shortcut/shortcut_api27_backup.xml").getBytes();
 
         addPackage(CALLING_PACKAGE_1, CALLING_UID_1, 10, "22222");
@@ -6657,7 +6658,7 @@
 
     }
 
-    public void SaveAndLoad_crossProfile() {
+    public void testSaveAndLoad_crossProfile() {
         prepareCrossProfileDataSet();
 
         dumpsysOnLogcat("Before save & load");
@@ -6860,7 +6861,7 @@
                         .getPackageUserId());
     }
 
-    public void OnApplicationActive_permission() {
+    public void testOnApplicationActive_permission() {
         assertExpectException(SecurityException.class, "Missing permission", () ->
                 mManager.onApplicationActive(CALLING_PACKAGE_1, USER_10));
 
@@ -6869,7 +6870,7 @@
         mManager.onApplicationActive(CALLING_PACKAGE_1, USER_10);
     }
 
-    public void GetShareTargets_permission() {
+    public void testGetShareTargets_permission() {
         addPackage(CHOOSER_ACTIVITY_PACKAGE, CHOOSER_ACTIVITY_UID, 10, "sig1");
         mInjectedChooserActivity =
                 ComponentName.createRelative(CHOOSER_ACTIVITY_PACKAGE, ".ChooserActivity");
@@ -6888,7 +6889,7 @@
         });
     }
 
-    public void HasShareTargets_permission() {
+    public void testHasShareTargets_permission() {
         assertExpectException(SecurityException.class, "Missing permission", () ->
                 mManager.hasShareTargets(CALLING_PACKAGE_1));
 
@@ -6897,7 +6898,7 @@
         mManager.hasShareTargets(CALLING_PACKAGE_1);
     }
 
-    public void isSharingShortcut_permission() throws IntentFilter.MalformedMimeTypeException {
+    public void testisSharingShortcut_permission() throws IntentFilter.MalformedMimeTypeException {
         setCaller(LAUNCHER_1, USER_10);
 
         IntentFilter filter_any = new IntentFilter();
@@ -6912,18 +6913,18 @@
         mManager.hasShareTargets(CALLING_PACKAGE_1);
     }
 
-    public void Dumpsys_crossProfile() {
+    public void testDumpsys_crossProfile() {
         prepareCrossProfileDataSet();
         dumpsysOnLogcat("test1", /* force= */ true);
     }
 
-    public void Dumpsys_withIcons() throws IOException {
-        Icons();
+    public void testDumpsys_withIcons() throws IOException {
+        testIcons();
         // Dump after having some icons.
         dumpsysOnLogcat("test1", /* force= */ true);
     }
 
-    public void ManifestShortcut_publishOnUnlockUser() {
+    public void testManifestShortcut_publishOnUnlockUser() {
         addManifestShortcutResource(
                 new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
                 R.xml.shortcut_1);
@@ -7137,7 +7138,7 @@
         assertNull(mService.getPackageShortcutForTest(LAUNCHER_1, USER_10));
     }
 
-    public void ManifestShortcut_publishOnBroadcast() {
+    public void testManifestShortcut_publishOnBroadcast() {
         // First, no packages are installed.
         uninstallPackage(USER_10, CALLING_PACKAGE_1);
         uninstallPackage(USER_10, CALLING_PACKAGE_2);
@@ -7393,7 +7394,7 @@
         });
     }
 
-    public void ManifestShortcuts_missingMandatoryFields() {
+    public void testManifestShortcuts_missingMandatoryFields() {
         // Start with no apps installed.
         uninstallPackage(USER_10, CALLING_PACKAGE_1);
         uninstallPackage(USER_10, CALLING_PACKAGE_2);
@@ -7462,7 +7463,7 @@
         });
     }
 
-    public void ManifestShortcuts_intentDefinitions() {
+    public void testManifestShortcuts_intentDefinitions() {
         addManifestShortcutResource(
                 new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
                 R.xml.shortcut_error_4);
@@ -7604,7 +7605,7 @@
         });
     }
 
-    public void ManifestShortcuts_checkAllFields() {
+    public void testManifestShortcuts_checkAllFields() {
         mService.handleUnlockUser(USER_10);
 
         // Package 1 updated, which has one valid manifest shortcut and one invalid.
@@ -7709,7 +7710,7 @@
         });
     }
 
-    public void ManifestShortcuts_localeChange() throws InterruptedException {
+    public void testManifestShortcuts_localeChange() throws InterruptedException {
         mService.handleUnlockUser(USER_10);
 
         // Package 1 updated, which has one valid manifest shortcut and one invalid.
@@ -7813,7 +7814,7 @@
         });
     }
 
-    public void ManifestShortcuts_updateAndDisabled_notPinned() {
+    public void testManifestShortcuts_updateAndDisabled_notPinned() {
         mService.handleUnlockUser(USER_10);
 
         // First, just publish a manifest shortcut.
@@ -7853,7 +7854,7 @@
         });
     }
 
-    public void ManifestShortcuts_updateAndDisabled_pinned() {
+    public void testManifestShortcuts_updateAndDisabled_pinned() {
         mService.handleUnlockUser(USER_10);
 
         // First, just publish a manifest shortcut.
@@ -7909,7 +7910,7 @@
         });
     }
 
-    public void ManifestShortcuts_duplicateInSingleActivity() {
+    public void testManifestShortcuts_duplicateInSingleActivity() {
         mService.handleUnlockUser(USER_10);
 
         // The XML has two shortcuts with the same ID.
@@ -7934,7 +7935,7 @@
         });
     }
 
-    public void ManifestShortcuts_duplicateInTwoActivities() {
+    public void testManifestShortcuts_duplicateInTwoActivities() {
         mService.handleUnlockUser(USER_10);
 
         // ShortcutActivity has shortcut ms1
@@ -7986,7 +7987,7 @@
     /**
      * Manifest shortcuts cannot override shortcuts that were published via the APIs.
      */
-    public void ManifestShortcuts_cannotOverrideNonManifest() {
+    public void testManifestShortcuts_cannotOverrideNonManifest() {
         mService.handleUnlockUser(USER_10);
 
         // Create a non-pinned dynamic shortcut and a non-dynamic pinned shortcut.
@@ -8059,7 +8060,7 @@
     /**
      * Make sure the APIs won't work on manifest shortcuts.
      */
-    public void ManifestShortcuts_immutable() {
+    public void testManifestShortcuts_immutable() {
         mService.handleUnlockUser(USER_10);
 
         // Create a non-pinned manifest shortcut, a pinned shortcut that was originally
@@ -8152,7 +8153,7 @@
     /**
      * Make sure the APIs won't work on manifest shortcuts.
      */
-    public void ManifestShortcuts_tooMany() {
+    public void testManifestShortcuts_tooMany() {
         // Change the max number of shortcuts.
         mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3");
 
@@ -8171,7 +8172,7 @@
         });
     }
 
-    public void MaxShortcutCount_set() {
+    public void testMaxShortcutCount_set() {
         // Change the max number of shortcuts.
         mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3");
 
@@ -8252,7 +8253,7 @@
         });
     }
 
-    public void MaxShortcutCount_add() {
+    public void testMaxShortcutCount_add() {
         // Change the max number of shortcuts.
         mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3");
 
@@ -8379,7 +8380,7 @@
         });
     }
 
-    public void MaxShortcutCount_update() {
+    public void testMaxShortcutCount_update() {
         // Change the max number of shortcuts.
         mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3");
 
@@ -8470,7 +8471,7 @@
         });
     }
 
-    public void ShortcutsPushedOutByManifest() {
+    public void testShortcutsPushedOutByManifest() {
         // Change the max number of shortcuts.
         mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3");
 
@@ -8578,7 +8579,7 @@
         });
     }
 
-    public void ReturnedByServer() {
+    public void disabled_testReturnedByServer() {
         // Package 1 updated, with manifest shortcuts.
         addManifestShortcutResource(
                 new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
@@ -8624,7 +8625,7 @@
         });
     }
 
-    public void IsForegroundDefaultLauncher_true() {
+    public void testIsForegroundDefaultLauncher_true() {
         // random uid in the USER_10 range.
         final int uid = 1000024;
 
@@ -8635,7 +8636,7 @@
     }
 
 
-    public void IsForegroundDefaultLauncher_defaultButNotForeground() {
+    public void testIsForegroundDefaultLauncher_defaultButNotForeground() {
         // random uid in the USER_10 range.
         final int uid = 1000024;
 
@@ -8645,7 +8646,7 @@
         assertFalse(mInternal.isForegroundDefaultLauncher("default", uid));
     }
 
-    public void IsForegroundDefaultLauncher_foregroundButNotDefault() {
+    public void testIsForegroundDefaultLauncher_foregroundButNotDefault() {
         // random uid in the USER_10 range.
         final int uid = 1000024;
 
@@ -8655,7 +8656,7 @@
         assertFalse(mInternal.isForegroundDefaultLauncher("another", uid));
     }
 
-    public void ParseShareTargetsFromManifest() {
+    public void testParseShareTargetsFromManifest() {
         // These values must exactly match the content of shortcuts_share_targets.xml resource
         List<ShareTargetInfo> expectedValues = new ArrayList<>();
         expectedValues.add(new ShareTargetInfo(
@@ -8707,7 +8708,7 @@
         }
     }
 
-    public void ShareTargetInfo_saveToXml() throws IOException, XmlPullParserException {
+    public void testShareTargetInfo_saveToXml() throws IOException, XmlPullParserException {
         List<ShareTargetInfo> expectedValues = new ArrayList<>();
         expectedValues.add(new ShareTargetInfo(
                 new ShareTargetInfo.TargetData[]{new ShareTargetInfo.TargetData(
@@ -8773,7 +8774,7 @@
         }
     }
 
-    public void IsSharingShortcut() throws IntentFilter.MalformedMimeTypeException {
+    public void testIsSharingShortcut() throws IntentFilter.MalformedMimeTypeException {
         addManifestShortcutResource(
                 new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
                 R.xml.shortcut_share_targets);
@@ -8823,7 +8824,7 @@
                 filter_any));
     }
 
-    public void IsSharingShortcut_PinnedAndCachedOnlyShortcuts()
+    public void testIsSharingShortcut_PinnedAndCachedOnlyShortcuts()
             throws IntentFilter.MalformedMimeTypeException {
         addManifestShortcutResource(
                 new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
@@ -8880,7 +8881,7 @@
                 filter_any));
     }
 
-    public void AddingShortcuts_ExcludesHiddenFromLauncherShortcuts() {
+    public void testAddingShortcuts_ExcludesHiddenFromLauncherShortcuts() {
         final ShortcutInfo s1 = makeShortcutExcludedFromLauncher("s1");
         final ShortcutInfo s2 = makeShortcutExcludedFromLauncher("s2");
         final ShortcutInfo s3 = makeShortcutExcludedFromLauncher("s3");
@@ -8901,7 +8902,7 @@
         });
     }
 
-    public void UpdateShortcuts_ExcludesHiddenFromLauncherShortcuts() {
+    public void testUpdateShortcuts_ExcludesHiddenFromLauncherShortcuts() {
         final ShortcutInfo s1 = makeShortcut("s1");
         final ShortcutInfo s2 = makeShortcut("s2");
         final ShortcutInfo s3 = makeShortcut("s3");
@@ -8914,7 +8915,7 @@
         });
     }
 
-    public void PinHiddenShortcuts_ThrowsException() {
+    public void testPinHiddenShortcuts_ThrowsException() {
         runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
             assertThrown(IllegalArgumentException.class, () -> {
                 mManager.requestPinShortcut(makeShortcutExcludedFromLauncher("s1"), null);
diff --git a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java
index 2ed71ce..952d8fa 100644
--- a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java
@@ -118,7 +118,6 @@
     private class ThermalHalFake extends ThermalHalWrapper {
         private static final int INIT_STATUS = Temperature.THROTTLING_NONE;
         private List<Temperature> mTemperatureList = new ArrayList<>();
-        private List<Temperature> mOverrideTemperatures = null;
         private List<CoolingDevice> mCoolingDeviceList = new ArrayList<>();
         private List<TemperatureThreshold> mTemperatureThresholdList = initializeThresholds();
 
@@ -132,6 +131,9 @@
                 INIT_STATUS);
         private CoolingDevice mCpu = new CoolingDevice(40, CoolingDevice.TYPE_BATTERY, "cpu");
         private CoolingDevice mGpu = new CoolingDevice(43, CoolingDevice.TYPE_BATTERY, "gpu");
+        private Map<Integer, Float> mForecastSkinTemperatures = null;
+        private int mForecastSkinTemperaturesCalled = 0;
+        private boolean mForecastSkinTemperaturesError = false;
 
         private List<TemperatureThreshold> initializeThresholds() {
             ArrayList<TemperatureThreshold> thresholds = new ArrayList<>();
@@ -173,12 +175,17 @@
             mCoolingDeviceList.add(mGpu);
         }
 
-        void setOverrideTemperatures(List<Temperature> temperatures) {
-            mOverrideTemperatures = temperatures;
+        void enableForecastSkinTemperature() {
+            mForecastSkinTemperatures = Map.of(0, 22.0f, 10, 25.0f, 20, 28.0f,
+                    30, 31.0f, 40, 34.0f, 50, 37.0f, 60, 40.0f);
         }
 
-        void resetOverrideTemperatures() {
-            mOverrideTemperatures = null;
+        void disableForecastSkinTemperature() {
+            mForecastSkinTemperatures = null;
+        }
+
+        void failForecastSkinTemperature() {
+            mForecastSkinTemperaturesError = true;
         }
 
         @Override
@@ -219,6 +226,18 @@
         }
 
         @Override
+        protected float forecastSkinTemperature(int forecastSeconds) {
+            mForecastSkinTemperaturesCalled++;
+            if (mForecastSkinTemperaturesError) {
+                throw new RuntimeException();
+            }
+            if (mForecastSkinTemperatures == null) {
+                throw new UnsupportedOperationException();
+            }
+            return mForecastSkinTemperatures.get(forecastSeconds);
+        }
+
+        @Override
         protected boolean connectToHal() {
             return true;
         }
@@ -388,7 +407,7 @@
         Thread.sleep(CALLBACK_TIMEOUT_MILLI_SEC);
         resetListenerMock();
         int status = Temperature.THROTTLING_SEVERE;
-        mFakeHal.setOverrideTemperatures(new ArrayList<>());
+        mFakeHal.mTemperatureList = new ArrayList<>();
 
         // Should not notify on non-skin type
         Temperature newBattery = new Temperature(37, Temperature.TYPE_BATTERY, "batt", status);
@@ -518,6 +537,99 @@
     }
 
     @Test
+    @EnableFlags({Flags.FLAG_ALLOW_THERMAL_HAL_SKIN_FORECAST})
+    public void testGetThermalHeadroom_halForecast() throws RemoteException {
+        mFakeHal.mForecastSkinTemperaturesCalled = 0;
+        mFakeHal.enableForecastSkinTemperature();
+        mService = new ThermalManagerService(mContext, mFakeHal);
+        mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);
+        assertTrue(mService.mIsHalSkinForecastSupported.get());
+        assertEquals(1, mFakeHal.mForecastSkinTemperaturesCalled);
+        mFakeHal.mForecastSkinTemperaturesCalled = 0;
+
+        assertEquals(1.0f, mService.mService.getThermalHeadroom(60), 0.01f);
+        assertEquals(0.9f, mService.mService.getThermalHeadroom(50), 0.01f);
+        assertEquals(0.8f, mService.mService.getThermalHeadroom(40), 0.01f);
+        assertEquals(0.7f, mService.mService.getThermalHeadroom(30), 0.01f);
+        assertEquals(0.6f, mService.mService.getThermalHeadroom(20), 0.01f);
+        assertEquals(0.5f, mService.mService.getThermalHeadroom(10), 0.01f);
+        assertEquals(0.4f, mService.mService.getThermalHeadroom(0), 0.01f);
+        assertEquals(7, mFakeHal.mForecastSkinTemperaturesCalled);
+    }
+
+    @Test
+    @EnableFlags({Flags.FLAG_ALLOW_THERMAL_HAL_SKIN_FORECAST})
+    public void testGetThermalHeadroom_halForecast_disabledOnMultiThresholds()
+            throws RemoteException {
+        mFakeHal.mForecastSkinTemperaturesCalled = 0;
+        List<TemperatureThreshold> thresholds = mFakeHal.initializeThresholds();
+        TemperatureThreshold skinThreshold = new TemperatureThreshold();
+        skinThreshold.type = Temperature.TYPE_SKIN;
+        skinThreshold.name = "skin2";
+        skinThreshold.hotThrottlingThresholds = new float[7 /*ThrottlingSeverity#len*/];
+        skinThreshold.coldThrottlingThresholds = new float[7 /*ThrottlingSeverity#len*/];
+        for (int i = 0; i < skinThreshold.hotThrottlingThresholds.length; ++i) {
+            // Sets NONE to 45.0f, SEVERE to 60.0f, and SHUTDOWN to 75.0f
+            skinThreshold.hotThrottlingThresholds[i] = 45.0f + 5.0f * i;
+        }
+        thresholds.add(skinThreshold);
+        mFakeHal.mTemperatureThresholdList = thresholds;
+        mFakeHal.enableForecastSkinTemperature();
+        mService = new ThermalManagerService(mContext, mFakeHal);
+        mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);
+        assertFalse("HAL skin forecast should be disabled on multiple SKIN thresholds",
+                mService.mIsHalSkinForecastSupported.get());
+        mService.mService.getThermalHeadroom(10);
+        assertEquals(0, mFakeHal.mForecastSkinTemperaturesCalled);
+    }
+
+    @Test
+    @EnableFlags({Flags.FLAG_ALLOW_THERMAL_HAL_SKIN_FORECAST,
+            Flags.FLAG_ALLOW_THERMAL_THRESHOLDS_CALLBACK})
+    public void testGetThermalHeadroom_halForecast_disabledOnMultiThresholdsCallback()
+            throws RemoteException {
+        mFakeHal.mForecastSkinTemperaturesCalled = 0;
+        mFakeHal.enableForecastSkinTemperature();
+        mService = new ThermalManagerService(mContext, mFakeHal);
+        mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);
+        assertTrue(mService.mIsHalSkinForecastSupported.get());
+        assertEquals(1, mFakeHal.mForecastSkinTemperaturesCalled);
+        mFakeHal.mForecastSkinTemperaturesCalled = 0;
+
+        TemperatureThreshold newThreshold = new TemperatureThreshold();
+        newThreshold.name = "skin2";
+        newThreshold.type = Temperature.TYPE_SKIN;
+        newThreshold.hotThrottlingThresholds = new float[]{
+                Float.NaN, 43.0f, 46.0f, 49.0f, Float.NaN, Float.NaN, Float.NaN
+        };
+        mFakeHal.mCallback.onThresholdChanged(newThreshold);
+        mService.mService.getThermalHeadroom(10);
+        assertEquals(0, mFakeHal.mForecastSkinTemperaturesCalled);
+    }
+
+    @Test
+    @EnableFlags({Flags.FLAG_ALLOW_THERMAL_HAL_SKIN_FORECAST})
+    public void testGetThermalHeadroom_halForecast_errorOnHal() throws RemoteException {
+        mFakeHal.mForecastSkinTemperaturesCalled = 0;
+        mFakeHal.enableForecastSkinTemperature();
+        mService = new ThermalManagerService(mContext, mFakeHal);
+        mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);
+        assertTrue(mService.mIsHalSkinForecastSupported.get());
+        assertEquals(1, mFakeHal.mForecastSkinTemperaturesCalled);
+        mFakeHal.mForecastSkinTemperaturesCalled = 0;
+
+        mFakeHal.disableForecastSkinTemperature();
+        assertTrue(Float.isNaN(mService.mService.getThermalHeadroom(10)));
+        assertEquals(1, mFakeHal.mForecastSkinTemperaturesCalled);
+        mFakeHal.enableForecastSkinTemperature();
+        assertFalse(Float.isNaN(mService.mService.getThermalHeadroom(10)));
+        assertEquals(2, mFakeHal.mForecastSkinTemperaturesCalled);
+        mFakeHal.failForecastSkinTemperature();
+        assertTrue(Float.isNaN(mService.mService.getThermalHeadroom(10)));
+        assertEquals(3, mFakeHal.mForecastSkinTemperaturesCalled);
+    }
+
+    @Test
     @EnableFlags({Flags.FLAG_ALLOW_THERMAL_THRESHOLDS_CALLBACK,
             Flags.FLAG_ALLOW_THERMAL_HEADROOM_THRESHOLDS})
     public void testTemperatureWatcherUpdateSevereThresholds() throws Exception {
diff --git a/services/tests/timetests/src/com/android/server/timezonedetector/ConfigInternalForTests.java b/services/tests/timetests/src/com/android/server/timezonedetector/ConfigInternalForTests.java
new file mode 100644
index 0000000..47e3dc8
--- /dev/null
+++ b/services/tests/timetests/src/com/android/server/timezonedetector/ConfigInternalForTests.java
@@ -0,0 +1,108 @@
+/*
+ * 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.server.timezonedetector;
+
+import android.annotation.UserIdInt;
+
+public final class ConfigInternalForTests {
+
+    static final @UserIdInt int USER_ID = 9876;
+
+    static final ConfigurationInternal CONFIG_USER_RESTRICTED_AUTO_DISABLED =
+            new ConfigurationInternal.Builder()
+                    .setUserId(USER_ID)
+                    .setTelephonyDetectionFeatureSupported(true)
+                    .setGeoDetectionFeatureSupported(true)
+                    .setTelephonyFallbackSupported(false)
+                    .setGeoDetectionRunInBackgroundEnabled(false)
+                    .setEnhancedMetricsCollectionEnabled(false)
+                    .setUserConfigAllowed(false)
+                    .setAutoDetectionEnabledSetting(false)
+                    .setLocationEnabledSetting(true)
+                    .setGeoDetectionEnabledSetting(false)
+                    .build();
+
+    static final ConfigurationInternal CONFIG_USER_RESTRICTED_AUTO_ENABLED =
+            new ConfigurationInternal.Builder()
+                    .setUserId(USER_ID)
+                    .setTelephonyDetectionFeatureSupported(true)
+                    .setGeoDetectionFeatureSupported(true)
+                    .setTelephonyFallbackSupported(false)
+                    .setGeoDetectionRunInBackgroundEnabled(false)
+                    .setEnhancedMetricsCollectionEnabled(false)
+                    .setUserConfigAllowed(false)
+                    .setAutoDetectionEnabledSetting(true)
+                    .setLocationEnabledSetting(true)
+                    .setGeoDetectionEnabledSetting(true)
+                    .build();
+
+    static final ConfigurationInternal CONFIG_AUTO_DETECT_NOT_SUPPORTED =
+            new ConfigurationInternal.Builder()
+                    .setUserId(USER_ID)
+                    .setTelephonyDetectionFeatureSupported(false)
+                    .setGeoDetectionFeatureSupported(false)
+                    .setTelephonyFallbackSupported(false)
+                    .setGeoDetectionRunInBackgroundEnabled(false)
+                    .setEnhancedMetricsCollectionEnabled(false)
+                    .setUserConfigAllowed(true)
+                    .setAutoDetectionEnabledSetting(false)
+                    .setLocationEnabledSetting(true)
+                    .setGeoDetectionEnabledSetting(false)
+                    .build();
+
+    static final ConfigurationInternal CONFIG_AUTO_DISABLED_GEO_DISABLED =
+            new ConfigurationInternal.Builder()
+                    .setUserId(USER_ID)
+                    .setTelephonyDetectionFeatureSupported(true)
+                    .setGeoDetectionFeatureSupported(true)
+                    .setTelephonyFallbackSupported(false)
+                    .setGeoDetectionRunInBackgroundEnabled(false)
+                    .setEnhancedMetricsCollectionEnabled(false)
+                    .setUserConfigAllowed(true)
+                    .setAutoDetectionEnabledSetting(false)
+                    .setLocationEnabledSetting(true)
+                    .setGeoDetectionEnabledSetting(false)
+                    .build();
+
+    static final ConfigurationInternal CONFIG_AUTO_ENABLED_GEO_DISABLED =
+            new ConfigurationInternal.Builder()
+                    .setUserId(USER_ID)
+                    .setTelephonyDetectionFeatureSupported(true)
+                    .setGeoDetectionFeatureSupported(true)
+                    .setTelephonyFallbackSupported(false)
+                    .setGeoDetectionRunInBackgroundEnabled(false)
+                    .setEnhancedMetricsCollectionEnabled(false)
+                    .setUserConfigAllowed(true)
+                    .setAutoDetectionEnabledSetting(true)
+                    .setLocationEnabledSetting(true)
+                    .setGeoDetectionEnabledSetting(false)
+                    .build();
+
+    static final ConfigurationInternal CONFIG_AUTO_ENABLED_GEO_ENABLED =
+            new ConfigurationInternal.Builder()
+                    .setUserId(USER_ID)
+                    .setTelephonyDetectionFeatureSupported(true)
+                    .setGeoDetectionFeatureSupported(true)
+                    .setTelephonyFallbackSupported(false)
+                    .setGeoDetectionRunInBackgroundEnabled(false)
+                    .setEnhancedMetricsCollectionEnabled(false)
+                    .setUserConfigAllowed(true)
+                    .setAutoDetectionEnabledSetting(true)
+                    .setLocationEnabledSetting(true)
+                    .setGeoDetectionEnabledSetting(true)
+                    .build();
+}
diff --git a/services/tests/timetests/src/com/android/server/timezonedetector/FakeServiceConfigAccessor.java b/services/tests/timetests/src/com/android/server/timezonedetector/FakeServiceConfigAccessor.java
index fc6afe4..aeb4d9a 100644
--- a/services/tests/timetests/src/com/android/server/timezonedetector/FakeServiceConfigAccessor.java
+++ b/services/tests/timetests/src/com/android/server/timezonedetector/FakeServiceConfigAccessor.java
@@ -31,7 +31,7 @@
 /**
  * A partially implemented, fake implementation of ServiceConfigAccessor for tests.
  *
- * <p>This class has rudamentary support for multiple users, but unlike the real thing, it doesn't
+ * <p>This class has rudimentary support for multiple users, but unlike the real thing, it doesn't
  * simulate that some settings are global and shared between users. It also delivers config updates
  * synchronously.
  */
diff --git a/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java b/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java
index e52e8b6..47a9b2c 100644
--- a/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java
+++ b/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java
@@ -35,6 +35,12 @@
 
 import static com.android.server.SystemTimeZone.TIME_ZONE_CONFIDENCE_HIGH;
 import static com.android.server.SystemTimeZone.TIME_ZONE_CONFIDENCE_LOW;
+import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_AUTO_DETECT_NOT_SUPPORTED;
+import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_AUTO_DISABLED_GEO_DISABLED;
+import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_AUTO_ENABLED_GEO_DISABLED;
+import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_AUTO_ENABLED_GEO_ENABLED;
+import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_USER_RESTRICTED_AUTO_ENABLED;
+import static com.android.server.timezonedetector.ConfigInternalForTests.USER_ID;
 import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_HIGH;
 import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_HIGHEST;
 import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_LOW;
@@ -68,6 +74,7 @@
 import android.app.timezonedetector.TelephonyTimeZoneSuggestion.MatchType;
 import android.app.timezonedetector.TelephonyTimeZoneSuggestion.Quality;
 import android.service.timezone.TimeZoneProviderStatus;
+import android.util.IndentingPrintWriter;
 
 import com.android.server.SystemTimeZone.TimeZoneConfidence;
 import com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.QualifiedTelephonyTimeZoneSuggestion;
@@ -82,6 +89,7 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.function.Function;
@@ -92,7 +100,6 @@
 @RunWith(JUnitParamsRunner.class)
 public class TimeZoneDetectorStrategyImplTest {
 
-    private static final @UserIdInt int USER_ID = 9876;
     private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 1234;
     /** A time zone used for initialization that does not occur elsewhere in tests. */
     private static final String ARBITRARY_TIME_ZONE_ID = "Etc/UTC";
@@ -101,7 +108,7 @@
 
     // Telephony test cases are ordered so that each successive one is of the same or higher score
     // than the previous.
-    private static final TelephonyTestCase[] TELEPHONY_TEST_CASES = new TelephonyTestCase[] {
+    private static final TelephonyTestCase[] TELEPHONY_TEST_CASES = new TelephonyTestCase[]{
             newTelephonyTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY,
                     QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS, TELEPHONY_SCORE_LOW),
             newTelephonyTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY,
@@ -118,90 +125,6 @@
                     TELEPHONY_SCORE_HIGHEST),
     };
 
-    private static final ConfigurationInternal CONFIG_USER_RESTRICTED_AUTO_DISABLED =
-            new ConfigurationInternal.Builder()
-                    .setUserId(USER_ID)
-                    .setTelephonyDetectionFeatureSupported(true)
-                    .setGeoDetectionFeatureSupported(true)
-                    .setTelephonyFallbackSupported(false)
-                    .setGeoDetectionRunInBackgroundEnabled(false)
-                    .setEnhancedMetricsCollectionEnabled(false)
-                    .setUserConfigAllowed(false)
-                    .setAutoDetectionEnabledSetting(false)
-                    .setLocationEnabledSetting(true)
-                    .setGeoDetectionEnabledSetting(false)
-                    .build();
-
-    private static final ConfigurationInternal CONFIG_USER_RESTRICTED_AUTO_ENABLED =
-            new ConfigurationInternal.Builder()
-                    .setUserId(USER_ID)
-                    .setTelephonyDetectionFeatureSupported(true)
-                    .setGeoDetectionFeatureSupported(true)
-                    .setTelephonyFallbackSupported(false)
-                    .setGeoDetectionRunInBackgroundEnabled(false)
-                    .setEnhancedMetricsCollectionEnabled(false)
-                    .setUserConfigAllowed(false)
-                    .setAutoDetectionEnabledSetting(true)
-                    .setLocationEnabledSetting(true)
-                    .setGeoDetectionEnabledSetting(true)
-                    .build();
-
-    private static final ConfigurationInternal CONFIG_AUTO_DETECT_NOT_SUPPORTED =
-            new ConfigurationInternal.Builder()
-                    .setUserId(USER_ID)
-                    .setTelephonyDetectionFeatureSupported(false)
-                    .setGeoDetectionFeatureSupported(false)
-                    .setTelephonyFallbackSupported(false)
-                    .setGeoDetectionRunInBackgroundEnabled(false)
-                    .setEnhancedMetricsCollectionEnabled(false)
-                    .setUserConfigAllowed(true)
-                    .setAutoDetectionEnabledSetting(false)
-                    .setLocationEnabledSetting(true)
-                    .setGeoDetectionEnabledSetting(false)
-                    .build();
-
-    private static final ConfigurationInternal CONFIG_AUTO_DISABLED_GEO_DISABLED =
-            new ConfigurationInternal.Builder()
-                    .setUserId(USER_ID)
-                    .setTelephonyDetectionFeatureSupported(true)
-                    .setGeoDetectionFeatureSupported(true)
-                    .setTelephonyFallbackSupported(false)
-                    .setGeoDetectionRunInBackgroundEnabled(false)
-                    .setEnhancedMetricsCollectionEnabled(false)
-                    .setUserConfigAllowed(true)
-                    .setAutoDetectionEnabledSetting(false)
-                    .setLocationEnabledSetting(true)
-                    .setGeoDetectionEnabledSetting(false)
-                    .build();
-
-    private static final ConfigurationInternal CONFIG_AUTO_ENABLED_GEO_DISABLED =
-            new ConfigurationInternal.Builder()
-                    .setUserId(USER_ID)
-                    .setTelephonyDetectionFeatureSupported(true)
-                    .setGeoDetectionFeatureSupported(true)
-                    .setTelephonyFallbackSupported(false)
-                    .setGeoDetectionRunInBackgroundEnabled(false)
-                    .setEnhancedMetricsCollectionEnabled(false)
-                    .setUserConfigAllowed(true)
-                    .setAutoDetectionEnabledSetting(true)
-                    .setLocationEnabledSetting(true)
-                    .setGeoDetectionEnabledSetting(false)
-                    .build();
-
-    private static final ConfigurationInternal CONFIG_AUTO_ENABLED_GEO_ENABLED =
-            new ConfigurationInternal.Builder()
-                    .setUserId(USER_ID)
-                    .setTelephonyDetectionFeatureSupported(true)
-                    .setGeoDetectionFeatureSupported(true)
-                    .setTelephonyFallbackSupported(false)
-                    .setGeoDetectionRunInBackgroundEnabled(false)
-                    .setEnhancedMetricsCollectionEnabled(false)
-                    .setUserConfigAllowed(true)
-                    .setAutoDetectionEnabledSetting(true)
-                    .setLocationEnabledSetting(true)
-                    .setGeoDetectionEnabledSetting(true)
-                    .build();
-
     private static final TelephonyTimeZoneAlgorithmStatus TELEPHONY_ALGORITHM_RUNNING_STATUS =
             new TelephonyTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING);
 
@@ -421,7 +344,7 @@
                 new QualifiedTelephonyTimeZoneSuggestion(slotIndex1TimeZoneSuggestion,
                         TELEPHONY_SCORE_NONE);
         script.verifyLatestQualifiedTelephonySuggestionReceived(
-                SLOT_INDEX1, expectedSlotIndex1ScoredSuggestion)
+                        SLOT_INDEX1, expectedSlotIndex1ScoredSuggestion)
                 .verifyLatestQualifiedTelephonySuggestionReceived(SLOT_INDEX2, null);
         assertEquals(expectedSlotIndex1ScoredSuggestion,
                 mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
@@ -629,7 +552,7 @@
      */
     @Test
     public void testTelephonySuggestionMultipleSlotIndexSuggestionScoringAndSlotIndexBias() {
-        String[] zoneIds = { "Europe/London", "Europe/Paris" };
+        String[] zoneIds = {"Europe/London", "Europe/Paris"};
         TelephonyTimeZoneSuggestion emptySlotIndex1Suggestion = createEmptySlotIndex1Suggestion();
         TelephonyTimeZoneSuggestion emptySlotIndex2Suggestion = createEmptySlotIndex2Suggestion();
         QualifiedTelephonyTimeZoneSuggestion expectedEmptySlotIndex1ScoredSuggestion =
@@ -672,7 +595,7 @@
 
             // Assert internal service state.
             script.verifyLatestQualifiedTelephonySuggestionReceived(
-                    SLOT_INDEX1, expectedZoneSlotIndex1ScoredSuggestion)
+                            SLOT_INDEX1, expectedZoneSlotIndex1ScoredSuggestion)
                     .verifyLatestQualifiedTelephonySuggestionReceived(
                             SLOT_INDEX2, expectedEmptySlotIndex2ScoredSuggestion);
             assertEquals(expectedZoneSlotIndex1ScoredSuggestion,
@@ -805,14 +728,14 @@
         boolean bypassUserPolicyChecks = false;
         boolean expectedResult = true;
         script.simulateManualTimeZoneSuggestion(
-                USER_ID, manualSuggestion, bypassUserPolicyChecks, expectedResult)
+                        USER_ID, manualSuggestion, bypassUserPolicyChecks, expectedResult)
                 .verifyTimeZoneChangedAndReset(manualSuggestion);
 
         assertEquals(manualSuggestion, mTimeZoneDetectorStrategy.getLatestManualSuggestion());
     }
 
     @Test
-    @Parameters({ "true,true", "true,false", "false,true", "false,false" })
+    @Parameters({"true,true", "true,false", "false,true", "false,false"})
     public void testManualSuggestion_autoTimeEnabled_userRestrictions(
             boolean userConfigAllowed, boolean bypassUserPolicyChecks) {
         ConfigurationInternal config =
@@ -834,7 +757,7 @@
     }
 
     @Test
-    @Parameters({ "true,true", "true,false", "false,true", "false,false" })
+    @Parameters({"true,true", "true,false", "false,true", "false,false"})
     public void testManualSuggestion_autoTimeDisabled_userRestrictions(
             boolean userConfigAllowed, boolean bypassUserPolicyChecks) {
         ConfigurationInternal config =
@@ -849,7 +772,7 @@
         ManualTimeZoneSuggestion manualSuggestion = createManualSuggestion("Europe/Paris");
         boolean expectedResult = userConfigAllowed || bypassUserPolicyChecks;
         script.simulateManualTimeZoneSuggestion(
-                        USER_ID, manualSuggestion, bypassUserPolicyChecks, expectedResult);
+                USER_ID, manualSuggestion, bypassUserPolicyChecks, expectedResult);
         if (expectedResult) {
             script.verifyTimeZoneChangedAndReset(manualSuggestion);
             assertEquals(manualSuggestion, mTimeZoneDetectorStrategy.getLatestManualSuggestion());
@@ -1258,7 +1181,6 @@
             script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
                     .verifyTimeZoneChangedAndReset(locationAlgorithmEvent)
                     .verifyTelephonyFallbackIsEnabled(false);
-
         }
 
         // Demonstrate what happens when geolocation is uncertain when telephony fallback is
@@ -1569,7 +1491,7 @@
         boolean bypassUserPolicyChecks = false;
         boolean expectedResult = true;
         script.simulateManualTimeZoneSuggestion(
-                USER_ID, manualSuggestion, bypassUserPolicyChecks, expectedResult)
+                        USER_ID, manualSuggestion, bypassUserPolicyChecks, expectedResult)
                 .verifyTimeZoneChangedAndReset(manualSuggestion);
         expectedDeviceTimeZoneId = manualSuggestion.getZoneId();
         assertMetricsState(expectedInternalConfig, expectedDeviceTimeZoneId,
@@ -1880,6 +1802,7 @@
             boolean actualResult = mTimeZoneDetectorStrategy.suggestManualTimeZone(
                     userId, manualTimeZoneSuggestion, bypassUserPolicyChecks);
             assertEquals(expectedResult, actualResult);
+
             return this;
         }
 
@@ -2001,4 +1924,34 @@
         return new TelephonyTestCase(matchType, quality, expectedScore);
     }
 
+    static class FakeTimeZoneChangeEventListener implements TimeZoneChangeListener {
+        private final List<TimeZoneChangeEvent> mEvents = new ArrayList<>();
+
+        FakeTimeZoneChangeEventListener() {
+        }
+
+        @Override
+        public void process(TimeZoneChangeEvent event) {
+            mEvents.add(event);
+        }
+
+        public List<TimeZoneChangeEvent> getTimeZoneChangeEvents() {
+            return mEvents;
+        }
+
+        @Override
+        public void dump(IndentingPrintWriter ipw) {
+            // No-op for tests
+        }
+    }
+
+    private static void assertEmpty(Collection<?> collection) {
+        assertTrue(
+                "Expected empty, but contains (" + collection.size() + ") elements: " + collection,
+                collection.isEmpty());
+    }
+
+    private static void assertNotEmpty(Collection<?> collection) {
+        assertFalse("Expected not empty: " + collection, collection.isEmpty());
+    }
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java
index af7f703..b332331 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java
@@ -101,9 +101,12 @@
 
         mProviders.notifyConditions("package", msi, conditionsToNotify);
 
-        verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(conditionsToNotify[0]));
-        verify(mCallback).onConditionChanged(eq(Uri.parse("b")), eq(conditionsToNotify[1]));
-        verify(mCallback).onConditionChanged(eq(Uri.parse("c")), eq(conditionsToNotify[2]));
+        verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(conditionsToNotify[0]),
+                eq(100));
+        verify(mCallback).onConditionChanged(eq(Uri.parse("b")), eq(conditionsToNotify[1]),
+                eq(100));
+        verify(mCallback).onConditionChanged(eq(Uri.parse("c")), eq(conditionsToNotify[2]),
+                eq(100));
         verifyNoMoreInteractions(mCallback);
     }
 
@@ -121,8 +124,10 @@
 
         mProviders.notifyConditions("package", msi, conditionsToNotify);
 
-        verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(conditionsToNotify[0]));
-        verify(mCallback).onConditionChanged(eq(Uri.parse("b")), eq(conditionsToNotify[1]));
+        verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(conditionsToNotify[0]),
+                eq(100));
+        verify(mCallback).onConditionChanged(eq(Uri.parse("b")), eq(conditionsToNotify[1]),
+                eq(100));
 
         verifyNoMoreInteractions(mCallback);
     }
@@ -141,8 +146,10 @@
 
         mProviders.notifyConditions("package", msi, conditionsToNotify);
 
-        verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(conditionsToNotify[0]));
-        verify(mCallback).onConditionChanged(eq(Uri.parse("b")), eq(conditionsToNotify[3]));
+        verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(conditionsToNotify[0]),
+                eq(100));
+        verify(mCallback).onConditionChanged(eq(Uri.parse("b")), eq(conditionsToNotify[3]),
+                eq(100));
         verifyNoMoreInteractions(mCallback);
     }
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
index 6cb2429..fa733e8 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
@@ -2509,6 +2509,134 @@
     }
 
     @Test
+    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS,
+            android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST})
+    public void testRepostWithNewChannel_afterAutogrouping_isRegrouped() {
+        final String pkg = "package";
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        // Post ungrouped notifications => will be autogrouped
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            NotificationRecord notification = getNotificationRecord(pkg, i + 42,
+                    String.valueOf(i + 42), UserHandle.SYSTEM, null, false);
+            notificationList.add(notification);
+            mGroupHelper.onNotificationPosted(notification, false);
+        }
+
+        final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
+                AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier());
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(expectedGroupKey), anyInt(), any());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(),
+                eq(expectedGroupKey), eq(true));
+
+        // Post ungrouped notifications to a different section, below autogroup limit
+        Mockito.reset(mCallback);
+        // Post ungrouped notifications => will be autogrouped
+        final NotificationChannel silentChannel = new NotificationChannel("TEST_CHANNEL_ID1",
+                "TEST_CHANNEL_ID1", IMPORTANCE_LOW);
+        for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
+            NotificationRecord notification = getNotificationRecord(pkg, i + 4242,
+                    String.valueOf(i + 4242), UserHandle.SYSTEM, null, false, silentChannel);
+            notificationList.add(notification);
+            mGroupHelper.onNotificationPosted(notification, false);
+        }
+
+        verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(),
+                anyString(), anyInt(), any());
+        verify(mCallback, never()).addAutoGroup(anyString(), anyString(), anyBoolean());
+
+        // Update a notification to a different channel that moves it to a different section
+        Mockito.reset(mCallback);
+        final NotificationRecord notifToInvalidate = notificationList.get(0);
+        final NotificationSectioner initialSection = GroupHelper.getSection(notifToInvalidate);
+        final NotificationChannel updatedChannel = new NotificationChannel("TEST_CHANNEL_ID2",
+                "TEST_CHANNEL_ID2", IMPORTANCE_LOW);
+        notifToInvalidate.updateNotificationChannel(updatedChannel);
+        assertThat(GroupHelper.getSection(notifToInvalidate)).isNotEqualTo(initialSection);
+        boolean needsAutogrouping = mGroupHelper.onNotificationPosted(notifToInvalidate, false);
+        assertThat(needsAutogrouping).isTrue();
+
+        // Check that the silent section was autogrouped
+        final String silentSectionGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
+                AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier());
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(silentSectionGroupKey), anyInt(), any());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(),
+                eq(silentSectionGroupKey), eq(true));
+        verify(mCallback, times(1)).removeAutoGroup(eq(notifToInvalidate.getKey()));
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(),
+                eq(expectedGroupKey), any());
+    }
+
+    @Test
+    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS,
+            android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST})
+    public void testRepostWithNewChannel_afterForceGrouping_isRegrouped() {
+        final String pkg = "package";
+        final String groupName = "testGroup";
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        // Post valid section summary notifications without children => force group
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            NotificationRecord notification = getNotificationRecord(pkg, i + 42,
+                    String.valueOf(i + 42), UserHandle.SYSTEM, groupName, false);
+            notificationList.add(notification);
+            mGroupHelper.onNotificationPostedWithDelay(notification, notificationList,
+                    summaryByGroup);
+        }
+
+        final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
+                AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier());
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(expectedGroupKey), anyInt(), any());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+                eq(expectedGroupKey), eq(true));
+
+        // Update a notification to a different channel that moves it to a different section
+        Mockito.reset(mCallback);
+        final NotificationRecord notifToInvalidate = notificationList.get(0);
+        final NotificationSectioner initialSection = GroupHelper.getSection(notifToInvalidate);
+        final NotificationChannel updatedChannel = new NotificationChannel("TEST_CHANNEL_ID2",
+                "TEST_CHANNEL_ID2", IMPORTANCE_LOW);
+        notifToInvalidate.updateNotificationChannel(updatedChannel);
+        assertThat(GroupHelper.getSection(notifToInvalidate)).isNotEqualTo(initialSection);
+        boolean needsAutogrouping = mGroupHelper.onNotificationPosted(notifToInvalidate, false);
+
+        mGroupHelper.onNotificationPostedWithDelay(notifToInvalidate, notificationList,
+                summaryByGroup);
+
+        // Check that the updated notification is removed from the autogroup
+        assertThat(needsAutogrouping).isFalse();
+        verify(mCallback, times(1)).removeAutoGroup(eq(notifToInvalidate.getKey()));
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(),
+                eq(expectedGroupKey), any());
+
+        // Post child notifications for the silent sectin => will be autogrouped
+        Mockito.reset(mCallback);
+        final NotificationChannel silentChannel = new NotificationChannel("TEST_CHANNEL_ID1",
+                "TEST_CHANNEL_ID1", IMPORTANCE_LOW);
+        for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
+            NotificationRecord notification = getNotificationRecord(pkg, i + 4242,
+                    String.valueOf(i + 4242), UserHandle.SYSTEM, "aGroup", false, silentChannel);
+            notificationList.add(notification);
+            needsAutogrouping = mGroupHelper.onNotificationPosted(notification, false);
+            assertThat(needsAutogrouping).isFalse();
+            mGroupHelper.onNotificationPostedWithDelay(notification, notificationList,
+                    summaryByGroup);
+        }
+
+        // Check that the silent section was autogrouped
+        final String silentSectionGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
+                AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier());
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(silentSectionGroupKey), anyInt(), any());
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+                eq(silentSectionGroupKey), eq(true));
+    }
+
+    @Test
     @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
     public void testMoveAggregateGroups_updateChannel() {
         final String pkg = "package";
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 301165f..e43b28b 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -6341,6 +6341,26 @@
     }
 
     @Test
+    public void testOnlyAutogroupIfNeeded_channelChanged_ghUpdate() {
+        NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0,
+                "testOnlyAutogroupIfNeeded_channelChanged_ghUpdate", null, false);
+        mService.addNotification(r);
+
+        NotificationRecord update = generateNotificationRecord(mSilentChannel, 0,
+                "testOnlyAutogroupIfNeeded_channelChanged_ghUpdate", null, false);
+        mService.addEnqueuedNotification(update);
+
+        NotificationManagerService.PostNotificationRunnable runnable =
+                mService.new PostNotificationRunnable(update.getKey(),
+                        update.getSbn().getPackageName(), update.getUid(),
+                        mPostNotificationTrackerFactory.newTracker(null));
+        runnable.run();
+        waitForIdle();
+
+        verify(mGroupHelper, times(1)).onNotificationPosted(any(), anyBoolean());
+    }
+
+    @Test
     public void testOnlyAutogroupIfGroupChanged_noValidChange_noGhUpdate() {
         NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0,
                 "testOnlyAutogroupIfGroupChanged_noValidChange_noGhUpdate", null, false);
@@ -11213,7 +11233,7 @@
         // Representative used to verify getCallingZenUser().
         mBinderService.getAutomaticZenRules();
 
-        verify(zenModeHelper).getAutomaticZenRules(eq(UserHandle.CURRENT));
+        verify(zenModeHelper).getAutomaticZenRules(eq(UserHandle.CURRENT), anyInt());
     }
 
     @Test
@@ -11225,7 +11245,7 @@
         // Representative used to verify getCallingZenUser().
         mBinderService.getAutomaticZenRules();
 
-        verify(zenModeHelper).getAutomaticZenRules(eq(Binder.getCallingUserHandle()));
+        verify(zenModeHelper).getAutomaticZenRules(eq(Binder.getCallingUserHandle()), anyInt());
     }
 
     /** Prepares for a zen-related test that uses a mocked {@link ZenModeHelper}. */
@@ -17901,4 +17921,63 @@
         verify(mGroupHelper, times(1)).onNotificationUnbundled(eq(r1), eq(hasOriginalSummary));
     }
 
+    @Test
+    @EnableFlags({FLAG_NOTIFICATION_CLASSIFICATION,
+            FLAG_NOTIFICATION_FORCE_GROUPING,
+            FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION})
+    public void testRebundleNotification_restoresBundleChannel() throws Exception {
+        NotificationManagerService.WorkerHandler handler = mock(
+                NotificationManagerService.WorkerHandler.class);
+        mService.setHandler(handler);
+        when(mAssistants.isSameUser(any(), anyInt())).thenReturn(true);
+        when(mAssistants.isServiceTokenValidLocked(any())).thenReturn(true);
+        when(mAssistants.isAdjustmentKeyTypeAllowed(anyInt())).thenReturn(true);
+        when(mAssistants.isTypeAdjustmentAllowedForPackage(anyString(), anyInt())).thenReturn(true);
+
+        // Post a single notification
+        final boolean hasOriginalSummary = false;
+        final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
+        final String keyToUnbundle = r.getKey();
+        mService.addNotification(r);
+
+        // Classify notification into the NEWS bundle
+        Bundle signals = new Bundle();
+        signals.putInt(Adjustment.KEY_TYPE, Adjustment.TYPE_NEWS);
+        Adjustment adjustment = new Adjustment(
+                r.getSbn().getPackageName(), r.getKey(), signals, "", r.getUser().getIdentifier());
+        mBinderService.applyAdjustmentFromAssistant(null, adjustment);
+        waitForIdle();
+        r.applyAdjustments();
+        // Check that the NotificationRecord channel is updated
+        assertThat(r.getChannel().getId()).isEqualTo(NEWS_ID);
+        assertThat(r.getBundleType()).isEqualTo(Adjustment.TYPE_NEWS);
+
+        // Unbundle the notification
+        mService.mNotificationDelegate.unbundleNotification(keyToUnbundle);
+
+        // Check that the original channel was restored
+        assertThat(r.getChannel().getId()).isEqualTo(TEST_CHANNEL_ID);
+        assertThat(r.getBundleType()).isEqualTo(Adjustment.TYPE_NEWS);
+        verify(mGroupHelper, times(1)).onNotificationUnbundled(eq(r), eq(hasOriginalSummary));
+
+        Mockito.reset(mRankingHandler);
+        Mockito.reset(mGroupHelper);
+
+        // Rebundle the notification
+        mService.mNotificationDelegate.rebundleNotification(keyToUnbundle);
+
+        // Actually apply the adjustments
+        doAnswer(invocationOnMock -> {
+            ((NotificationRecord) invocationOnMock.getArguments()[0]).applyAdjustments();
+            ((NotificationRecord) invocationOnMock.getArguments()[0]).calculateImportance();
+            return null;
+        }).when(mRankingHelper).extractSignals(any(NotificationRecord.class));
+        mService.handleRankingSort();
+        verify(handler, times(1)).scheduleSendRankingUpdate();
+
+        // Check that the bundle channel was restored
+        verify(mRankingHandler, times(1)).requestSort();
+        assertThat(r.getChannel().getId()).isEqualTo(NEWS_ID);
+    }
+
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 8e79514..f41805d 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -66,7 +66,6 @@
 import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__DENIED;
 import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED;
 import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED;
-import static com.android.server.notification.Flags.FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI;
 import static com.android.server.notification.Flags.FLAG_PERSIST_INCOMPLETE_RESTORE_DATA;
 import static com.android.server.notification.NotificationChannelLogger.NotificationChannelEvent.NOTIFICATION_CHANNEL_UPDATED_BY_USER;
 import static com.android.server.notification.PreferencesHelper.DEFAULT_BUBBLE_PREFERENCE;
@@ -164,6 +163,7 @@
 import com.android.os.AtomsProto.PackageNotificationPreferences;
 import com.android.server.UiServiceTestCase;
 import com.android.server.notification.PermissionHelper.PackagePermission;
+import com.android.server.uri.UriGrantsManagerInternal;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -179,6 +179,9 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
 import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
 import java.io.ByteArrayInputStream;
@@ -199,9 +202,6 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ThreadLocalRandom;
 
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
-import platform.test.runner.parameterized.Parameters;
-
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4.class)
 @EnableFlags(FLAG_PERSIST_INCOMPLETE_RESTORE_DATA)
@@ -239,9 +239,10 @@
 
     private NotificationManager.Policy mTestNotificationPolicy;
 
-    private PreferencesHelper mHelper;
-    // fresh object for testing xml reading
-    private PreferencesHelper mXmlHelper;
+    private TestPreferencesHelper mHelper;
+    // fresh object for testing xml reading; also TestPreferenceHelper in order to avoid interacting
+    // with real IpcDataCaches
+    private TestPreferencesHelper mXmlHelper;
     private AudioAttributes mAudioAttributes;
     private NotificationChannelLoggerFake mLogger = new NotificationChannelLoggerFake();
 
@@ -378,10 +379,10 @@
         when(mUserProfiles.getCurrentProfileIds()).thenReturn(currentProfileIds);
         when(mClock.millis()).thenReturn(System.currentTimeMillis());
 
-        mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+        mHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
                 mUgmInternal, false, mClock);
-        mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+        mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
                 mUgmInternal, false, mClock);
         resetZenModeHelper();
@@ -793,7 +794,7 @@
 
     @Test
     public void testReadXml_oldXml_migrates() throws Exception {
-        mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+        mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
                 mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock);
 
@@ -929,7 +930,7 @@
 
     @Test
     public void testReadXml_newXml_noMigration_showPermissionNotification() throws Exception {
-        mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+        mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
                 mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock);
 
@@ -988,7 +989,7 @@
 
     @Test
     public void testReadXml_newXml_permissionNotificationOff() throws Exception {
-        mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+        mHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
                 mUgmInternal, /* showReviewPermissionsNotification= */ false, mClock);
 
@@ -1047,7 +1048,7 @@
 
     @Test
     public void testReadXml_newXml_noMigration_noPermissionNotification() throws Exception {
-        mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+        mHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
                 mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock);
 
@@ -1641,7 +1642,7 @@
         serializer.flush();
 
         // simulate load after reboot
-        mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+        mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
                 mUgmInternal, false, mClock);
         loadByteArrayXml(baos.toByteArray(), false, USER_ALL);
@@ -1696,7 +1697,7 @@
                 Duration.ofDays(2).toMillis() + System.currentTimeMillis());
 
         // simulate load after reboot
-        mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
+        mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
                 mUgmInternal, false, mClock);
         loadByteArrayXml(xml.getBytes(), false, USER_ALL);
@@ -1774,10 +1775,10 @@
         when(contentResolver.getResourceId(ANDROID_RES_SOUND_URI)).thenReturn(resId).thenThrow(
                 new FileNotFoundException("")).thenReturn(resId);
 
-        mHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper,
+        mHelper = new TestPreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
                 mUgmInternal, false, mClock);
-        mXmlHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper,
+        mXmlHelper = new TestPreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper,
                 mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles,
                 mUgmInternal, false, mClock);
 
@@ -3190,7 +3191,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI)
     public void testCreateChannel_noSoundUriPermission_contentSchemeVerified() {
         final Uri sound = Uri.parse(SCHEME_CONTENT + "://media/test/sound/uri");
 
@@ -3210,7 +3210,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI)
     public void testCreateChannel_noSoundUriPermission_fileSchemaIgnored() {
         final Uri sound = Uri.parse(SCHEME_FILE + "://path/sound");
 
@@ -3229,7 +3228,6 @@
     }
 
     @Test
-    @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI)
     public void testCreateChannel_noSoundUriPermission_resourceSchemaIgnored() {
         final Uri sound = Uri.parse(SCHEME_ANDROID_RESOURCE + "://resId/sound");
 
@@ -6573,4 +6571,223 @@
         mHelper.setCanBePromoted(PKG_P, UID_P, false, false);
         assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isTrue();
     }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+    public void testInvalidateChannelCache_invalidateOnCreationAndChange() {
+        mHelper.resetCacheInvalidation();
+        NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_DEFAULT);
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1,
+                false);
+
+        // new channel should invalidate the cache.
+        assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+
+        // when the channel data is updated, should invalidate the cache again after that.
+        mHelper.resetCacheInvalidation();
+        NotificationChannel newChannel = channel.copy();
+        newChannel.setName("new name");
+        newChannel.setImportance(IMPORTANCE_HIGH);
+        mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, newChannel, true, UID_N_MR1, false);
+        assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+
+        // also for conversations
+        mHelper.resetCacheInvalidation();
+        String parentId = "id";
+        String convId = "conversation";
+        NotificationChannel conv = new NotificationChannel(
+                String.format(CONVERSATION_CHANNEL_ID_FORMAT, parentId, convId), "conversation",
+                IMPORTANCE_DEFAULT);
+        conv.setConversationId(parentId, convId);
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, conv, true, false, UID_N_MR1,
+                false);
+        assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+
+        mHelper.resetCacheInvalidation();
+        NotificationChannel newConv = conv.copy();
+        newConv.setName("changed");
+        mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, newConv, true, UID_N_MR1, false);
+        assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+    public void testInvalidateChannelCache_invalidateOnDelete() {
+        NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_DEFAULT);
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1,
+                false);
+
+        // ignore any invalidations up until now
+        mHelper.resetCacheInvalidation();
+
+        mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", UID_N_MR1, false);
+        assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+
+        // recreate channel and now permanently delete
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1,
+                false);
+        mHelper.resetCacheInvalidation();
+        mHelper.permanentlyDeleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "id");
+        assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+    public void testInvalidateChannelCache_noInvalidationWhenNoChange() {
+        NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_DEFAULT);
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1,
+                false);
+
+        // ignore any invalidations up until now
+        mHelper.resetCacheInvalidation();
+
+        // newChannel, same as the old channel
+        NotificationChannel newChannel = channel.copy();
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, newChannel, true, false, UID_N_MR1,
+                false);
+        mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, newChannel, true, UID_N_MR1, false);
+
+        // because there were no effective changes, we should not see any cache invalidations
+        assertThat(mHelper.hasCacheBeenInvalidated()).isFalse();
+
+        // deletions of a nonexistent channel also don't change anything
+        mHelper.resetCacheInvalidation();
+        mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "nonexistent", UID_N_MR1, false);
+        assertThat(mHelper.hasCacheBeenInvalidated()).isFalse();
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+    public void testInvalidateCache_multipleUsersAndPackages() {
+        // Setup: create channels for:
+        // pkg O, user
+        // pkg O, work (same channel ID, different user)
+        // pkg N_MR1, user
+        // pkg N_MR1, user, conversation child of above
+        String p2u1ConvId = String.format(CONVERSATION_CHANNEL_ID_FORMAT, "p2", "conv");
+        NotificationChannel p1u1 = new NotificationChannel("p1", "p1u1", IMPORTANCE_DEFAULT);
+        NotificationChannel p1u2 = new NotificationChannel("p1", "p1u2", IMPORTANCE_DEFAULT);
+        NotificationChannel p2u1 = new NotificationChannel("p2", "p2u1", IMPORTANCE_DEFAULT);
+        NotificationChannel p2u1Conv = new NotificationChannel(p2u1ConvId, "p2u1 conv",
+                IMPORTANCE_DEFAULT);
+        p2u1Conv.setConversationId("p2", "conv");
+
+        mHelper.createNotificationChannel(PKG_O, UID_O, p1u1, true,
+                false, UID_O, false);
+        mHelper.createNotificationChannel(PKG_O, UID_O + UserHandle.PER_USER_RANGE, p1u2, true,
+                false, UID_O + UserHandle.PER_USER_RANGE, false);
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, p2u1, true,
+                false, UID_N_MR1, false);
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, p2u1Conv, true,
+                false, UID_N_MR1, false);
+        mHelper.resetCacheInvalidation();
+
+        // Update to an existent channel, with a change: should invalidate
+        NotificationChannel p1u1New = p1u1.copy();
+        p1u1New.setName("p1u1 new");
+        mHelper.updateNotificationChannel(PKG_O, UID_O, p1u1New, true, UID_O, false);
+        assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+
+        // Do it again, but no change for this user
+        mHelper.resetCacheInvalidation();
+        mHelper.updateNotificationChannel(PKG_O, UID_O, p1u1New.copy(), true, UID_O, false);
+        assertThat(mHelper.hasCacheBeenInvalidated()).isFalse();
+
+        // Delete conversations, but for a package without those conversations
+        mHelper.resetCacheInvalidation();
+        mHelper.deleteConversations(PKG_O, UID_O, Set.of(p2u1Conv.getConversationId()), UID_O,
+                false);
+        assertThat(mHelper.hasCacheBeenInvalidated()).isFalse();
+
+        // Now delete conversations for the right package
+        mHelper.resetCacheInvalidation();
+        mHelper.deleteConversations(PKG_N_MR1, UID_N_MR1, Set.of(p2u1Conv.getConversationId()),
+                UID_N_MR1, false);
+        assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+    public void testInvalidateCache_userRemoved() throws Exception {
+        NotificationChannel c1 = new NotificationChannel("id1", "name1", IMPORTANCE_DEFAULT);
+        int uid1 = UserHandle.getUid(1, 1);
+        setUpPackageWithUid("pkg1", uid1);
+        mHelper.createNotificationChannel("pkg1", uid1, c1, true, false, uid1, false);
+        mHelper.resetCacheInvalidation();
+
+        // delete user 1; should invalidate cache
+        mHelper.onUserRemoved(1);
+        assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+    }
+
+    @Test
+    @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+    public void testInvalidateCache_packagesChanged() {
+        NotificationChannel channel1 =
+                new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH);
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false,
+                UID_N_MR1, false);
+
+        // package deleted: expect cache invalidation
+        mHelper.resetCacheInvalidation();
+        mHelper.onPackagesChanged(true, USER_SYSTEM, new String[]{PKG_N_MR1},
+                new int[]{UID_N_MR1});
+        assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+
+        // re-created: expect cache invalidation again
+        mHelper.resetCacheInvalidation();
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false,
+                UID_N_MR1, false);
+        mHelper.onPackagesChanged(false, USER_SYSTEM, new String[]{PKG_N_MR1},
+                new int[]{UID_N_MR1});
+        assertThat(mHelper.hasCacheBeenInvalidated()).isTrue();
+    }
+
+    @Test
+    @DisableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS)
+    public void testInvalidateCache_flagOff_neverTouchesCache() {
+        // Do a bunch of channel-changing operations.
+        NotificationChannel channel =
+                new NotificationChannel("id", "name1", NotificationManager.IMPORTANCE_HIGH);
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false,
+                UID_N_MR1, false);
+
+        NotificationChannel copy = channel.copy();
+        copy.setName("name2");
+        mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, copy, true, UID_N_MR1, false);
+        mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", UID_N_MR1, false);
+
+        assertThat(mHelper.hasCacheBeenInvalidated()).isFalse();
+    }
+
+    // Test version of PreferencesHelper whose only functional difference is that it does not
+    // interact with the real IpcDataCache, and instead tracks whether or not the cache has been
+    // invalidated since creation or the last reset.
+    private static class TestPreferencesHelper extends PreferencesHelper {
+        private boolean mCacheInvalidated = false;
+
+        TestPreferencesHelper(Context context, PackageManager pm, RankingHandler rankingHandler,
+                ZenModeHelper zenHelper, PermissionHelper permHelper, PermissionManager permManager,
+                NotificationChannelLogger notificationChannelLogger,
+                AppOpsManager appOpsManager, ManagedServices.UserProfiles userProfiles,
+                UriGrantsManagerInternal ugmInternal,
+                boolean showReviewPermissionsNotification, Clock clock) {
+            super(context, pm, rankingHandler, zenHelper, permHelper, permManager,
+                    notificationChannelLogger, appOpsManager, userProfiles, ugmInternal,
+                    showReviewPermissionsNotification, clock);
+        }
+
+        @Override
+        protected void invalidateNotificationChannelCache() {
+            mCacheInvalidated = true;
+        }
+
+        boolean hasCacheBeenInvalidated() {
+            return mCacheInvalidated;
+        }
+
+        void resetCacheInvalidation() {
+            mCacheInvalidated = false;
+        }
+    }
 }
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 1884bbd..6ef078b 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -291,7 +291,8 @@
 
     @Parameters(name = "{0}")
     public static List<FlagsParameterization> getParams() {
-        return FlagsParameterization.allCombinationsOf(FLAG_MODES_UI, FLAG_BACKUP_RESTORE_LOGGING);
+        return FlagsParameterization.allCombinationsOf(FLAG_MODES_UI, FLAG_BACKUP_RESTORE_LOGGING,
+                com.android.server.notification.Flags.FLAG_FIX_CALLING_UID_FROM_CPS);
     }
 
     public ZenModeHelperTest(FlagsParameterization flags) {
@@ -2617,7 +2618,7 @@
     }
 
     @Test
-    public void testSetAutomaticZenRuleState_nullPkg() {
+    public void testSetAutomaticZenRuleStateFromConditionProvider_nullPkg() {
         AutomaticZenRule zenRule = new AutomaticZenRule("name",
                 null,
                 new ComponentName(mContext.getPackageName(), "ScheduleConditionProvider"),
@@ -2627,10 +2628,9 @@
 
         String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, null, zenRule,
                 ORIGIN_APP, "test", CUSTOM_PKG_UID);
-        mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, zenRule.getConditionId(),
-                new Condition(zenRule.getConditionId(), "", STATE_TRUE),
-                ORIGIN_APP,
-                CUSTOM_PKG_UID);
+        mZenModeHelper.setAutomaticZenRuleStateFromConditionProvider(UserHandle.CURRENT,
+                zenRule.getConditionId(), new Condition(zenRule.getConditionId(), "", STATE_TRUE),
+                ORIGIN_APP, CUSTOM_PKG_UID);
 
         ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id);
         assertEquals(STATE_TRUE, ruleInConfig.condition.state);
@@ -2726,8 +2726,8 @@
                 ORIGIN_SYSTEM, "test", SYSTEM_UID);
 
         Condition condition = new Condition(sharedUri, "", STATE_TRUE);
-        mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, sharedUri, condition,
-                ORIGIN_SYSTEM, SYSTEM_UID);
+        mZenModeHelper.setAutomaticZenRuleStateFromConditionProvider(UserHandle.CURRENT, sharedUri,
+                condition, ORIGIN_SYSTEM, SYSTEM_UID);
 
         for (ZenModeConfig.ZenRule rule : mZenModeHelper.mConfig.automaticRules.values()) {
             if (rule.id.equals(id)) {
@@ -2741,8 +2741,8 @@
         }
 
         condition = new Condition(sharedUri, "", STATE_FALSE);
-        mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, sharedUri, condition,
-                ORIGIN_SYSTEM, SYSTEM_UID);
+        mZenModeHelper.setAutomaticZenRuleStateFromConditionProvider(UserHandle.CURRENT, sharedUri,
+                condition, ORIGIN_SYSTEM, SYSTEM_UID);
 
         for (ZenModeConfig.ZenRule rule : mZenModeHelper.mConfig.automaticRules.values()) {
             if (rule.id.equals(id)) {
@@ -2780,9 +2780,10 @@
                         .setOwner(OWNER)
                         .setDeviceEffects(zde)
                         .build(),
-                ORIGIN_APP, "reasons", 0);
+                ORIGIN_APP, "reasons", CUSTOM_PKG_UID);
 
-        AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+        AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                CUSTOM_PKG_UID);
         assertThat(savedRule.getDeviceEffects()).isEqualTo(
                 new ZenDeviceEffects.Builder()
                         .setShouldDisplayGrayscale(true)
@@ -2814,9 +2815,10 @@
                         .setOwner(OWNER)
                         .setDeviceEffects(zde)
                         .build(),
-                ORIGIN_SYSTEM, "reasons", 0);
+                ORIGIN_SYSTEM, "reasons", SYSTEM_UID);
 
-        AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+        AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                SYSTEM_UID);
         assertThat(savedRule.getDeviceEffects()).isEqualTo(zde);
     }
 
@@ -2845,7 +2847,8 @@
                 ORIGIN_USER_IN_SYSTEMUI,
                 "reasons", 0);
 
-        AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+        AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                SYSTEM_UID);
 
         assertThat(savedRule.getDeviceEffects()).isEqualTo(zde);
     }
@@ -2863,7 +2866,7 @@
                         .setOwner(OWNER)
                         .setDeviceEffects(original)
                         .build(),
-                ORIGIN_SYSTEM, "reasons", 0);
+                ORIGIN_SYSTEM, "reasons", SYSTEM_UID);
 
         ZenDeviceEffects updateFromApp = new ZenDeviceEffects.Builder()
                 .setShouldUseNightMode(true) // Good
@@ -2875,9 +2878,10 @@
                         .setOwner(OWNER)
                         .setDeviceEffects(updateFromApp)
                         .build(),
-                ORIGIN_APP, "reasons", 0);
+                ORIGIN_APP, "reasons", CUSTOM_PKG_UID);
 
-        AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+        AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                CUSTOM_PKG_UID);
         assertThat(savedRule.getDeviceEffects()).isEqualTo(
                 new ZenDeviceEffects.Builder()
                         .setShouldUseNightMode(true) // From update.
@@ -2898,7 +2902,7 @@
                         .setOwner(OWNER)
                         .setDeviceEffects(original)
                         .build(),
-                ORIGIN_SYSTEM, "reasons", 0);
+                ORIGIN_SYSTEM, "reasons", SYSTEM_UID);
 
         ZenDeviceEffects updateFromSystem = new ZenDeviceEffects.Builder()
                 .setShouldUseNightMode(true) // Good
@@ -2908,9 +2912,10 @@
                 new AutomaticZenRule.Builder("Rule", CONDITION_ID)
                         .setDeviceEffects(updateFromSystem)
                         .build(),
-                ORIGIN_SYSTEM, "reasons", 0);
+                ORIGIN_SYSTEM, "reasons", SYSTEM_UID);
 
-        AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+        AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                SYSTEM_UID);
         assertThat(savedRule.getDeviceEffects()).isEqualTo(updateFromSystem);
     }
 
@@ -2926,7 +2931,7 @@
                         .setOwner(OWNER)
                         .setDeviceEffects(original)
                         .build(),
-                ORIGIN_SYSTEM, "reasons", 0);
+                ORIGIN_SYSTEM, "reasons", SYSTEM_UID);
 
         ZenDeviceEffects updateFromUser = new ZenDeviceEffects.Builder()
                 .setShouldUseNightMode(true)
@@ -2939,9 +2944,10 @@
                 new AutomaticZenRule.Builder("Rule", CONDITION_ID)
                         .setDeviceEffects(updateFromUser)
                         .build(),
-                ORIGIN_USER_IN_SYSTEMUI, "reasons", 0);
+                ORIGIN_USER_IN_SYSTEMUI, "reasons", SYSTEM_UID);
 
-        AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+        AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                SYSTEM_UID);
 
         assertThat(savedRule.getDeviceEffects()).isEqualTo(updateFromUser);
     }
@@ -2959,15 +2965,16 @@
                                 .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE) // default is stars
                                 .build())
                         .build(),
-                ORIGIN_APP, "reasons", 0);
+                ORIGIN_APP, "reasons", CUSTOM_PKG_UID);
 
         mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId,
                 new AutomaticZenRule.Builder("Rule", CONDITION_ID)
                         // no zen policy
                         .build(),
-                ORIGIN_APP, "reasons", 0);
+                ORIGIN_APP, "reasons", CUSTOM_PKG_UID);
 
-        AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+        AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                CUSTOM_PKG_UID);
         assertThat(savedRule.getZenPolicy().getPriorityCategoryCalls())
                 .isEqualTo(STATE_DISALLOW);
     }
@@ -2988,7 +2995,7 @@
                                 .allowReminders(true)
                                 .build())
                         .build(),
-                ORIGIN_SYSTEM, "reasons", 0);
+                ORIGIN_SYSTEM, "reasons", SYSTEM_UID);
 
         mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId,
                 new AutomaticZenRule.Builder("Rule", CONDITION_ID)
@@ -2996,9 +3003,10 @@
                                 .allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS)
                                 .build())
                         .build(),
-                ORIGIN_APP, "reasons", 0);
+                ORIGIN_APP, "reasons", CUSTOM_PKG_UID);
 
-        AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+        AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                CUSTOM_PKG_UID);
         assertThat(savedRule.getZenPolicy().getPriorityCategoryCalls())
                 .isEqualTo(STATE_ALLOW);  // from update
         assertThat(savedRule.getZenPolicy().getPriorityCallSenders())
@@ -4441,7 +4449,8 @@
         rule.triggerDescription = TRIGGER_DESC;
 
         mZenModeHelper.mConfig.automaticRules.put(rule.id, rule);
-        AutomaticZenRule actual = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, rule.id);
+        AutomaticZenRule actual = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, rule.id,
+                SYSTEM_UID);
 
         assertEquals(NAME, actual.getName());
         assertEquals(OWNER, actual.getOwner());
@@ -4508,16 +4517,17 @@
                 .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
                 .build();
         String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
-                mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID);
-        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+                mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", CUSTOM_PKG_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                CUSTOM_PKG_UID);
 
         // Checks the name can be changed by the app because the user has not modified it.
         AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule)
                 .setName("NewName")
                 .build();
         mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azrUpdate, ORIGIN_APP,
-                "reason", SYSTEM_UID);
-        rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+                "reason", CUSTOM_PKG_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, CUSTOM_PKG_UID);
         assertThat(rule.getName()).isEqualTo("NewName");
 
         // The user modifies some other field in the rule, which makes the rule as a whole not
@@ -4534,8 +4544,8 @@
                 .setName("NewAppName")
                 .build();
         mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azrUpdate, ORIGIN_APP,
-                "reason", SYSTEM_UID);
-        rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+                "reason", CUSTOM_PKG_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, CUSTOM_PKG_UID);
         assertThat(rule.getName()).isEqualTo("NewAppName");
 
         // The user modifies the name.
@@ -4544,7 +4554,7 @@
                 .build();
         mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azrUpdate,
                 ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID);
-        rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+        rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, CUSTOM_PKG_UID);
         assertThat(rule.getName()).isEqualTo("UserProvidedName");
 
         // The app is no longer able to modify the name.
@@ -4552,8 +4562,8 @@
                 .setName("NewAppName")
                 .build();
         mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azrUpdate, ORIGIN_APP,
-                "reason", SYSTEM_UID);
-        rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+                "reason", CUSTOM_PKG_UID);
+        rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, CUSTOM_PKG_UID);
         assertThat(rule.getName()).isEqualTo("UserProvidedName");
     }
 
@@ -4568,8 +4578,9 @@
                 .build();
         // Adds the rule using the app, to avoid having any user modified bits set.
         String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
-                mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID);
-        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+                mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", CUSTOM_PKG_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                CUSTOM_PKG_UID);
 
         // Modifies the filter, icon, zen policy, and device effects
         ZenPolicy policy = new ZenPolicy.Builder(rule.getZenPolicy())
@@ -4589,7 +4600,7 @@
         // Update the rule with the AZR from origin user.
         mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azrUpdate,
                 ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID);
-        rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+        rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, CUSTOM_PKG_UID);
 
         // UPDATE_ORIGIN_USER should change the bitmask and change the values.
         assertThat(rule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY);
@@ -4625,8 +4636,9 @@
                 .build();
         // Adds the rule using the app, to avoid having any user modified bits set.
         String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
-                mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID);
-        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+                mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", CUSTOM_PKG_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                CUSTOM_PKG_UID);
 
         // Modifies the icon, zen policy and device effects
         ZenPolicy policy = new ZenPolicy.Builder(rule.getZenPolicy())
@@ -4646,7 +4658,7 @@
         // Update the rule with the AZR from origin systemUI.
         mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azrUpdate, ORIGIN_SYSTEM,
                 "reason", SYSTEM_UID);
-        rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+        rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, CUSTOM_PKG_UID);
 
         // UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI should change the value but NOT update the bitmask.
         assertThat(rule.getIconResId()).isEqualTo(ICON_RES_ID);
@@ -4675,8 +4687,9 @@
                 .build();
         // Adds the rule using the app, to avoid having any user modified bits set.
         String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
-                mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID);
-        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+                mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", CUSTOM_PKG_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                CUSTOM_PKG_UID);
 
         ZenPolicy policy = new ZenPolicy.Builder()
                 .allowReminders(true)
@@ -4693,7 +4706,7 @@
         // Since the rule is not already user modified, UPDATE_ORIGIN_APP can modify the rule.
         // The bitmask is not modified.
         mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azrUpdate, ORIGIN_APP,
-                "reason", SYSTEM_UID);
+                "reason", CUSTOM_PKG_UID);
 
         ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
         assertThat(storedRule.userModifiedFields).isEqualTo(0);
@@ -4717,9 +4730,9 @@
         // Zen rule update coming from the app again. This cannot fully update the rule, because
         // the rule is already considered user modified.
         mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleIdUser, azrUpdate, ORIGIN_APP,
-                "reason", SYSTEM_UID);
+                "reason", CUSTOM_PKG_UID);
         AutomaticZenRule ruleUser = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT,
-                ruleIdUser);
+                ruleIdUser, CUSTOM_PKG_UID);
 
         // The app can only change the value if the rule is not already user modified,
         // so the rule is not changed, and neither is the bitmask.
@@ -4749,8 +4762,9 @@
                         .build())
                 .build();
         String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
-                mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID);
-        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+                mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", CUSTOM_PKG_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                CUSTOM_PKG_UID);
 
         // The values are modified but the bitmask is not.
         assertThat(rule.getZenPolicy().getPriorityCategoryReminders())
@@ -4771,7 +4785,7 @@
                 .build();
         // Adds the rule using the app, to avoid having any user modified bits set.
         String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
-                mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID);
+                mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", CUSTOM_PKG_UID);
 
         AutomaticZenRule azr = new AutomaticZenRule.Builder(azrBase)
                 // Sets Device Effects to null
@@ -4781,8 +4795,9 @@
         // Zen rule update coming from app, but since the rule isn't already
         // user modified, it can be updated.
         mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azr, ORIGIN_APP, "reason",
-                SYSTEM_UID);
-        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+                CUSTOM_PKG_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                CUSTOM_PKG_UID);
 
         // When AZR's ZenDeviceEffects is null, the updated rule's device effects are kept.
         assertThat(rule.getDeviceEffects()).isEqualTo(zde);
@@ -4797,8 +4812,7 @@
                 .build();
         // Adds the rule using the app, to avoid having any user modified bits set.
         String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
-                mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID);
-        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+                mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", CUSTOM_PKG_UID);
 
         AutomaticZenRule azr = new AutomaticZenRule.Builder(azrBase)
                 // Set zen policy to null
@@ -4808,8 +4822,9 @@
         // Zen rule update coming from app, but since the rule isn't already
         // user modified, it can be updated.
         mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azr, ORIGIN_APP, "reason",
-                SYSTEM_UID);
-        rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+                CUSTOM_PKG_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                CUSTOM_PKG_UID);
 
         // When AZR's ZenPolicy is null, we expect the updated rule's policy to be unchanged
         // (equivalent to the provided policy, with additional fields filled in with defaults).
@@ -4829,8 +4844,7 @@
                 .build();
         // Adds the rule using the app, to avoid having any user modified bits set.
         String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
-                mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID);
-        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+                mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", CUSTOM_PKG_UID);
 
         // Create a fully populated ZenPolicy.
         ZenPolicy policy = new ZenPolicy.Builder()
@@ -4860,7 +4874,8 @@
         // Default config defined in getDefaultConfigParser() is used as the original rule.
         mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azr,
                 ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID);
-        rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                CUSTOM_PKG_UID);
 
         // New ZenPolicy differs from the default config
         assertThat(rule.getZenPolicy()).isNotNull();
@@ -4890,8 +4905,9 @@
                 .build();
         // Adds the rule using the app, to avoid having any user modified bits set.
         String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
-                mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID);
-        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+                mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", CUSTOM_PKG_UID);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                CUSTOM_PKG_UID);
 
         ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder()
                 .setShouldDisplayGrayscale(true)
@@ -4903,7 +4919,7 @@
         // Applies the update to the rule.
         mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azr,
                 ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID);
-        rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+        rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, CUSTOM_PKG_UID);
 
         // New ZenDeviceEffects is used; all fields considered set, since previously were null.
         assertThat(rule.getDeviceEffects()).isNotNull();
@@ -5286,7 +5302,8 @@
         mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, update,
                 ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID);
 
-        AutomaticZenRule result = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+        AutomaticZenRule result = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                SYSTEM_UID);
         assertThat(result).isNotNull();
         assertThat(result.getOwner().getClassName()).isEqualTo("brand.new.cps");
     }
@@ -5306,7 +5323,8 @@
         mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, update,
                 ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID);
 
-        AutomaticZenRule result = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+        AutomaticZenRule result = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                CUSTOM_PKG_UID);
         assertThat(result).isNotNull();
         assertThat(result.getOwner().getClassName()).isEqualTo("old.third.party.cps");
     }
@@ -5518,8 +5536,8 @@
                 .build();
         String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
                 mContext.getPackageName(), rule, ORIGIN_APP, "add it", CUSTOM_PKG_UID);
-        assertThat(mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId).getCreationTime())
-                .isEqualTo(1000);
+        assertThat(mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                CUSTOM_PKG_UID).getCreationTime()).isEqualTo(1000);
 
         // User customizes it.
         AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule)
@@ -5546,7 +5564,7 @@
         // - ZenPolicy is the one that the user had set.
         // - rule still has the user-modified fields.
         AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT,
-                newRuleId);
+                newRuleId, CUSTOM_PKG_UID);
         assertThat(finalRule.getCreationTime()).isEqualTo(1000); // And not 3000.
         assertThat(newRuleId).isEqualTo(ruleId);
         assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS);
@@ -5575,8 +5593,8 @@
                 .build();
         String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
                 mContext.getPackageName(), rule, ORIGIN_APP, "add it", CUSTOM_PKG_UID);
-        assertThat(mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId).getCreationTime())
-                .isEqualTo(1000);
+        assertThat(mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                CUSTOM_PKG_UID).getCreationTime()).isEqualTo(1000);
 
         // App deletes it.
         mTestClock.advanceByMillis(1000);
@@ -5592,7 +5610,7 @@
 
         // Verify that the rule was recreated. This means id and creation time are new.
         AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT,
-                newRuleId);
+                newRuleId, CUSTOM_PKG_UID);
         assertThat(finalRule.getCreationTime()).isEqualTo(3000);
         assertThat(newRuleId).isNotEqualTo(ruleId);
     }
@@ -5609,8 +5627,8 @@
                 .build();
         String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
                 mContext.getPackageName(), rule, ORIGIN_APP, "add it", CUSTOM_PKG_UID);
-        assertThat(mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId).getCreationTime())
-                .isEqualTo(1000);
+        assertThat(mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId, CUSTOM_PKG_UID)
+                .getCreationTime()).isEqualTo(1000);
 
         // User customizes it.
         mTestClock.advanceByMillis(1000);
@@ -5637,7 +5655,7 @@
         // Verify that the rule was recreated. This means id and creation time are new, and the rule
         // matches the latest data supplied to addAZR.
         AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT,
-                newRuleId);
+                newRuleId, CUSTOM_PKG_UID);
         assertThat(finalRule.getCreationTime()).isEqualTo(4000);
         assertThat(newRuleId).isNotEqualTo(ruleId);
         assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY);
@@ -5660,8 +5678,8 @@
                 .build();
         String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT,
                 mContext.getPackageName(), rule, ORIGIN_APP, "add it", CUSTOM_PKG_UID);
-        assertThat(mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId).getCreationTime())
-                .isEqualTo(1000);
+        assertThat(mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                CUSTOM_PKG_UID).getCreationTime()).isEqualTo(1000);
 
         // User customizes it.
         mTestClock.advanceByMillis(1000);
@@ -5686,7 +5704,7 @@
 
         // Verify that the rule was recreated. This means id and creation time are new.
         AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT,
-                newRuleId);
+                newRuleId, CUSTOM_PKG_UID);
         assertThat(finalRule.getCreationTime()).isEqualTo(4000);
         assertThat(newRuleId).isNotEqualTo(ruleId);
     }
@@ -5728,7 +5746,7 @@
         // Verify that the rule was NOT restored:
         assertThat(newRuleId).isNotEqualTo(ruleId);
         AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT,
-                newRuleId);
+                newRuleId, CUSTOM_PKG_UID);
         assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS);
         assertThat(finalRule.getOwner()).isEqualTo(new ComponentName("second", "owner"));
 
@@ -5869,7 +5887,7 @@
         // The rule is restored...
         assertThat(newRuleId).isEqualTo(ruleId);
         AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT,
-                newRuleId);
+                newRuleId, CUSTOM_PKG_UID);
         assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS);
 
         // ... but it is NOT active
@@ -5923,7 +5941,7 @@
         // The rule is restored...
         assertThat(newRuleId).isEqualTo(ruleId);
         AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT,
-                newRuleId);
+                newRuleId, CUSTOM_PKG_UID);
         assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS);
 
         // ... but it is NEITHER active NOR snoozed.
@@ -6005,22 +6023,22 @@
                 ORIGIN_APP, "reasons", CUSTOM_PKG_UID);
 
         // Null condition -> STATE_FALSE
-        assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, id))
+        assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, id, CUSTOM_PKG_UID))
                 .isEqualTo(Condition.STATE_FALSE);
 
         mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, CONDITION_TRUE, ORIGIN_APP,
                 CUSTOM_PKG_UID);
-        assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, id))
+        assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, id, CUSTOM_PKG_UID))
                 .isEqualTo(Condition.STATE_TRUE);
 
         mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, CONDITION_FALSE, ORIGIN_APP,
                 CUSTOM_PKG_UID);
-        assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, id))
+        assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, id, CUSTOM_PKG_UID))
                 .isEqualTo(Condition.STATE_FALSE);
 
         mZenModeHelper.removeAutomaticZenRule(UserHandle.CURRENT, id, ORIGIN_APP, "",
                 CUSTOM_PKG_UID);
-        assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, id))
+        assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, id, CUSTOM_PKG_UID))
                 .isEqualTo(Condition.STATE_UNKNOWN);
     }
 
@@ -6036,8 +6054,8 @@
         mZenModeHelper.setConfig(config, null, ORIGIN_INIT, "", SYSTEM_UID);
         assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS);
 
-        assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, "systemRule"))
-                .isEqualTo(Condition.STATE_UNKNOWN);
+        assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, "systemRule",
+                CUSTOM_PKG_UID)).isEqualTo(Condition.STATE_UNKNOWN);
     }
 
     @Test
@@ -6063,7 +6081,7 @@
 
     @Test
     @EnableFlags(FLAG_MODES_API)
-    public void setAutomaticZenRuleState_conditionForNotOwnedRule_ignored() {
+    public void setAutomaticZenRuleStateFromConditionProvider_conditionForNotOwnedRule_ignored() {
         // Assume existence of an other-package-owned rule that is currently ACTIVE.
         assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
         ZenRule otherRule = newZenRule("another.package", Instant.now(), null);
@@ -6075,7 +6093,8 @@
         assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS);
 
         // Should be ignored.
-        mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, otherRule.conditionId,
+        mZenModeHelper.setAutomaticZenRuleStateFromConditionProvider(UserHandle.CURRENT,
+                otherRule.conditionId,
                 new Condition(otherRule.conditionId, "off", Condition.STATE_FALSE),
                 ORIGIN_APP, CUSTOM_PKG_UID);
 
@@ -6182,7 +6201,8 @@
                 .isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS);
 
         // From user, update that rule's interruption filter.
-        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                SYSTEM_UID);
         AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule)
                 .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
                 .build();
@@ -6214,7 +6234,8 @@
                 .isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS);
 
         // From user, update something in that rule, but not the interruption filter.
-        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                SYSTEM_UID);
         AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule)
                 .setName("Renamed")
                 .build();
@@ -6315,7 +6336,8 @@
         String ruleId = ZenModeConfig.implicitRuleId(mContext.getPackageName());
 
         // User chooses a new name.
-        AutomaticZenRule azr = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+        AutomaticZenRule azr = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                SYSTEM_UID);
         mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId,
                 new AutomaticZenRule.Builder(azr).setName("User chose this").build(),
                 ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID);
@@ -6414,7 +6436,8 @@
                 mZenModeHelper.mConfig.getZenPolicy()).allowMedia(true).build();
 
         // From user, update that rule's policy.
-        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                SYSTEM_UID);
         ZenPolicy userUpdateZenPolicy = new ZenPolicy.Builder().disallowAllSounds()
                 .allowAlarms(true).build();
         AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule)
@@ -6456,7 +6479,8 @@
                 mZenModeHelper.mConfig.getZenPolicy()).allowMedia(true).build();
 
         // From user, update something in that rule, but not the ZenPolicy.
-        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+        AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                SYSTEM_UID);
         AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule)
                 .setName("Rule renamed, not touching policy")
                 .build();
@@ -6509,7 +6533,8 @@
         String ruleId = ZenModeConfig.implicitRuleId(mContext.getPackageName());
 
         // User chooses a new name.
-        AutomaticZenRule azr = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId);
+        AutomaticZenRule azr = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId,
+                SYSTEM_UID);
         mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId,
                 new AutomaticZenRule.Builder(azr).setName("User chose this").build(),
                 ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID);
@@ -6645,7 +6670,7 @@
                 new AutomaticZenRule.Builder("Rule", CONDITION_ID).setIconResId(resourceId).build(),
                 ORIGIN_APP, "reason", CUSTOM_PKG_UID);
         AutomaticZenRule storedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT,
-                ruleId);
+                ruleId, CUSTOM_PKG_UID);
 
         assertThat(storedRule.getIconResId()).isEqualTo(0);
     }
@@ -7087,8 +7112,8 @@
                 ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID);
 
         implicitRule = getZenRule(implicitRuleId(CUSTOM_PKG_NAME));
-        assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, implicitRule.id))
-                .isEqualTo(STATE_TRUE);
+        assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, implicitRule.id,
+                CUSTOM_PKG_UID)).isEqualTo(STATE_TRUE);
         assertThat(implicitRule.isActive()).isTrue();
         assertThat(implicitRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE);
     }
@@ -7108,8 +7133,8 @@
                 ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID);
 
         implicitRule = getZenRule(implicitRuleId(CUSTOM_PKG_NAME));
-        assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, implicitRule.id))
-                .isEqualTo(STATE_FALSE);
+        assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, implicitRule.id,
+                CUSTOM_PKG_UID)).isEqualTo(STATE_FALSE);
         assertThat(implicitRule.isActive()).isFalse();
         assertThat(implicitRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE);
     }
@@ -7177,7 +7202,7 @@
         mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId,
                 new Condition(rule.getConditionId(), "manual-on", STATE_TRUE, SOURCE_USER_ACTION),
                 ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID);
-        assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId))
+        assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId, SYSTEM_UID))
                 .isEqualTo(STATE_TRUE);
         ZenRule zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
         assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_ACTIVATE);
@@ -7192,14 +7217,14 @@
         mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null);
 
         if (Flags.modesUi()) {
-            assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId))
-                    .isEqualTo(STATE_TRUE);
+            assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId,
+                    SYSTEM_UID)).isEqualTo(STATE_TRUE);
             zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
             assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_ACTIVATE);
             assertThat(zenRule.condition).isNull();
         } else {
-            assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId))
-                    .isEqualTo(STATE_FALSE);
+            assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId,
+                    SYSTEM_UID)).isEqualTo(STATE_FALSE);
         }
     }
 
@@ -7218,7 +7243,8 @@
         mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId,
                 new Condition(rule.getConditionId(), "snooze", STATE_FALSE, SOURCE_USER_ACTION),
                 ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID);
-        assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId))
+        assertThat(
+                mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CUSTOM_PKG_UID))
                 .isEqualTo(STATE_FALSE);
         ZenRule zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
         assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_DEACTIVATE);
@@ -7232,7 +7258,8 @@
         TypedXmlPullParser parser = getParserForByteStream(xmlBytes);
         mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null);
 
-        assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId))
+        assertThat(
+                mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CUSTOM_PKG_UID))
                 .isEqualTo(STATE_TRUE);
         zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
         assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE);
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/BasicToPwleSegmentAdapterTest.java b/services/tests/vibrator/src/com/android/server/vibrator/BasicToPwleSegmentAdapterTest.java
new file mode 100644
index 0000000..09f573c
--- /dev/null
+++ b/services/tests/vibrator/src/com/android/server/vibrator/BasicToPwleSegmentAdapterTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.vibrator;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.hardware.vibrator.IVibrator;
+import android.os.VibratorInfo;
+import android.os.vibrator.BasicPwleSegment;
+import android.os.vibrator.Flags;
+import android.os.vibrator.PwleSegment;
+import android.os.vibrator.StepSegment;
+import android.os.vibrator.VibrationEffectSegment;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.IntStream;
+
+public class BasicToPwleSegmentAdapterTest {
+
+    private static final float TEST_RESONANT_FREQUENCY = 150;
+    private static final float[] TEST_FREQUENCIES =
+            new float[]{90f, 120f, 150f, 60f, 30f, 210f, 270f, 300f, 240f, 180f};
+    private static final float[] TEST_OUTPUT_ACCELERATIONS =
+            new float[]{1.2f, 1.8f, 2.4f, 0.6f, 0.1f, 2.2f, 1.0f, 0.5f, 1.9f, 3.0f};
+
+    private static final VibratorInfo.FrequencyProfile TEST_FREQUENCY_PROFILE =
+            new VibratorInfo.FrequencyProfile(TEST_RESONANT_FREQUENCY, TEST_FREQUENCIES,
+                    TEST_OUTPUT_ACCELERATIONS);
+
+    private static final VibratorInfo.FrequencyProfile EMPTY_FREQUENCY_PROFILE =
+            new VibratorInfo.FrequencyProfile(TEST_RESONANT_FREQUENCY, null, null);
+
+    private BasicToPwleSegmentAdapter mAdapter;
+
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    @Before
+    public void setUp() throws Exception {
+        mAdapter = new BasicToPwleSegmentAdapter();
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testBasicPwleSegments_withFeatureFlagDisabled_returnsOriginalSegments() {
+        List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList(
+                //  startIntensity, endIntensity, startSharpness, endSharpness, duration
+                new BasicPwleSegment(0.2f, 0.8f, 0.2f, 0.4f, 20),
+                new BasicPwleSegment(0.8f, 0.2f, 0.4f, 0.5f, 100),
+                new BasicPwleSegment(0.2f, 0.65f, 0.5f, 0.5f, 50)));
+        List<VibrationEffectSegment> originalSegments = new ArrayList<>(segments);
+
+        VibratorInfo vibratorInfo = createVibratorInfo(
+                TEST_FREQUENCY_PROFILE, IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2);
+
+        assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ -1))
+                .isEqualTo(-1);
+        assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ 1))
+                .isEqualTo(1);
+
+        assertThat(segments).isEqualTo(originalSegments);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testBasicPwleSegments_noPwleCapability_returnsOriginalSegments() {
+        List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList(
+                //  startIntensity, endIntensity, startSharpness, endSharpness, duration
+                new BasicPwleSegment(0.2f, 0.8f, 0.2f, 0.4f, 20),
+                new BasicPwleSegment(0.8f, 0.2f, 0.4f, 0.5f, 100),
+                new BasicPwleSegment(0.2f, 0.65f, 0.5f, 0.5f, 50)));
+        List<VibrationEffectSegment> originalSegments = new ArrayList<>(segments);
+
+        VibratorInfo vibratorInfo = createVibratorInfo(TEST_FREQUENCY_PROFILE);
+
+        assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ -1))
+                .isEqualTo(-1);
+        assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ 1))
+                .isEqualTo(1);
+
+        assertThat(segments).isEqualTo(originalSegments);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testBasicPwleSegments_invalidFrequencyProfile_returnsOriginalSegments() {
+        List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList(
+                //  startIntensity, endIntensity, startSharpness, endSharpness, duration
+                new BasicPwleSegment(0.2f, 0.8f, 0.2f, 0.4f, 20),
+                new BasicPwleSegment(0.8f, 0.2f, 0.4f, 0.5f, 100),
+                new BasicPwleSegment(0.2f, 0.65f, 0.5f, 0.5f, 50)));
+        List<VibrationEffectSegment> originalSegments = new ArrayList<>(segments);
+        VibratorInfo vibratorInfo = createVibratorInfo(
+                EMPTY_FREQUENCY_PROFILE, IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2);
+
+        assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ -1))
+                .isEqualTo(-1);
+        assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ 1))
+                .isEqualTo(1);
+
+        assertThat(segments).isEqualTo(originalSegments);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testBasicPwleSegments_withPwleCapability_adaptSegmentsCorrectly() {
+        List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList(
+                new StepSegment(/* amplitude= */ 1, /* frequencyHz= */ 40f, /* duration= */ 100),
+                //  startIntensity, endIntensity, startSharpness, endSharpness, duration
+                new BasicPwleSegment(0.0f, 1.0f, 0.0f, 1.0f, 100),
+                new BasicPwleSegment(0.0f, 1.0f, 0.0f, 1.0f, 100),
+                new BasicPwleSegment(0.0f, 1.0f, 0.0f, 1.0f, 100)));
+        List<VibrationEffectSegment> expectedSegments = Arrays.asList(
+                new StepSegment(/* amplitude= */ 1, /* frequencyHz= */ 40f, /* duration= */ 100),
+                //  startAmplitude, endAmplitude, startFrequencyHz, endFrequencyHz, duration
+                new PwleSegment(0.0f, 1.0f, 30.0f, 300.0f, 100),
+                new PwleSegment(0.0f, 1.0f, 30.0f, 300.0f, 100),
+                new PwleSegment(0.0f, 1.0f, 30.0f, 300.0f, 100));
+        VibratorInfo vibratorInfo = createVibratorInfo(
+                TEST_FREQUENCY_PROFILE, IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2);
+
+        assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ 1))
+                .isEqualTo(1);
+
+        assertThat(segments).isEqualTo(expectedSegments);
+    }
+
+    private static VibratorInfo createVibratorInfo(VibratorInfo.FrequencyProfile frequencyProfile,
+            int... capabilities) {
+        return new VibratorInfo.Builder(0)
+                .setCapabilities(IntStream.of(capabilities).reduce((a, b) -> a | b).orElse(0))
+                .setFrequencyProfile(frequencyProfile)
+                .build();
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
index 9d4d94b..85ef466 100644
--- a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
@@ -758,6 +758,18 @@
     }
 
     @Test
+    @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_VOICE_ACCESS_KEY_GESTURES)
+    public void testKeyGestureToggleVoiceAccess() {
+        Assert.assertTrue(
+                sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS));
+        mPhoneWindowManager.assertVoiceAccess(true);
+
+        Assert.assertTrue(
+                sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS));
+        mPhoneWindowManager.assertVoiceAccess(false);
+    }
+
+    @Test
     public void testKeyGestureToggleDoNotDisturb() {
         mPhoneWindowManager.overrideZenMode(Settings.Global.ZEN_MODE_OFF);
         Assert.assertTrue(
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index 6c48ba2..4ff3d43 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -201,6 +201,8 @@
     private boolean mIsTalkBackEnabled;
     private boolean mIsTalkBackShortcutGestureEnabled;
 
+    private boolean mIsVoiceAccessEnabled;
+
     private Intent mBrowserIntent;
     private Intent mSmsIntent;
 
@@ -225,6 +227,18 @@
         }
     }
 
+    private class TestVoiceAccessShortcutController extends VoiceAccessShortcutController {
+        TestVoiceAccessShortcutController(Context context) {
+            super(context);
+        }
+
+        @Override
+        boolean toggleVoiceAccess(int currentUserId) {
+            mIsVoiceAccessEnabled = !mIsVoiceAccessEnabled;
+            return mIsVoiceAccessEnabled;
+        }
+    }
+
     private class TestInjector extends PhoneWindowManager.Injector {
         TestInjector(Context context, WindowManagerPolicy.WindowManagerFuncs funcs) {
             super(context, funcs);
@@ -260,6 +274,10 @@
             return new TestTalkbackShortcutController(mContext);
         }
 
+        VoiceAccessShortcutController getVoiceAccessShortcutController() {
+            return new TestVoiceAccessShortcutController(mContext);
+        }
+
         WindowWakeUpPolicy getWindowWakeUpPolicy() {
             return mWindowWakeUpPolicy;
         }
@@ -1024,6 +1042,11 @@
         Assert.assertEquals(expectEnabled, mIsTalkBackEnabled);
     }
 
+    void assertVoiceAccess(boolean expectEnabled) {
+        mTestLooper.dispatchAll();
+        Assert.assertEquals(expectEnabled, mIsVoiceAccessEnabled);
+    }
+
     void assertKeyGestureEventSentToKeyGestureController(int gestureType) {
         verify(mInputManagerInternal)
                 .handleKeyGestureInKeyGestureController(anyInt(), any(), anyInt(), eq(gestureType));
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 c9cbe0f..6fad82b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -210,7 +210,7 @@
     }
 
     private TestStartingWindowOrganizer registerTestStartingWindowOrganizer() {
-        return new TestStartingWindowOrganizer(mAtm);
+        return new TestStartingWindowOrganizer(mAtm, mDisplayContent);
     }
 
     @Test
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 9d191ce..a0727a7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java
@@ -335,7 +335,7 @@
         }
 
         private AppCompatOrientationOverrides getTopOrientationOverrides() {
-            return activity().top().mAppCompatController.getAppCompatOrientationOverrides();
+            return activity().top().mAppCompatController.getOrientationOverrides();
         }
     }
 }
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 a21ab5d..4faa714 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
@@ -601,7 +601,7 @@
         }
 
         private AppCompatOrientationOverrides getTopOrientationOverrides() {
-            return activity().top().mAppCompatController.getAppCompatOrientationOverrides();
+            return activity().top().mAppCompatController.getOrientationOverrides();
         }
 
         private AppCompatOrientationPolicy getTopAppCompatOrientationPolicy() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java
index 463254c..50419d4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java
@@ -159,8 +159,8 @@
         @Override
         void onPostActivityCreation(@NonNull ActivityRecord activity) {
             super.onPostActivityCreation(activity);
-            spyOn(activity.mAppCompatController.getAppCompatReachabilityOverrides());
-            activity.mAppCompatController.getAppCompatReachabilityPolicy()
+            spyOn(activity.mAppCompatController.getReachabilityOverrides());
+            activity.mAppCompatController.getReachabilityPolicy()
                     .setLetterboxInnerBoundsSupplier(mLetterboxInnerBoundsSupplier);
         }
 
@@ -196,7 +196,7 @@
 
         @NonNull
         private AppCompatReachabilityOverrides getAppCompatReachabilityOverrides() {
-            return activity().top().mAppCompatController.getAppCompatReachabilityOverrides();
+            return activity().top().mAppCompatController.getReachabilityOverrides();
         }
 
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityPolicyTest.java
index ddc4de9..09b8bce 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityPolicyTest.java
@@ -246,8 +246,8 @@
         @Override
         void onPostActivityCreation(@NonNull ActivityRecord activity) {
             super.onPostActivityCreation(activity);
-            spyOn(activity.mAppCompatController.getAppCompatReachabilityOverrides());
-            activity.mAppCompatController.getAppCompatReachabilityPolicy()
+            spyOn(activity.mAppCompatController.getReachabilityOverrides());
+            activity.mAppCompatController.getReachabilityPolicy()
                     .setLetterboxInnerBoundsSupplier(mLetterboxInnerBoundsSupplier);
         }
 
@@ -281,12 +281,12 @@
 
         @NonNull
         private AppCompatReachabilityOverrides getAppCompatReachabilityOverrides() {
-            return activity().top().mAppCompatController.getAppCompatReachabilityOverrides();
+            return activity().top().mAppCompatController.getReachabilityOverrides();
         }
 
         @NonNull
         private AppCompatReachabilityPolicy getAppCompatReachabilityPolicy() {
-            return activity().top().mAppCompatController.getAppCompatReachabilityPolicy();
+            return activity().top().mAppCompatController.getReachabilityPolicy();
         }
 
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatResizeOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatResizeOverridesTest.java
index b8d554b..98a4fb3c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatResizeOverridesTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatResizeOverridesTest.java
@@ -184,12 +184,12 @@
 
         void checkShouldOverrideForceResizeApp(boolean expected) {
             Assert.assertEquals(expected, activity().top().mAppCompatController
-                    .getAppCompatResizeOverrides().shouldOverrideForceResizeApp());
+                    .getResizeOverrides().shouldOverrideForceResizeApp());
         }
 
         void checkShouldOverrideForceNonResizeApp(boolean expected) {
             Assert.assertEquals(expected, activity().top().mAppCompatController
-                    .getAppCompatResizeOverrides().shouldOverrideForceNonResizeApp());
+                    .getResizeOverrides().shouldOverrideForceNonResizeApp());
         }
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
index b6e393d..03d9042 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
@@ -342,8 +342,8 @@
     public void testCancelRemoteAnimationWhenFreeze() {
         final DisplayContent dc = createNewDisplay(Display.STATE_ON);
         doReturn(false).when(dc).onDescendantOrientationChanged(any());
-        final WindowState exitingAppWindow = createWindow(null /* parent */, TYPE_BASE_APPLICATION,
-                dc, "exiting app");
+        final WindowState exitingAppWindow = newWindowBuilder("exiting app",
+                TYPE_BASE_APPLICATION).setDisplay(dc).build();
         final ActivityRecord exitingActivity = exitingAppWindow.mActivityRecord;
         // Wait until everything in animation handler get executed to prevent the exiting window
         // from being removed during WindowSurfacePlacer Traversal.
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
index 14276ae..7033d79 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
@@ -266,10 +266,10 @@
         mSetFlagsRule.enableFlags(Flags.FLAG_WAIT_FOR_TRANSITION_ON_DISPLAY_SWITCH);
         prepareSecondaryDisplay();
 
-        final WindowState defaultDisplayWindow = createWindow(/* parent= */ null,
-                TYPE_BASE_APPLICATION, mDisplayContent, "DefaultDisplayWindow");
-        final WindowState secondaryDisplayWindow = createWindow(/* parent= */ null,
-                TYPE_BASE_APPLICATION, mSecondaryDisplayContent, "SecondaryDisplayWindow");
+        final WindowState defaultDisplayWindow = newWindowBuilder("DefaultDisplayWindow",
+                TYPE_BASE_APPLICATION).setDisplay(mDisplayContent).build();
+        final WindowState secondaryDisplayWindow = newWindowBuilder("SecondaryDisplayWindow",
+                TYPE_BASE_APPLICATION).setDisplay(mSecondaryDisplayContent).build();
         makeWindowVisibleAndNotDrawn(defaultDisplayWindow, secondaryDisplayWindow);
 
         // Mark as display switching only for the default display as we filter out
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 5486aa3..dfd10ec 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -1183,6 +1183,18 @@
         assertEquals(prev, mDisplayContent.getLastOrientationSource());
         // The top will use the rotation from "prev" with fixed rotation.
         assertTrue(top.hasFixedRotationTransform());
+
+        mDisplayContent.continueUpdateOrientationForDiffOrienLaunchingApp();
+        assertFalse(top.hasFixedRotationTransform());
+
+        // Assume that the requested orientation of "prev" is landscape. And the display is also
+        // rotated to landscape. The activities from bottom to top are TaskB{"prev, "behindTop"},
+        // TaskB{"top"}. Then "behindTop" should also get landscape according to ORIENTATION_BEHIND
+        // instead of resolving as undefined which causes to unexpected fixed portrait rotation.
+        final ActivityRecord behindTop = new ActivityBuilder(mAtm).setTask(prev.getTask())
+                .setOnTop(false).setScreenOrientation(SCREEN_ORIENTATION_BEHIND).build();
+        mDisplayContent.applyFixedRotationForNonTopVisibleActivityIfNeeded(behindTop);
+        assertFalse(behindTop.hasFixedRotationTransform());
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
index ea925c0..4854f0d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
@@ -88,7 +88,7 @@
     }
 
     private WindowState createDreamWindow() {
-        final WindowState win = createDreamWindow(null, TYPE_BASE_APPLICATION, "dream");
+        final WindowState win = createDreamWindow("dream", TYPE_BASE_APPLICATION);
         final WindowManager.LayoutParams attrs = win.mAttrs;
         attrs.width = MATCH_PARENT;
         attrs.height = MATCH_PARENT;
diff --git a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
index de4b6fa..dc16de1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
@@ -34,6 +34,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
 import static com.android.server.wm.DragDropController.MSG_UNHANDLED_DROP_LISTENER_TIMEOUT;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
@@ -42,16 +43,19 @@
 import static org.junit.Assume.assumeTrue;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
+import android.annotation.Nullable;
 import android.app.PendingIntent;
 import android.content.ClipData;
 import android.content.ClipDescription;
 import android.content.Intent;
 import android.content.pm.ShortcutServiceInternal;
 import android.graphics.PixelFormat;
+import android.graphics.Rect;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
@@ -60,6 +64,7 @@
 import android.os.Parcelable;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
 import android.view.DragEvent;
 import android.view.InputChannel;
@@ -74,6 +79,7 @@
 
 import com.android.server.LocalServices;
 import com.android.server.pm.UserManagerInternal;
+import com.android.window.flags.Flags;
 
 import org.junit.After;
 import org.junit.AfterClass;
@@ -87,6 +93,7 @@
 import java.util.ArrayList;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
 
 /**
  * Tests for the {@link DragDropController} class.
@@ -141,17 +148,28 @@
         }
     }
 
+    private WindowState createDropTargetWindow(String name) {
+        return createDropTargetWindow(name, null /* targetDisplay */);
+    }
+
     /**
      * Creates a window state which can be used as a drop target.
      */
-    private WindowState createDropTargetWindow(String name, int ownerId) {
-        final Task task = new TaskBuilder(mSupervisor).setUserId(ownerId).build();
-        final ActivityRecord activity = new ActivityBuilder(mAtm).setTask(task).setUseProcess(
-                mProcess).build();
+    private WindowState createDropTargetWindow(String name,
+            @Nullable DisplayContent targetDisplay) {
+        final WindowState window;
+        if (targetDisplay == null) {
+            final Task task = new TaskBuilder(mSupervisor).build();
+            final ActivityRecord activity = new ActivityBuilder(mAtm).setTask(task).setUseProcess(
+                    mProcess).build();
+            window = newWindowBuilder(name, TYPE_BASE_APPLICATION).setWindowToken(
+                    activity).setClientWindow(new TestIWindow()).build();
+        } else {
+            window = newWindowBuilder(name, TYPE_BASE_APPLICATION).setDisplay(
+                    targetDisplay).setClientWindow(new TestIWindow()).build();
+        }
 
         // Use a new TestIWindow so we don't collect events for other windows
-        final WindowState window = newWindowBuilder(name, TYPE_BASE_APPLICATION).setWindowToken(
-                activity).setOwnerId(ownerId).setClientWindow(new TestIWindow()).build();
         InputChannel channel = new InputChannel();
         window.openInputChannel(channel);
         window.mHasSurface = true;
@@ -174,7 +192,7 @@
     public void setUp() throws Exception {
         mTarget = new TestDragDropController(mWm, mWm.mH.getLooper());
         mProcess = mSystemServicesTestRule.addProcess(TEST_PACKAGE, "testProc", TEST_PID, TEST_UID);
-        mWindow = createDropTargetWindow("Drag test window", 0);
+        mWindow = createDropTargetWindow("Drag test window");
         doReturn(mWindow).when(mDisplayContent).getTouchableWinAtPointLocked(0, 0);
         when(mWm.mInputManager.startDragAndDrop(any(IBinder.class), any(IBinder.class))).thenReturn(
                 true);
@@ -239,7 +257,7 @@
         iwindow.setDragEventJournal(dragEvents);
 
         startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ,
-                ClipData.newPlainText("label", "text"), () -> {
+                ClipData.newPlainText("label", "text"), (unused) -> {
                     // Verify the start-drag event is sent for invisible windows
                     final DragEvent dragEvent = dragEvents.get(0);
                     assertTrue(dragEvent.getAction() == ACTION_DRAG_STARTED);
@@ -263,8 +281,8 @@
 
     @Test
     public void testPrivateInterceptGlobalDragDropIgnoresNonLocalWindows() {
-        WindowState nonLocalWindow = createDropTargetWindow("App drag test window", 0);
-        WindowState globalInterceptWindow = createDropTargetWindow("Global drag test window", 0);
+        WindowState nonLocalWindow = createDropTargetWindow("App drag test window");
+        WindowState globalInterceptWindow = createDropTargetWindow("Global drag test window");
         globalInterceptWindow.mAttrs.privateFlags |= PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP;
 
         // Necessary for now since DragState.sendDragStartedLocked() will recycle drag events
@@ -281,7 +299,7 @@
         globalInterceptIWindow.setDragEventJournal(globalInterceptWindowDragEvents);
 
         startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ,
-                createClipDataForActivity(null, mock(UserHandle.class)), () -> {
+                createClipDataForActivity(null, mock(UserHandle.class)), (unused) -> {
                     // Verify the start-drag event is sent for the local and global intercept window
                     // but not the other window
                     assertTrue(nonLocalWindowDragEvents.isEmpty());
@@ -324,7 +342,7 @@
         iwindow.setDragEventJournal(dragEvents);
 
         startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG,
-                ClipData.newPlainText("label", "text"), () -> {
+                ClipData.newPlainText("label", "text"), (unused) -> {
                     // Verify the start-drag event has the drag flags
                     final DragEvent dragEvent = dragEvents.get(0);
                     assertTrue(dragEvent.getAction() == ACTION_DRAG_STARTED);
@@ -347,6 +365,184 @@
                 });
     }
 
+    @Test
+    public void testDragEventCoordinates() {
+        int dragStartX = mWindow.getBounds().centerX();
+        int dragStartY = mWindow.getBounds().centerY();
+        int startOffsetPx = 10;
+        int dropCoordsPx = 15;
+        WindowState window2 = createDropTargetWindow("App drag test window");
+        Rect bounds = new Rect(dragStartX + startOffsetPx, dragStartY + startOffsetPx,
+                mWindow.getBounds().right, mWindow.getBounds().bottom);
+        window2.setBounds(bounds);
+        window2.getFrame().set(bounds);
+
+        // Necessary for now since DragState.sendDragStartedLocked() will recycle drag events
+        // immediately after dispatching, which is a problem when using mockito arguments captor
+        // because it returns and modifies the same drag event.
+        TestIWindow iwindow = (TestIWindow) mWindow.mClient;
+        final ArrayList<DragEvent> dragEvents = new ArrayList<>();
+        iwindow.setDragEventJournal(dragEvents);
+        TestIWindow iwindow2 = (TestIWindow) window2.mClient;
+        final ArrayList<DragEvent> dragEvents2 = new ArrayList<>();
+        iwindow2.setDragEventJournal(dragEvents2);
+
+        startDrag(dragStartX, dragStartY, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ,
+                ClipData.newPlainText("label", "text"), (unused) -> {
+                    // Verify the start-drag event is sent as-is for the drag origin window.
+                    final DragEvent dragEvent = dragEvents.get(0);
+                    assertEquals(ACTION_DRAG_STARTED, dragEvent.getAction());
+                    assertEquals(dragStartX, dragEvent.getX(), 0.0 /* delta */);
+                    assertEquals(dragStartY, dragEvent.getY(), 0.0 /* delta */);
+                    // Verify the start-drag event is sent relative to the window top-left.
+                    final DragEvent dragEvent2 = dragEvents2.get(0);
+                    assertEquals(ACTION_DRAG_STARTED, dragEvent2.getAction());
+                    assertEquals(-startOffsetPx, dragEvent2.getX(),  0.0 /* delta */);
+                    assertEquals(-startOffsetPx, dragEvent2.getY(), 0.0 /* delta */);
+
+                    try {
+                        mTarget.mDeferDragStateClosed = true;
+                        // x, y is window-local coordinate.
+                        mTarget.reportDropWindow(window2.mInputChannelToken, dropCoordsPx,
+                                dropCoordsPx);
+                        mTarget.handleMotionEvent(false, window2.getDisplayId(), dropCoordsPx,
+                                dropCoordsPx);
+                        mToken = window2.mClient.asBinder();
+                        // Verify only window2 received the DROP event and coords are sent as-is.
+                        assertEquals(1, dragEvents.size());
+                        assertEquals(2, dragEvents2.size());
+                        final DragEvent dropEvent = last(dragEvents2);
+                        assertEquals(ACTION_DROP, dropEvent.getAction());
+                        assertEquals(dropCoordsPx, dropEvent.getX(),  0.0 /* delta */);
+                        assertEquals(dropCoordsPx, dropEvent.getY(),  0.0 /* delta */);
+                        assertEquals(window2.getDisplayId(), dropEvent.getDisplayId());
+
+                        mTarget.reportDropResult(iwindow2, true);
+                        // Verify both windows received ACTION_DRAG_ENDED event.
+                        assertEquals(ACTION_DRAG_ENDED, last(dragEvents).getAction());
+                        assertEquals(window2.getDisplayId(), last(dragEvents).getDisplayId());
+                        assertEquals(ACTION_DRAG_ENDED, last(dragEvents2).getAction());
+                        assertEquals(window2.getDisplayId(), last(dragEvents2).getDisplayId());
+                    } finally {
+                        mTarget.mDeferDragStateClosed = false;
+                    }
+                });
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_DND)
+    public void testDragEventConnectedDisplaysCoordinates() {
+        final DisplayContent testDisplay = createMockSimulatedDisplay();
+        int dragStartX = mWindow.getBounds().centerX();
+        int dragStartY = mWindow.getBounds().centerY();
+        int dropCoordsPx = 15;
+        WindowState window2 = createDropTargetWindow("App drag test window", testDisplay);
+
+        // Necessary for now since DragState.sendDragStartedLocked() will recycle drag events
+        // immediately after dispatching, which is a problem when using mockito arguments captor
+        // because it returns and modifies the same drag event.
+        TestIWindow iwindow = (TestIWindow) mWindow.mClient;
+        final ArrayList<DragEvent> dragEvents = new ArrayList<>();
+        iwindow.setDragEventJournal(dragEvents);
+        TestIWindow iwindow2 = (TestIWindow) window2.mClient;
+        final ArrayList<DragEvent> dragEvents2 = new ArrayList<>();
+        iwindow2.setDragEventJournal(dragEvents2);
+
+        startDrag(dragStartX, dragStartY, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ,
+                ClipData.newPlainText("label", "text"), (unused) -> {
+                    // Verify the start-drag event is sent as-is for the drag origin window.
+                    final DragEvent dragEvent = dragEvents.get(0);
+                    assertEquals(ACTION_DRAG_STARTED, dragEvent.getAction());
+                    assertEquals(dragStartX, dragEvent.getX(), 0.0 /* delta */);
+                    assertEquals(dragStartY, dragEvent.getY(), 0.0 /* delta */);
+                    // Verify the start-drag event from different display is sent out of display
+                    // bounds.
+                    final DragEvent dragEvent2 = dragEvents2.get(0);
+                    assertEquals(ACTION_DRAG_STARTED, dragEvent2.getAction());
+                    assertEquals(-window2.getBounds().left - 1, dragEvent2.getX(), 0.0 /* delta */);
+                    assertEquals(-window2.getBounds().top - 1, dragEvent2.getY(), 0.0 /* delta */);
+
+                    try {
+                        mTarget.mDeferDragStateClosed = true;
+                        mTarget.handleMotionEvent(true, testDisplay.getDisplayId(), dropCoordsPx,
+                                dropCoordsPx);
+                        // x, y is window-local coordinate.
+                        mTarget.reportDropWindow(window2.mInputChannelToken, dropCoordsPx,
+                                dropCoordsPx);
+                        mTarget.handleMotionEvent(false, testDisplay.getDisplayId(), dropCoordsPx,
+                                dropCoordsPx);
+                        mToken = window2.mClient.asBinder();
+                        // Verify only window2 received the DROP event and coords are sent as-is
+                        assertEquals(1, dragEvents.size());
+                        assertEquals(2, dragEvents2.size());
+                        final DragEvent dropEvent = last(dragEvents2);
+                        assertEquals(ACTION_DROP, dropEvent.getAction());
+                        assertEquals(dropCoordsPx, dropEvent.getX(),  0.0 /* delta */);
+                        assertEquals(dropCoordsPx, dropEvent.getY(),  0.0 /* delta */);
+                        assertEquals(testDisplay.getDisplayId(), dropEvent.getDisplayId());
+
+                        mTarget.reportDropResult(iwindow2, true);
+                        // Verify both windows received ACTION_DRAG_ENDED event.
+                        assertEquals(ACTION_DRAG_ENDED, last(dragEvents).getAction());
+                        assertEquals(testDisplay.getDisplayId(), last(dragEvents).getDisplayId());
+                        assertEquals(ACTION_DRAG_ENDED, last(dragEvents2).getAction());
+                        assertEquals(testDisplay.getDisplayId(), last(dragEvents2).getDisplayId());
+                    } finally {
+                        mTarget.mDeferDragStateClosed = false;
+                    }
+                });
+    }
+
+    @Test
+    public void testDragMove() {
+        startDrag(0, 0, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ,
+                ClipData.newPlainText("label", "text"), (surface) -> {
+                    int dragMoveX = mWindow.getBounds().centerX();
+                    int dragMoveY = mWindow.getBounds().centerY();
+                    final SurfaceControl.Transaction transaction =
+                            mSystemServicesTestRule.mTransaction;
+                    clearInvocations(transaction);
+
+                    mTarget.handleMotionEvent(true, mWindow.getDisplayId(), dragMoveX, dragMoveY);
+                    verify(transaction).setPosition(surface, dragMoveX, dragMoveY);
+
+                    // Clean-up.
+                    mTarget.reportDropWindow(mWindow.mInputChannelToken, 0, 0);
+                    mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), 0,
+                            0);
+                    mToken = mWindow.mClient.asBinder();
+                });
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_DND)
+    public void testConnectedDisplaysDragMoveToOtherDisplay() {
+        final float testDensityMultiplier = 1.5f;
+        final DisplayContent testDisplay = createMockSimulatedDisplay();
+        testDisplay.mBaseDisplayDensity =
+                (int) (mDisplayContent.mBaseDisplayDensity * testDensityMultiplier);
+        WindowState testWindow = createDropTargetWindow("App drag test window", testDisplay);
+
+        // Test starts from mWindow which is on default display.
+        startDrag(0, 0, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ,
+                ClipData.newPlainText("label", "text"), (surface) -> {
+                    final SurfaceControl.Transaction transaction =
+                            mSystemServicesTestRule.mTransaction;
+                    clearInvocations(transaction);
+                    mTarget.handleMotionEvent(true, testWindow.getDisplayId(), 0, 0);
+
+                    verify(transaction).reparent(surface, testDisplay.getSurfaceControl());
+                    verify(transaction).setScale(surface, testDensityMultiplier,
+                            testDensityMultiplier);
+
+                    // Clean-up.
+                    mTarget.reportDropWindow(mWindow.mInputChannelToken, 0, 0);
+                    mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), 0,
+                            0);
+                    mToken = mWindow.mClient.asBinder();
+                });
+    }
+
     private DragEvent last(ArrayList<DragEvent> list) {
         return list.get(list.size() - 1);
     }
@@ -503,7 +699,7 @@
 
     @Test
     public void testRequestSurfaceForReturnAnimationFlag_dropSuccessful() {
-        WindowState otherWindow = createDropTargetWindow("App drag test window", 0);
+        WindowState otherWindow = createDropTargetWindow("App drag test window");
         TestIWindow otherIWindow = (TestIWindow) otherWindow.mClient;
 
         // Necessary for now since DragState.sendDragStartedLocked() will recycle drag events
@@ -515,7 +711,7 @@
 
         startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ
                         | View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION,
-                ClipData.newPlainText("label", "text"), () -> {
+                ClipData.newPlainText("label", "text"), (unused) -> {
                     assertTrue(dragEvents.get(0).getAction() == ACTION_DRAG_STARTED);
 
                     // Verify after consuming that the drag surface is relinquished
@@ -534,7 +730,7 @@
 
     @Test
     public void testRequestSurfaceForReturnAnimationFlag_dropUnsuccessful() {
-        WindowState otherWindow = createDropTargetWindow("App drag test window", 0);
+        WindowState otherWindow = createDropTargetWindow("App drag test window");
         TestIWindow otherIWindow = (TestIWindow) otherWindow.mClient;
 
         // Necessary for now since DragState.sendDragStartedLocked() will recycle drag events
@@ -546,7 +742,7 @@
 
         startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ
                         | View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION,
-                ClipData.newPlainText("label", "text"), () -> {
+                ClipData.newPlainText("label", "text"), (unused) -> {
                     assertTrue(dragEvents.get(0).getAction() == ACTION_DRAG_STARTED);
 
                     // Verify after consuming that the drag surface is relinquished
@@ -583,7 +779,7 @@
         mTarget.setGlobalDragListener(listener);
         final int invalidXY = 100_000;
         startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG,
-                ClipData.newPlainText("label", "Test"), () -> {
+                ClipData.newPlainText("label", "Test"), (unused) -> {
                     // Trigger an unhandled drop and verify the global drag listener was called
                     mTarget.reportDropWindow(mWindow.mInputChannelToken, invalidXY, invalidXY);
                     mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(),
@@ -608,7 +804,7 @@
         mTarget.setGlobalDragListener(listener);
         final int invalidXY = 100_000;
         startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG,
-                ClipData.newPlainText("label", "Test"), () -> {
+                ClipData.newPlainText("label", "Test"), (unused) -> {
                     // Trigger an unhandled drop and verify the global drag listener was called
                     mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY);
                     mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(),
@@ -631,7 +827,7 @@
         doReturn(mock(Binder.class)).when(listener).asBinder();
         mTarget.setGlobalDragListener(listener);
         final int invalidXY = 100_000;
-        startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), () -> {
+        startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), (unused) -> {
             // Trigger an unhandled drop and verify the global drag listener was not called
             mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY);
             mTarget.handleMotionEvent(false /* keepHandling */, mDisplayContent.getDisplayId(),
@@ -654,7 +850,7 @@
         mTarget.setGlobalDragListener(listener);
         final int invalidXY = 100_000;
         startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG,
-                ClipData.newPlainText("label", "Test"), () -> {
+                ClipData.newPlainText("label", "Test"), (unused) -> {
                     // Trigger an unhandled drop and verify the global drag listener was called
                     mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY);
                     mTarget.handleMotionEvent(false /* keepHandling */,
@@ -675,7 +871,7 @@
     }
 
     private void doDragAndDrop(int flags, ClipData data, float dropX, float dropY) {
-        startDrag(flags, data, () -> {
+        startDrag(flags, data, (unused) -> {
             mTarget.reportDropWindow(mWindow.mInputChannelToken, dropX, dropY);
             mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), dropX,
                     dropY);
@@ -686,19 +882,26 @@
     /**
      * Starts a drag with the given parameters, calls Runnable `r` after drag is started.
      */
-    private void startDrag(int flag, ClipData data, Runnable r) {
+    private void startDrag(int flag, ClipData data, Consumer<SurfaceControl> c) {
+        startDrag(0, 0, flag, data, c);
+    }
+
+    /**
+     * Starts a drag with the given parameters, calls Runnable `r` after drag is started.
+     */
+    private void startDrag(float startInWindowX, float startInWindowY, int flag, ClipData data,
+            Consumer<SurfaceControl> c) {
         final SurfaceSession appSession = new SurfaceSession();
         try {
             final SurfaceControl surface = new SurfaceControl.Builder(appSession).setName(
                     "drag surface").setBufferSize(100, 100).setFormat(
                     PixelFormat.TRANSLUCENT).build();
-
             assertTrue(mWm.mInputManager.startDragAndDrop(new Binder(), new Binder()));
-            mToken = mTarget.performDrag(TEST_PID, 0, mWindow.mClient, flag, surface, 0, 0, 0, 0, 0,
-                    0, 0, data);
+            mToken = mTarget.performDrag(TEST_PID, 0, mWindow.mClient, flag, surface, 0, 0, 0,
+                    startInWindowX, startInWindowY, 0, 0, data);
             assertNotNull(mToken);
 
-            r.run();
+            c.accept(surface);
         } finally {
             appSession.kill();
         }
diff --git a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java
index bd15bc4..347d1bc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java
@@ -379,13 +379,11 @@
         assertThat(imeContainer.getRootDisplayArea()).isEqualTo(mDisplay);
         assertThat(mDisplay.findAreaForTokenInLayer(imeToken)).isEqualTo(imeContainer);
 
-        final WindowState firstActivityWin =
-                createWindow(null /* parent */, TYPE_APPLICATION_STARTING, mFirstActivity,
-                        "firstActivityWin");
+        final WindowState firstActivityWin = newWindowBuilder("firstActivityWin",
+                TYPE_APPLICATION_STARTING).setWindowToken(mFirstActivity).build();
         spyOn(firstActivityWin);
-        final WindowState secondActivityWin =
-                createWindow(null /* parent */, TYPE_APPLICATION_STARTING, mSecondActivity,
-                        "firstActivityWin");
+        final WindowState secondActivityWin = newWindowBuilder("secondActivityWin",
+                TYPE_APPLICATION_STARTING).setWindowToken(mSecondActivity).build();
         spyOn(secondActivityWin);
 
         // firstActivityWin should be the target
@@ -424,13 +422,11 @@
         setupImeWindow();
         final DisplayArea.Tokens imeContainer = mDisplay.getImeContainer();
         final WindowToken imeToken = tokenOfType(TYPE_INPUT_METHOD);
-        final WindowState firstActivityWin =
-                createWindow(null /* parent */, TYPE_APPLICATION_STARTING, mFirstActivity,
-                        "firstActivityWin");
+        final WindowState firstActivityWin = newWindowBuilder("firstActivityWin",
+                TYPE_APPLICATION_STARTING).setWindowToken(mFirstActivity).build();
         spyOn(firstActivityWin);
-        final WindowState secondActivityWin =
-                createWindow(null /* parent */, TYPE_APPLICATION_STARTING, mSecondActivity,
-                        "secondActivityWin");
+        final WindowState secondActivityWin = newWindowBuilder("secondActivityWin",
+                TYPE_APPLICATION_STARTING).setWindowToken(mSecondActivity).build();
         spyOn(secondActivityWin);
 
         // firstActivityWin should be the target
@@ -464,9 +460,8 @@
         assertThat(imeContainer.getRootDisplayArea()).isEqualTo(mDisplay);
         assertThat(mDisplay.findAreaForTokenInLayer(imeToken)).isEqualTo(imeContainer);
 
-        final WindowState firstActivityWin =
-                createWindow(null /* parent */, TYPE_APPLICATION_STARTING, mFirstActivity,
-                        "firstActivityWin");
+        final WindowState firstActivityWin = newWindowBuilder("firstActivityWin",
+                TYPE_APPLICATION_STARTING).setWindowToken(mFirstActivity).build();
         spyOn(firstActivityWin);
         // firstActivityWin should be the target
         doReturn(true).when(firstActivityWin).canBeImeTarget();
@@ -499,9 +494,8 @@
         assertThat(imeContainer.getRootDisplayArea()).isEqualTo(mDisplay);
 
         // firstActivityWin should be the target
-        final WindowState firstActivityWin =
-                createWindow(null /* parent */, TYPE_APPLICATION_STARTING, mFirstActivity,
-                        "firstActivityWin");
+        final WindowState firstActivityWin = newWindowBuilder("firstActivityWin",
+                TYPE_APPLICATION_STARTING).setWindowToken(mFirstActivity).build();
         spyOn(firstActivityWin);
         doReturn(true).when(firstActivityWin).canBeImeTarget();
         WindowState imeTarget = mDisplay.computeImeTarget(true /* updateImeTarget */);
@@ -560,8 +554,8 @@
     }
 
     private void setupImeWindow() {
-        final WindowState imeWindow = createWindow(null /* parent */,
-                TYPE_INPUT_METHOD, mDisplay, "mImeWindow");
+        final WindowState imeWindow = newWindowBuilder("mImeWindow", TYPE_INPUT_METHOD).setDisplay(
+                mDisplay).build();
         imeWindow.mAttrs.flags |= FLAG_NOT_FOCUSABLE;
         mDisplay.mInputMethodWindow = imeWindow;
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java b/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java
index 3e87f1f..ee9673f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java
@@ -177,15 +177,16 @@
         assertTrue("Target didn't call callback enough times.",
                 mListener.waitForAllExpectedCallbackDone(TIMEOUT_ALLOWANCE));
 
+        // Wait until writing thread is waiting, which indicates the thread is waiting for new tasks
+        // to appear.
+        assertTrue("Failed to wait until the writing thread is waiting.",
+                mTarget.waitUntilWritingThreadIsWaiting(TIMEOUT_ALLOWANCE));
+
         // Second item
         mFactory.setExpectedProcessedItemNumber(1);
         mListener.setExpectedOnPreProcessItemCallbackTimes(1);
         dispatchTime = SystemClock.uptimeMillis();
-        // Synchronize on the instance to make sure we schedule the item after it starts to wait for
-        // task indefinitely.
-        synchronized (mTarget) {
-            mTarget.addItem(mFactory.createItem(), false);
-        }
+        mTarget.addItem(mFactory.createItem(), false);
         assertTrue("Target didn't process item enough times.",
                 mFactory.waitForAllExpectedItemsProcessed(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE));
         assertEquals("Target didn't process all items.", 2, mFactory.getTotalProcessedItemCount());
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 9d9f24c..07ee09a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -185,31 +185,46 @@
     }
 
     private ActivityRecord setUpApp(DisplayContent display) {
-        return setUpApp(display, null /* appBuilder */);
+        return setUpApp(display, null /* appBuilder */, null /* taskBuilder */);
     }
 
     private ActivityRecord setUpApp(DisplayContent display, ActivityBuilder appBuilder) {
+        return setUpApp(display, appBuilder, null /* taskBuilder */);
+    }
+
+    private ActivityRecord setUpApp(DisplayContent display, ActivityBuilder aBuilder,
+            TaskBuilder tBuilder) {
         // Use the real package name (com.android.frameworks.wmtests) so that
         // EnableCompatChanges/DisableCompatChanges can take effect.
         // Otherwise the fake WindowTestsBase.DEFAULT_COMPONENT_PACKAGE_NAME will make
         // PlatformCompat#isChangeEnabledByPackageName always return default value.
         final ComponentName componentName = ComponentName.createRelative(
                 mContext, SizeCompatTests.class.getName());
-        mTask = new TaskBuilder(mSupervisor).setDisplay(display).setComponent(componentName)
+        final TaskBuilder taskBuilder = tBuilder != null ? tBuilder : new TaskBuilder(mSupervisor);
+        mTask = taskBuilder.setDisplay(display).setComponent(componentName)
                 .build();
-        final ActivityBuilder builder = appBuilder != null ? appBuilder : new ActivityBuilder(mAtm);
-        mActivity = builder.setTask(mTask).setComponent(componentName).build();
+        final ActivityBuilder appBuilder = aBuilder != null ? aBuilder : new ActivityBuilder(mAtm);
+        mActivity = appBuilder.setTask(mTask).setComponent(componentName).build();
         doReturn(false).when(mActivity).isImmersiveMode(any());
         return mActivity;
     }
 
     private ActivityRecord setUpDisplaySizeWithApp(int dw, int dh) {
-        return setUpDisplaySizeWithApp(dw, dh, null /* appBuilder */);
+        return setUpDisplaySizeWithApp(dw, dh, null /* appBuilder */, null /* taskBuilder */);
     }
 
     private ActivityRecord setUpDisplaySizeWithApp(int dw, int dh, ActivityBuilder appBuilder) {
+        return setUpDisplaySizeWithApp(dw, dh, appBuilder, null /* taskBuilder */);
+    }
+
+    private ActivityRecord setUpDisplaySizeWithApp(int dw, int dh, TaskBuilder taskBuilder) {
+        return setUpDisplaySizeWithApp(dw, dh, null /* appBuilder */, taskBuilder);
+    }
+
+    private ActivityRecord setUpDisplaySizeWithApp(int dw, int dh, ActivityBuilder appBuilder,
+            TaskBuilder taskBuilder) {
         final TestDisplayContent.Builder builder = new TestDisplayContent.Builder(mAtm, dw, dh);
-        return setUpApp(builder.build(), appBuilder);
+        return setUpApp(builder.build(), appBuilder, taskBuilder);
     }
 
     private void setUpLargeScreenDisplayWithApp(int dw, int dh) {
@@ -330,7 +345,7 @@
         if (horizontalReachability) {
             final Consumer<Integer> doubleClick =
                     (Integer x) -> {
-                        mActivity.mAppCompatController.getAppCompatReachabilityPolicy()
+                        mActivity.mAppCompatController.getReachabilityPolicy()
                                 .handleDoubleTap(x, displayHeight / 2);
                         mActivity.mRootWindowContainer.performSurfacePlacement();
                     };
@@ -360,7 +375,7 @@
         } else {
             final Consumer<Integer> doubleClick =
                     (Integer y) -> {
-                        mActivity.mAppCompatController.getAppCompatReachabilityPolicy()
+                        mActivity.mAppCompatController.getReachabilityPolicy()
                                 .handleDoubleTap(displayWidth / 2, y);
                         mActivity.mRootWindowContainer.performSurfacePlacement();
                     };
@@ -421,7 +436,7 @@
 
         final Consumer<Integer> doubleClick =
                 (Integer y) -> {
-                    activity.mAppCompatController.getAppCompatReachabilityPolicy()
+                    activity.mAppCompatController.getReachabilityPolicy()
                             .handleDoubleTap(dw / 2, y);
                     activity.mRootWindowContainer.performSurfacePlacement();
                 };
@@ -834,7 +849,7 @@
         // Change the fixed orientation.
         mActivity.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE);
         assertTrue(mActivity.isRelaunching());
-        assertTrue(mActivity.mAppCompatController.getAppCompatOrientationOverrides()
+        assertTrue(mActivity.mAppCompatController.getOrientationOverrides()
                 .getIsRelaunchingAfterRequestedOrientationChanged());
 
         assertFitted();
@@ -3427,7 +3442,7 @@
 
         setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ false);
         final AppCompatReachabilityOverrides reachabilityOverrides =
-                mActivity.mAppCompatController.getAppCompatReachabilityOverrides();
+                mActivity.mAppCompatController.getReachabilityOverrides();
         assertFalse(reachabilityOverrides.isVerticalReachabilityEnabled());
         assertFalse(reachabilityOverrides.isHorizontalReachabilityEnabled());
     }
@@ -3451,7 +3466,7 @@
         assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode());
 
         // Horizontal reachability is disabled because the app is in split screen.
-        assertFalse(mActivity.mAppCompatController.getAppCompatReachabilityOverrides()
+        assertFalse(mActivity.mAppCompatController.getReachabilityOverrides()
                 .isHorizontalReachabilityEnabled());
     }
 
@@ -3475,7 +3490,7 @@
         assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode());
 
         // Vertical reachability is disabled because the app is in split screen.
-        assertFalse(mActivity.mAppCompatController.getAppCompatReachabilityOverrides()
+        assertFalse(mActivity.mAppCompatController.getReachabilityOverrides()
                 .isVerticalReachabilityEnabled());
     }
 
@@ -3498,7 +3513,7 @@
         // Vertical reachability is disabled because the app does not match parent width
         assertNotEquals(mActivity.getScreenResolvedBounds().width(),
                 mActivity.mDisplayContent.getBounds().width());
-        assertFalse(mActivity.mAppCompatController.getAppCompatReachabilityOverrides()
+        assertFalse(mActivity.mAppCompatController.getReachabilityOverrides()
                 .isVerticalReachabilityEnabled());
     }
 
@@ -3516,7 +3531,7 @@
         assertEquals(new Rect(0, 0, 0, 0), mActivity.getBounds());
 
         // Vertical reachability is still enabled as resolved bounds is not empty
-        assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides()
+        assertTrue(mActivity.mAppCompatController.getReachabilityOverrides()
                 .isVerticalReachabilityEnabled());
     }
 
@@ -3533,7 +3548,7 @@
         assertEquals(new Rect(0, 0, 0, 0), mActivity.getBounds());
 
         // Horizontal reachability is still enabled as resolved bounds is not empty
-        assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides()
+        assertTrue(mActivity.mAppCompatController.getReachabilityOverrides()
                 .isHorizontalReachabilityEnabled());
     }
 
@@ -3548,7 +3563,7 @@
         prepareMinAspectRatio(mActivity, OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE,
                 SCREEN_ORIENTATION_PORTRAIT);
 
-        assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides()
+        assertTrue(mActivity.mAppCompatController.getReachabilityOverrides()
                 .isHorizontalReachabilityEnabled());
     }
 
@@ -3563,7 +3578,7 @@
         prepareMinAspectRatio(mActivity, OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE,
                 SCREEN_ORIENTATION_LANDSCAPE);
 
-        assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides()
+        assertTrue(mActivity.mAppCompatController.getReachabilityOverrides()
                 .isVerticalReachabilityEnabled());
     }
 
@@ -3585,7 +3600,7 @@
         // Horizontal reachability is disabled because the app does not match parent height
         assertNotEquals(mActivity.getScreenResolvedBounds().height(),
                 mActivity.mDisplayContent.getBounds().height());
-        assertFalse(mActivity.mAppCompatController.getAppCompatReachabilityOverrides()
+        assertFalse(mActivity.mAppCompatController.getReachabilityOverrides()
                 .isHorizontalReachabilityEnabled());
     }
 
@@ -3608,7 +3623,7 @@
         // Horizontal reachability is enabled because the app matches parent height
         assertEquals(mActivity.getScreenResolvedBounds().height(),
                 mActivity.mDisplayContent.getBounds().height());
-        assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides()
+        assertTrue(mActivity.mAppCompatController.getReachabilityOverrides()
                 .isHorizontalReachabilityEnabled());
     }
 
@@ -3631,7 +3646,7 @@
         // Vertical reachability is enabled because the app matches parent width
         assertEquals(mActivity.getScreenResolvedBounds().width(),
                 mActivity.mDisplayContent.getBounds().width());
-        assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides()
+        assertTrue(mActivity.mAppCompatController.getReachabilityOverrides()
                 .isVerticalReachabilityEnabled());
     }
 
@@ -4315,7 +4330,7 @@
 
         // Make sure app doesn't jump to top (default tabletop position) when unfolding.
         assertEquals(1.0f, mActivity.mAppCompatController
-                .getAppCompatReachabilityOverrides().getVerticalPositionMultiplier(mActivity
+                .getReachabilityOverrides().getVerticalPositionMultiplier(mActivity
                         .getParent().getConfiguration()), 0);
 
         // Simulate display fully open after unfolding.
@@ -4323,7 +4338,7 @@
         doReturn(false).when(mActivity.mDisplayContent).inTransition();
 
         assertEquals(1.0f, mActivity.mAppCompatController
-                .getAppCompatReachabilityOverrides().getVerticalPositionMultiplier(mActivity
+                .getReachabilityOverrides().getVerticalPositionMultiplier(mActivity
                         .getParent().getConfiguration()), 0);
     }
 
@@ -4469,6 +4484,80 @@
         assertEquals(new Rect(0, 0, 1000, 2800), bounds);
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES)
+    public void testUserAspectRatioOverridesNotAppliedToResizeableFreeformActivity() {
+        final TaskBuilder taskBuilder =
+                new TaskBuilder(mSupervisor).setWindowingMode(WINDOWING_MODE_FREEFORM);
+        setUpDisplaySizeWithApp(2500, 1600, taskBuilder);
+
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        spyOn(mActivity.mWmService.mAppCompatConfiguration);
+        doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration)
+                .isUserAppAspectRatioSettingsEnabled();
+        final AppCompatController appCompatController = mActivity.mAppCompatController;
+        final AppCompatAspectRatioOverrides aspectRatioOverrides =
+                appCompatController.getAppCompatAspectRatioOverrides();
+        spyOn(aspectRatioOverrides);
+        // Set user aspect ratio override.
+        doReturn(USER_MIN_ASPECT_RATIO_16_9).when(aspectRatioOverrides)
+                .getUserMinAspectRatioOverrideCode();
+
+        prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ false);
+        assertFalse(appCompatController.getAppCompatAspectRatioPolicy().isAspectRatioApplied());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES)
+    public void testUserAspectRatioOverridesAppliedToNonResizeableFreeformActivity() {
+        final TaskBuilder taskBuilder =
+                new TaskBuilder(mSupervisor).setWindowingMode(WINDOWING_MODE_FREEFORM);
+        setUpDisplaySizeWithApp(2500, 1600, taskBuilder);
+
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        spyOn(mActivity.mWmService.mAppCompatConfiguration);
+        doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration)
+                .isUserAppAspectRatioSettingsEnabled();
+        final AppCompatController appCompatController = mActivity.mAppCompatController;
+        final AppCompatAspectRatioOverrides aspectRatioOverrides =
+                appCompatController.getAppCompatAspectRatioOverrides();
+        spyOn(aspectRatioOverrides);
+        // Set user aspect ratio override.
+        doReturn(USER_MIN_ASPECT_RATIO_16_9).when(aspectRatioOverrides)
+                .getUserMinAspectRatioOverrideCode();
+
+        prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ true);
+        assertTrue(appCompatController.getAppCompatAspectRatioPolicy().isAspectRatioApplied());
+    }
+
+    @Test
+    @EnableCompatChanges({ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO,
+            ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE})
+    @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES)
+    public void testSystemAspectRatioOverridesNotAppliedToResizeableFreeformActivity() {
+        final TaskBuilder taskBuilder =
+                new TaskBuilder(mSupervisor).setWindowingMode(WINDOWING_MODE_FREEFORM);
+        setUpDisplaySizeWithApp(2500, 1600, taskBuilder);
+        prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ false);
+
+        assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy()
+                .isAspectRatioApplied());
+    }
+
+    @Test
+    @EnableCompatChanges({ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO,
+            ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE})
+    @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES)
+    public void testSystemAspectRatioOverridesAppliedToNonResizeableFreeformActivity() {
+        final TaskBuilder taskBuilder =
+                new TaskBuilder(mSupervisor).setWindowingMode(WINDOWING_MODE_FREEFORM);
+        setUpDisplaySizeWithApp(2500, 1600, taskBuilder);
+        prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ true);
+
+        assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy()
+                .isAspectRatioApplied());
+    }
+
     private void assertVerticalPositionForDifferentDisplayConfigsForLandscapeActivity(
             float letterboxVerticalPositionMultiplier, Rect fixedOrientationLetterbox,
             Rect sizeCompatUnscaled, Rect sizeCompatScaled) {
@@ -5028,7 +5117,7 @@
 
     private void setUpAllowThinLetterboxed(boolean thinLetterboxAllowed) {
         final AppCompatReachabilityOverrides reachabilityOverrides =
-                mActivity.mAppCompatController.getAppCompatReachabilityOverrides();
+                mActivity.mAppCompatController.getReachabilityOverrides();
         spyOn(reachabilityOverrides);
         doReturn(thinLetterboxAllowed).when(reachabilityOverrides)
                 .allowVerticalReachabilityForThinLetterbox();
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index dc4adcc..2997173 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -878,8 +878,10 @@
                 .build();
         final ActivityRecord activity0 = tf0.getTopMostActivity();
         final ActivityRecord activity1 = tf1.getTopMostActivity();
-        final WindowState win0 = createWindow(null, TYPE_BASE_APPLICATION, activity0, "win0");
-        final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity1, "win1");
+        final WindowState win0 = newWindowBuilder("win0", TYPE_BASE_APPLICATION).setWindowToken(
+                activity0).build();
+        final WindowState win1 = newWindowBuilder("win1", TYPE_BASE_APPLICATION).setWindowToken(
+                activity1).build();
         doReturn(false).when(mDisplayContent).shouldImeAttachedToApp();
 
         mDisplayContent.setImeInputTarget(win0);
@@ -1174,8 +1176,8 @@
     }
 
     private WindowState createAppWindow(ActivityRecord app, String name) {
-        final WindowState win = createWindow(null, TYPE_BASE_APPLICATION, app, name,
-                0 /* ownerId */, false /* ownerCanAddInternalSystemWindow */, new TestIWindow());
+        final WindowState win = newWindowBuilder(name, TYPE_BASE_APPLICATION).setWindowToken(
+                app).setClientWindow(new TestIWindow()).build();
         mWm.mWindowMap.put(win.mClient.asBinder(), win);
         return win;
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotCacheTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotCacheTest.java
index f145b40..f9250f9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotCacheTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotCacheTest.java
@@ -63,7 +63,7 @@
 
     @Test
     public void testAppRemoved() {
-        final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+        final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build();
         mCache.putSnapshot(window.getTask(), createSnapshot());
         assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */));
         mCache.onAppRemoved(window.mActivityRecord);
@@ -72,7 +72,7 @@
 
     @Test
     public void testAppDied() {
-        final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+        final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build();
         mCache.putSnapshot(window.getTask(), createSnapshot());
         assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */));
         mCache.onAppDied(window.mActivityRecord);
@@ -81,7 +81,7 @@
 
     @Test
     public void testTaskRemoved() {
-        final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+        final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build();
         mCache.putSnapshot(window.getTask(), createSnapshot());
         assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */));
         mCache.onIdRemoved(window.getTask().mTaskId);
@@ -90,7 +90,7 @@
 
     @Test
     public void testReduced_notCached() {
-        final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+        final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build();
         mPersister.persistSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId, createSnapshot());
         mSnapshotPersistQueue.waitForQueueEmpty();
         assertNull(mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */));
@@ -105,7 +105,7 @@
 
     @Test
     public void testRestoreFromDisk() {
-        final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+        final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build();
         mPersister.persistSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId, createSnapshot());
         mSnapshotPersistQueue.waitForQueueEmpty();
         assertNull(mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */));
@@ -117,7 +117,7 @@
 
     @Test
     public void testClearCache() {
-        final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+        final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build();
         mCache.putSnapshot(window.getTask(), mSnapshot);
         assertEquals(mSnapshot, mCache.getSnapshot(window.getTask().mTaskId,
                 false /* isLowResolution */));
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java
index c6b2a6b..1bca53a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java
@@ -74,8 +74,8 @@
 
     @Test
     public void testGetClosingApps_closing() {
-        final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW,
-                "closingWindow");
+        final WindowState closingWindow = newWindowBuilder("closingWindow",
+                FIRST_APPLICATION_WINDOW).build();
         closingWindow.mActivityRecord.commitVisibility(
                 false /* visible */, true /* performLayout */);
         final ArraySet<ActivityRecord> closingApps = new ArraySet<>();
@@ -88,8 +88,8 @@
 
     @Test
     public void testGetClosingApps_notClosing() {
-        final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW,
-                "closingWindow");
+        final WindowState closingWindow = newWindowBuilder("closingWindow",
+                FIRST_APPLICATION_WINDOW).build();
         final WindowState openingWindow = createAppWindow(closingWindow.getTask(),
                 FIRST_APPLICATION_WINDOW, "openingWindow");
         closingWindow.mActivityRecord.commitVisibility(
@@ -105,8 +105,8 @@
 
     @Test
     public void testGetClosingApps_skipClosingAppsSnapshotTasks() {
-        final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW,
-                "closingWindow");
+        final WindowState closingWindow = newWindowBuilder("closingWindow",
+                FIRST_APPLICATION_WINDOW).build();
         closingWindow.mActivityRecord.commitVisibility(
                 false /* visible */, true /* performLayout */);
         final ArraySet<ActivityRecord> closingApps = new ArraySet<>();
@@ -133,19 +133,19 @@
 
     @Test
     public void testGetSnapshotMode() {
-        final WindowState disabledWindow = createWindow(null,
-                FIRST_APPLICATION_WINDOW, mDisplayContent, "disabledWindow");
+        final WindowState disabledWindow = newWindowBuilder("disabledWindow",
+                FIRST_APPLICATION_WINDOW).setDisplay(mDisplayContent).build();
         disabledWindow.mActivityRecord.setRecentsScreenshotEnabled(false);
         assertEquals(SNAPSHOT_MODE_APP_THEME,
                 mWm.mTaskSnapshotController.getSnapshotMode(disabledWindow.getTask()));
 
-        final WindowState normalWindow = createWindow(null,
-                FIRST_APPLICATION_WINDOW, mDisplayContent, "normalWindow");
+        final WindowState normalWindow = newWindowBuilder("normalWindow",
+                FIRST_APPLICATION_WINDOW).setDisplay(mDisplayContent).build();
         assertEquals(SNAPSHOT_MODE_REAL,
                 mWm.mTaskSnapshotController.getSnapshotMode(normalWindow.getTask()));
 
-        final WindowState secureWindow = createWindow(null,
-                FIRST_APPLICATION_WINDOW, mDisplayContent, "secureWindow");
+        final WindowState secureWindow = newWindowBuilder("secureWindow",
+                FIRST_APPLICATION_WINDOW).setDisplay(mDisplayContent).build();
         secureWindow.mAttrs.flags |= FLAG_SECURE;
         assertEquals(SNAPSHOT_MODE_APP_THEME,
                 mWm.mTaskSnapshotController.getSnapshotMode(secureWindow.getTask()));
@@ -297,8 +297,8 @@
         spyOn(mWm.mTaskSnapshotController);
         doReturn(false).when(mWm.mTaskSnapshotController).shouldDisableSnapshots();
 
-        final WindowState normalWindow = createWindow(null,
-                FIRST_APPLICATION_WINDOW, mDisplayContent, "normalWindow");
+        final WindowState normalWindow = newWindowBuilder("normalWindow",
+                FIRST_APPLICATION_WINDOW).setDisplay(mDisplayContent).build();
         final TaskSnapshot snapshot = new TaskSnapshotPersisterTestBase.TaskSnapshotBuilder()
                 .setTopActivityComponent(normalWindow.mActivityRecord.mActivityComponent).build();
         doReturn(snapshot).when(mWm.mTaskSnapshotController).snapshot(any());
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java
index 9bde066..51ea498 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java
@@ -41,7 +41,7 @@
  * Test class for {@link TaskSnapshotPersister} and {@link AppSnapshotLoader}
  *
  * Build/Install/Run:
- * atest TaskSnapshotPersisterLoaderTest
+ * atest TaskSnapshotLowResDisabledTest
  */
 @MediumTest
 @Presubmit
@@ -126,7 +126,7 @@
 
     @Test
     public void testReduced_notCached() {
-        final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+        final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build();
         mPersister.persistSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId, createSnapshot());
         mSnapshotPersistQueue.waitForQueueEmpty();
         assertNull(mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */));
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
index 1fa6578..5ed2df3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
@@ -504,8 +504,8 @@
         assertTrue(child.isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION));
         assertFalse(child.isAnimating(PARENTS, ANIMATION_TYPE_SCREEN_ROTATION));
 
-        final WindowState windowState = createWindow(null /* parent */, TYPE_BASE_APPLICATION,
-                mDisplayContent, "TestWindowState");
+        final WindowState windowState = newWindowBuilder("TestWindowState",
+                TYPE_BASE_APPLICATION).setDisplay(mDisplayContent).build();
         WindowContainer parent = windowState.getParent();
         spyOn(windowState.mSurfaceAnimator);
         doReturn(true).when(windowState.mSurfaceAnimator).isAnimating();
@@ -1045,8 +1045,8 @@
 
         // An animating window with mRemoveOnExit can be removed by handleCompleteDeferredRemoval
         // once it no longer animates.
-        final WindowState exitingWindow = createWindow(null, TYPE_APPLICATION_OVERLAY,
-                displayContent, "exiting window");
+        final WindowState exitingWindow = newWindowBuilder("exiting window",
+                TYPE_APPLICATION_OVERLAY).setDisplay(displayContent).build();
         exitingWindow.startAnimation(exitingWindow.getPendingTransaction(),
                 mock(AnimationAdapter.class), false /* hidden */,
                 SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION);
@@ -1063,7 +1063,7 @@
         final ActivityRecord r = new TaskBuilder(mSupervisor).setCreateActivity(true)
                 .setDisplay(displayContent).build().getTopMostActivity();
         // Add a window and make the activity animating so the removal of activity is deferred.
-        createWindow(null, TYPE_BASE_APPLICATION, r, "win");
+        newWindowBuilder("win", TYPE_BASE_APPLICATION).setWindowToken(r).build();
         doReturn(true).when(r).isAnimating(anyInt(), anyInt());
 
         displayContent.remove();
@@ -1216,7 +1216,8 @@
     public void testFreezeInsets() {
         final Task task = createTask(mDisplayContent);
         final ActivityRecord activity = createActivityRecord(mDisplayContent, task);
-        final WindowState win = createWindow(null, TYPE_BASE_APPLICATION, activity, "win");
+        final WindowState win = newWindowBuilder("win", TYPE_BASE_APPLICATION).setWindowToken(
+                activity).build();
 
         // Set visibility to false, verify the main window of the task will be set the frozen
         // insets state immediately.
@@ -1233,7 +1234,8 @@
         final Task rootTask = createTask(mDisplayContent);
         final Task task = createTaskInRootTask(rootTask, 0 /* userId */);
         final ActivityRecord activity = createActivityRecord(mDisplayContent, task);
-        final WindowState win = createWindow(null, TYPE_BASE_APPLICATION, activity, "win");
+        final WindowState win = newWindowBuilder("win", TYPE_BASE_APPLICATION).setWindowToken(
+                activity).build();
         task.getDisplayContent().prepareAppTransition(TRANSIT_CLOSE);
         spyOn(win);
         doReturn(true).when(task).okToAnimate();
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 72935cb..8606581 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java
@@ -49,9 +49,10 @@
     @SetupWindows(addWindows = { W_DOCK_DIVIDER, W_INPUT_METHOD })
     @Test
     public void testDockedDividerPosition() {
-        final WindowState splitScreenWindow = createWindow(null,
-                WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, TYPE_BASE_APPLICATION,
-                mDisplayContent, "splitScreenWindow");
+        final WindowState splitScreenWindow = newWindowBuilder("splitScreenWindow",
+                TYPE_BASE_APPLICATION).setWindowingMode(
+                WINDOWING_MODE_MULTI_WINDOW).setActivityType(ACTIVITY_TYPE_STANDARD).setDisplay(
+                mDisplayContent).build();
 
         mDisplayContent.setImeLayeringTarget(splitScreenWindow);
 
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 d1f5d15..be79160 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -76,6 +76,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.ActivityManager;
 import android.app.ActivityThread;
 import android.app.IApplicationThread;
 import android.content.pm.ActivityInfo;
@@ -1522,7 +1523,7 @@
     @EnableFlags(Flags.FLAG_CONDENSE_CONFIGURATION_CHANGE_FOR_SIMPLE_MODE)
     public void setConfigurationChangeSettingsForUser_createsFromParcel_callsSettingImpl()
             throws Settings.SettingNotFoundException {
-        final int userId = 0;
+        final int currentUserId = ActivityManager.getCurrentUser();
         final int forcedDensity = 400;
         final float forcedFontScaleFactor = 1.15f;
         final Parcelable.Creator<ConfigurationChangeSetting> creator =
@@ -1536,10 +1537,10 @@
 
         mWm.setConfigurationChangeSettingsForUser(settings, UserHandle.USER_CURRENT);
 
-        verify(mDisplayContent).setForcedDensity(forcedDensity, userId);
+        verify(mDisplayContent).setForcedDensity(forcedDensity, currentUserId);
         assertEquals(forcedFontScaleFactor, Settings.System.getFloat(
                 mContext.getContentResolver(), Settings.System.FONT_SCALE), 0.1f /* delta */);
-        verify(mAtm).updateFontScaleIfNeeded(userId);
+        verify(mAtm).updateFontScaleIfNeeded(currentUserId);
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index 50e0e18..ab9abfc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -154,9 +154,11 @@
 
     @Test
     public void testIsParentWindowHidden() {
-        final WindowState parentWindow = createWindow(null, TYPE_APPLICATION, "parentWindow");
-        final WindowState child1 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child1");
-        final WindowState child2 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child2");
+        final WindowState parentWindow = newWindowBuilder("parentWindow", TYPE_APPLICATION).build();
+        final WindowState child1 = newWindowBuilder("child1", FIRST_SUB_WINDOW).setParent(
+                parentWindow).build();
+        final WindowState child2 = newWindowBuilder("child2", FIRST_SUB_WINDOW).setParent(
+                parentWindow).build();
 
         // parentWindow is initially set to hidden.
         assertTrue(parentWindow.mHidden);
@@ -172,10 +174,12 @@
 
     @Test
     public void testIsChildWindow() {
-        final WindowState parentWindow = createWindow(null, TYPE_APPLICATION, "parentWindow");
-        final WindowState child1 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child1");
-        final WindowState child2 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child2");
-        final WindowState randomWindow = createWindow(null, TYPE_APPLICATION, "randomWindow");
+        final WindowState parentWindow = newWindowBuilder("parentWindow", TYPE_APPLICATION).build();
+        final WindowState child1 = newWindowBuilder("child1", FIRST_SUB_WINDOW).setParent(
+                parentWindow).build();
+        final WindowState child2 = newWindowBuilder("child2", FIRST_SUB_WINDOW).setParent(
+                parentWindow).build();
+        final WindowState randomWindow = newWindowBuilder("randomWindow", TYPE_APPLICATION).build();
 
         assertFalse(parentWindow.isChildWindow());
         assertTrue(child1.isChildWindow());
@@ -185,12 +189,15 @@
 
     @Test
     public void testHasChild() {
-        final WindowState win1 = createWindow(null, TYPE_APPLICATION, "win1");
-        final WindowState win11 = createWindow(win1, FIRST_SUB_WINDOW, "win11");
-        final WindowState win12 = createWindow(win1, FIRST_SUB_WINDOW, "win12");
-        final WindowState win2 = createWindow(null, TYPE_APPLICATION, "win2");
-        final WindowState win21 = createWindow(win2, FIRST_SUB_WINDOW, "win21");
-        final WindowState randomWindow = createWindow(null, TYPE_APPLICATION, "randomWindow");
+        final WindowState win1 = newWindowBuilder("win1", TYPE_APPLICATION).build();
+        final WindowState win11 = newWindowBuilder("win11", FIRST_SUB_WINDOW).setParent(
+                win1).build();
+        final WindowState win12 = newWindowBuilder("win12", FIRST_SUB_WINDOW).setParent(
+                win1).build();
+        final WindowState win2 = newWindowBuilder("win2", TYPE_APPLICATION).build();
+        final WindowState win21 = newWindowBuilder("win21", FIRST_SUB_WINDOW).setParent(
+                win2).build();
+        final WindowState randomWindow = newWindowBuilder("randomWindow", TYPE_APPLICATION).build();
 
         assertTrue(win1.hasChild(win11));
         assertTrue(win1.hasChild(win12));
@@ -206,9 +213,11 @@
 
     @Test
     public void testGetParentWindow() {
-        final WindowState parentWindow = createWindow(null, TYPE_APPLICATION, "parentWindow");
-        final WindowState child1 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child1");
-        final WindowState child2 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child2");
+        final WindowState parentWindow = newWindowBuilder("parentWindow", TYPE_APPLICATION).build();
+        final WindowState child1 = newWindowBuilder("child1", FIRST_SUB_WINDOW).setParent(
+                parentWindow).build();
+        final WindowState child2 = newWindowBuilder("child2", FIRST_SUB_WINDOW).setParent(
+                parentWindow).build();
 
         assertNull(parentWindow.getParentWindow());
         assertEquals(parentWindow, child1.getParentWindow());
@@ -217,8 +226,8 @@
 
     @Test
     public void testOverlayWindowHiddenWhenSuspended() {
-        final WindowState overlayWindow = spy(createWindow(null, TYPE_APPLICATION_OVERLAY,
-                "overlayWindow"));
+        final WindowState overlayWindow = spy(
+                newWindowBuilder("overlayWindow", TYPE_APPLICATION_OVERLAY).build());
         overlayWindow.setHiddenWhileSuspended(true);
         verify(overlayWindow).hide(true /* doAnimation */, true /* requestAnim */);
         overlayWindow.setHiddenWhileSuspended(false);
@@ -227,9 +236,11 @@
 
     @Test
     public void testGetTopParentWindow() {
-        final WindowState root = createWindow(null, TYPE_APPLICATION, "root");
-        final WindowState child1 = createWindow(root, FIRST_SUB_WINDOW, "child1");
-        final WindowState child2 = createWindow(child1, FIRST_SUB_WINDOW, "child2");
+        final WindowState root = newWindowBuilder("root", TYPE_APPLICATION).build();
+        final WindowState child1 = newWindowBuilder("child1", FIRST_SUB_WINDOW).setParent(
+                root).build();
+        final WindowState child2 = newWindowBuilder("child2", FIRST_SUB_WINDOW).setParent(
+                child1).build();
 
         assertEquals(root, root.getTopParentWindow());
         assertEquals(root, child1.getTopParentWindow());
@@ -244,7 +255,7 @@
 
     @Test
     public void testIsOnScreen_hiddenByPolicy() {
-        final WindowState window = createWindow(null, TYPE_APPLICATION, "window");
+        final WindowState window = newWindowBuilder("window", TYPE_APPLICATION).build();
         window.setHasSurface(true);
         assertTrue(window.isOnScreen());
         window.hide(false /* doAnimation */, false /* requestAnim */);
@@ -273,8 +284,8 @@
 
     @Test
     public void testCanBeImeTarget() {
-        final WindowState appWindow = createWindow(null, TYPE_APPLICATION, "appWindow");
-        final WindowState imeWindow = createWindow(null, TYPE_INPUT_METHOD, "imeWindow");
+        final WindowState appWindow = newWindowBuilder("appWindow", TYPE_APPLICATION).build();
+        final WindowState imeWindow = newWindowBuilder("imeWindow", TYPE_INPUT_METHOD).build();
 
         // Setting FLAG_NOT_FOCUSABLE prevents the window from being an IME target.
         appWindow.mAttrs.flags |= FLAG_NOT_FOCUSABLE;
@@ -328,16 +339,17 @@
 
     @Test
     public void testGetWindow() {
-        final WindowState root = createWindow(null, TYPE_APPLICATION, "root");
-        final WindowState mediaChild = createWindow(root, TYPE_APPLICATION_MEDIA, "mediaChild");
-        final WindowState mediaOverlayChild = createWindow(root,
-                TYPE_APPLICATION_MEDIA_OVERLAY, "mediaOverlayChild");
-        final WindowState attachedDialogChild = createWindow(root,
-                TYPE_APPLICATION_ATTACHED_DIALOG, "attachedDialogChild");
-        final WindowState subPanelChild = createWindow(root,
-                TYPE_APPLICATION_SUB_PANEL, "subPanelChild");
-        final WindowState aboveSubPanelChild = createWindow(root,
-                TYPE_APPLICATION_ABOVE_SUB_PANEL, "aboveSubPanelChild");
+        final WindowState root = newWindowBuilder("root", TYPE_APPLICATION).build();
+        final WindowState mediaChild = newWindowBuilder("mediaChild",
+                TYPE_APPLICATION_MEDIA).setParent(root).build();
+        final WindowState mediaOverlayChild = newWindowBuilder("mediaOverlayChild",
+                TYPE_APPLICATION_MEDIA_OVERLAY).setParent(root).build();
+        final WindowState attachedDialogChild = newWindowBuilder("attachedDialogChild",
+                TYPE_APPLICATION_ATTACHED_DIALOG).setParent(root).build();
+        final WindowState subPanelChild = newWindowBuilder("subPanelChild",
+                TYPE_APPLICATION_SUB_PANEL).setParent(root).build();
+        final WindowState aboveSubPanelChild = newWindowBuilder("aboveSubPanelChild",
+                TYPE_APPLICATION_ABOVE_SUB_PANEL).setParent(root).build();
 
         final LinkedList<WindowState> windows = new LinkedList<>();
 
@@ -358,7 +370,7 @@
 
     @Test
     public void testDestroySurface() {
-        final WindowState win = createWindow(null, TYPE_APPLICATION, "win");
+        final WindowState win = newWindowBuilder("win", TYPE_APPLICATION).build();
         win.mHasSurface = win.mAnimatingExit = true;
         win.mWinAnimator.mSurfaceControl = mock(SurfaceControl.class);
         win.onExitAnimationDone();
@@ -384,8 +396,10 @@
         // Call prepareWindowToDisplayDuringRelayout for a window without FLAG_TURN_SCREEN_ON before
         // calling setCurrentLaunchCanTurnScreenOn for windows with flag in the same activity.
         final ActivityRecord activity = createActivityRecord(mDisplayContent);
-        final WindowState first = createWindow(null, TYPE_APPLICATION, activity, "first");
-        final WindowState second = createWindow(null, TYPE_APPLICATION, activity, "second");
+        final WindowState first = newWindowBuilder("first", TYPE_APPLICATION).setWindowToken(
+                activity).build();
+        final WindowState second = newWindowBuilder("second", TYPE_APPLICATION).setWindowToken(
+                activity).build();
 
         testPrepareWindowToDisplayDuringRelayout(first, false /* expectedWakeupCalled */,
                 true /* expectedCurrentLaunchCanTurnScreenOn */);
@@ -423,10 +437,10 @@
         // Call prepareWindowToDisplayDuringRelayout for a windows that are not children of an
         // activity. Both windows have the FLAG_TURNS_SCREEN_ON so both should call wakeup
         final WindowToken windowToken = createTestWindowToken(FIRST_SUB_WINDOW, mDisplayContent);
-        final WindowState firstWindow = createWindow(null, TYPE_APPLICATION, windowToken,
-                "firstWindow");
-        final WindowState secondWindow = createWindow(null, TYPE_APPLICATION, windowToken,
-                "secondWindow");
+        final WindowState firstWindow = newWindowBuilder("firstWindow",
+                TYPE_APPLICATION).setWindowToken(windowToken).build();
+        final WindowState secondWindow = newWindowBuilder("secondWindow",
+                TYPE_APPLICATION).setWindowToken(windowToken).build();
         firstWindow.mAttrs.flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
         secondWindow.mAttrs.flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
 
@@ -459,7 +473,7 @@
 
     @Test
     public void testCanAffectSystemUiFlags() {
-        final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+        final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
         app.mActivityRecord.setVisible(true);
         assertTrue(app.canAffectSystemUiFlags());
         app.mActivityRecord.setVisible(false);
@@ -471,7 +485,7 @@
 
     @Test
     public void testCanAffectSystemUiFlags_starting() {
-        final WindowState app = createWindow(null, TYPE_APPLICATION_STARTING, "app");
+        final WindowState app = newWindowBuilder("app", TYPE_APPLICATION_STARTING).build();
         app.mActivityRecord.setVisible(true);
         app.mStartingData = new SnapshotStartingData(mWm, null, 0);
         assertFalse(app.canAffectSystemUiFlags());
@@ -481,7 +495,7 @@
 
     @Test
     public void testCanAffectSystemUiFlags_disallow() {
-        final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+        final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
         app.mActivityRecord.setVisible(true);
         assertTrue(app.canAffectSystemUiFlags());
         app.getTask().setCanAffectSystemUiFlags(false);
@@ -538,9 +552,11 @@
 
     @Test
     public void testIsSelfOrAncestorWindowAnimating() {
-        final WindowState root = createWindow(null, TYPE_APPLICATION, "root");
-        final WindowState child1 = createWindow(root, FIRST_SUB_WINDOW, "child1");
-        final WindowState child2 = createWindow(child1, FIRST_SUB_WINDOW, "child2");
+        final WindowState root = newWindowBuilder("root", TYPE_APPLICATION).build();
+        final WindowState child1 = newWindowBuilder("child1", FIRST_SUB_WINDOW).setParent(
+                root).build();
+        final WindowState child2 = newWindowBuilder("child2", FIRST_SUB_WINDOW).setParent(
+                child1).build();
         assertFalse(child2.isSelfOrAncestorWindowAnimatingExit());
         child2.mAnimatingExit = true;
         assertTrue(child2.isSelfOrAncestorWindowAnimatingExit());
@@ -551,7 +567,7 @@
 
     @Test
     public void testDeferredRemovalByAnimating() {
-        final WindowState appWindow = createWindow(null, TYPE_APPLICATION, "appWindow");
+        final WindowState appWindow = newWindowBuilder("appWindow", TYPE_APPLICATION).build();
         makeWindowVisible(appWindow);
         spyOn(appWindow.mWinAnimator);
         doReturn(true).when(appWindow.mWinAnimator).getShown();
@@ -571,8 +587,9 @@
 
     @Test
     public void testOnExitAnimationDone() {
-        final WindowState parent = createWindow(null, TYPE_APPLICATION, "parent");
-        final WindowState child = createWindow(parent, TYPE_APPLICATION_PANEL, "child");
+        final WindowState parent = newWindowBuilder("parent", TYPE_APPLICATION).build();
+        final WindowState child = newWindowBuilder("child", TYPE_APPLICATION_PANEL).setParent(
+                parent).build();
         final SurfaceControl.Transaction t = parent.getPendingTransaction();
         child.startAnimation(t, mock(AnimationAdapter.class), false /* hidden */,
                 SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION);
@@ -609,7 +626,7 @@
 
     @Test
     public void testLayoutSeqResetOnReparent() {
-        final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+        final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
         app.mLayoutSeq = 1;
         mDisplayContent.mLayoutSeq = 1;
 
@@ -622,7 +639,7 @@
 
     @Test
     public void testDisplayIdUpdatedOnReparent() {
-        final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+        final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
         // fake a different display
         app.mInputWindowHandle.setDisplayId(mDisplayContent.getDisplayId() + 1);
         app.onDisplayChanged(mDisplayContent);
@@ -633,7 +650,7 @@
 
     @Test
     public void testApplyWithNextDraw() {
-        final WindowState win = createWindow(null, TYPE_APPLICATION_OVERLAY, "app");
+        final WindowState win = newWindowBuilder("app", TYPE_APPLICATION_OVERLAY).build();
         final SurfaceControl.Transaction[] handledT = { null };
         // The normal case that the draw transaction is applied with finishing drawing.
         win.applyWithNextDraw(t -> handledT[0] = t);
@@ -657,7 +674,7 @@
 
     @Test
     public void testSeamlesslyRotateWindow() {
-        final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+        final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
         final SurfaceControl.Transaction t = spy(StubTransaction.class);
 
         makeWindowVisible(app);
@@ -707,7 +724,7 @@
 
     @Test
     public void testVisibilityChangeSwitchUser() {
-        final WindowState window = createWindow(null, TYPE_APPLICATION, "app");
+        final WindowState window = newWindowBuilder("app", TYPE_APPLICATION).build();
         window.mHasSurface = true;
         spyOn(window);
         doReturn(false).when(window).showForAllUsers();
@@ -729,8 +746,9 @@
         final CompatModePackages cmp = mWm.mAtmService.mCompatModePackages;
         spyOn(cmp);
         doReturn(overrideScale).when(cmp).getCompatScale(anyString(), anyInt());
-        final WindowState w = createWindow(null, TYPE_APPLICATION_OVERLAY, "win");
-        final WindowState child = createWindow(w, TYPE_APPLICATION_PANEL, "child");
+        final WindowState w = newWindowBuilder("win", TYPE_APPLICATION_OVERLAY).build();
+        final WindowState child = newWindowBuilder("child", TYPE_APPLICATION_PANEL).setParent(
+                w).build();
 
         assertTrue(w.hasCompatScale());
         assertTrue(child.hasCompatScale());
@@ -788,7 +806,8 @@
 
         // Child window without scale (e.g. different app) should apply inverse scale of parent.
         doReturn(1f).when(cmp).getCompatScale(anyString(), anyInt());
-        final WindowState child2 = createWindow(w, TYPE_APPLICATION_SUB_PANEL, "child2");
+        final WindowState child2 = newWindowBuilder("child2", TYPE_APPLICATION_SUB_PANEL).setParent(
+                w).build();
         makeWindowVisible(w, child2);
         clearInvocations(t);
         child2.prepareSurfaces();
@@ -798,10 +817,10 @@
     @SetupWindows(addWindows = { W_ABOVE_ACTIVITY, W_NOTIFICATION_SHADE })
     @Test
     public void testRequestDrawIfNeeded() {
-        final WindowState startingApp = createWindow(null /* parent */,
-                TYPE_BASE_APPLICATION, "startingApp");
-        final WindowState startingWindow = createWindow(null /* parent */,
-                TYPE_APPLICATION_STARTING, startingApp.mToken, "starting");
+        final WindowState startingApp = newWindowBuilder("startingApp",
+                TYPE_BASE_APPLICATION).build();
+        final WindowState startingWindow = newWindowBuilder("starting",
+                TYPE_APPLICATION_STARTING).setWindowToken(startingApp.mToken).build();
         startingApp.mActivityRecord.mStartingWindow = startingWindow;
         final WindowState keyguardHostWindow = mNotificationShadeWindow;
         final WindowState allDrawnApp = mAppWindow;
@@ -878,7 +897,7 @@
 
     @Test
     public void testRequestResizeForBlastSync() {
-        final WindowState win = createWindow(null, TYPE_APPLICATION, "window");
+        final WindowState win = newWindowBuilder("window", TYPE_APPLICATION).build();
         makeWindowVisible(win);
         makeLastConfigReportedToClient(win, true /* visible */);
         win.mLayoutSeq = win.getDisplayContent().mLayoutSeq;
@@ -926,8 +945,8 @@
         final Task task = createTask(mDisplayContent);
         final TaskFragment embeddedTf = createTaskFragmentWithEmbeddedActivity(task, organizer);
         final ActivityRecord embeddedActivity = embeddedTf.getTopMostActivity();
-        final WindowState win = createWindow(null /* parent */, TYPE_APPLICATION, embeddedActivity,
-                "App window");
+        final WindowState win = newWindowBuilder("App window", TYPE_APPLICATION).setWindowToken(
+                embeddedActivity).build();
         doReturn(true).when(embeddedActivity).isVisible();
         embeddedActivity.setVisibleRequested(true);
         makeWindowVisible(win);
@@ -949,14 +968,14 @@
 
     @Test
     public void testCantReceiveTouchWhenAppTokenHiddenRequested() {
-        final WindowState win0 = createWindow(null, TYPE_APPLICATION, "win0");
+        final WindowState win0 = newWindowBuilder("win0", TYPE_APPLICATION).build();
         win0.mActivityRecord.setVisibleRequested(false);
         assertFalse(win0.canReceiveTouchInput());
     }
 
     @Test
     public void testCantReceiveTouchWhenNotFocusable() {
-        final WindowState win0 = createWindow(null, TYPE_APPLICATION, "win0");
+        final WindowState win0 = newWindowBuilder("win0", TYPE_APPLICATION).build();
         final Task rootTask = win0.mActivityRecord.getRootTask();
         spyOn(rootTask);
         when(rootTask.shouldIgnoreInput()).thenReturn(true);
@@ -969,7 +988,7 @@
 
     @Test
     public void testUpdateInputWindowHandle() {
-        final WindowState win = createWindow(null, TYPE_APPLICATION, "win");
+        final WindowState win = newWindowBuilder("win", TYPE_APPLICATION).build();
         win.mAttrs.inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY;
         win.mAttrs.flags = FLAG_WATCH_OUTSIDE_TOUCH | FLAG_SPLIT_TOUCH;
         final InputWindowHandle handle = new InputWindowHandle(
@@ -982,7 +1001,6 @@
 
         assertTrue(handleWrapper.isChanged());
         assertTrue(testFlag(handle.inputConfig, InputConfig.WATCH_OUTSIDE_TOUCH));
-        assertFalse(testFlag(handle.inputConfig, InputConfig.PREVENT_SPLITTING));
         assertTrue(testFlag(handle.inputConfig, InputConfig.DISABLE_USER_ACTIVITY));
         // The window of standard resizable task should not use surface crop as touchable region.
         assertFalse(handle.replaceTouchableRegionWithCrop);
@@ -1026,7 +1044,7 @@
     @DisableFlags(Flags.FLAG_SCROLLING_FROM_LETTERBOX)
     @Test
     public void testTouchRegionUsesLetterboxBoundsIfTransformedBoundsAndLetterboxScrolling() {
-        final WindowState win = createWindow(null, TYPE_APPLICATION, "win");
+        final WindowState win = newWindowBuilder("win", TYPE_APPLICATION).build();
 
         // Transformed bounds used for size of touchable region if letterbox inner bounds are empty.
         final Rect transformedBounds = new Rect(0, 0, 300, 500);
@@ -1051,7 +1069,7 @@
     @DisableFlags(Flags.FLAG_SCROLLING_FROM_LETTERBOX)
     @Test
     public void testTouchRegionUsesLetterboxBoundsIfNullTransformedBoundsAndLetterboxScrolling() {
-        final WindowState win = createWindow(null, TYPE_APPLICATION, "win");
+        final WindowState win = newWindowBuilder("win", TYPE_APPLICATION).build();
 
         // Fragment bounds used for size of touchable region if letterbox inner bounds are empty
         // and Transform bounds are null.
@@ -1083,7 +1101,7 @@
     @EnableFlags(Flags.FLAG_SCROLLING_FROM_LETTERBOX)
     @Test
     public void testTouchRegionUsesTransformedBoundsIfLetterboxScrolling() {
-        final WindowState win = createWindow(null, TYPE_APPLICATION, "win");
+        final WindowState win = newWindowBuilder("win", TYPE_APPLICATION).build();
 
         // Transformed bounds used for size of touchable region if letterbox inner bounds are empty.
         final Rect transformedBounds = new Rect(0, 0, 300, 500);
@@ -1109,7 +1127,7 @@
     public void testHasActiveVisibleWindow() {
         final int uid = ActivityBuilder.DEFAULT_FAKE_UID;
 
-        final WindowState app = createWindow(null, TYPE_APPLICATION, "app", uid);
+        final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).setOwnerId(uid).build();
         app.mActivityRecord.setVisible(false);
         app.mActivityRecord.setVisibility(false);
         assertFalse(mAtm.hasActiveVisibleWindow(uid));
@@ -1120,15 +1138,17 @@
         // Make the activity invisible and add a visible toast. The uid should have no active
         // visible window because toast can be misused by legacy app to bypass background check.
         app.mActivityRecord.setVisibility(false);
-        final WindowState overlay = createWindow(null, TYPE_APPLICATION_OVERLAY, "overlay", uid);
-        final WindowState toast = createWindow(null, TYPE_TOAST, app.mToken, "toast", uid);
+        final WindowState overlay = newWindowBuilder("overlay",
+                TYPE_APPLICATION_OVERLAY).setOwnerId(uid).build();
+        final WindowState toast = newWindowBuilder("toast", TYPE_TOAST).setWindowToken(
+                app.mToken).setOwnerId(uid).build();
         toast.onSurfaceShownChanged(true);
         assertFalse(mAtm.hasActiveVisibleWindow(uid));
 
         // Though starting window should belong to system. Make sure it is ignored to avoid being
         // allow-list unexpectedly, see b/129563343.
-        final WindowState starting =
-                createWindow(null, TYPE_APPLICATION_STARTING, app.mToken, "starting", uid);
+        final WindowState starting = newWindowBuilder("starting",
+                TYPE_APPLICATION_STARTING).setWindowToken(app.mToken).setOwnerId(uid).build();
         starting.onSurfaceShownChanged(true);
         assertFalse(mAtm.hasActiveVisibleWindow(uid));
 
@@ -1145,8 +1165,8 @@
     @SetupWindows(addWindows = { W_ACTIVITY, W_INPUT_METHOD })
     @Test
     public void testNeedsRelativeLayeringToIme_notAttached() {
-        WindowState sameTokenWindow = createWindow(null, TYPE_BASE_APPLICATION, mAppWindow.mToken,
-                "SameTokenWindow");
+        WindowState sameTokenWindow = newWindowBuilder("SameTokenWindow",
+                TYPE_BASE_APPLICATION).setWindowToken(mAppWindow.mToken).build();
         mDisplayContent.setImeLayeringTarget(mAppWindow);
         makeWindowVisible(mImeWindow);
         sameTokenWindow.mActivityRecord.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
@@ -1158,8 +1178,8 @@
     @SetupWindows(addWindows = { W_ACTIVITY, W_INPUT_METHOD })
     @Test
     public void testNeedsRelativeLayeringToIme_startingWindow() {
-        WindowState sameTokenWindow = createWindow(null, TYPE_APPLICATION_STARTING,
-                mAppWindow.mToken, "SameTokenWindow");
+        WindowState sameTokenWindow = newWindowBuilder("SameTokenWindow",
+                TYPE_APPLICATION_STARTING).setWindowToken(mAppWindow.mToken).build();
         mDisplayContent.setImeLayeringTarget(mAppWindow);
         makeWindowVisible(mImeWindow);
         sameTokenWindow.mActivityRecord.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
@@ -1169,9 +1189,9 @@
     @UseTestDisplay(addWindows = {W_ACTIVITY, W_INPUT_METHOD})
     @Test
     public void testNeedsRelativeLayeringToIme_systemDialog() {
-        WindowState systemDialogWindow = createWindow(null, TYPE_SECURE_SYSTEM_OVERLAY,
-                mDisplayContent,
-                "SystemDialog", true);
+        WindowState systemDialogWindow = newWindowBuilder("SystemDialog",
+                TYPE_SECURE_SYSTEM_OVERLAY).setDisplay(
+                mDisplayContent).setOwnerCanAddInternalSystemWindow(true).build();
         mDisplayContent.setImeLayeringTarget(mAppWindow);
         mAppWindow.getTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
         makeWindowVisible(mImeWindow);
@@ -1182,20 +1202,21 @@
     @UseTestDisplay(addWindows = {W_INPUT_METHOD})
     @Test
     public void testNeedsRelativeLayeringToIme_notificationShadeShouldNotHideSystemDialog() {
-        WindowState systemDialogWindow = createWindow(null, TYPE_SECURE_SYSTEM_OVERLAY,
-                mDisplayContent,
-                "SystemDialog", true);
+        WindowState systemDialogWindow = newWindowBuilder("SystemDialog",
+                TYPE_SECURE_SYSTEM_OVERLAY).setDisplay(
+                mDisplayContent).setOwnerCanAddInternalSystemWindow(true).build();
         mDisplayContent.setImeLayeringTarget(systemDialogWindow);
         makeWindowVisible(mImeWindow);
-        WindowState notificationShade = createWindow(null, TYPE_NOTIFICATION_SHADE,
-                mDisplayContent, "NotificationShade", true);
+        WindowState notificationShade = newWindowBuilder("NotificationShade",
+                TYPE_NOTIFICATION_SHADE).setDisplay(
+                mDisplayContent).setOwnerCanAddInternalSystemWindow(true).build();
         notificationShade.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM;
         assertFalse(notificationShade.needsRelativeLayeringToIme());
     }
 
     @Test
     public void testSetFreezeInsetsState() {
-        final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+        final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
         spyOn(app);
         doReturn(true).when(app).isVisible();
 
@@ -1216,7 +1237,7 @@
         verify(app).notifyInsetsChanged();
 
         // Verify that invisible non-activity window won't dispatch insets changed.
-        final WindowState overlay = createWindow(null, TYPE_APPLICATION_OVERLAY, "overlay");
+        final WindowState overlay = newWindowBuilder("overlay", TYPE_APPLICATION_OVERLAY).build();
         makeWindowVisible(overlay);
         assertTrue(overlay.isReadyToDispatchInsetsState());
         overlay.mHasSurface = false;
@@ -1244,9 +1265,9 @@
 
     @Test
     public void testAdjustImeInsetsVisibilityWhenSwitchingApps() {
-        final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
-        final WindowState app2 = createWindow(null, TYPE_APPLICATION, "app2");
-        final WindowState imeWindow = createWindow(null, TYPE_APPLICATION, "imeWindow");
+        final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
+        final WindowState app2 = newWindowBuilder("app2", TYPE_APPLICATION).build();
+        final WindowState imeWindow = newWindowBuilder("imeWindow", TYPE_APPLICATION).build();
         spyOn(imeWindow);
         doReturn(true).when(imeWindow).isVisible();
         mDisplayContent.mInputMethodWindow = imeWindow;
@@ -1279,10 +1300,11 @@
 
     @Test
     public void testAdjustImeInsetsVisibilityWhenSwitchingApps_toAppInMultiWindowMode() {
-        final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
-        final WindowState app2 = createWindow(null, WINDOWING_MODE_MULTI_WINDOW,
-                ACTIVITY_TYPE_STANDARD, TYPE_APPLICATION, mDisplayContent, "app2");
-        final WindowState imeWindow = createWindow(null, TYPE_APPLICATION, "imeWindow");
+        final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
+        final WindowState app2 = newWindowBuilder("app2", TYPE_APPLICATION).setWindowingMode(
+                WINDOWING_MODE_MULTI_WINDOW).setActivityType(ACTIVITY_TYPE_STANDARD).setDisplay(
+                mDisplayContent).build();
+        final WindowState imeWindow = newWindowBuilder("imeWindow", TYPE_APPLICATION).build();
         spyOn(imeWindow);
         doReturn(true).when(imeWindow).isVisible();
         mDisplayContent.mInputMethodWindow = imeWindow;
@@ -1321,8 +1343,8 @@
     @SetupWindows(addWindows = W_ACTIVITY)
     @Test
     public void testUpdateImeControlTargetWhenLeavingMultiWindow() {
-        WindowState app = createWindow(null, TYPE_BASE_APPLICATION,
-                mAppWindow.mToken, "app");
+        WindowState app = newWindowBuilder("app", TYPE_BASE_APPLICATION).setWindowToken(
+                mAppWindow.mToken).build();
         mDisplayContent.setRemoteInsetsController(createDisplayWindowInsetsController());
 
         spyOn(app);
@@ -1349,8 +1371,8 @@
     @SetupWindows(addWindows = { W_ACTIVITY, W_INPUT_METHOD, W_NOTIFICATION_SHADE })
     @Test
     public void testNotificationShadeHasImeInsetsWhenMultiWindow() {
-        WindowState app = createWindow(null, TYPE_BASE_APPLICATION,
-                mAppWindow.mToken, "app");
+        WindowState app = newWindowBuilder("app", TYPE_BASE_APPLICATION).setWindowToken(
+                mAppWindow.mToken).build();
 
         // Simulate entering multi-window mode and windowing mode is multi-window.
         app.mActivityRecord.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
@@ -1376,7 +1398,7 @@
 
     @Test
     public void testRequestedVisibility() {
-        final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+        final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
         app.mActivityRecord.setVisible(false);
         app.mActivityRecord.setVisibility(false);
         assertFalse(app.isVisibleRequested());
@@ -1391,7 +1413,7 @@
 
     @Test
     public void testKeepClearAreas() {
-        final WindowState window = createWindow(null, TYPE_APPLICATION, "window");
+        final WindowState window = newWindowBuilder("window", TYPE_APPLICATION).build();
         makeWindowVisible(window);
 
         final Rect keepClearArea1 = new Rect(0, 0, 10, 10);
@@ -1433,7 +1455,7 @@
 
     @Test
     public void testUnrestrictedKeepClearAreas() {
-        final WindowState window = createWindow(null, TYPE_APPLICATION, "window");
+        final WindowState window = newWindowBuilder("window", TYPE_APPLICATION).build();
         makeWindowVisible(window);
 
         final Rect keepClearArea1 = new Rect(0, 0, 10, 10);
@@ -1481,8 +1503,9 @@
         final InputMethodManagerInternal immi = InputMethodManagerInternal.get();
         spyOn(immi);
 
-        final WindowState imeTarget = createWindow(null /* parent */, TYPE_BASE_APPLICATION,
-                createActivityRecord(mDisplayContent), "imeTarget");
+        final WindowState imeTarget = newWindowBuilder("imeTarget",
+                TYPE_BASE_APPLICATION).setWindowToken(
+                createActivityRecord(mDisplayContent)).build();
 
         imeTarget.mActivityRecord.setVisibleRequested(true);
         makeWindowVisible(imeTarget);
@@ -1562,8 +1585,8 @@
 
     @Test
     public void testIsSecureLocked_flagSecureSet() {
-        WindowState window = createWindow(null /* parent */, TYPE_APPLICATION, "test-window",
-                1 /* ownerId */);
+        WindowState window = newWindowBuilder("test-window", TYPE_APPLICATION).setOwnerId(
+                1).build();
         window.mAttrs.flags |= WindowManager.LayoutParams.FLAG_SECURE;
 
         assertTrue(window.isSecureLocked());
@@ -1571,8 +1594,8 @@
 
     @Test
     public void testIsSecureLocked_flagSecureNotSet() {
-        WindowState window = createWindow(null /* parent */, TYPE_APPLICATION, "test-window",
-                1 /* ownerId */);
+        WindowState window = newWindowBuilder("test-window", TYPE_APPLICATION).setOwnerId(
+                1).build();
 
         assertFalse(window.isSecureLocked());
     }
@@ -1581,8 +1604,8 @@
     public void testIsSecureLocked_disableSecureWindows() {
         assumeTrue(Build.IS_DEBUGGABLE);
 
-        WindowState window = createWindow(null /* parent */, TYPE_APPLICATION, "test-window",
-                1 /* ownerId */);
+        WindowState window = newWindowBuilder("test-window", TYPE_APPLICATION).setOwnerId(
+                1).build();
         window.mAttrs.flags |= WindowManager.LayoutParams.FLAG_SECURE;
         ContentResolver cr = useFakeSettingsProvider();
 
@@ -1617,8 +1640,10 @@
         String testPackage = "test";
         int ownerId1 = 20;
         int ownerId2 = 21;
-        final WindowState window1 = createWindow(null, TYPE_APPLICATION, "window1", ownerId1);
-        final WindowState window2 = createWindow(null, TYPE_APPLICATION, "window2", ownerId2);
+        final WindowState window1 = newWindowBuilder("window1", TYPE_APPLICATION).setOwnerId(
+                ownerId1).build();
+        final WindowState window2 = newWindowBuilder("window2", TYPE_APPLICATION).setOwnerId(
+                ownerId2).build();
 
         // Setting packagename for targeted feature
         window1.mAttrs.packageName = testPackage;
@@ -1638,7 +1663,8 @@
     public void testIsSecureLocked_sensitiveContentBlockOrClearScreenCaptureForApp() {
         String testPackage = "test";
         int ownerId = 20;
-        final WindowState window = createWindow(null, TYPE_APPLICATION, "window", ownerId);
+        final WindowState window = newWindowBuilder("window", TYPE_APPLICATION).setOwnerId(
+                ownerId).build();
         window.mAttrs.packageName = testPackage;
         assertFalse(window.isSecureLocked());
 
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 ce0d912..37d2a75 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -478,7 +478,7 @@
     }
 
     private WindowState createCommonWindow(WindowState parent, int type, String name) {
-        final WindowState win = createWindow(parent, type, name);
+        final WindowState win = newWindowBuilder(name, type).setParent(parent).build();
         // Prevent common windows from been IME targets.
         win.mAttrs.flags |= FLAG_NOT_FOCUSABLE;
         return win;
@@ -502,7 +502,8 @@
     }
 
     WindowState createNavBarWithProvidedInsets(DisplayContent dc) {
-        final WindowState navbar = createWindow(null, TYPE_NAVIGATION_BAR, dc, "navbar");
+        final WindowState navbar = newWindowBuilder("navbar", TYPE_NAVIGATION_BAR).setDisplay(
+                dc).build();
         final Binder owner = new Binder();
         navbar.mAttrs.providedInsets = new InsetsFrameProvider[] {
                 new InsetsFrameProvider(owner, 0, WindowInsets.Type.navigationBars())
@@ -513,7 +514,8 @@
     }
 
     WindowState createStatusBarWithProvidedInsets(DisplayContent dc) {
-        final WindowState statusBar = createWindow(null, TYPE_STATUS_BAR, dc, "statusBar");
+        final WindowState statusBar = newWindowBuilder("statusBar", TYPE_STATUS_BAR).setDisplay(
+                dc).build();
         final Binder owner = new Binder();
         statusBar.mAttrs.providedInsets = new InsetsFrameProvider[] {
                 new InsetsFrameProvider(owner, 0, WindowInsets.Type.statusBars())
@@ -575,92 +577,13 @@
     WindowState createAppWindow(Task task, int type, String name) {
         final ActivityRecord activity = createNonAttachedActivityRecord(task.getDisplayContent());
         task.addChild(activity, 0);
-        return createWindow(null, type, activity, name);
+        return newWindowBuilder(name, type).setWindowToken(activity).build();
     }
 
-    WindowState createDreamWindow(WindowState parent, int type, String name) {
+    WindowState createDreamWindow(String name, int type) {
         final WindowToken token = createWindowToken(
                 mDisplayContent, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_DREAM, type);
-        return createWindow(parent, type, token, name);
-    }
-
-    // TODO: Move these calls to a builder?
-    WindowState createWindow(WindowState parent, int type, String name) {
-        return (parent == null)
-                ? createWindow(parent, type, mDisplayContent, name)
-                : createWindow(parent, type, parent.mToken, name);
-    }
-
-    WindowState createWindow(WindowState parent, int type, String name, int ownerId) {
-        return (parent == null)
-                ? createWindow(parent, type, mDisplayContent, name, ownerId)
-                : createWindow(parent, type, parent.mToken, name, ownerId);
-    }
-
-    WindowState createWindow(WindowState parent, int windowingMode, int activityType,
-            int type, DisplayContent dc, String name) {
-        final WindowToken token = createWindowToken(dc, windowingMode, activityType, type);
-        return createWindow(parent, type, token, name);
-    }
-
-    WindowState createWindow(WindowState parent, int type, DisplayContent dc, String name) {
-        return createWindow(
-                parent, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, type, dc, name);
-    }
-
-    WindowState createWindow(WindowState parent, int type, DisplayContent dc, String name,
-            int ownerId) {
-        final WindowToken token = createWindowToken(
-                dc, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, type);
-        return createWindow(parent, type, token, name, ownerId);
-    }
-
-    WindowState createWindow(WindowState parent, int type, DisplayContent dc, String name,
-            boolean ownerCanAddInternalSystemWindow) {
-        final WindowToken token = createWindowToken(
-                dc, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, type);
-        return createWindow(parent, type, token, name, 0 /* ownerId */,
-                ownerCanAddInternalSystemWindow);
-    }
-
-    WindowState createWindow(WindowState parent, int type, WindowToken token, String name) {
-        return createWindow(parent, type, token, name, 0 /* ownerId */,
-                false /* ownerCanAddInternalSystemWindow */);
-    }
-
-    WindowState createWindow(WindowState parent, int type, WindowToken token, String name,
-            int ownerId) {
-        return createWindow(parent, type, token, name, ownerId,
-                false /* ownerCanAddInternalSystemWindow */);
-    }
-
-    WindowState createWindow(WindowState parent, int type, WindowToken token, String name,
-            int ownerId, boolean ownerCanAddInternalSystemWindow) {
-        return createWindow(parent, type, token, name, ownerId, ownerCanAddInternalSystemWindow,
-                mIWindow);
-    }
-
-    WindowState createWindow(WindowState parent, int type, WindowToken token, String name,
-            int ownerId, boolean ownerCanAddInternalSystemWindow, IWindow iwindow) {
-        return createWindow(parent, type, token, name, ownerId, UserHandle.getUserId(ownerId),
-                ownerCanAddInternalSystemWindow, mWm, getTestSession(token), iwindow);
-    }
-
-    static WindowState createWindow(WindowState parent, int type, WindowToken token,
-            String name, int ownerId, int userId, boolean ownerCanAddInternalSystemWindow,
-            WindowManagerService service, Session session, IWindow iWindow) {
-        SystemServicesTestRule.checkHoldsLock(service.mGlobalLock);
-
-        final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(type);
-        attrs.setTitle(name);
-        attrs.packageName = "test";
-
-        final WindowState w = new WindowState(service, session, iWindow, token, parent,
-                OP_NONE, attrs, VISIBLE, ownerId, userId, ownerCanAddInternalSystemWindow);
-        // TODO: Probably better to make this call in the WindowState ctor to avoid errors with
-        // adding it to the token...
-        token.addWindow(w);
-        return w;
+        return newWindowBuilder(name, type).setWindowToken(token).build();
     }
 
     static void makeWindowVisible(WindowState... windows) {
@@ -1920,11 +1843,14 @@
         private final WindowManagerService mWMService;
         private final SparseArray<IBinder> mTaskAppMap = new SparseArray<>();
         private final HashMap<IBinder, WindowState> mAppWindowMap = new HashMap<>();
+        private final DisplayContent mDisplayContent;
 
-        TestStartingWindowOrganizer(ActivityTaskManagerService service) {
+        TestStartingWindowOrganizer(ActivityTaskManagerService service,
+                DisplayContent displayContent) {
             mAtm = service;
             mWMService = mAtm.mWindowManager;
             mAtm.mTaskOrganizerController.registerTaskOrganizer(this);
+            mDisplayContent = displayContent;
         }
 
         @Override
@@ -1933,10 +1859,11 @@
                 final ActivityRecord activity = ActivityRecord.forTokenLocked(info.appToken);
                 IWindow iWindow = mock(IWindow.class);
                 doReturn(mock(IBinder.class)).when(iWindow).asBinder();
-                final WindowState window = WindowTestsBase.createWindow(null,
-                        TYPE_APPLICATION_STARTING, activity,
-                        "Starting window", 0 /* ownerId */, 0 /* userId*/,
-                        false /* internalWindows */, mWMService, createTestSession(mAtm), iWindow);
+                // WindowToken is already passed, windowTokenCreator is not needed here.
+                final WindowState window = new WindowTestsBase.WindowStateBuilder("Starting window",
+                        TYPE_APPLICATION_STARTING, mWMService, mDisplayContent, iWindow,
+                        (unused) -> createTestSession(mAtm),
+                        null /* windowTokenCreator */).setWindowToken(activity).build();
                 activity.mStartingWindow = window;
                 mAppWindowMap.put(info.appToken, window);
                 mTaskAppMap.put(info.taskInfo.taskId, info.appToken);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java
index f226b9d..a02c3db 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java
@@ -74,11 +74,16 @@
 
         assertEquals(0, token.getWindowsCount());
 
-        final WindowState window1 = createWindow(null, TYPE_APPLICATION, token, "window1");
-        final WindowState window11 = createWindow(window1, FIRST_SUB_WINDOW, token, "window11");
-        final WindowState window12 = createWindow(window1, FIRST_SUB_WINDOW, token, "window12");
-        final WindowState window2 = createWindow(null, TYPE_APPLICATION, token, "window2");
-        final WindowState window3 = createWindow(null, TYPE_APPLICATION, token, "window3");
+        final WindowState window1 = newWindowBuilder("window1", TYPE_APPLICATION).setWindowToken(
+                token).build();
+        final WindowState window11 = newWindowBuilder("window11", FIRST_SUB_WINDOW).setParent(
+                window1).setWindowToken(token).build();
+        final WindowState window12 = newWindowBuilder("window12", FIRST_SUB_WINDOW).setParent(
+                window1).setWindowToken(token).build();
+        final WindowState window2 = newWindowBuilder("window2", TYPE_APPLICATION).setWindowToken(
+                token).build();
+        final WindowState window3 = newWindowBuilder("window3", TYPE_APPLICATION).setWindowToken(
+                token).build();
 
         token.addWindow(window1);
         // NOTE: Child windows will not be added to the token as window containers can only
@@ -105,8 +110,10 @@
     public void testAddWindow_assignsLayers() {
         final TestWindowToken token1 = createTestWindowToken(0, mDisplayContent);
         final TestWindowToken token2 = createTestWindowToken(0, mDisplayContent);
-        final WindowState window1 = createWindow(null, TYPE_STATUS_BAR, token1, "window1");
-        final WindowState window2 = createWindow(null, TYPE_STATUS_BAR, token2, "window2");
+        final WindowState window1 = newWindowBuilder("window1", TYPE_STATUS_BAR).setWindowToken(
+                token1).build();
+        final WindowState window2 = newWindowBuilder("window2", TYPE_STATUS_BAR).setWindowToken(
+                token2).build();
 
         token1.addWindow(window1);
         token2.addWindow(window2);
@@ -122,8 +129,10 @@
 
         assertEquals(token, dc.getWindowToken(token.token));
 
-        final WindowState window1 = createWindow(null, TYPE_APPLICATION, token, "window1");
-        final WindowState window2 = createWindow(null, TYPE_APPLICATION, token, "window2");
+        final WindowState window1 = newWindowBuilder("window1", TYPE_APPLICATION).setWindowToken(
+                token).build();
+        final WindowState window2 = newWindowBuilder("window2", TYPE_APPLICATION).setWindowToken(
+                token).build();
 
         window2.removeImmediately();
         // The token should still be mapped in the display content since it still has a child.
@@ -147,8 +156,10 @@
         // Verify that the token is on the display
         assertNotNull(mDisplayContent.getWindowToken(token.token));
 
-        final WindowState window1 = createWindow(null, TYPE_TOAST, token, "window1");
-        final WindowState window2 = createWindow(null, TYPE_TOAST, token, "window2");
+        final WindowState window1 = newWindowBuilder("window1", TYPE_TOAST).setWindowToken(
+                token).build();
+        final WindowState window2 = newWindowBuilder("window2", TYPE_TOAST).setWindowToken(
+                token).build();
 
         mDisplayContent.removeWindowToken(token.token, true /* animateExit */);
         // Verify that the token is no longer mapped on the display
@@ -231,7 +242,8 @@
 
         assertNull(fromClientToken.mSurfaceControl);
 
-        createWindow(null, TYPE_APPLICATION_OVERLAY, fromClientToken, "window");
+        newWindowBuilder("window", TYPE_APPLICATION_OVERLAY).setWindowToken(
+                fromClientToken).build();
         assertNotNull(fromClientToken.mSurfaceControl);
 
         final WindowToken nonClientToken = new WindowToken.Builder(mDisplayContent.mWmService,
@@ -285,7 +297,7 @@
 
         // Simulate an app window to be the IME layering target, assume the app window has no
         // frozen insets state by default.
-        final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+        final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
         mDisplayContent.setImeLayeringTarget(app);
         assertNull(app.getFrozenInsetsState());
         assertTrue(app.isImeLayeringTarget());
@@ -299,7 +311,8 @@
     @Test
     public void testRemoveWindowToken_noAnimateExitWhenSet() {
         final TestWindowToken token = createTestWindowToken(0, mDisplayContent);
-        final WindowState win = createWindow(null, TYPE_APPLICATION, token, "win");
+        final WindowState win = newWindowBuilder("win", TYPE_APPLICATION).setWindowToken(
+                token).build();
         makeWindowVisible(win);
         assertTrue(win.isOnScreen());
         spyOn(win);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
index 4f60106..84e2118 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
@@ -221,7 +221,7 @@
     }
 
     WindowState createWindow(String name) {
-        return createWindow(null, TYPE_BASE_APPLICATION, mDisplayContent, name);
+        return newWindowBuilder(name, TYPE_BASE_APPLICATION).setDisplay(mDisplayContent).build();
     }
 
     @Test
@@ -263,12 +263,12 @@
     @Test
     public void testAssignWindowLayers_ForImeWithAppTargetWithChildWindows() {
         final WindowState imeAppTarget = createWindow("imeAppTarget");
-        final WindowState imeAppTargetChildAboveWindow = createWindow(imeAppTarget,
-                TYPE_APPLICATION_ATTACHED_DIALOG, imeAppTarget.mToken,
-                "imeAppTargetChildAboveWindow");
-        final WindowState imeAppTargetChildBelowWindow = createWindow(imeAppTarget,
-                TYPE_APPLICATION_MEDIA_OVERLAY, imeAppTarget.mToken,
-                "imeAppTargetChildBelowWindow");
+        final WindowState imeAppTargetChildAboveWindow = newWindowBuilder(
+                "imeAppTargetChildAboveWindow", TYPE_APPLICATION_ATTACHED_DIALOG).setParent(
+                imeAppTarget).setWindowToken(imeAppTarget.mToken).build();
+        final WindowState imeAppTargetChildBelowWindow = newWindowBuilder(
+                "imeAppTargetChildBelowWindow", TYPE_APPLICATION_MEDIA_OVERLAY).setParent(
+                imeAppTarget).setWindowToken(imeAppTarget.mToken).build();
 
         mDisplayContent.setImeLayeringTarget(imeAppTarget);
         makeWindowVisible(mImeWindow);
@@ -313,9 +313,9 @@
 
     @Test
     public void testAssignWindowLayers_ForImeNonAppImeTarget() {
-        final WindowState imeSystemOverlayTarget = createWindow(null, TYPE_SYSTEM_OVERLAY,
-                mDisplayContent, "imeSystemOverlayTarget",
-                true /* ownerCanAddInternalSystemWindow */);
+        final WindowState imeSystemOverlayTarget = newWindowBuilder("imeSystemOverlayTarget",
+                TYPE_SYSTEM_OVERLAY).setDisplay(mDisplayContent).setOwnerCanAddInternalSystemWindow(
+                true).build();
 
         mDisplayContent.setImeLayeringTarget(imeSystemOverlayTarget);
         mDisplayContent.assignChildLayers(mTransaction);
@@ -354,18 +354,19 @@
     @Test
     public void testStackLayers() {
         final WindowState anyWindow1 = createWindow("anyWindow");
-        final WindowState pinnedStackWindow = createWindow(null, WINDOWING_MODE_PINNED,
-                ACTIVITY_TYPE_STANDARD, TYPE_BASE_APPLICATION, mDisplayContent,
-                "pinnedStackWindow");
-        final WindowState dockedStackWindow = createWindow(null,
-                WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, TYPE_BASE_APPLICATION,
-                mDisplayContent, "dockedStackWindow");
-        final WindowState assistantStackWindow = createWindow(null,
-                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_ASSISTANT, TYPE_BASE_APPLICATION,
-                mDisplayContent, "assistantStackWindow");
-        final WindowState homeActivityWindow = createWindow(null, WINDOWING_MODE_FULLSCREEN,
-                ACTIVITY_TYPE_HOME, TYPE_BASE_APPLICATION,
-                mDisplayContent, "homeActivityWindow");
+        final WindowState pinnedStackWindow = newWindowBuilder("pinnedStackWindow",
+                TYPE_BASE_APPLICATION).setWindowingMode(WINDOWING_MODE_PINNED).setActivityType(
+                ACTIVITY_TYPE_STANDARD).setDisplay(mDisplayContent).build();
+        final WindowState dockedStackWindow = newWindowBuilder("dockedStackWindow",
+                TYPE_BASE_APPLICATION).setWindowingMode(
+                WINDOWING_MODE_MULTI_WINDOW).setActivityType(ACTIVITY_TYPE_STANDARD).setDisplay(
+                mDisplayContent).build();
+        final WindowState assistantStackWindow = newWindowBuilder("assistantStackWindow",
+                TYPE_BASE_APPLICATION).setWindowingMode(WINDOWING_MODE_FULLSCREEN).setActivityType(
+                ACTIVITY_TYPE_ASSISTANT).setDisplay(mDisplayContent).build();
+        final WindowState homeActivityWindow = newWindowBuilder("homeActivityWindow",
+                TYPE_BASE_APPLICATION).setWindowingMode(WINDOWING_MODE_FULLSCREEN).setActivityType(
+                ACTIVITY_TYPE_HOME).setDisplay(mDisplayContent).build();
         final WindowState anyWindow2 = createWindow("anyWindow2");
 
         mDisplayContent.assignChildLayers(mTransaction);
@@ -383,13 +384,12 @@
 
     @Test
     public void testAssignWindowLayers_ForSysUiPanels() {
-        final WindowState navBarPanel =
-                createWindow(null, TYPE_NAVIGATION_BAR_PANEL, mDisplayContent, "NavBarPanel");
-        final WindowState statusBarPanel =
-                createWindow(null, TYPE_STATUS_BAR_ADDITIONAL, mDisplayContent,
-                        "StatusBarAdditional");
-        final WindowState statusBarSubPanel =
-                createWindow(null, TYPE_STATUS_BAR_SUB_PANEL, mDisplayContent, "StatusBarSubPanel");
+        final WindowState navBarPanel = newWindowBuilder("NavBarPanel",
+                TYPE_NAVIGATION_BAR_PANEL).setDisplay(mDisplayContent).build();
+        final WindowState statusBarPanel = newWindowBuilder("StatusBarAdditional",
+                TYPE_STATUS_BAR_ADDITIONAL).setDisplay(mDisplayContent).build();
+        final WindowState statusBarSubPanel = newWindowBuilder("StatusBarSubPanel",
+                TYPE_STATUS_BAR_SUB_PANEL).setDisplay(mDisplayContent).build();
         mDisplayContent.assignChildLayers(mTransaction);
 
         // Ime should be above all app windows and below system windows if it is targeting an app
@@ -401,15 +401,16 @@
 
     @Test
     public void testAssignWindowLayers_ForImeOnPopupImeLayeringTarget() {
-        final WindowState imeAppTarget = createWindow(null, TYPE_APPLICATION,
-                mAppWindow.mActivityRecord, "imeAppTarget");
+        final WindowState imeAppTarget = newWindowBuilder("imeAppTarget",
+                TYPE_APPLICATION).setWindowToken(mAppWindow.mActivityRecord).build();
         mDisplayContent.setImeInputTarget(imeAppTarget);
         mDisplayContent.setImeLayeringTarget(imeAppTarget);
         mDisplayContent.setImeControlTarget(imeAppTarget);
 
         // Set a popup IME layering target and keeps the original IME control target behinds it.
-        final WindowState popupImeTargetWin = createWindow(imeAppTarget,
-                TYPE_APPLICATION_SUB_PANEL, mAppWindow.mActivityRecord, "popupImeTargetWin");
+        final WindowState popupImeTargetWin = newWindowBuilder("popupImeTargetWin",
+                TYPE_APPLICATION_SUB_PANEL).setParent(imeAppTarget).setWindowToken(
+                mAppWindow.mActivityRecord).build();
         mDisplayContent.setImeLayeringTarget(popupImeTargetWin);
         mDisplayContent.updateImeParent();
 
@@ -424,11 +425,11 @@
         // then we can drop all negative layering on the windowing side.
 
         final WindowState anyWindow = createWindow("anyWindow");
-        final WindowState child = createWindow(anyWindow, TYPE_APPLICATION_MEDIA, mDisplayContent,
-                "TypeApplicationMediaChild");
-        final WindowState mediaOverlayChild = createWindow(anyWindow,
-                TYPE_APPLICATION_MEDIA_OVERLAY,
-                mDisplayContent, "TypeApplicationMediaOverlayChild");
+        final WindowState child = newWindowBuilder("TypeApplicationMediaChild",
+                TYPE_APPLICATION_MEDIA).setParent(anyWindow).setDisplay(mDisplayContent).build();
+        final WindowState mediaOverlayChild = newWindowBuilder("TypeApplicationMediaOverlayChild",
+                TYPE_APPLICATION_MEDIA_OVERLAY).setParent(anyWindow).setDisplay(
+                mDisplayContent).build();
 
         mDisplayContent.assignChildLayers(mTransaction);
 
@@ -440,14 +441,17 @@
     public void testAssignWindowLayers_ForPostivelyZOrderedSubtype() {
         final WindowState anyWindow = createWindow("anyWindow");
         final ArrayList<WindowState> childList = new ArrayList<>();
-        childList.add(createWindow(anyWindow, TYPE_APPLICATION_PANEL, mDisplayContent,
-                "TypeApplicationPanelChild"));
-        childList.add(createWindow(anyWindow, TYPE_APPLICATION_SUB_PANEL, mDisplayContent,
-                "TypeApplicationSubPanelChild"));
-        childList.add(createWindow(anyWindow, TYPE_APPLICATION_ATTACHED_DIALOG, mDisplayContent,
-                "TypeApplicationAttachedDialogChild"));
-        childList.add(createWindow(anyWindow, TYPE_APPLICATION_ABOVE_SUB_PANEL, mDisplayContent,
-                "TypeApplicationAboveSubPanelPanelChild"));
+        childList.add(newWindowBuilder("TypeApplicationPanelChild",
+                TYPE_APPLICATION_PANEL).setParent(anyWindow).setDisplay(mDisplayContent).build());
+        childList.add(newWindowBuilder("TypeApplicationSubPanelChild",
+                TYPE_APPLICATION_SUB_PANEL).setParent(anyWindow).setDisplay(
+                mDisplayContent).build());
+        childList.add(newWindowBuilder("TypeApplicationAttachedDialogChild",
+                TYPE_APPLICATION_ATTACHED_DIALOG).setParent(anyWindow).setDisplay(
+                mDisplayContent).build());
+        childList.add(newWindowBuilder("TypeApplicationAboveSubPanelPanelChild",
+                TYPE_APPLICATION_ABOVE_SUB_PANEL).setParent(anyWindow).setDisplay(
+                mDisplayContent).build());
 
         final LayerRecordingTransaction t = mTransaction;
         mDisplayContent.assignChildLayers(t);
@@ -469,8 +473,8 @@
 
         // Create a popupWindow
         assertWindowHigher(mImeWindow, mAppWindow);
-        final WindowState popupWindow = createWindow(mAppWindow, TYPE_APPLICATION_PANEL,
-                mDisplayContent, "PopupWindow");
+        final WindowState popupWindow = newWindowBuilder("PopupWindow",
+                TYPE_APPLICATION_PANEL).setParent(mAppWindow).setDisplay(mDisplayContent).build();
         spyOn(popupWindow);
 
         mDisplayContent.assignChildLayers(mTransaction);
@@ -492,8 +496,9 @@
         makeWindowVisible(mImeWindow);
 
         // Create a popupWindow
-        final WindowState systemDialogWindow = createWindow(null, TYPE_SECURE_SYSTEM_OVERLAY,
-                mDisplayContent, "SystemDialog", true);
+        final WindowState systemDialogWindow = newWindowBuilder("SystemDialog",
+                TYPE_SECURE_SYSTEM_OVERLAY).setDisplay(
+                mDisplayContent).setOwnerCanAddInternalSystemWindow(true).build();
         systemDialogWindow.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM;
         spyOn(systemDialogWindow);
 
diff --git a/services/texttospeech/java/com/android/server/texttospeech/TextToSpeechManagerPerUserService.java b/services/texttospeech/java/com/android/server/texttospeech/TextToSpeechManagerPerUserService.java
index d49214a..a9ae5f7 100644
--- a/services/texttospeech/java/com/android/server/texttospeech/TextToSpeechManagerPerUserService.java
+++ b/services/texttospeech/java/com/android/server/texttospeech/TextToSpeechManagerPerUserService.java
@@ -16,6 +16,10 @@
 
 package com.android.server.texttospeech;
 
+import static android.content.Context.BIND_AUTO_CREATE;
+import static android.content.Context.BIND_FOREGROUND_SERVICE;
+import static android.content.Context.BIND_SCHEDULE_LIKE_TOP_APP;
+
 import static com.android.internal.infra.AbstractRemoteService.PERMANENT_BOUND_TIMEOUT_MS;
 
 import android.annotation.NonNull;
@@ -95,7 +99,7 @@
                 ITextToSpeechSessionCallback callback) {
             super(context,
                     new Intent(TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE).setPackage(engine),
-                    Context.BIND_AUTO_CREATE | Context.BIND_SCHEDULE_LIKE_TOP_APP,
+                    BIND_AUTO_CREATE | BIND_SCHEDULE_LIKE_TOP_APP | BIND_FOREGROUND_SERVICE,
                     userId,
                     ITextToSpeechService.Stub::asInterface);
             mEngine = engine;
diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java
index 7082f00..e65e4b0 100644
--- a/telecomm/java/android/telecom/TelecomManager.java
+++ b/telecomm/java/android/telecom/TelecomManager.java
@@ -29,6 +29,7 @@
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
+import android.annotation.TestApi;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledSince;
 import android.compat.annotation.UnsupportedAppUsage;
@@ -1886,6 +1887,34 @@
     }
 
     /**
+     * This test API determines the foreground service delegation state for a VoIP app that adds
+     * calls via {@link TelecomManager#addCall(CallAttributes, Executor, OutcomeReceiver,
+     * CallControlCallback, CallEventCallback)}.  Foreground Service Delegation allows applications
+     * to operate in the background  starting in Android 14 and is granted by Telecom via a request
+     * to the ActivityManager.
+     *
+     * @param handle of the voip app that is being checked
+     * @return true if the app has foreground service delegation. Otherwise, false.
+     *
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_VOIP_CALL_MONITOR_REFACTOR)
+    @TestApi
+    public boolean hasForegroundServiceDelegation(@Nullable PhoneAccountHandle handle) {
+        ITelecomService service = getTelecomService();
+        if (service != null) {
+            try {
+                return service.hasForegroundServiceDelegation(handle, mContext.getOpPackageName());
+            } catch (RemoteException e) {
+                Log.e(TAG,
+                        "RemoteException calling ITelecomService#hasForegroundServiceDelegation.",
+                        e);
+            }
+        }
+        return false;
+    }
+
+    /**
      * Return the line 1 phone number for given phone account.
      *
      * <p>Requires Permission:
diff --git a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
index c85374e..b32379a 100644
--- a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
+++ b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
@@ -409,4 +409,10 @@
      */
     void addCall(in CallAttributes callAttributes, in ICallEventCallback callback, String callId,
         String callingPackage);
+
+    /**
+     * @see TelecomServiceImpl#hasForegroundServiceDelegation
+     */
+    boolean hasForegroundServiceDelegation(in PhoneAccountHandle phoneAccountHandle,
+                                                       String callingPackage);
 }
diff --git a/telephony/java/android/telephony/CellularIdentifierDisclosure.java b/telephony/java/android/telephony/CellularIdentifierDisclosure.java
index 0b6a70f..92c51ec 100644
--- a/telephony/java/android/telephony/CellularIdentifierDisclosure.java
+++ b/telephony/java/android/telephony/CellularIdentifierDisclosure.java
@@ -74,6 +74,14 @@
     /** IMEI DETATCH INDICATION. Reference: 3GPP TS 24.008 9.2.14.
      * Applies to 2g and 3g networks. Used for circuit-switched detach. */
     public static final int NAS_PROTOCOL_MESSAGE_IMSI_DETACH_INDICATION = 11;
+    /** Vendor-specific enumeration to identify a disclosure as potentially benign.
+     * Enables vendors to semantically classify disclosures based on their own logic. */
+    @FlaggedApi(Flags.FLAG_VENDOR_SPECIFIC_CELLULAR_IDENTIFIER_DISCLOSURE_INDICATIONS)
+    public static final int NAS_PROTOCOL_MESSAGE_THREAT_IDENTIFIER_FALSE = 12;
+    /** Vendor-specific enumeration to identify a disclosure as potentially harmful.
+     * Enables vendors to semantically classify disclosures based on their own logic. */
+    @FlaggedApi(Flags.FLAG_VENDOR_SPECIFIC_CELLULAR_IDENTIFIER_DISCLOSURE_INDICATIONS)
+    public static final int NAS_PROTOCOL_MESSAGE_THREAT_IDENTIFIER_TRUE = 13;
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
@@ -84,7 +92,9 @@
             NAS_PROTOCOL_MESSAGE_AUTHENTICATION_AND_CIPHERING_RESPONSE,
             NAS_PROTOCOL_MESSAGE_REGISTRATION_REQUEST, NAS_PROTOCOL_MESSAGE_DEREGISTRATION_REQUEST,
             NAS_PROTOCOL_MESSAGE_CM_REESTABLISHMENT_REQUEST,
-            NAS_PROTOCOL_MESSAGE_CM_SERVICE_REQUEST, NAS_PROTOCOL_MESSAGE_IMSI_DETACH_INDICATION})
+            NAS_PROTOCOL_MESSAGE_CM_SERVICE_REQUEST, NAS_PROTOCOL_MESSAGE_IMSI_DETACH_INDICATION,
+            NAS_PROTOCOL_MESSAGE_THREAT_IDENTIFIER_FALSE,
+            NAS_PROTOCOL_MESSAGE_THREAT_IDENTIFIER_TRUE})
     public @interface NasProtocolMessage {
     }
 
@@ -156,6 +166,14 @@
         return mIsEmergency;
     }
 
+    /**
+     * @return if the modem vendor classifies the disclosure as benign.
+     */
+    @FlaggedApi(Flags.FLAG_VENDOR_SPECIFIC_CELLULAR_IDENTIFIER_DISCLOSURE_INDICATIONS)
+    public boolean isBenign() {
+        return mNasProtocolMessage == NAS_PROTOCOL_MESSAGE_THREAT_IDENTIFIER_FALSE;
+    }
+
     @Override
     public int describeContents() {
         return 0;
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 63a1281..b7b209b 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -3690,8 +3690,8 @@
      * @param list The list of provisioned satellite subscriber infos.
      * @param executor The executor on which the callback will be called.
      * @param callback The callback object to which the result will be delivered.
-     *                 If the request is successful, {@link OutcomeReceiver#onResult(Object)}
-     *                 will return {@code true}.
+     *                 If the request is successful, {@link OutcomeReceiver#onResult}
+     *                 will be called.
      *                 If the request is not successful,
      *                 {@link OutcomeReceiver#onError(Throwable)} will return an error with
      *                 a SatelliteException.
@@ -3704,7 +3704,7 @@
     @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public void provisionSatellite(@NonNull List<SatelliteSubscriberInfo> list,
             @NonNull @CallbackExecutor Executor executor,
-            @NonNull OutcomeReceiver<Boolean, SatelliteException> callback) {
+            @NonNull OutcomeReceiver<Void, SatelliteException> callback) {
         Objects.requireNonNull(executor);
         Objects.requireNonNull(callback);
 
@@ -3718,8 +3718,8 @@
                             if (resultData.containsKey(KEY_PROVISION_SATELLITE_TOKENS)) {
                                 boolean isUpdated =
                                         resultData.getBoolean(KEY_PROVISION_SATELLITE_TOKENS);
-                                executor.execute(() -> Binder.withCleanCallingIdentity(() ->
-                                        callback.onResult(isUpdated)));
+                                executor.execute(() -> Binder.withCleanCallingIdentity(
+                                        () -> callback.onResult(null)));
                             } else {
                                 loge("KEY_REQUEST_PROVISION_TOKENS does not exist.");
                                 executor.execute(() -> Binder.withCleanCallingIdentity(() ->
@@ -3751,8 +3751,8 @@
      * @param list The list of deprovisioned satellite subscriber infos.
      * @param executor The executor on which the callback will be called.
      * @param callback The callback object to which the result will be delivered.
-     *                 If the request is successful, {@link OutcomeReceiver#onResult(Object)}
-     *                 will return {@code true}.
+     *                 If the request is successful, {@link OutcomeReceiver#onResult}
+     *                 will be called.
      *                 If the request is not successful,
      *                 {@link OutcomeReceiver#onError(Throwable)} will return an error with
      *                 a SatelliteException.
@@ -3765,7 +3765,7 @@
     @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public void deprovisionSatellite(@NonNull List<SatelliteSubscriberInfo> list,
             @NonNull @CallbackExecutor Executor executor,
-            @NonNull OutcomeReceiver<Boolean, SatelliteException> callback) {
+            @NonNull OutcomeReceiver<Void, SatelliteException> callback) {
         Objects.requireNonNull(executor);
         Objects.requireNonNull(callback);
 
@@ -3780,7 +3780,7 @@
                                 boolean isUpdated =
                                         resultData.getBoolean(KEY_DEPROVISION_SATELLITE_TOKENS);
                                 executor.execute(() -> Binder.withCleanCallingIdentity(() ->
-                                        callback.onResult(isUpdated)));
+                                        callback.onResult(null)));
                             } else {
                                 loge("KEY_DEPROVISION_SATELLITE_TOKENS does not exist.");
                                 executor.execute(() -> Binder.withCleanCallingIdentity(() ->
diff --git a/telephony/java/android/telephony/satellite/SatelliteTransmissionUpdateCallback.java b/telephony/java/android/telephony/satellite/SatelliteTransmissionUpdateCallback.java
index b5dfb63..e18fad3 100644
--- a/telephony/java/android/telephony/satellite/SatelliteTransmissionUpdateCallback.java
+++ b/telephony/java/android/telephony/satellite/SatelliteTransmissionUpdateCallback.java
@@ -78,6 +78,9 @@
     /**
      * Called when framework receives a request to send a datagram.
      *
+     * Informs external apps that device is working on sending a datagram out and is in the process
+     * of checking if all the conditions required to send datagrams are met.
+     *
      * @param datagramType The type of the requested datagram.
      */
     @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
diff --git a/test-mock/Android.bp b/test-mock/Android.bp
index 71f3033..cadb0bd 100644
--- a/test-mock/Android.bp
+++ b/test-mock/Android.bp
@@ -72,6 +72,7 @@
         "tests/**/*.java",
     ],
     auto_gen_config: true,
+    team: "trendy_team_ravenwood",
 }
 
 // Make the current.txt available for use by the cts/tests/signature and /vendor tests.
diff --git a/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java b/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java
index df92898..9640a84 100644
--- a/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java
+++ b/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java
@@ -29,6 +29,7 @@
         AppJankStats jankStats = new AppJankStats(
                 /*App Uid*/APP_ID,
                 /*Widget Id*/"test widget id",
+                /*navigationComponent*/null,
                 /*Widget Category*/AppJankStats.WIDGET_CATEGORY_SCROLL,
                 /*Widget State*/AppJankStats.WIDGET_STATE_SCROLLING,
                 /*Total Frames*/100,
diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt
index 08b5f38..75bd5d1 100644
--- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt
+++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt
@@ -17,7 +17,6 @@
 package com.android.server.wm.flicker.activityembedding.open
 
 import android.graphics.Rect
-import android.platform.test.annotations.Presubmit
 import android.tools.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.flicker.legacy.FlickerBuilder
 import android.tools.flicker.legacy.LegacyFlickerTest
@@ -68,13 +67,21 @@
         }
     }
 
-    @Ignore("Not applicable to this CUJ.") override fun navBarWindowIsVisibleAtStartAndEnd() {}
+    @Ignore("Not applicable to this CUJ.")
+    @Test
+    override fun navBarWindowIsVisibleAtStartAndEnd() {}
 
-    @FlakyTest(bugId = 291575593) override fun entireScreenCovered() {}
+    @FlakyTest(bugId = 291575593)
+    @Test
+    override fun entireScreenCovered() {}
 
-    @Ignore("Not applicable to this CUJ.") override fun statusBarWindowIsAlwaysVisible() {}
+    @Ignore("Not applicable to this CUJ.")
+    @Test
+    override fun statusBarWindowIsAlwaysVisible() {}
 
-    @Ignore("Not applicable to this CUJ.") override fun statusBarLayerPositionAtStartAndEnd() {}
+    @Ignore("Not applicable to this CUJ.")
+    @Test
+    override fun statusBarLayerPositionAtStartAndEnd() {}
 
     /** Transition begins with a split. */
     @FlakyTest(bugId = 286952194)
@@ -122,7 +129,6 @@
 
     /** Always expand activity is on top of the split. */
     @FlakyTest(bugId = 286952194)
-    @Presubmit
     @Test
     fun endsWithAlwaysExpandActivityOnTop() {
         flicker.assertWmEnd {
diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt
index 0ca8f37..e413645 100644
--- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt
+++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt
@@ -176,12 +176,15 @@
     }
 
     @Ignore("Not applicable to this CUJ.")
+    @Test
     override fun visibleLayersShownMoreThanOneConsecutiveEntry() {}
 
     @FlakyTest(bugId = 342596801)
+    @Test
     override fun entireScreenCovered() = super.entireScreenCovered()
 
     @FlakyTest(bugId = 342596801)
+    @Test
     override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
         super.visibleWindowsShownMoreThanOneConsecutiveEntry()
 
diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt
index b8f11dc..ad083fa 100644
--- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt
+++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt
@@ -16,7 +16,6 @@
 
 package com.android.server.wm.flicker.ime
 
-import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
 import android.tools.Rotation
 import android.tools.flicker.junit.FlickerParametersRunnerFactory
@@ -81,7 +80,6 @@
     }
 
     @FlakyTest(bugId = 290767483)
-    @Postsubmit
     @Test
     fun imeLayerAlphaOneAfterSnapshotStartingWindowRemoval() {
         val layerTrace = flicker.reader.readLayersTrace() ?: error("Unable to read layers trace")
diff --git a/tests/Input/AndroidManifest.xml b/tests/Input/AndroidManifest.xml
index 914adc4..8d380f0 100644
--- a/tests/Input/AndroidManifest.xml
+++ b/tests/Input/AndroidManifest.xml
@@ -32,7 +32,7 @@
              android:process=":externalProcess">
         </activity>
 
-        <activity android:name="com.android.test.input.CaptureEventActivity"
+        <activity android:name="com.android.cts.input.CaptureEventActivity"
             android:label="Capture events"
             android:configChanges="touchscreen|uiMode|orientation|screenSize|screenLayout|keyboardHidden|uiMode|navigation|keyboard|density|fontScale|layoutDirection|locale|mcc|mnc|smallestScreenSize"
             android:enableOnBackInvokedCallback="false"
diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
index 4d7085f..8c04f647 100644
--- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
@@ -59,6 +59,7 @@
 import org.junit.After
 import org.junit.Assert.assertArrayEquals
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
 import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Rule
@@ -830,6 +831,18 @@
                 KeyEvent.META_META_ON or KeyEvent.META_ALT_ON,
                 intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
             ),
+            TestData(
+                "META + ALT + 'V' -> Toggle Voice Access",
+                intArrayOf(
+                    KeyEvent.KEYCODE_META_LEFT,
+                    KeyEvent.KEYCODE_ALT_LEFT,
+                    KeyEvent.KEYCODE_V
+                ),
+                KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS,
+                intArrayOf(KeyEvent.KEYCODE_V),
+                KeyEvent.META_META_ON or KeyEvent.META_ALT_ON,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            ),
         )
     }
 
@@ -843,6 +856,7 @@
         com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG,
         com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_MOUSE_KEYS,
         com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES,
+        com.android.hardware.input.Flags.FLAG_ENABLE_VOICE_ACCESS_KEY_GESTURES,
         com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT,
         com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS
     )
@@ -861,6 +875,7 @@
         com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG,
         com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_MOUSE_KEYS,
         com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES,
+        com.android.hardware.input.Flags.FLAG_ENABLE_VOICE_ACCESS_KEY_GESTURES,
         com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT,
         com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS
     )
@@ -1452,20 +1467,32 @@
     @Parameters(method = "customInputGesturesTestArguments")
     fun testCustomKeyGestures(test: TestData) {
         setupKeyGestureController()
+        val trigger = InputGestureData.createKeyTrigger(
+            test.expectedKeys[0],
+            test.expectedModifierState
+        )
         val builder = InputGestureData.Builder()
             .setKeyGestureType(test.expectedKeyGestureType)
-            .setTrigger(
-                InputGestureData.createKeyTrigger(
-                    test.expectedKeys[0],
-                    test.expectedModifierState
-                )
-            )
+            .setTrigger(trigger)
         if (test.expectedAppLaunchData != null) {
             builder.setAppLaunchData(test.expectedAppLaunchData)
         }
         val inputGestureData = builder.build()
 
-        keyGestureController.addCustomInputGesture(0, inputGestureData.aidlData)
+        assertNull(
+            test.toString(),
+            keyGestureController.getInputGesture(0, trigger.aidlTrigger)
+        )
+        assertEquals(
+            test.toString(),
+            InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS,
+            keyGestureController.addCustomInputGesture(0, builder.build().aidlData)
+        )
+        assertEquals(
+            test.toString(),
+            inputGestureData.aidlData,
+            keyGestureController.getInputGesture(0, trigger.aidlTrigger)
+        )
         testKeyGestureInternal(test)
     }
 
diff --git a/tests/Input/src/com/android/test/input/CaptureEventActivity.kt b/tests/Input/src/com/android/test/input/CaptureEventActivity.kt
deleted file mode 100644
index d54e3470..0000000
--- a/tests/Input/src/com/android/test/input/CaptureEventActivity.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * 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.test.input
-
-import android.app.Activity
-import android.os.Bundle
-import android.view.InputEvent
-import android.view.KeyEvent
-import android.view.MotionEvent
-import java.util.concurrent.LinkedBlockingQueue
-import java.util.concurrent.TimeUnit
-import org.junit.Assert.assertNull
-
-class CaptureEventActivity : Activity() {
-    private val events = LinkedBlockingQueue<InputEvent>()
-    var shouldHandleKeyEvents = true
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-
-        // Set the fixed orientation if requested
-        if (intent.hasExtra(EXTRA_FIXED_ORIENTATION)) {
-            val orientation = intent.getIntExtra(EXTRA_FIXED_ORIENTATION, 0)
-            setRequestedOrientation(orientation)
-        }
-
-        // Set the flag if requested
-        if (intent.hasExtra(EXTRA_WINDOW_FLAGS)) {
-            val flags = intent.getIntExtra(EXTRA_WINDOW_FLAGS, 0)
-            window.addFlags(flags)
-        }
-    }
-
-    override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean {
-        events.add(MotionEvent.obtain(ev))
-        return true
-    }
-
-    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
-        events.add(MotionEvent.obtain(ev))
-        return true
-    }
-
-    override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
-        events.add(KeyEvent(event))
-        return shouldHandleKeyEvents
-    }
-
-    override fun dispatchTrackballEvent(ev: MotionEvent?): Boolean {
-        events.add(MotionEvent.obtain(ev))
-        return true
-    }
-
-    fun getInputEvent(): InputEvent? {
-        return events.poll(5, TimeUnit.SECONDS)
-    }
-
-    fun hasReceivedEvents(): Boolean {
-        return !events.isEmpty()
-    }
-
-    fun assertNoEvents() {
-        val event = events.poll(100, TimeUnit.MILLISECONDS)
-        assertNull("Expected no events, but received $event", event)
-    }
-
-    companion object {
-        const val EXTRA_FIXED_ORIENTATION = "fixed_orientation"
-        const val EXTRA_WINDOW_FLAGS = "window_flags"
-    }
-}
diff --git a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt
index 0b281d8..9e0f734 100644
--- a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt
+++ b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt
@@ -28,6 +28,7 @@
 import android.view.MotionEvent
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.cts.input.BatchedEventSplitter
+import com.android.cts.input.CaptureEventActivity
 import com.android.cts.input.InputJsonParser
 import com.android.cts.input.VirtualDisplayActivityScenario
 import com.android.cts.input.inputeventmatchers.isResampled
diff --git a/tests/InputScreenshotTest/robotests/Android.bp b/tests/InputScreenshotTest/robotests/Android.bp
index b2414a8..63a1384 100644
--- a/tests/InputScreenshotTest/robotests/Android.bp
+++ b/tests/InputScreenshotTest/robotests/Android.bp
@@ -66,7 +66,6 @@
         "android.test.mock.stubs.system",
         "truth",
     ],
-    upstream: true,
     java_resource_dirs: ["config"],
     instrumentation_for: "InputRoboApp",
 
diff --git a/tests/Internal/Android.bp b/tests/Internal/Android.bp
index 9f35c7b..e294da1 100644
--- a/tests/Internal/Android.bp
+++ b/tests/Internal/Android.bp
@@ -65,6 +65,7 @@
         "src/com/android/internal/util/ParcellingTests.java",
     ],
     auto_gen_config: true,
+    team: "trendy_team_ravenwood",
 }
 
 java_test_helper_library {
diff --git a/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java b/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java
index 060133d..e7e3d10 100644
--- a/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java
+++ b/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java
@@ -81,7 +81,8 @@
         thrown.expect(SecurityException.class);
         final String packageName = mContext.getPackageName();
 
-        mPlatformCompat.reportChange(1, mPackageManager.getApplicationInfo(packageName, 0));
+        mPlatformCompat.reportChange(1,
+            mPackageManager.getApplicationInfo(packageName, Process.myUid()));
     }
 
     @Test
@@ -90,7 +91,8 @@
         mUiAutomation.adoptShellPermissionIdentity(LOG_COMPAT_CHANGE);
         final String packageName = mContext.getPackageName();
 
-        mPlatformCompat.reportChange(1, mPackageManager.getApplicationInfo(packageName, 0));
+        mPlatformCompat.reportChange(1,
+            mPackageManager.getApplicationInfo(packageName, Process.myUid()));
     }
 
     @Test
@@ -99,7 +101,7 @@
         thrown.expect(SecurityException.class);
         final String packageName = mContext.getPackageName();
 
-        mPlatformCompat.reportChangeByPackageName(1, packageName, 0);
+        mPlatformCompat.reportChangeByPackageName(1, packageName, Process.myUid());
     }
 
     @Test
@@ -108,7 +110,7 @@
         mUiAutomation.adoptShellPermissionIdentity(LOG_COMPAT_CHANGE);
         final String packageName = mContext.getPackageName();
 
-        mPlatformCompat.reportChangeByPackageName(1, packageName, 0);
+        mPlatformCompat.reportChangeByPackageName(1, packageName, Process.myUid());
     }
 
     @Test
@@ -133,7 +135,8 @@
         thrown.expect(SecurityException.class);
         final String packageName = mContext.getPackageName();
 
-        mPlatformCompat.isChangeEnabled(1, mPackageManager.getApplicationInfo(packageName, 0));
+        mPlatformCompat.isChangeEnabled(1,
+            mPackageManager.getApplicationInfo(packageName, Process.myUid()));
     }
 
     @Test
@@ -143,7 +146,8 @@
         mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG);
         final String packageName = mContext.getPackageName();
 
-        mPlatformCompat.isChangeEnabled(1, mPackageManager.getApplicationInfo(packageName, 0));
+        mPlatformCompat.isChangeEnabled(1,
+            mPackageManager.getApplicationInfo(packageName, Process.myUid()));
     }
 
     @Test
@@ -152,7 +156,8 @@
         mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG, LOG_COMPAT_CHANGE);
         final String packageName = mContext.getPackageName();
 
-        mPlatformCompat.isChangeEnabled(1, mPackageManager.getApplicationInfo(packageName, 0));
+        mPlatformCompat.isChangeEnabled(1,
+            mPackageManager.getApplicationInfo(packageName, Process.myUid()));
     }
 
     @Test
@@ -161,7 +166,7 @@
         thrown.expect(SecurityException.class);
         final String packageName = mContext.getPackageName();
 
-        mPlatformCompat.isChangeEnabledByPackageName(1, packageName, 0);
+        mPlatformCompat.isChangeEnabledByPackageName(1, packageName, Process.myUid());
     }
 
     @Test
@@ -171,7 +176,7 @@
         mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG);
         final String packageName = mContext.getPackageName();
 
-        mPlatformCompat.isChangeEnabledByPackageName(1, packageName, 0);
+        mPlatformCompat.isChangeEnabledByPackageName(1, packageName, Process.myUid());
     }
 
     @Test
@@ -180,7 +185,7 @@
         mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG, LOG_COMPAT_CHANGE);
         final String packageName = mContext.getPackageName();
 
-        mPlatformCompat.isChangeEnabledByPackageName(1, packageName, 0);
+        mPlatformCompat.isChangeEnabledByPackageName(1, packageName, Process.myUid());
     }
 
     @Test
diff --git a/tests/testables/tests/AndroidTest.xml b/tests/testables/tests/AndroidTest.xml
index 85f6e62..392bf67 100644
--- a/tests/testables/tests/AndroidTest.xml
+++ b/tests/testables/tests/AndroidTest.xml
@@ -45,6 +45,7 @@
 
     <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
         <option name="directory-keys" value="/data/user/0/com.android.testables/files"/>
+        <option name="directory-keys" value="/data/user/10/com.android.testables/files"/>
         <option name="collect-on-run-ended-only" value="true"/>
         <option name="clean-up" value="true"/>
     </metrics_collector>
diff --git a/tools/aapt2/cmd/Command.cpp b/tools/aapt2/cmd/Command.cpp
index 449d93d..f00a6ca 100644
--- a/tools/aapt2/cmd/Command.cpp
+++ b/tools/aapt2/cmd/Command.cpp
@@ -53,61 +53,79 @@
 
 void Command::AddRequiredFlag(StringPiece name, StringPiece description, std::string* value,
                               uint32_t flags) {
-  auto func = [value, flags](StringPiece arg) -> bool {
-    *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg);
+  auto func = [value, flags](StringPiece arg, std::ostream*) -> bool {
+    if (value) {
+      *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg);
+    }
     return true;
   };
 
-  flags_.emplace_back(Flag(name, description, /* required */ true, /* num_args */ 1, func));
+  flags_.emplace_back(
+      Flag(name, description, /* required */ true, /* num_args */ 1, std::move(func)));
 }
 
 void Command::AddRequiredFlagList(StringPiece name, StringPiece description,
                                   std::vector<std::string>* value, uint32_t flags) {
-  auto func = [value, flags](StringPiece arg) -> bool {
-    value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg));
+  auto func = [value, flags](StringPiece arg, std::ostream*) -> bool {
+    if (value) {
+      value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg));
+    }
     return true;
   };
 
-  flags_.emplace_back(Flag(name, description, /* required */ true, /* num_args */ 1, func));
+  flags_.emplace_back(
+      Flag(name, description, /* required */ true, /* num_args */ 1, std::move(func)));
 }
 
 void Command::AddOptionalFlag(StringPiece name, StringPiece description,
                               std::optional<std::string>* value, uint32_t flags) {
-  auto func = [value, flags](StringPiece arg) -> bool {
-    *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg);
+  auto func = [value, flags](StringPiece arg, std::ostream*) -> bool {
+    if (value) {
+      *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg);
+    }
     return true;
   };
 
-  flags_.emplace_back(Flag(name, description, /* required */ false, /* num_args */ 1, func));
+  flags_.emplace_back(
+      Flag(name, description, /* required */ false, /* num_args */ 1, std::move(func)));
 }
 
 void Command::AddOptionalFlagList(StringPiece name, StringPiece description,
                                   std::vector<std::string>* value, uint32_t flags) {
-  auto func = [value, flags](StringPiece arg) -> bool {
-    value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg));
+  auto func = [value, flags](StringPiece arg, std::ostream*) -> bool {
+    if (value) {
+      value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg));
+    }
     return true;
   };
 
-  flags_.emplace_back(Flag(name, description, /* required */ false, /* num_args */ 1, func));
+  flags_.emplace_back(
+      Flag(name, description, /* required */ false, /* num_args */ 1, std::move(func)));
 }
 
 void Command::AddOptionalFlagList(StringPiece name, StringPiece description,
                                   std::unordered_set<std::string>* value) {
-  auto func = [value](StringPiece arg) -> bool {
-    value->emplace(arg);
+  auto func = [value](StringPiece arg, std::ostream* out_error) -> bool {
+    if (value) {
+      value->emplace(arg);
+    }
     return true;
   };
 
-  flags_.emplace_back(Flag(name, description, /* required */ false, /* num_args */ 1, func));
+  flags_.emplace_back(
+      Flag(name, description, /* required */ false, /* num_args */ 1, std::move(func)));
 }
 
 void Command::AddOptionalSwitch(StringPiece name, StringPiece description, bool* value) {
-  auto func = [value](StringPiece arg) -> bool {
-    *value = true;
+  auto func = [value](StringPiece arg, std::ostream* out_error) -> bool {
+    if (value) {
+      *value = true;
+    }
     return true;
   };
 
-  flags_.emplace_back(Flag(name, description, /* required */ false, /* num_args */ 0, func));
+  flags_.emplace_back(
+      Flag(name, description, /* required */ false, /* num_args */ 0, std::move(func)));
 }
 
 void Command::AddOptionalSubcommand(std::unique_ptr<Command>&& subcommand, bool experimental) {
@@ -172,19 +190,74 @@
       argline = " ";
     }
   }
-  *out << " " << std::setw(kWidth) << std::left << "-h"
-       << "Displays this help menu\n";
   out->flush();
 }
 
-int Command::Execute(const std::vector<StringPiece>& args, std::ostream* out_error) {
+const std::string& Command::addEnvironmentArg(const Flag& flag, const char* env) {
+  if (*env && flag.num_args > 0) {
+    return environment_args_.emplace_back(flag.name + '=' + env);
+  }
+  return flag.name;
+}
+
+//
+// Looks for the flags specified in the environment and adds them to |args|.
+// Expected format:
+// - _AAPT2_UPPERCASE_NAME are added before all of the command line flags, so it's
+//   a default for the flag that may get overridden by the command line.
+// - AAPT2_UPPERCASE_NAME_ are added after them, making this to be the final value
+//   even if there was something on the command line.
+// - All dashes in the flag name get replaced with underscores, the rest of it is
+//   intact.
+//
+// E.g.
+//  --set-some-flag becomes either _AAPT2_SET_SOME_FLAG or AAPT2_SET_SOME_FLAG_
+//  --set-param=2 is _AAPT2_SET_SOME_FLAG=2
+//
+// Values get passed as it, with no processing or quoting.
+//
+// This way one can make sure aapt2 has the flags they need even when it is
+// launched in a way they can't control, e.g. deep inside a build.
+//
+void Command::parseFlagsFromEnvironment(std::vector<StringPiece>& args) {
+  // If the first argument is a subcommand then skip it and prepend the flags past that (the root
+  // command should only have a single '-h' flag anyway).
+  const int insert_pos = args.empty() ? 0 : args.front().starts_with('-') ? 0 : 1;
+
+  std::string env_name;
+  for (const Flag& flag : flags_) {
+    // First, the prefix version.
+    env_name.assign("_AAPT2_");
+    // Append the uppercased flag name, skipping all dashes in front and replacing them with
+    // underscores later.
+    auto name_start = flag.name.begin();
+    while (name_start != flag.name.end() && *name_start == '-') {
+      ++name_start;
+    }
+    std::transform(name_start, flag.name.end(), std::back_inserter(env_name),
+                   [](char c) { return c == '-' ? '_' : toupper(c); });
+    if (auto prefix_env = getenv(env_name.c_str())) {
+      args.insert(args.begin() + insert_pos, addEnvironmentArg(flag, prefix_env));
+    }
+    // Now reuse the same name variable to construct a suffix version: append the
+    // underscore and just skip the one in front.
+    env_name += '_';
+    if (auto suffix_env = getenv(env_name.c_str() + 1)) {
+      args.push_back(addEnvironmentArg(flag, suffix_env));
+    }
+  }
+}
+
+int Command::Execute(std::vector<StringPiece>& args, std::ostream* out_error) {
   TRACE_NAME_ARGS("Command::Execute", args);
   std::vector<std::string> file_args;
 
+  parseFlagsFromEnvironment(args);
+
   for (size_t i = 0; i < args.size(); i++) {
     StringPiece arg = args[i];
     if (*(arg.data()) != '-') {
-      // Continue parsing as the subcommand if the first argument matches one of the subcommands
+      // Continue parsing as a subcommand if the first argument matches one of the subcommands
       if (i == 0) {
         for (auto& subcommand : subcommands_) {
           if (arg == subcommand->name_ || (!subcommand->short_name_.empty()
@@ -211,37 +284,67 @@
       return 1;
     }
 
+    static constexpr auto matchShortArg = [](std::string_view arg, const Flag& flag) static {
+      return flag.name.starts_with("--") &&
+             arg.compare(0, 2, std::string_view(flag.name.c_str() + 1, 2)) == 0;
+    };
+
     bool match = false;
     for (Flag& flag : flags_) {
-      // Allow both "--arg value" and "--arg=value" syntax.
+      // Allow both "--arg value" and "--arg=value" syntax, and look for the cases where we can
+      // safely deduce the "--arg" flag from the short "-a" version when there's no value expected
+      bool matched_current = false;
       if (arg.starts_with(flag.name) &&
           (arg.size() == flag.name.size() || (flag.num_args > 0 && arg[flag.name.size()] == '='))) {
-        if (flag.num_args > 0) {
-          if (arg.size() == flag.name.size()) {
-            i++;
-            if (i >= args.size()) {
-              *out_error << flag.name << " missing argument.\n\n";
-              Usage(out_error);
-              return 1;
-            }
-            arg = args[i];
-          } else {
-            arg.remove_prefix(flag.name.size() + 1);
-            // Disallow empty arguments after '='.
-            if (arg.empty()) {
-              *out_error << flag.name << " has empty argument.\n\n";
-              Usage(out_error);
-              return 1;
-            }
+        matched_current = true;
+      } else if (flag.num_args == 0 && matchShortArg(arg, flag)) {
+        matched_current = true;
+        // It matches, now need to make sure no other flag would match as well.
+        // This is really inefficient, but we don't expect to have enough flags for it to matter
+        // (famous last words).
+        for (const Flag& other_flag : flags_) {
+          if (&other_flag == &flag) {
+            continue;
           }
-          flag.action(arg);
-        } else {
-          flag.action({});
+          if (matchShortArg(arg, other_flag)) {
+            matched_current = false;  // ambiguous, skip this match
+            break;
+          }
         }
-        flag.found = true;
-        match = true;
-        break;
       }
+      if (!matched_current) {
+        continue;
+      }
+
+      if (flag.num_args > 0) {
+        if (arg.size() == flag.name.size()) {
+          i++;
+          if (i >= args.size()) {
+            *out_error << flag.name << " missing argument.\n\n";
+            Usage(out_error);
+            return 1;
+          }
+          arg = args[i];
+        } else {
+          arg.remove_prefix(flag.name.size() + 1);
+          // Disallow empty arguments after '='.
+          if (arg.empty()) {
+            *out_error << flag.name << " has empty argument.\n\n";
+            Usage(out_error);
+            return 1;
+          }
+        }
+        if (!flag.action(arg, out_error)) {
+          return 1;
+        }
+      } else {
+        if (!flag.action({}, out_error)) {
+          return 1;
+        }
+      }
+      flag.found = true;
+      match = true;
+      break;
     }
 
     if (!match) {
diff --git a/tools/aapt2/cmd/Command.h b/tools/aapt2/cmd/Command.h
index 1416e98..767ca9b 100644
--- a/tools/aapt2/cmd/Command.h
+++ b/tools/aapt2/cmd/Command.h
@@ -14,10 +14,11 @@
  * limitations under the License.
  */
 
-#ifndef AAPT_COMMAND_H
-#define AAPT_COMMAND_H
+#pragma once
 
+#include <deque>
 #include <functional>
+#include <memory>
 #include <optional>
 #include <ostream>
 #include <string>
@@ -30,10 +31,17 @@
 
 class Command {
  public:
-  explicit Command(android::StringPiece name) : name_(name), full_subcommand_name_(name){};
+  explicit Command(android::StringPiece name) : Command(name, {}) {
+  }
 
   explicit Command(android::StringPiece name, android::StringPiece short_name)
-      : name_(name), short_name_(short_name), full_subcommand_name_(name){};
+      : name_(name), short_name_(short_name), full_subcommand_name_(name) {
+    flags_.emplace_back("--help", "Displays this help menu", false, 0,
+                        [this](android::StringPiece arg, std::ostream* out) {
+                          Usage(out);
+                          return false;
+                        });
+  }
 
   Command(Command&&) = default;
   Command& operator=(Command&&) = default;
@@ -76,41 +84,51 @@
   // Parses the command line arguments, sets the flag variable values, and runs the action of
   // the command. If the arguments fail to parse to the command and its subcommands, then the action
   // will not be run and the usage will be printed instead.
-  int Execute(const std::vector<android::StringPiece>& args, std::ostream* outError);
+  int Execute(std::vector<android::StringPiece>& args, std::ostream* out_error);
+
+  // Same, but for a temporary vector of args.
+  int Execute(std::vector<android::StringPiece>&& args, std::ostream* out_error) {
+    return Execute(args, out_error);
+  }
 
   // The action to preform when the command is executed.
   virtual int Action(const std::vector<std::string>& args) = 0;
 
  private:
   struct Flag {
-    explicit Flag(android::StringPiece name, android::StringPiece description,
-                  const bool is_required, const size_t num_args,
-                  std::function<bool(android::StringPiece value)>&& action)
+    explicit Flag(android::StringPiece name, android::StringPiece description, bool is_required,
+                  const size_t num_args,
+                  std::function<bool(android::StringPiece value, std::ostream* out_err)>&& action)
         : name(name),
           description(description),
-          is_required(is_required),
+          action(std::move(action)),
           num_args(num_args),
-          action(std::move(action)) {
+          is_required(is_required) {
     }
 
-    const std::string name;
-    const std::string description;
-    const bool is_required;
-    const size_t num_args;
-    const std::function<bool(android::StringPiece value)> action;
+    std::string name;
+    std::string description;
+    std::function<bool(android::StringPiece value, std::ostream* out_error)> action;
+    size_t num_args;
+    bool is_required;
     bool found = false;
   };
 
+  const std::string& addEnvironmentArg(const Flag& flag, const char* env);
+  void parseFlagsFromEnvironment(std::vector<android::StringPiece>& args);
+
   std::string name_;
   std::string short_name_;
-  std::string description_ = "";
+  std::string description_;
   std::string full_subcommand_name_;
 
   std::vector<Flag> flags_;
   std::vector<std::unique_ptr<Command>> subcommands_;
   std::vector<std::unique_ptr<Command>> experimental_subcommands_;
+  // A collection of arguments loaded from environment variables, with stable positions
+  // in memory - we add them to the vector of string views so the pointers may not change,
+  // with or without short string buffer utilization in std::string.
+  std::deque<std::string> environment_args_;
 };
 
 }  // namespace aapt
-
-#endif  // AAPT_COMMAND_H
diff --git a/tools/aapt2/cmd/Command_test.cpp b/tools/aapt2/cmd/Command_test.cpp
index 20d87e0..ad167c9 100644
--- a/tools/aapt2/cmd/Command_test.cpp
+++ b/tools/aapt2/cmd/Command_test.cpp
@@ -118,4 +118,63 @@
   EXPECT_NE(0, command.Execute({"--flag1"s, "2"s}, &std::cerr));
 }
 
+TEST(CommandTest, ShortOptions) {
+  TestCommand command;
+  bool flag = false;
+  command.AddOptionalSwitch("--flag", "", &flag);
+
+  ASSERT_EQ(0, command.Execute({"--flag"s}, &std::cerr));
+  EXPECT_TRUE(flag);
+
+  // Short version of a switch should work.
+  flag = false;
+  ASSERT_EQ(0, command.Execute({"-f"s}, &std::cerr));
+  EXPECT_TRUE(flag);
+
+  // Ambiguous names shouldn't parse via short options.
+  command.AddOptionalSwitch("--flag-2", "", &flag);
+  ASSERT_NE(0, command.Execute({"-f"s}, &std::cerr));
+
+  // But when we have a proper flag like that it should still work.
+  flag = false;
+  command.AddOptionalSwitch("-f", "", &flag);
+  ASSERT_EQ(0, command.Execute({"-f"s}, &std::cerr));
+  EXPECT_TRUE(flag);
+
+  // A regular short flag works fine as well.
+  flag = false;
+  command.AddOptionalSwitch("-d", "", &flag);
+  ASSERT_EQ(0, command.Execute({"-d"s}, &std::cerr));
+  EXPECT_TRUE(flag);
+
+  // A flag with a value only works via its long name syntax.
+  std::optional<std::string> val;
+  command.AddOptionalFlag("--with-val", "", &val);
+  ASSERT_EQ(0, command.Execute({"--with-val"s, "1"s}, &std::cerr));
+  EXPECT_TRUE(val);
+  EXPECT_STREQ("1", val->c_str());
+
+  // Make sure the flags that require a value can't be parsed via short syntax, -w=blah
+  // looks weird.
+  ASSERT_NE(0, command.Execute({"-w"s, "2"s}, &std::cerr));
+}
+
+TEST(CommandTest, OptionsWithNullptrToAcceptValues) {
+  TestCommand command;
+  command.AddRequiredFlag("--rflag", "", nullptr);
+  command.AddRequiredFlagList("--rlflag", "", nullptr);
+  command.AddOptionalFlag("--oflag", "", nullptr);
+  command.AddOptionalFlagList("--olflag", "", (std::vector<std::string>*)nullptr);
+  command.AddOptionalFlagList("--olflag2", "", (std::unordered_set<std::string>*)nullptr);
+  command.AddOptionalSwitch("--switch", "", nullptr);
+
+  ASSERT_EQ(0, command.Execute({
+    "--rflag"s, "1"s,
+    "--rlflag"s, "1"s,
+    "--oflag"s, "1"s,
+    "--olflag"s, "1"s,
+    "--olflag2"s, "1"s,
+    "--switch"s}, &std::cerr));
+}
+
 }  // namespace aapt
\ No newline at end of file
diff --git a/tools/aapt2/cmd/Convert.cpp b/tools/aapt2/cmd/Convert.cpp
index 6c3eae1..060bc5f 100644
--- a/tools/aapt2/cmd/Convert.cpp
+++ b/tools/aapt2/cmd/Convert.cpp
@@ -425,9 +425,6 @@
                                     << output_format_.value());
     return 1;
   }
-  if (enable_sparse_encoding_) {
-    table_flattener_options_.sparse_entries = SparseEntriesMode::Enabled;
-  }
   if (force_sparse_encoding_) {
     table_flattener_options_.sparse_entries = SparseEntriesMode::Forced;
   }
diff --git a/tools/aapt2/cmd/Convert.h b/tools/aapt2/cmd/Convert.h
index 9452e58..98c8f5f 100644
--- a/tools/aapt2/cmd/Convert.h
+++ b/tools/aapt2/cmd/Convert.h
@@ -36,11 +36,9 @@
         kOutputFormatProto, kOutputFormatBinary, kOutputFormatBinary), &output_format_);
     AddOptionalSwitch(
         "--enable-sparse-encoding",
-        "Enables encoding sparse entries using a binary search tree.\n"
-        "This decreases APK size at the cost of resource retrieval performance.\n"
-        "Only applies sparse encoding to Android O+ resources or all resources if minSdk of "
-        "the APK is O+",
-        &enable_sparse_encoding_);
+        "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n"
+        "enabled if minSdk of the APK is >= 32.",
+        nullptr);
     AddOptionalSwitch("--force-sparse-encoding",
                       "Enables encoding sparse entries using a binary search tree.\n"
                       "This decreases APK size at the cost of resource retrieval performance.\n"
@@ -87,7 +85,6 @@
   std::string output_path_;
   std::optional<std::string> output_format_;
   bool verbose_ = false;
-  bool enable_sparse_encoding_ = false;
   bool force_sparse_encoding_ = false;
   bool enable_compact_entries_ = false;
   std::optional<std::string> resources_config_path_;
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index 232b402..eb71189 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -2504,9 +2504,6 @@
                 << "the --merge-only flag can be only used when building a static library");
     return 1;
   }
-  if (options_.use_sparse_encoding) {
-    options_.table_flattener_options.sparse_entries = SparseEntriesMode::Enabled;
-  }
 
   // The default build type.
   context.SetPackageType(PackageType::kApp);
diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h
index 2f17853..b5bd905 100644
--- a/tools/aapt2/cmd/Link.h
+++ b/tools/aapt2/cmd/Link.h
@@ -75,7 +75,6 @@
   bool no_resource_removal = false;
   bool no_xml_namespaces = false;
   bool do_not_compress_anything = false;
-  bool use_sparse_encoding = false;
   std::unordered_set<std::string> extensions_to_not_compress;
   std::optional<std::regex> regex_to_not_compress;
   FeatureFlagValues feature_flag_values;
@@ -163,9 +162,11 @@
     AddOptionalSwitch("--no-resource-removal", "Disables automatic removal of resources without\n"
             "defaults. Use this only when building runtime resource overlay packages.",
         &options_.no_resource_removal);
-    AddOptionalSwitch("--enable-sparse-encoding",
-                      "This decreases APK size at the cost of resource retrieval performance.",
-                      &options_.use_sparse_encoding);
+    AddOptionalSwitch(
+        "--enable-sparse-encoding",
+        "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n"
+        "enabled if minSdk of the APK is >= 32.",
+        nullptr);
     AddOptionalSwitch("--enable-compact-entries",
         "This decreases APK size by using compact resource entries for simple data types.",
         &options_.table_flattener_options.use_compact_entries);
diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp
index 762441e..f218307 100644
--- a/tools/aapt2/cmd/Optimize.cpp
+++ b/tools/aapt2/cmd/Optimize.cpp
@@ -406,9 +406,6 @@
     return 1;
   }
 
-  if (options_.enable_sparse_encoding) {
-    options_.table_flattener_options.sparse_entries = SparseEntriesMode::Enabled;
-  }
   if (options_.force_sparse_encoding) {
     options_.table_flattener_options.sparse_entries = SparseEntriesMode::Forced;
   }
diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h
index 012b0f2..e3af584 100644
--- a/tools/aapt2/cmd/Optimize.h
+++ b/tools/aapt2/cmd/Optimize.h
@@ -61,9 +61,6 @@
   // TODO(b/246489170): keep the old option and format until transform to the new one
   std::optional<std::string> shortened_paths_map_path;
 
-  // Whether sparse encoding should be used for O+ resources.
-  bool enable_sparse_encoding = false;
-
   // Whether sparse encoding should be used for all resources.
   bool force_sparse_encoding = false;
 
@@ -106,11 +103,9 @@
         &kept_artifacts_);
     AddOptionalSwitch(
         "--enable-sparse-encoding",
-        "Enables encoding sparse entries using a binary search tree.\n"
-        "This decreases APK size at the cost of resource retrieval performance.\n"
-        "Only applies sparse encoding to Android O+ resources or all resources if minSdk of "
-        "the APK is O+",
-        &options_.enable_sparse_encoding);
+        "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n"
+        "enabled if minSdk of the APK is >= 32.",
+        nullptr);
     AddOptionalSwitch("--force-sparse-encoding",
                       "Enables encoding sparse entries using a binary search tree.\n"
                       "This decreases APK size at the cost of resource retrieval performance.\n"
diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp
index 1a82021..b8ac792 100644
--- a/tools/aapt2/format/binary/TableFlattener.cpp
+++ b/tools/aapt2/format/binary/TableFlattener.cpp
@@ -201,7 +201,7 @@
         (context_->GetMinSdkVersion() == 0 && config.sdkVersion == 0)) {
       // Sparse encode if forced or sdk version is not set in context and config.
     } else {
-      // Otherwise, only sparse encode if the entries will be read on platforms S_V2+.
+      // Otherwise, only sparse encode if the entries will be read on platforms S_V2+ (32).
       sparse_encode = sparse_encode && (context_->GetMinSdkVersion() >= SDK_S_V2);
     }
 
diff --git a/tools/aapt2/format/binary/TableFlattener.h b/tools/aapt2/format/binary/TableFlattener.h
index 0633bc81..f1c4c35 100644
--- a/tools/aapt2/format/binary/TableFlattener.h
+++ b/tools/aapt2/format/binary/TableFlattener.h
@@ -37,8 +37,7 @@
 enum class SparseEntriesMode {
   // Disables sparse encoding for entries.
   Disabled,
-  // Enables sparse encoding for all entries for APKs with O+ minSdk. For APKs with minSdk less
-  // than O only applies sparse encoding for resource configuration available on O+.
+  // Enables sparse encoding for all entries for APKs with minSdk >= 32 (S_V2).
   Enabled,
   // Enables sparse encoding for all entries regardless of minSdk.
   Forced,
@@ -47,7 +46,7 @@
 struct TableFlattenerOptions {
   // When enabled, types for configurations with a sparse set of entries are encoded
   // as a sparse map of entry ID and offset to actual data.
-  SparseEntriesMode sparse_entries = SparseEntriesMode::Disabled;
+  SparseEntriesMode sparse_entries = SparseEntriesMode::Enabled;
 
   // When true, use compact entries for simple data
   bool use_compact_entries = false;
diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp
index 0f11685..e3d589e 100644
--- a/tools/aapt2/format/binary/TableFlattener_test.cpp
+++ b/tools/aapt2/format/binary/TableFlattener_test.cpp
@@ -337,13 +337,13 @@
   auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f);
 
   TableFlattenerOptions options;
-  options.sparse_entries = SparseEntriesMode::Enabled;
+  options.sparse_entries = SparseEntriesMode::Disabled;
 
   std::string no_sparse_contents;
-  ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents));
+  ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &no_sparse_contents));
 
   std::string sparse_contents;
-  ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents));
+  ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &sparse_contents));
 
   EXPECT_GT(no_sparse_contents.size(), sparse_contents.size());
 
@@ -421,13 +421,13 @@
   auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f);
 
   TableFlattenerOptions options;
-  options.sparse_entries = SparseEntriesMode::Enabled;
+  options.sparse_entries = SparseEntriesMode::Disabled;
 
   std::string no_sparse_contents;
-  ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents));
+  ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &no_sparse_contents));
 
   std::string sparse_contents;
-  ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents));
+  ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &sparse_contents));
 
   EXPECT_GT(no_sparse_contents.size(), sparse_contents.size());
 
diff --git a/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt b/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt
index 6da6fc6..d0807f2 100644
--- a/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt
+++ b/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt
@@ -877,7 +877,7 @@
   }
   tool_fingerprint {
     tool: "Android Asset Packaging Tool (aapt)"
-    version: "2.19-SOONG BUILD NUMBER PLACEHOLDER"
+    version: "2.20-SOONG BUILD NUMBER PLACEHOLDER"
   }
 }
 xml_files {
diff --git a/tools/aapt2/readme.md b/tools/aapt2/readme.md
index 8368f9d..664d841 100644
--- a/tools/aapt2/readme.md
+++ b/tools/aapt2/readme.md
@@ -1,5 +1,11 @@
 # Android Asset Packaging Tool 2.0 (AAPT2) release notes
 
+## Version 2.20
+- Too many features, bug fixes, and improvements to list since the last minor version update in
+  2017. This README will be updated more frequently in the future.
+- Sparse encoding is now always enabled by default if the minSdkVersion is >= 32 (S_V2). The
+  `--enable-sparse-encoding` flag still exists, but is a no-op.
+
 ## Version 2.19
 - Added navigation resource type.
 - Fixed issue with resource deduplication. (bug 64397629)
diff --git a/tools/aapt2/util/Util.cpp b/tools/aapt2/util/Util.cpp
index 3d83caf2..6a4dfa6 100644
--- a/tools/aapt2/util/Util.cpp
+++ b/tools/aapt2/util/Util.cpp
@@ -227,7 +227,7 @@
   static const char* const sMajorVersion = "2";
 
   // Update minor version whenever a feature or flag is added.
-  static const char* const sMinorVersion = "19";
+  static const char* const sMinorVersion = "20";
 
   // The build id of aapt2 binary.
   static const std::string sBuildId = [] {
diff --git a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt
index ea660b0..22d364e 100644
--- a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt
+++ b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt
@@ -263,7 +263,7 @@
                 .returns(Boolean::class.java)
                 .addParameter(CONTEXT_CLASS, "context")
                 .addParameter(String::class.java, "featureName")
-                .addStatement("return context.getPackageManager().hasSystemFeature(featureName, 0)")
+                .addStatement("return context.getPackageManager().hasSystemFeature(featureName)")
                 .build()
         )
     }
diff --git a/tools/systemfeatures/tests/golden/RoFeatures.java.gen b/tools/systemfeatures/tests/golden/RoFeatures.java.gen
index ee97b26..730dacb 100644
--- a/tools/systemfeatures/tests/golden/RoFeatures.java.gen
+++ b/tools/systemfeatures/tests/golden/RoFeatures.java.gen
@@ -70,7 +70,7 @@
     }
 
     private static boolean hasFeatureFallback(Context context, String featureName) {
-        return context.getPackageManager().hasSystemFeature(featureName, 0);
+        return context.getPackageManager().hasSystemFeature(featureName);
     }
 
     /**
diff --git a/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen b/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen
index 40c7db7..fe268c7 100644
--- a/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen
+++ b/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen
@@ -25,7 +25,7 @@
     }
 
     private static boolean hasFeatureFallback(Context context, String featureName) {
-        return context.getPackageManager().hasSystemFeature(featureName, 0);
+        return context.getPackageManager().hasSystemFeature(featureName);
     }
 
     /**
diff --git a/tools/systemfeatures/tests/golden/RwFeatures.java.gen b/tools/systemfeatures/tests/golden/RwFeatures.java.gen
index 7bf8961..bcf978d 100644
--- a/tools/systemfeatures/tests/golden/RwFeatures.java.gen
+++ b/tools/systemfeatures/tests/golden/RwFeatures.java.gen
@@ -55,7 +55,7 @@
     }
 
     private static boolean hasFeatureFallback(Context context, String featureName) {
-        return context.getPackageManager().hasSystemFeature(featureName, 0);
+        return context.getPackageManager().hasSystemFeature(featureName);
     }
 
     /**
diff --git a/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen b/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen
index eb7ec63..7bad5a2 100644
--- a/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen
+++ b/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen
@@ -14,7 +14,7 @@
  */
 public final class RwNoFeatures {
     private static boolean hasFeatureFallback(Context context, String featureName) {
-        return context.getPackageManager().hasSystemFeature(featureName, 0);
+        return context.getPackageManager().hasSystemFeature(featureName);
     }
 
     /**
diff --git a/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java b/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java
index ed3f5c9..491b55e 100644
--- a/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java
+++ b/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java
@@ -76,28 +76,28 @@
 
         // Also ensure we fall back to the PackageManager for feature APIs without an accompanying
         // versioned feature definition.
-        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(true);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true);
         assertThat(RwFeatures.hasFeatureWatch(mContext)).isTrue();
-        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(false);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(false);
         assertThat(RwFeatures.hasFeatureWatch(mContext)).isFalse();
     }
 
     @Test
     public void testReadonlyDisabledWithDefinedFeatures() {
         // Always fall back to the PackageManager for defined, explicit features queries.
-        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(true);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true);
         assertThat(RwFeatures.hasFeatureWatch(mContext)).isTrue();
 
-        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(false);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(false);
         assertThat(RwFeatures.hasFeatureWatch(mContext)).isFalse();
 
-        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI, 0)).thenReturn(true);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true);
         assertThat(RwFeatures.hasFeatureWifi(mContext)).isTrue();
 
-        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN, 0)).thenReturn(false);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN)).thenReturn(false);
         assertThat(RwFeatures.hasFeatureVulkan(mContext)).isFalse();
 
-        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO, 0)).thenReturn(false);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO)).thenReturn(false);
         assertThat(RwFeatures.hasFeatureAuto(mContext)).isFalse();
 
         // For defined and undefined features, conditional queries should report null (unknown).
@@ -139,9 +139,9 @@
         assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 100)).isFalse();
 
         // VERSION=
-        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO, 0)).thenReturn(false);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO)).thenReturn(false);
         assertThat(RoFeatures.hasFeatureAuto(mContext)).isFalse();
-        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO, 0)).thenReturn(true);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO)).thenReturn(true);
         assertThat(RoFeatures.hasFeatureAuto(mContext)).isTrue();
         assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, -1)).isNull();
         assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull();
@@ -149,9 +149,9 @@
 
         // For feature APIs without an associated feature definition, conditional queries should
         // report null, and explicit queries should report runtime-defined versions.
-        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_PC, 0)).thenReturn(true);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_PC)).thenReturn(true);
         assertThat(RoFeatures.hasFeaturePc(mContext)).isTrue();
-        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_PC, 0)).thenReturn(false);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_PC)).thenReturn(false);
         assertThat(RoFeatures.hasFeaturePc(mContext)).isFalse();
         assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_PC, -1)).isNull();
         assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_PC, 0)).isNull();