Merge "Block visible background user unallowed key event gestures" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index b1c091b..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",
@@ -523,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 {
@@ -548,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
@@ -828,8 +831,8 @@
     min_sdk_version: "30",
     apex_available: [
         "//apex_available:platform",
-        "com.android.permission",
         "com.android.nfcservices",
+        "com.android.permission",
     ],
 }
 
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/core/api/current.txt b/core/api/current.txt
index c410939..6707c15d 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";
   }
 
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 1286851..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);
   }
 
@@ -8094,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);
   }
 
@@ -15044,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
@@ -15061,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
   }
@@ -18567,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 33a0616..a352d9d 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -3415,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 {
@@ -4538,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/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 b198811..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;
@@ -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/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/Notification.java b/core/java/android/app/Notification.java
index 24594ab..252978f 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);
@@ -6204,7 +6214,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);
@@ -6804,8 +6814,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 +6989,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/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/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/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/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 82%
rename from core/java/android/companion/virtual/flags.aconfig
rename to core/java/android/companion/virtual/flags/deprecated_flags_do_not_edit.aconfig
index f31e7d4..eae5062 100644
--- a/core/java/android/companion/virtual/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/deprecated_flags_do_not_edit.aconfig
@@ -1,11 +1,13 @@
-# 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"
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/Intent.java b/core/java/android/content/Intent.java
index 3d75423..350048d 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);
             }
         }
     }
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/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/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/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/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 9f5dabf..290f526 100644
--- a/core/java/android/hardware/input/KeyGestureEvent.java
+++ b/core/java/android/hardware/input/KeyGestureEvent.java
@@ -129,6 +129,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;
 
@@ -225,6 +226,7 @@
             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 {
@@ -827,6 +829,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/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/GraphicsEnvironment.java b/core/java/android/os/GraphicsEnvironment.java
index 8f6a508..12080ca 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;
             }
 
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/provider/Settings.java b/core/java/android/provider/Settings.java
index c94526b..11dddfb 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)
@@ -13304,6 +13328,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
          */
@@ -17395,13 +17429,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/text/Layout.java b/core/java/android/text/Layout.java
index e254bf3..d53b98c 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -77,7 +77,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 = {
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/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java
index f50ea91..25bd713 100644
--- a/core/java/android/view/WindowManagerGlobal.java
+++ b/core/java/android/view/WindowManagerGlobal.java
@@ -453,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/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java
index fd57aec..544f42b 100644
--- a/core/java/android/view/accessibility/AccessibilityManager.java
+++ b/core/java/android/view/accessibility/AccessibilityManager.java
@@ -148,6 +148,15 @@
     /** @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;
+
     /**
      * 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/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/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 a864132..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,6 +415,17 @@
 }
 
 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: "windowing_frontend"
     description: "Port window-resize animation from legacy to shell"
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/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/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..f0b5493 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,12 @@
 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 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 +56,25 @@
  * 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 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<NotificationProgressDrawable.Part> mProgressDrawableParts = null;
 
     @Nullable
     private Drawable mTracker = null;
+    private boolean mHasTrackerIcon = false;
 
     /** @see R.styleable#NotificationProgressBar_trackerHeight */
     private final int mTrackerHeight;
@@ -76,7 +83,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;
 
@@ -104,12 +117,13 @@
 
         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);
 
@@ -137,20 +151,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 +219,7 @@
         } else {
             progressTrackerDrawable = null;
         }
-        return () -> {
-            setTracker(progressTrackerDrawable);
-        };
+        return () -> setTracker(progressTrackerDrawable);
     }
 
     private void setTracker(@Nullable Drawable tracker) {
@@ -226,8 +243,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 +316,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 +325,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 +335,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 +344,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 +354,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();
     }
@@ -372,6 +403,59 @@
         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() {
+        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) {
+                Log.d(TAG, "Clearing mProgressDrawableParts");
+                mProgressDrawableParts.clear();
+                mNotificationProgressDrawable.setParts(mProgressDrawableParts);
+            }
+            return;
+        }
+
+        mProgressDrawableParts = processAndConvertToDrawableParts(
+                mParts,
+                width,
+                mNotificationProgressDrawable.getSegSegGap(),
+                mNotificationProgressDrawable.getSegPointGap(),
+                mNotificationProgressDrawable.getPointRadius(),
+                mHasTrackerIcon
+        );
+        Pair<List<NotificationProgressDrawable.Part>, Float> p = maybeStretchAndRescaleSegments(
+                mParts,
+                mProgressDrawableParts,
+                mNotificationProgressDrawable.getSegmentMinWidth(),
+                mNotificationProgressDrawable.getPointRadius(),
+                getProgressFraction(),
+                width,
+                mProgressModel.isStyledByProgress(),
+                mHasTrackerIcon ? 0F : mNotificationProgressDrawable.getSegSegGap());
+
+        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 +486,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;
@@ -418,17 +502,17 @@
      *
      * @param w Width of the view, including padding
      * @param tracker Drawable used for the tracker
-     * @param scale Current progress between 0 and 1
+     * @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) {
@@ -482,7 +566,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 +615,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 +625,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 +653,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 +666,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);
+                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;
@@ -624,32 +705,22 @@
         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));
+                        (float) seg.getLength() / progressMax, seg.getColor()));
             }
         }
 
@@ -660,11 +731,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 +765,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 +778,404 @@
 
     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 NotificationProgressDrawable.Part}.
+     */
+    @VisibleForTesting
+    public static List<NotificationProgressDrawable.Part> processAndConvertToDrawableParts(
+            List<Part> parts,
+            float totalWidth,
+            float segSegGap,
+            float segPointGap,
+            float pointRadius,
+            boolean hasTrackerIcon
+    ) {
+        List<NotificationProgressDrawable.Part> 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 NotificationProgressDrawable.Segment(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 NotificationProgressDrawable.Point(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 NotificationProgressBar.Part} data and convert to a pair of:
+     *   - list of {@code NotificationProgressDrawable.Part}.
+     *   - location of progress on the stretched and rescaled progress bar.
+     */
+    @VisibleForTesting
+    public static Pair<List<NotificationProgressDrawable.Part>, Float>
+                maybeStretchAndRescaleSegments(
+            List<Part> parts,
+            List<NotificationProgressDrawable.Part> drawableParts,
+            float segmentMinWidth,
+            float pointRadius,
+            float progressFraction,
+            float totalWidth,
+            boolean isStyledByProgress,
+            float progressGap
+    ) {
+        final List<NotificationProgressDrawable.Segment> drawableSegments = drawableParts
+                .stream()
+                .filter(NotificationProgressDrawable.Segment.class::isInstance)
+                .map(NotificationProgressDrawable.Segment.class::cast)
+                .toList();
+        float totalExcessWidth = 0;
+        float totalPositiveExcessWidth = 0;
+        for (NotificationProgressDrawable.Segment 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 NotificationProgressDrawable.Part drawablePart = drawableParts.get(iPart);
+            if (drawablePart instanceof NotificationProgressDrawable.Segment 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 NotificationProgressDrawable.Point 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 segment with the progress value in its range.
+    private static Pair<List<NotificationProgressDrawable.Part>, Float>
+                maybeSplitDrawableSegmentsByProgress(
+            // Needed to get the original segment start and end positions in pixels.
+            List<Part> parts,
+            List<NotificationProgressDrawable.Part> 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<NotificationProgressDrawable.Part> splitDrawableParts = new ArrayList<>();
+        boolean styleRemainingParts = false;
+        for (int iPart = 0; iPart < nParts; iPart++) {
+            final NotificationProgressDrawable.Part drawablePart = drawableParts.get(iPart);
+            if (drawablePart instanceof NotificationProgressDrawable.Point drawablePoint) {
+                final int color = maybeGetFadedColor(drawablePoint.getColor(), styleRemainingParts);
+                splitDrawableParts.add(
+                        new NotificationProgressDrawable.Point(drawablePoint.getStart(),
+                                drawablePoint.getEnd(), color));
+            }
+            if (iPart == iPartFirstSegmentToStyle) styleRemainingParts = true;
+            if (drawablePart instanceof NotificationProgressDrawable.Segment drawableSegment) {
+                if (iPart == iPartSegmentToSplit) {
+                    if (rescaledProgressX <= drawableSegment.getStart()) {
+                        styleRemainingParts = true;
+                        final int color = maybeGetFadedColor(drawableSegment.getColor(), true);
+                        splitDrawableParts.add(
+                                new NotificationProgressDrawable.Segment(drawableSegment.getStart(),
+                                        drawableSegment.getEnd(),
+                                        color, true));
+                    } else if (drawableSegment.getStart() < rescaledProgressX
+                            && rescaledProgressX < drawableSegment.getEnd()) {
+                        splitDrawableParts.add(
+                                new NotificationProgressDrawable.Segment(drawableSegment.getStart(),
+                                        rescaledProgressX - progressGap,
+                                        drawableSegment.getColor()));
+                        final int color = maybeGetFadedColor(drawableSegment.getColor(), true);
+                        splitDrawableParts.add(
+                                new NotificationProgressDrawable.Segment(rescaledProgressX,
+                                        drawableSegment.getEnd(), color, true));
+                        styleRemainingParts = true;
+                    } else {
+                        splitDrawableParts.add(
+                                new NotificationProgressDrawable.Segment(drawableSegment.getStart(),
+                                        drawableSegment.getEnd(),
+                                        drawableSegment.getColor()));
+                        styleRemainingParts = true;
+                    }
+                } else {
+                    final int color = maybeGetFadedColor(drawableSegment.getColor(),
+                            styleRemainingParts);
+                    splitDrawableParts.add(
+                            new NotificationProgressDrawable.Segment(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.
+    // TODO: b/372908709 - rename to BarPart, BarSegment, BarPoint. This avoids naming ambiguity
+    //  with the types in NotificationProgressDrawable.
+    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..ef0a5d5 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,7 +48,8 @@
 
 /**
  * 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
@@ -57,14 +57,15 @@
 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 RectF mSegRectF = new RectF();
-    private final Rect mPointRect = new Rect();
     private final RectF mPointRectF = new RectF();
 
     private final Paint mFillPaint = new Paint();
@@ -80,27 +81,31 @@
     }
 
     /**
-     * <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;
     }
 
     /**
@@ -120,47 +125,18 @@
         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);
+            final float start = left + part.mStart;
+            final float end = left + part.mEnd;
             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;
-
                 // 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));
+                // 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,58 @@
     }
 
     /**
-     * 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 bar, which is either a {@link Segment} with non-zero length and
+     * varying drawing width, or a {@link Point} 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 Part {
+        // 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 Part(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 +464,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;
+            Part that = (Part) 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 Segment extends Part {
+        /**
+         * 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 Segment(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 Segment(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;
+            Segment that = (Segment) 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 Point extends Part {
+        public Point(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 +561,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 +583,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 +602,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 +680,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_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 96d34a0..4f7ba93 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;
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/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_template_collapsed_base.xml b/core/res/res/layout/notification_2025_template_collapsed_base.xml
index f108ce5..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>
 
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 bd17a3a..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>
 
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 edbebb1..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>
 
diff --git a/core/res/res/layout/notification_2025_template_header.xml b/core/res/res/layout/notification_2025_template_header.xml
index 0c07053..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>
 
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..ce9a0c6 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
@@ -4244,12 +4256,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..debc5e9 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] -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index a18f923..6c014e9 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" />
@@ -2377,6 +2378,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 +3171,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 +3843,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 +3950,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 +4030,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 +4038,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" />
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/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/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 d26bb35..f105ec3 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,13 @@
 
 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 org.junit.Test;
 import org.junit.runner.RunWith;
@@ -37,183 +38,303 @@
 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<NotificationProgressDrawable.Part> drawableParts =
+                NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth,
+                        segSegGap, segPointGap, pointRadius, hasTrackerIcon
+                );
+
+        List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Segment(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<NotificationProgressDrawable.Part>, Float> p =
+                NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts,
+                        segmentMinWidth, pointRadius, (float) progress / progressMax, 300,
+                        isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
 
         // Colors with 40% opacity
         int fadedRed = 0x66FF0000;
+        expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Segment(0, 300, fadedRed, true)));
 
-        List<Part> expected = new ArrayList<>(List.of(new Segment(1f, 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<NotificationProgressDrawable.Part> drawableParts =
+                NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth,
+                        segSegGap, segPointGap, pointRadius, hasTrackerIcon
+                );
+
+        List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Segment(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<NotificationProgressDrawable.Part>, 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;
-        boolean isStyledByProgress = true;
 
-        List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
-                segments, points, progress, progressMax, isStyledByProgress);
+        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<NotificationProgressDrawable.Part> drawableParts =
+                NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth,
+                        segSegGap, segPointGap, pointRadius, hasTrackerIcon
+                );
+
+        List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Segment(0, 146, Color.RED),
+                        new NotificationProgressDrawable.Segment(150, 300, Color.GREEN)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 16;
+        boolean isStyledByProgress = true;
+        Pair<List<NotificationProgressDrawable.Part>, 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 NotificationProgressDrawable.Segment(0, 146, Color.RED),
+                        new NotificationProgressDrawable.Segment(150, 180, Color.GREEN),
+                        new NotificationProgressDrawable.Segment(180, 300, fadedGreen, true)));
 
-        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<NotificationProgressDrawable.Part> drawableParts =
+                NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth,
+                        segSegGap, segPointGap, pointRadius, hasTrackerIcon
+                );
+
+        List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Segment(0, 146, Color.RED),
+                        new NotificationProgressDrawable.Segment(150, 300, Color.GREEN)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 16;
+        boolean isStyledByProgress = true;
+        Pair<List<NotificationProgressDrawable.Part>, 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 NotificationProgressDrawable.Segment(0, 146, Color.RED),
+                        new NotificationProgressDrawable.Segment(150, 176, Color.GREEN),
+                        new NotificationProgressDrawable.Segment(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<>();
@@ -223,31 +344,77 @@
         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<NotificationProgressDrawable.Part> drawableParts =
+                NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth,
+                        segSegGap, segPointGap, pointRadius, hasTrackerIcon
+                );
+
+        List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Segment(0, 35, Color.BLUE),
+                        new NotificationProgressDrawable.Point(39, 51, Color.RED),
+                        new NotificationProgressDrawable.Segment(55, 65, Color.BLUE),
+                        new NotificationProgressDrawable.Point(69, 81, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(85, 170, Color.BLUE),
+                        new NotificationProgressDrawable.Point(174, 186, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(190, 215, Color.BLUE),
+                        new NotificationProgressDrawable.Point(219, 231, Color.YELLOW),
+                        new NotificationProgressDrawable.Segment(235, 300, Color.BLUE)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 16;
         boolean isStyledByProgress = true;
 
+        Pair<List<NotificationProgressDrawable.Part>, Float> p =
+                NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts,
+                        segmentMinWidth, pointRadius, (float) progress / progressMax, 300,
+                        isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+
         // Colors with 40% opacity
         int fadedBlue = 0x660000FF;
         int fadedYellow = 0x66FFFF00;
+        expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Segment(0, 34.219177F, Color.BLUE),
+                        new NotificationProgressDrawable.Point(38.219177F, 50.219177F, Color.RED),
+                        new NotificationProgressDrawable.Segment(54.219177F, 70.21918F, Color.BLUE),
+                        new NotificationProgressDrawable.Point(74.21918F, 86.21918F, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(90.21918F, 172.38356F, Color.BLUE),
+                        new NotificationProgressDrawable.Point(176.38356F, 188.38356F, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(192.38356F, 217.0137F, fadedBlue,
+                                true),
+                        new NotificationProgressDrawable.Point(221.0137F, 233.0137F, fadedYellow),
+                        new NotificationProgressDrawable.Segment(237.0137F, 300F, fadedBlue,
+                                true)));
 
-        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)));
-
-        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));
@@ -258,32 +425,81 @@
         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<NotificationProgressDrawable.Part> drawableParts =
+                NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth,
+                        segSegGap, segPointGap, pointRadius, hasTrackerIcon
+                );
+
+        List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Segment(0, 35, Color.RED),
+                        new NotificationProgressDrawable.Point(39, 51, Color.RED),
+                        new NotificationProgressDrawable.Segment(55, 65, Color.RED),
+                        new NotificationProgressDrawable.Point(69, 81, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(85, 146, Color.RED),
+                        new NotificationProgressDrawable.Segment(150, 170, Color.GREEN),
+                        new NotificationProgressDrawable.Point(174, 186, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(190, 215, Color.GREEN),
+                        new NotificationProgressDrawable.Point(219, 231, Color.YELLOW),
+                        new NotificationProgressDrawable.Segment(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<NotificationProgressDrawable.Part>, Float> p =
+                NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts,
+                        segmentMinWidth, pointRadius, (float) progress / progressMax, 300,
+                        isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
 
         // Colors with 40% opacity
         int fadedGreen = 0x6600FF00;
         int fadedYellow = 0x66FFFF00;
+        expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Segment(0, 34.095238F, Color.RED),
+                        new NotificationProgressDrawable.Point(38.095238F, 50.095238F, Color.RED),
+                        new NotificationProgressDrawable.Segment(54.095238F, 70.09524F, Color.RED),
+                        new NotificationProgressDrawable.Point(74.09524F, 86.09524F, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(90.09524F, 148.9524F, Color.RED),
+                        new NotificationProgressDrawable.Segment(152.95238F, 172.7619F,
+                                Color.GREEN),
+                        new NotificationProgressDrawable.Point(176.7619F, 188.7619F, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(192.7619F, 217.33333F,
+                                fadedGreen, true),
+                        new NotificationProgressDrawable.Point(221.33333F, 233.33333F,
+                                fadedYellow),
+                        new NotificationProgressDrawable.Segment(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));
@@ -293,21 +509,251 @@
         points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW));
         int progress = 60;
         int progressMax = 100;
-        boolean isStyledByProgress = false;
 
-        List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
-                segments, points, progress, progressMax, isStyledByProgress);
+        List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points,
+                progress, progressMax);
 
-        List<Part> expected = new ArrayList<>(List.of(
+        List<Part> expectedParts = new ArrayList<>(List.of(
                 new Segment(0.15f, Color.RED),
-                new Point(null, Color.RED),
+                new Point(Color.RED),
                 new Segment(0.10f, Color.RED),
-                new Point(null, Color.BLUE),
+                new Point(Color.BLUE),
                 new Segment(0.25f, Color.RED),
                 new Segment(0.25f, Color.GREEN),
-                new Point(null, Color.YELLOW),
+                new Point(Color.YELLOW),
                 new Segment(0.25f, Color.GREEN)));
 
-        assertThat(parts).isEqualTo(expected);
+        assertThat(parts).isEqualTo(expectedParts);
+
+        float drawableWidth = 300;
+        float segSegGap = 4;
+        float segPointGap = 4;
+        float pointRadius = 6;
+        boolean hasTrackerIcon = true;
+
+        List<NotificationProgressDrawable.Part> drawableParts =
+                NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth,
+                        segSegGap, segPointGap, pointRadius, hasTrackerIcon
+                );
+
+        List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Segment(0, 35, Color.RED),
+                        new NotificationProgressDrawable.Point(39, 51, Color.RED),
+                        new NotificationProgressDrawable.Segment(55, 65, Color.RED),
+                        new NotificationProgressDrawable.Point(69, 81, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(85, 146, Color.RED),
+                        new NotificationProgressDrawable.Segment(150, 215, Color.GREEN),
+                        new NotificationProgressDrawable.Point(219, 231, Color.YELLOW),
+                        new NotificationProgressDrawable.Segment(235, 300, Color.GREEN)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 16;
+        boolean isStyledByProgress = false;
+
+        Pair<List<NotificationProgressDrawable.Part>, Float> p =
+                NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts,
+                        segmentMinWidth, pointRadius, (float) progress / progressMax, 300,
+                        isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+
+        expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Segment(0, 34.296295F, Color.RED),
+                        new NotificationProgressDrawable.Point(38.296295F, 50.296295F, Color.RED),
+                        new NotificationProgressDrawable.Segment(54.296295F, 70.296295F, Color.RED),
+                        new NotificationProgressDrawable.Point(74.296295F, 86.296295F, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(90.296295F, 149.62962F, Color.RED),
+                        new NotificationProgressDrawable.Segment(153.62962F, 216.8148F,
+                                Color.GREEN),
+                        new NotificationProgressDrawable.Point(220.81482F, 232.81482F,
+                                Color.YELLOW),
+                        new NotificationProgressDrawable.Segment(236.81482F, 300, Color.GREEN)));
+
+        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<NotificationProgressDrawable.Part> drawableParts =
+                NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth,
+                        segSegGap, segPointGap, pointRadius, hasTrackerIcon
+                );
+
+        List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(16, 16, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(20, 56, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(60, 116, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(120, 200, Color.BLUE)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 16;
+        boolean isStyledByProgress = true;
+
+        Pair<List<NotificationProgressDrawable.Part>, Float> p =
+                NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts,
+                        segmentMinWidth, pointRadius, (float) progress / progressMax, 200,
+                        isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+
+        expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(16, 32, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(36, 69.41936F, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(73.41936F, 124.25807F, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(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<NotificationProgressDrawable.Part> drawableParts =
+                NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth,
+                        segSegGap, segPointGap, pointRadius, hasTrackerIcon
+                );
+
+        List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(16, 16, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(20, 56, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(60, 116, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(120, 200, Color.BLUE)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 10;
+        boolean isStyledByProgress = true;
+
+        Pair<List<NotificationProgressDrawable.Part>, Float> p =
+                NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts,
+                        segmentMinWidth, pointRadius, (float) progress / progressMax, 200,
+                        isStyledByProgress, hasTrackerIcon ? 0 : segSegGap);
+
+        expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(16, 26, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(30, 64.169014F, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(68.169014F, 120.92958F,
+                                Color.BLUE),
+                        new NotificationProgressDrawable.Segment(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<NotificationProgressDrawable.Part> drawableParts =
+                NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth,
+                        segSegGap, segPointGap, pointRadius, hasTrackerIcon
+                );
+
+        List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>(
+                List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(16, 36, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(40, 56, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(60, 116, Color.BLUE),
+                        new NotificationProgressDrawable.Segment(120, 200, Color.BLUE)));
+
+        assertThat(drawableParts).isEqualTo(expectedDrawableParts);
+
+        float segmentMinWidth = 10;
+        boolean isStyledByProgress = true;
+
+        Pair<List<NotificationProgressDrawable.Part>, 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/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/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index 50c95a9..3378cc1 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -16,9 +16,9 @@
 
 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_VERTICAL_TEXT_LAYOUT;
 
 import android.annotation.ColorInt;
@@ -34,7 +34,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;
@@ -2100,14 +2099,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 +2127,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
@@ -2154,39 +2141,6 @@
      * @see FontVariationAxis
      */
     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))) {
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/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/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/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/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..8cd7b0f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java
@@ -0,0 +1,158 @@
+/*
+ * 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.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);
+
+        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.
+        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/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..ab3c33e 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
@@ -91,6 +91,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 +109,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;
@@ -273,8 +275,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 +300,7 @@
                         compatUIShellCommandHandler.get(),
                         accessibilityManager.get(),
                         compatUIStatusManager,
-                        desktopUserRepositories,
-                        focusTransitionObserver));
+                        desktopUserRepositories));
     }
 
     @WMSingleton
@@ -438,29 +438,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 +1034,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 +1110,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/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/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index ee817b3..6013648 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(
@@ -893,7 +912,12 @@
         }
 
         if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
-            performDesktopExitCleanupIfNeeded(task.taskId, task.displayId, wct)
+            performDesktopExitCleanupIfNeeded(
+                task.taskId,
+                task.displayId,
+                wct,
+                forceToFullscreen = false,
+            )
         }
 
         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
@@ -1457,21 +1489,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,
@@ -1921,7 +1938,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)
@@ -2048,7 +2070,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 +2109,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. */
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/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..a62dd1c 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;
         }
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/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 76496b0..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);
                 }
@@ -1273,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/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/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/DesktopModeEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
index c0ff2f0..9b24c1c 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 {
@@ -652,6 +509,171 @@
             .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 {
         private const val TASK_ID = 1
         private const val TASK_UID = 1
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 da27c08..4bb7430 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
@@ -569,6 +570,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)
@@ -1497,7 +1530,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 +1563,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()
 
@@ -1965,7 +1998,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()
 
@@ -2011,7 +2044,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()
@@ -2025,7 +2058,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()
@@ -2039,6 +2072,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()
@@ -2055,10 +2123,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()))
 
@@ -2069,7 +2136,7 @@
     }
 
     @Test
-    fun onDesktopWindowMinimize_pipTask_autoEnterDisabled_startMinimizeTransition() {
+    fun onPipTaskMinimize_autoEnterDisabled_startMinimizeTransition() {
         val task = setUpPipTask(autoEnterEnabled = false)
         whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
             .thenReturn(Binder())
@@ -2081,6 +2148,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()
@@ -2095,7 +2178,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()
@@ -2147,7 +2230,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)
@@ -2808,7 +2891,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()
@@ -2849,7 +2932,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)
@@ -2867,7 +2950,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)
@@ -2934,7 +3017,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()
@@ -2974,7 +3057,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)
@@ -2992,7 +3075,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)
@@ -3084,7 +3167,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()
@@ -3125,6 +3208,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()
@@ -3596,7 +3704,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()
@@ -4851,7 +4959,8 @@
     }
 
     private fun setUpPipTask(autoEnterEnabled: Boolean): RunningTaskInfo {
-        return setUpFreeformTask().apply {
+        // active = false marks the task as non-visible; PiP window doesn't count as visible tasks
+        return setUpFreeformTask(active = false).apply {
             pictureInPictureParams =
                 PictureInPictureParams.Builder().setAutoEnterEnabled(autoEnterEnabled).build()
         }
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 3cc30cb..96ed214 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,6 +306,115 @@
         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,
@@ -331,7 +446,7 @@
         task: RunningTaskInfo?,
         type: Int = TRANSIT_OPEN,
     ): TransitionInfo {
-        return TransitionInfo(TRANSIT_OPEN, /* flags= */ 0).apply {
+        return TransitionInfo(type, /* flags= */ 0).apply {
             addChange(
                 Change(mock(), mock()).apply {
                     mode = TRANSIT_OPEN
@@ -369,6 +484,19 @@
         }
     }
 
+    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(
         @WindowManager.TransitionType type: Int = TRANSIT_OPEN,
         handlerClass: Class<out Transitions.TransitionHandler>? = null,
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..32bb8bb 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,25 @@
         mController.onDrag(dragLayout, event);
         verify(mDragAndDropListener, never()).onDragStarted();
     }
+
+    @Test
+    public void testOnDragStarted_withNoClipData() {
+        final View dragLayout = mock(View.class);
+        final Display display = mock(Display.class);
+        doReturn(display).when(dragLayout).getDisplay();
+        doReturn(DEFAULT_DISPLAY).when(display).getDisplayId();
+
+        final ClipData clipData = createAppClipData(MIMETYPE_APPLICATION_SHORTCUT);
+        final DragEvent event = mock(DragEvent.class);
+        doReturn(ACTION_DRAG_STARTED).when(event).getAction();
+        doReturn(null).when(event).getClipData();
+        doReturn(clipData.getDescription()).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/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/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 6b02aef..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
@@ -169,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);
 
@@ -396,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()
@@ -634,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);
@@ -659,7 +686,8 @@
                 /* inFullImmersiveMode */ true,
                 new InsetsState(),
                 DEFAULT_HAS_GLOBAL_FOCUS,
-                mExclusionRegion);
+                mExclusionRegion,
+                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
 
         assertThat(relayoutParams.mIsInsetSource).isFalse();
     }
@@ -683,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();
@@ -707,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();
     }
@@ -730,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();
     }
@@ -753,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();
     }
@@ -777,7 +809,8 @@
                 /* inFullImmersiveMode */ true,
                 new InsetsState(),
                 DEFAULT_HAS_GLOBAL_FOCUS,
-                mExclusionRegion);
+                mExclusionRegion,
+                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
 
         assertThat(relayoutParams.mIsCaptionVisible).isTrue();
 
@@ -793,7 +826,8 @@
                 /* inFullImmersiveMode */ true,
                 new InsetsState(),
                 DEFAULT_HAS_GLOBAL_FOCUS,
-                mExclusionRegion);
+                mExclusionRegion,
+                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
 
         assertThat(relayoutParams.mIsCaptionVisible).isFalse();
     }
@@ -817,7 +851,8 @@
                 /* inFullImmersiveMode */ true,
                 new InsetsState(),
                 DEFAULT_HAS_GLOBAL_FOCUS,
-                mExclusionRegion);
+                mExclusionRegion,
+                DEFAULT_SHOULD_IGNORE_CORNER_RADIUS);
 
         assertThat(relayoutParams.mIsCaptionVisible).isFalse();
     }
@@ -1480,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(
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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 1fc1f05..dd28402 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,
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 d0e88d5..b01f622 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);
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 ef0bc3b..c1c3e04 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
@@ -212,10 +212,8 @@
     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_CONVERT_NETWORK_POLICIES =
-        "failed_to_convert_network_policies";
-    private static final String ERROR_UNKNOWN_BACKUP_SERIALIZATION_VERSION =
-        "unknown_backup_serialization_version";
+    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
@@ -1438,7 +1436,6 @@
             try {
                 out.writeInt(NETWORK_POLICIES_BACKUP_VERSION);
                 out.writeInt(policies.length);
-                int numberOfPoliciesBackedUp = 0;
                 for (NetworkPolicy policy : policies) {
                     // We purposefully only backup policies that the user has
                     // defined; any inferred policies might include
@@ -1448,30 +1445,26 @@
                         out.writeByte(BackupUtils.NOT_NULL);
                         out.writeInt(marshaledPolicy.length);
                         out.write(marshaledPolicy);
-                        if (areAgentMetricsEnabled) {
-                            numberOfPoliciesBackedUp++;
-                        }
                     } else {
                         out.writeByte(BackupUtils.NULL);
                     }
                 }
-                if (areAgentMetricsEnabled) {
-                    numberOfSettingsPerKey.put(KEY_NETWORK_POLICIES, numberOfPoliciesBackedUp);
-                }
             } catch (IOException ioe) {
                 Log.e(TAG, "Failed to convert NetworkPolicies to byte array " + ioe.getMessage());
                 baos.reset();
-                mBackupRestoreEventLogger.logItemsBackupFailed(
-                    KEY_NETWORK_POLICIES,
-                    policies.length,
-                    ERROR_FAILED_TO_CONVERT_NETWORK_POLICIES);
             }
         }
         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() {
@@ -1483,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) {
@@ -1498,10 +1502,6 @@
             try {
                 int version = in.readInt();
                 if (version < 1 || version > NETWORK_POLICIES_BACKUP_VERSION) {
-                    mBackupRestoreEventLogger.logItemsRestoreFailed(
-                            KEY_NETWORK_POLICIES,
-                            /* count= */ 1,
-                            ERROR_UNKNOWN_BACKUP_SERIALIZATION_VERSION);
                     throw new BackupUtils.BadVersionException(
                             "Unknown Backup Serialization Version");
                 }
@@ -1518,15 +1518,10 @@
                 }
                 // Only set the policies if there was no error in the restore operation
                 networkPolicyManager.setNetworkPolicies(policies);
-                mBackupRestoreEventLogger.logItemsRestored(KEY_NETWORK_POLICIES, policies.length);
             } catch (NullPointerException | IOException | BackupUtils.BadVersionException
                     | DateTimeException e) {
                 // NPE can be thrown when trying to instantiate a NetworkPolicy
                 Log.e(TAG, "Failed to convert byte array to NetworkPolicies " + e.getMessage());
-                mBackupRestoreEventLogger.logItemsRestoreFailed(
-                        KEY_NETWORK_POLICIES,
-                        /* count= */ 1,
-                        ERROR_FAILED_TO_CONVERT_NETWORK_POLICIES);
             }
         }
     }
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index dedd7eb..5ad4b8a 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,
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 95dd0db..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,7 @@
 
 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;
@@ -28,6 +29,8 @@
 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;
@@ -69,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;
 
@@ -834,6 +838,74 @@
         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()) {
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 227fff5..6b2449f 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -92,7 +92,6 @@
         "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",
@@ -288,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",
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/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 715d223..7d5fd90 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, "
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..96401ce 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)
                 }
@@ -448,22 +494,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 +545,21 @@
         }
 
         val sign = offset.sign
-        if (nestedScrollController == null && draggable.shouldConsumeNestedScroll(sign)) {
+        if (
+            nestedScrollController == null &&
+                // TODO(b/388231324): Remove this.
+                !lastEventWasScrollWheel &&
+                draggable.shouldConsumeNestedScroll(sign)
+        ) {
             val startedPosition = checkNotNull(lastFirstDown) { "lastFirstDown is not set" }
 
-            // 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..5de0f12 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,25 +18,37 @@
 
 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.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
@@ -653,6 +665,114 @@
         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")
+    }
+
     private fun ComposeContentTestRule.setContentWithTouchSlop(
         content: @Composable () -> Unit
     ): Float {
@@ -688,6 +808,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 +817,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/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/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 ee8535e..6d24fc16 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/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/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index 3f6bce7..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 {
@@ -61,84 +57,62 @@
     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? {
-    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
-}
-
 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/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index 5a35d11..dbac62f 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/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/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
index 0f8ca94..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
@@ -77,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
@@ -148,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) {
@@ -213,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())
@@ -238,9 +219,6 @@
             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() {
@@ -353,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
@@ -373,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
@@ -381,7 +360,7 @@
         when (verticalAlignment) {
             VerticalAlignment.CENTER -> {
                 localTranslation.y =
-                    ((viewHeight - interpolatedTextBounds.height()) / 2 -
+                    ((measuredHeight - interpolatedTextBounds.height()) / 2 -
                         interpolatedTextBounds.top -
                         correctedBaseline)
             }
@@ -392,7 +371,7 @@
             }
             VerticalAlignment.BOTTOM -> {
                 localTranslation.y =
-                    viewHeight -
+                    measuredHeight -
                         interpolatedTextBounds.bottom -
                         lockScreenPaint.strokeWidth.toInt() -
                         correctedBaseline
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/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/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/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/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/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/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 20474c8..deaf579 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -526,7 +526,7 @@
         mLockscreenUserManager.mLastLockTime
                 .set(mSensitiveNotifPostTime - TimeUnit.DAYS.toMillis(1));
         // Device is not currently locked
-        when(mKeyguardManager.isDeviceLocked()).thenReturn(false);
+        mLockscreenUserManager.mLocked.set(false);
 
         // Sensitive Content notifications are always redacted
         assertEquals(REDACTION_TYPE_NONE,
@@ -540,7 +540,7 @@
         mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
                 mCurrentUser.id);
         changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
-        when(mKeyguardManager.isDeviceLocked()).thenReturn(true);
+        mLockscreenUserManager.mLocked.set(true);
         // Device was locked after this notification arrived
         mLockscreenUserManager.mLastLockTime
                 .set(mSensitiveNotifPostTime + TimeUnit.DAYS.toMillis(1));
@@ -560,7 +560,7 @@
         // Device has been locked for 1 second before the notification came in, which is too short
         mLockscreenUserManager.mLastLockTime
                 .set(mSensitiveNotifPostTime - TimeUnit.SECONDS.toMillis(1));
-        when(mKeyguardManager.isDeviceLocked()).thenReturn(true);
+        mLockscreenUserManager.mLocked.set(true);
 
         // Sensitive Content notifications are always redacted
         assertEquals(REDACTION_TYPE_NONE,
@@ -577,7 +577,7 @@
         // Claim the device was last locked 1 day ago
         mLockscreenUserManager.mLastLockTime
                 .set(mSensitiveNotifPostTime - TimeUnit.DAYS.toMillis(1));
-        when(mKeyguardManager.isDeviceLocked()).thenReturn(true);
+        mLockscreenUserManager.mLocked.set(true);
 
         // Sensitive Content notifications are always redacted
         assertEquals(REDACTION_TYPE_NONE,
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/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/layout/StatusBarContentInsetsProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt
index c9c9617..49b95d9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt
@@ -45,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/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/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/KeyguardStatusBarViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
index ffd349d..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,7 +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.statusBarContentInsetsProvider
+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
@@ -153,7 +153,8 @@
         shadeViewStateProvider = TestShadeViewStateProvider()
 
         Mockito.`when`(
-                kosmos.statusBarContentInsetsProvider.getStatusBarContentInsetsForCurrentRotation()
+                kosmos.mockStatusBarContentInsetsProvider
+                    .getStatusBarContentInsetsForCurrentRotation()
             )
             .thenReturn(Insets.of(0, 0, 0, 0))
 
@@ -162,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/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/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/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..58f2d3c 100644
--- a/packages/SystemUI/res/layout/volume_dialog.xml
+++ b/packages/SystemUI/res/layout/volume_dialog.xml
@@ -44,7 +44,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 a01ff3d..d445ed9 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3919,6 +3919,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/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/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/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/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/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..129a6bb 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -35,7 +35,6 @@
 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 +56,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
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/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/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/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/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/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/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 f295c0c..7ec523b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt
@@ -104,7 +104,6 @@
             mediaProjectionMetricsLogger,
             defaultSelectedMode,
             displayManager,
-            dialog,
             controller,
             activityStarter,
             userContextProvider,
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionViewBinder.kt
index 691bdd4..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,15 +67,14 @@
         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 { startButtonOnClicked() }
     }
@@ -91,7 +88,8 @@
             )
         }
         if (selectedScreenShareOption.mode == SINGLE_APP) {
-            val intent = Intent(dialog.context, MediaProjectionAppSelectorActivity::class.java)
+            val intent =
+                Intent(containerView.context, MediaProjectionAppSelectorActivity::class.java)
             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
 
             // We can't start activity for result here so we use result receiver to get
@@ -116,10 +114,10 @@
 
     @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
@@ -127,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/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/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/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..1180599 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")
+    private val t = TrackTracer(trackName = "ShadeTraceLogger", trackGroup = "shade")
 
     @JvmStatic
     fun logOnMovedToDisplay(displayId: Int, config: Configuration) {
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/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 2bcd3fc..10b726b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -92,6 +92,7 @@
 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;
@@ -297,6 +298,9 @@
     // 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;
 
@@ -369,6 +373,7 @@
                         if (!unlocked) {
                             mLastLockTime.set(System.currentTimeMillis());
                         }
+                        mLocked.set(!unlocked);
                     }));
         }
     }
@@ -737,7 +742,7 @@
             return false;
         }
 
-        if (!mKeyguardManager.isDeviceLocked()) {
+        if (!mLocked.get()) {
             return false;
         }
 
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/ui/binder/OngoingActivityChipBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
index 69ef09d..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,
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/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/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
index eff959d..351cdc8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
@@ -29,6 +29,7 @@
 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
@@ -39,6 +40,7 @@
 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
@@ -169,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/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/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/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/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/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 071d232..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);
@@ -4848,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;
@@ -4866,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) {
@@ -4890,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) {
@@ -4949,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;
@@ -5244,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();
@@ -5266,8 +5143,6 @@
 
     public void setQsExpansionFraction(float qsExpansionFraction) {
         SceneContainerFlag.assertInLegacyMode();
-        boolean footerAffected = getQsExpansionFraction() != qsExpansionFraction
-                && (getQsExpansionFraction() == 1 || qsExpansionFraction == 1);
         mQsExpansionFraction = qsExpansionFraction;
         updateUseRoundedRectClipping();
 
@@ -5276,9 +5151,6 @@
         if (getOwnScrollY() > 0) {
             setOwnScrollY((int) MathUtils.lerp(getOwnScrollY(), 0, getQsExpansionFraction()));
         }
-        if (!FooterViewRefactor.isEnabled() && footerAffected) {
-            updateFooter();
-        }
     }
 
     @VisibleForTesting
@@ -5456,14 +5328,6 @@
         requestChildrenUpdate();
     }
 
-    void setUpcomingStatusBarState(int upcomingStatusBarState) {
-        FooterViewRefactor.assertInLegacyMode();
-        mUpcomingStatusBarState = upcomingStatusBarState;
-        if (mUpcomingStatusBarState != mStatusBarState) {
-            updateFooter();
-        }
-    }
-
     void onStatePostChange(boolean fromShadeLocked) {
         boolean onKeyguard = onKeyguard();
 
@@ -5472,9 +5336,6 @@
         }
 
         setExpandingEnabled(!onKeyguard);
-        if (!FooterViewRefactor.isEnabled()) {
-            updateFooter();
-        }
         requestChildrenUpdate();
         onUpdateRowStates();
         updateVisibility();
@@ -5490,8 +5351,7 @@
         if (mEmptyShadeView == null || mEmptyShadeView.getVisibility() == GONE) {
             return getMinExpansionHeight();
         } else {
-            return FooterViewRefactor.isEnabled() ? getAppearEndPosition()
-                    : getAppearEndPositionLegacy();
+            return getAppearEndPosition();
         }
     }
 
@@ -5583,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();
@@ -5615,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();
     }
@@ -5764,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.
@@ -5826,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();
 
@@ -6091,11 +5879,6 @@
         mHighPriorityBeforeSpeedBump = highPriorityBeforeSpeedBump;
     }
 
-    void setFooterClearAllListener(FooterClearAllListener listener) {
-        FooterViewRefactor.assertInLegacyMode();
-        mFooterClearAllListener = listener;
-    }
-
     void setClearAllFinishedWhilePanelExpandedRunnable(Runnable runnable) {
         mClearAllFinishedWhilePanelExpandedRunnable = runnable;
     }
@@ -6394,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/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/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 1474789..b146b92 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -1487,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);
     }
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/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/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..b78e010 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
@@ -53,13 +53,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 +71,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,
@@ -164,11 +166,19 @@
                         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..d731752 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)) {
@@ -356,6 +368,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 +380,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/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/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/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
index ccd16ac..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
@@ -69,6 +69,7 @@
             viewModel.setStreamVolume(value.roundToInt(), fromUser)
         }
 
+        viewModel.isDisabledByZenMode.onEach { sliderView.isEnabled = !it }.launchIn(this)
         viewModel.state
             .onEach {
                 sliderView.setModel(it, animation, isInitialUpdate)
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 06d9426..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
@@ -73,6 +73,7 @@
             .stateIn(coroutineScope, SharingStarted.Eagerly, null)
             .filterNotNull()
 
+    val isDisabledByZenMode: Flow<Boolean> = interactor.isDisabledByZenMode
     val state: Flow<VolumeDialogSliderStateModel> =
         model
             .flatMapLatest { streamModel ->
@@ -82,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/binder/VolumeDialogViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
index 1da2491..46d7d5f 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
@@ -24,11 +24,15 @@
 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
@@ -43,6 +47,7 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.onEach
@@ -71,6 +76,8 @@
         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
@@ -88,6 +95,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) }
@@ -103,8 +126,10 @@
                 when (it) {
                     is VolumeDialogVisibilityModel.Visible -> {
                         tracer.traceVisibilityEnd(it)
-                        calculateTranslationX(view)?.let(view::setTranslationX)
-                        view.animateShow(dialogShowAnimationDurationMs)
+                        view.animateShow(
+                            duration = dialogShowAnimationDurationMs,
+                            translationX = calculateTranslationX(view),
+                        )
                     }
                     is VolumeDialogVisibilityModel.Dismissed -> {
                         tracer.traceVisibilityEnd(it)
@@ -134,24 +159,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?) {
@@ -160,22 +176,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/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index 4abbbac..047b78e 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -28,9 +28,11 @@
 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
@@ -159,14 +161,12 @@
         dndModeId = MANUAL_DND_INACTIVE.id
         zenModeRepository.addMode(MANUAL_DND_INACTIVE)
 
-        repository = FakeKeyguardRepository()
+        repository = kosmos.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,7 +177,7 @@
                 mainExecutor,
                 bgExecutor,
                 clockBuffers,
-                withDeps.featureFlags,
+                kosmos.fakeFeatureFlagsClassic,
                 zenModeController,
                 kosmos.zenModeInteractor,
                 userTracker,
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/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
index 7a3089f..77c40a1 100644
--- a/packages/SystemUI/tests/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/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/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/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/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/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/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 1881a94..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,7 +1,7 @@
 package com.android.systemui.kosmos
 
-import androidx.compose.runtime.snapshots.Snapshot
 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
@@ -17,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() }
@@ -53,26 +52,10 @@
 
 /**
  * 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() }
-
-/**
- * Runs the given [Kosmos]-scoped test [block] in an environment where 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 Kosmos.runTestWithSnapshots(block: suspend Kosmos.() -> Unit) {
-    val handle = Snapshot.registerGlobalWriteObserver { Snapshot.sendApplyNotifications() }
-
-    try {
-        testScope.runTest { block() }
-    } finally {
-        handle.dispose()
-    }
+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/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/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt
index 69e215d..90897fa 100644
--- 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
@@ -16,13 +16,28 @@
 
 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>() }
 
-var Kosmos.statusBarContentInsetsProvider by Kosmos.Fixture { mockStatusBarContentInsetsProvider }
+val Kosmos.statusBarContentInsetsProvider by
+    Kosmos.Fixture {
+        StatusBarContentInsetsProviderImpl(
+            applicationContext,
+            configurationController.fake,
+            dumpManager,
+            commandRegistry,
+            mock<SysUICutoutProvider>(),
+        )
+    }
 
 val Kosmos.fakeStatusBarContentInsetsProviderFactory 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/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/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/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/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 e3d7062..b94fa2f 100644
--- a/services/accessibility/java/com/android/server/accessibility/AutoclickController.java
+++ b/services/accessibility/java/com/android/server/accessibility/AutoclickController.java
@@ -22,6 +22,7 @@
 
 import android.accessibilityservice.AccessibilityTrace;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.database.ContentObserver;
@@ -69,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;
@@ -89,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);
@@ -156,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();
@@ -191,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;
         }
@@ -216,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.");
             }
@@ -233,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);
+            }
         }
 
         /**
@@ -248,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);
@@ -262,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);
+                }
+            }
         }
     }
 
@@ -317,6 +349,10 @@
             mScheduledShowIndicatorTime = -1;
             mHandler.removeCallbacks(this);
         }
+
+        public void updateCursorAreaSize(int size) {
+            mAutoclickIndicatorView.setRadius(size);
+        }
     }
 
     /**
diff --git a/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java b/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java
index 816d8e45..bf50151 100644
--- a/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java
+++ b/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java
@@ -16,6 +16,8 @@
 
 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;
@@ -35,8 +37,7 @@
 
     static final int MINIMAL_ANIMATION_DURATION = 50;
 
-    // TODO(b/383901288): allow users to customize the indicator area.
-    static final float RADIUS = 50;
+    private float mRadius = AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT;
 
     private final Paint mPaint;
 
@@ -84,10 +85,10 @@
 
         if (showIndicator) {
             mRingRect.set(
-                    /* left= */ mX - RADIUS,
-                    /* top= */ mY - RADIUS,
-                    /* right= */ mX + RADIUS,
-                    /* bottom= */ mY + RADIUS);
+                    /* left= */ mX - mRadius,
+                    /* top= */ mY - mRadius,
+                    /* right= */ mX + mRadius,
+                    /* bottom= */ mY + mRadius);
             canvas.drawArc(mRingRect, /* startAngle= */ -90, mSweepAngle, false, mPaint);
         }
     }
@@ -107,6 +108,10 @@
         mY = y;
     }
 
+    public void setRadius(int radius) {
+        mRadius = radius;
+    }
+
     public void redrawIndicator() {
         showIndicator = true;
         invalidate();
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/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/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 aeb2f5e..40726b4 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -767,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/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index b536dc5..5a198a1 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -19374,7 +19374,7 @@
             }
             if (preventIntentRedirectCollectNestedKeysOnServerIfNotCollected()) {
                 // this flag will be ramped to public.
-                intent.collectExtraIntentKeys();
+                intent.collectExtraIntentKeys(true);
             }
         }
 
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/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 bd27142..00d23cc 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -7214,7 +7214,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")
@@ -8571,6 +8571,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) {
@@ -8579,8 +8585,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();
                     }
@@ -9019,6 +9024,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
@@ -9036,7 +9046,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());
             }
@@ -9059,7 +9069,7 @@
                     String name = getSettingNameForDevice(device);
                     index = mSettings.getSystemIntForUser(
                             mContentResolver, name, defaultIndex,
-                            isMusic() ? UserHandle.USER_SYSTEM : UserHandle.USER_CURRENT);
+                            getVolumePersistenceUserId());
                     if (index == -1) {
                         continue;
                     }
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/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java
index 9f785ac..2429640 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,
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..b2c35e1 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 {
@@ -3575,8 +3575,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 +3716,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 +3734,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 +3754,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/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/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
index 87d809b..3018cfd 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
@@ -302,6 +323,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 +385,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 +400,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 +415,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 +448,7 @@
                                 + sessionId
                                 + ") with message: "
                                 + message);
-                sendMessageDeliveryStatus(
-                        sessionId, message.getMessageSequenceNumber(), ErrorCode.PERMANENT_ERROR);
-                return;
+                return ErrorCode.PERMANENT_ERROR;
             }
             remote = mSessionInfoMap.get(sessionId).getRemoteEndpointInfo();
         }
@@ -435,31 +470,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 +536,46 @@
         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);
+                        }
+                    }
+                });
+    }
+
+    /**
+     * 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/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 286238e..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;
     }
 
@@ -1537,7 +1537,7 @@
                         + userId);
             }
         } finally {
-            Arrays.fill(password, (byte) 0);
+            LockPatternUtils.zeroize(password);
         }
     }
 
@@ -1570,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);
@@ -2263,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/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 9567c81..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();
 
@@ -7133,6 +7164,7 @@
                     adjustments.putParcelable(KEY_TYPE, newChannel);
 
                     logClassificationChannelAdjustmentReceived(r, isPosted, classification);
+                    r.setBundleType(classification);
                 }
             }
             r.addAdjustment(adjustment);
@@ -9536,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/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index 2b4d71e..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"
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 cc5c88b..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;
@@ -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..4cb7769 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -3060,6 +3060,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/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/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/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java
index a6f2a37..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;
 
@@ -1604,8 +1603,7 @@
                         }
                     }
                 }
-                if (cpuHeadroomAffinityCheck() && mCheckHeadroomAffinity
-                        && params.tids.length > 1) {
+                if (mCheckHeadroomAffinity && params.tids.length > 1) {
                     checkThreadAffinityForTids(params.tids);
                 }
                 halParams.tids = params.tids;
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/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 29f1f93..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;
@@ -2814,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;
         }
@@ -4261,7 +4280,7 @@
     }
 
     void finishRelaunching() {
-        mAppCompatController.getAppCompatOrientationOverrides()
+        mAppCompatController.getOrientationOverrides()
                 .setRelaunchingAfterRequestedOrientationChanged(false);
         mTaskSupervisor.getActivityMetricsLogger().notifyActivityRelaunched(this);
 
@@ -4655,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.
@@ -8125,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);
             }
@@ -8222,7 +8241,7 @@
                 mLastReportedConfiguration.getMergedConfiguration())) {
             ensureActivityConfiguration(false /* ignoreVisibility */);
             if (mPendingRelaunchCount > originalRelaunchingCount) {
-                mAppCompatController.getAppCompatOrientationOverrides()
+                mAppCompatController.getOrientationOverrides()
                         .setRelaunchingAfterRequestedOrientationChanged(true);
             }
             if (mTransitionController.inPlayingTransition(this)) {
@@ -8744,7 +8763,7 @@
             navBarInsets = Insets.NONE;
         }
         final AppCompatReachabilityOverrides reachabilityOverrides =
-                mAppCompatController.getAppCompatReachabilityOverrides();
+                mAppCompatController.getReachabilityOverrides();
         // Horizontal position
         int offsetX = 0;
         if (parentBounds.width() != screenResolvedBoundsWidth) {
@@ -10217,7 +10236,7 @@
                 mAppCompatController.getAppCompatAspectRatioOverrides()
                         .shouldOverrideMinAspectRatio());
         proto.write(SHOULD_IGNORE_ORIENTATION_REQUEST_LOOP,
-                mAppCompatController.getAppCompatOrientationOverrides()
+                mAppCompatController.getOrientationOverrides()
                         .shouldIgnoreOrientationRequestLoop());
         proto.write(SHOULD_OVERRIDE_FORCE_RESIZE_APP,
                 mAppCompatController.getResizeOverrides().shouldOverrideForceResizeApp());
diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java
index 0967078..6d0e8ea 100644
--- a/services/core/java/com/android/server/wm/AppCompatController.java
+++ b/services/core/java/com/android/server/wm/AppCompatController.java
@@ -33,7 +33,7 @@
     @NonNull
     private final AppCompatAspectRatioPolicy mAppCompatAspectRatioPolicy;
     @NonNull
-    private final AppCompatReachabilityPolicy mAppCompatReachabilityPolicy;
+    private final AppCompatReachabilityPolicy mReachabilityPolicy;
     @NonNull
     private final DesktopAppCompatAspectRatioPolicy mDesktopAppCompatAspectRatioPolicy;
     @NonNull
@@ -58,7 +58,7 @@
         mOrientationPolicy = new AppCompatOrientationPolicy(activityRecord, mAppCompatOverrides);
         mAppCompatAspectRatioPolicy = new AppCompatAspectRatioPolicy(activityRecord,
                 mTransparentPolicy, mAppCompatOverrides);
-        mAppCompatReachabilityPolicy = new AppCompatReachabilityPolicy(activityRecord,
+        mReachabilityPolicy = new AppCompatReachabilityPolicy(activityRecord,
                 wmService.mAppCompatConfiguration);
         mAppCompatLetterboxPolicy = new AppCompatLetterboxPolicy(activityRecord,
                 wmService.mAppCompatConfiguration);
@@ -89,8 +89,8 @@
     }
 
     @NonNull
-    AppCompatOrientationOverrides getAppCompatOrientationOverrides() {
-        return mAppCompatOverrides.getAppCompatOrientationOverrides();
+    AppCompatOrientationOverrides getOrientationOverrides() {
+        return mAppCompatOverrides.getOrientationOverrides();
     }
 
     @NonNull
@@ -109,8 +109,8 @@
     }
 
     @NonNull
-    AppCompatReachabilityPolicy getAppCompatReachabilityPolicy() {
-        return mAppCompatReachabilityPolicy;
+    AppCompatReachabilityPolicy getReachabilityPolicy() {
+        return mReachabilityPolicy;
     }
 
     @NonNull
@@ -124,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 58b37be..9fb54db 100644
--- a/services/core/java/com/android/server/wm/AppCompatOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatOverrides.java
@@ -27,7 +27,7 @@
 public class AppCompatOverrides {
 
     @NonNull
-    private final AppCompatOrientationOverrides mAppCompatOrientationOverrides;
+    private final AppCompatOrientationOverrides mOrientationOverrides;
     @NonNull
     private final AppCompatCameraOverrides mAppCompatCameraOverrides;
     @NonNull
@@ -37,7 +37,7 @@
     @NonNull
     private final AppCompatResizeOverrides mResizeOverrides;
     @NonNull
-    private final AppCompatReachabilityOverrides mAppCompatReachabilityOverrides;
+    private final AppCompatReachabilityOverrides mReachabilityOverrides;
     @NonNull
     private final AppCompatLetterboxOverrides mAppCompatLetterboxOverrides;
 
@@ -48,13 +48,13 @@
             @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);
         mResizeOverrides = new AppCompatResizeOverrides(activityRecord, packageManager,
@@ -64,8 +64,8 @@
     }
 
     @NonNull
-    AppCompatOrientationOverrides getAppCompatOrientationOverrides() {
-        return mAppCompatOrientationOverrides;
+    AppCompatOrientationOverrides getOrientationOverrides() {
+        return mOrientationOverrides;
     }
 
     @NonNull
@@ -89,8 +89,8 @@
     }
 
     @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/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..acd47da 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"
diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java
index 3a0e41a..b3e9244 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;
@@ -542,10 +544,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()
+                    && mDisplayContent.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
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/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..9c1cf6e 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -5255,6 +5255,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..92e0931 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -10177,9 +10177,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 c5d42ad..8e06ed8 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -2714,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/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..0210791
--- /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: "print"
+    description: "Do not use the flag Context.BIND_INCLUDE_CAPABILITIES when binding to the service"
+    bug: "291281543"
+}
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/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/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/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/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/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..60a4b9a 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,8 @@
         });
     }
 
-    public void PushDynamicShortcut() {
+    /**
+    public void testPushDynamicShortcut() {
         // Change the max number of shortcuts.
         mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=5,"
                 + ShortcutService.ConfigConstants.KEY_SAVE_DELAY_MILLIS + "=1");
@@ -543,8 +544,9 @@
         verify(mMockUsageStatsManagerInternal, times(1)).reportShortcutUsage(
                 eq(CALLING_PACKAGE_1), eq("s9"), eq(USER_10));
     }
+    */
 
-    public void PushDynamicShortcut_CallsToUsageStatsManagerAreThrottled()
+    public void testPushDynamicShortcut_CallsToUsageStatsManagerAreThrottled()
             throws InterruptedException {
         mService.updateConfigurationLocked(
                 ShortcutService.ConfigConstants.KEY_SAVE_DELAY_MILLIS + "=500");
@@ -576,6 +578,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 +598,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 +629,7 @@
         assertEquals(3, mManager.getRemainingCallCount());
     }
 
-    public void PublishWithNoActivity() {
+    public void testPublishWithNoActivity() {
         // If activity is not explicitly set, use the default one.
 
         mRunningUsers.put(USER_11, true);
@@ -732,7 +735,7 @@
         });
     }
 
-    public void PublishWithNoActivity_noMainActivityInPackage() {
+    public void testPublishWithNoActivity_noMainActivityInPackage() {
         mRunningUsers.put(USER_11, true);
 
         runWithCaller(CALLING_PACKAGE_2, USER_11, () -> {
@@ -751,7 +754,7 @@
         });
     }
 
-    public void DeleteDynamicShortcuts() {
+    public void testDeleteDynamicShortcuts() {
         final ShortcutInfo si1 = makeShortcut("shortcut1");
         final ShortcutInfo si2 = makeShortcut("shortcut2");
         final ShortcutInfo si3 = makeShortcut("shortcut3");
@@ -792,7 +795,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 +824,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 +1038,7 @@
 */
     }
 
-    public void CleanupDanglingBitmaps() throws Exception {
+    public void testCleanupDanglingBitmaps() throws Exception {
         assertBitmapDirectories(USER_10, EMPTY_STRINGS);
         assertBitmapDirectories(USER_11, EMPTY_STRINGS);
 
@@ -1204,7 +1207,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 +1230,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 +1304,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 +1435,7 @@
         });
     }
 
-    public void UpdateShortcuts_icons() {
+    public void testUpdateShortcuts_icons() {
         runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
             assertTrue(mManager.setDynamicShortcuts(list(
                     makeShortcut("s1")
@@ -1526,7 +1529,7 @@
         });
     }
 
-    public void ShortcutManagerGetShortcuts_shortcutTypes() {
+    public void testShortcutManagerGetShortcuts_shortcutTypes() {
 
         // Create 3 manifest and 3 dynamic shortcuts
         addManifestShortcutResource(
@@ -1617,7 +1620,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 +1704,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 +1746,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 +1784,7 @@
 
     // === Test for launcher side APIs ===
 
-    public void GetShortcuts() {
+    public void testGetShortcuts() {
 
         // Set up shortcuts.
 
@@ -1998,7 +2001,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 +2112,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 +2160,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 +2208,7 @@
     }
 
     // TODO resource
-    public void GetShortcutInfo() {
+    public void testGetShortcutInfo() {
         // Create shortcuts.
         setCaller(CALLING_PACKAGE_1);
         final ShortcutInfo s1_1 = makeShortcut(
@@ -2280,7 +2283,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 +2364,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 +2489,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 +2559,7 @@
         });
     }
 
-    public void PinShortcutAndGetPinnedShortcuts_multi() {
+    public void testPinShortcutAndGetPinnedShortcuts_multi() {
         // Create some shortcuts.
         runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
             assertTrue(mManager.setDynamicShortcuts(list(
@@ -2832,7 +2835,7 @@
         });
     }
 
-    public void PinShortcutAndGetPinnedShortcuts_assistant() {
+    public void testPinShortcutAndGetPinnedShortcuts_assistant() {
         // Create some shortcuts.
         runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
             assertTrue(mManager.setDynamicShortcuts(list(
@@ -2888,7 +2891,7 @@
         });
     }
 
-    public void PinShortcutAndGetPinnedShortcuts_crossProfile_plusLaunch() {
+    public void testPinShortcutAndGetPinnedShortcuts_crossProfile_plusLaunch() {
         // Create some shortcuts.
         runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
             assertTrue(mManager.setDynamicShortcuts(list(
@@ -3477,7 +3480,7 @@
         });
     }
 
-    public void StartShortcut() {
+    public void testStartShortcut() {
         // Create some shortcuts.
         runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
             final ShortcutInfo s1_1 = makeShortcut(
@@ -3612,7 +3615,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 +3781,7 @@
                 .isEmpty();
     }
 
-    public void LauncherCallback_crossProfile() throws Throwable {
+    public void testLauncherCallback_crossProfile() throws Throwable {
         prepareCrossProfileDataSet();
 
         final Handler h = new Handler(Looper.getMainLooper());
@@ -3901,7 +3904,7 @@
 
     // === Test for persisting ===
 
-    public void SaveAndLoadUser_empty() {
+    public void testSaveAndLoadUser_empty() {
         assertTrue(mManager.setDynamicShortcuts(list()));
 
         Log.i(TAG, "Saved state");
@@ -3918,7 +3921,7 @@
     /**
      * Try save and load, also stop/start the user.
      */
-    public void SaveAndLoadUser() {
+    public void 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 +4062,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 +4076,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 +4232,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 +4509,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 +4577,7 @@
         });
     }
 
-    public void HandleGonePackage_crossProfile() {
+    public void testHandleGonePackage_crossProfile() {
         // Create some shortcuts.
         runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
             assertTrue(mManager.setDynamicShortcuts(list(
@@ -4846,7 +4849,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 +4912,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 +4920,7 @@
         });
     }
 
-    public void HandlePackageDisable() {
+    public void testHandlePackageDisable() {
         checkHandlePackageDeleteInner((userId, packageName) -> {
             disablePackage(userId, packageName);
             mService.mPackageMonitor.onReceive(getTestContext(),
@@ -5049,7 +5052,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 +5128,7 @@
         assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_3, USER_11));
     }
 
-    public void HandlePackageClearData_manifestRepublished() {
+    public void testHandlePackageClearData_manifestRepublished() {
 
         mRunningUsers.put(USER_11, true);
 
@@ -5167,7 +5170,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 +5344,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 +5419,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 +5525,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 +5655,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 +5741,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 +5762,7 @@
         checkBackupAndRestore_success(/*firstRestore=*/ true);
     }
 
-    public void BackupAndRestore_backupRestoreTwice() {
+    public void testBackupAndRestore_backupRestoreTwice() {
         prepareForBackupTest();
 
         checkBackupAndRestore_success(/*firstRestore=*/ true);
@@ -5775,7 +5778,7 @@
         checkBackupAndRestore_success(/*firstRestore=*/ false);
     }
 
-    public void BackupAndRestore_restoreToNewVersion() {
+    public void testBackupAndRestore_restoreToNewVersion() {
         prepareForBackupTest();
 
         addPackage(CALLING_PACKAGE_1, CALLING_UID_1, 2);
@@ -5784,7 +5787,7 @@
         checkBackupAndRestore_success(/*firstRestore=*/ true);
     }
 
-    public void BackupAndRestore_restoreToSuperSetSignatures() {
+    public void testBackupAndRestore_restoreToSuperSetSignatures() {
         prepareForBackupTest();
 
         // Change package signatures.
@@ -5981,7 +5984,7 @@
         });
     }
 
-    public void BackupAndRestore_publisherWrongSignature() {
+    public void testBackupAndRestore_publisherWrongSignature() {
         prepareForBackupTest();
 
         addPackage(CALLING_PACKAGE_1, CALLING_UID_1, 10, "sigx"); // different signature
@@ -5989,7 +5992,7 @@
         checkBackupAndRestore_publisherNotRestored(ShortcutInfo.DISABLED_REASON_SIGNATURE_MISMATCH);
     }
 
-    public void BackupAndRestore_publisherNoLongerBackupTarget() {
+    public void testBackupAndRestore_publisherNoLongerBackupTarget() {
         prepareForBackupTest();
 
         updatePackageInfo(CALLING_PACKAGE_1,
@@ -6118,7 +6121,7 @@
         });
     }
 
-    public void BackupAndRestore_launcherLowerVersion() {
+    public void testBackupAndRestore_launcherLowerVersion() {
         prepareForBackupTest();
 
         addPackage(LAUNCHER_1, LAUNCHER_UID_1, 0); // Lower version
@@ -6127,7 +6130,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 +6138,7 @@
         checkBackupAndRestore_launcherNotRestored(true);
     }
 
-    public void BackupAndRestore_launcherNoLongerBackupTarget() {
+    public void testBackupAndRestore_launcherNoLongerBackupTarget() {
         prepareForBackupTest();
 
         updatePackageInfo(LAUNCHER_1,
@@ -6240,7 +6243,7 @@
         });
     }
 
-    public void BackupAndRestore_launcherAndPackageNoLongerBackupTarget() {
+    public void testBackupAndRestore_launcherAndPackageNoLongerBackupTarget() {
         prepareForBackupTest();
 
         updatePackageInfo(CALLING_PACKAGE_1,
@@ -6338,7 +6341,7 @@
         });
     }
 
-    public void BackupAndRestore_disabled() {
+    public void testBackupAndRestore_disabled() {
         prepareCrossProfileDataSet();
 
         // Before doing backup & restore, disable s1.
@@ -6403,7 +6406,7 @@
     }
 
 
-    public void BackupAndRestore_manifestRePublished() {
+    public void testBackupAndRestore_manifestRePublished() {
         // Publish two manifest shortcuts.
         addManifestShortcutResource(
                 new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
@@ -6494,7 +6497,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 +6622,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 +6660,7 @@
 
     }
 
-    public void SaveAndLoad_crossProfile() {
+    public void testSaveAndLoad_crossProfile() {
         prepareCrossProfileDataSet();
 
         dumpsysOnLogcat("Before save & load");
@@ -6860,7 +6863,7 @@
                         .getPackageUserId());
     }
 
-    public void OnApplicationActive_permission() {
+    public void testOnApplicationActive_permission() {
         assertExpectException(SecurityException.class, "Missing permission", () ->
                 mManager.onApplicationActive(CALLING_PACKAGE_1, USER_10));
 
@@ -6869,7 +6872,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 +6891,7 @@
         });
     }
 
-    public void HasShareTargets_permission() {
+    public void testHasShareTargets_permission() {
         assertExpectException(SecurityException.class, "Missing permission", () ->
                 mManager.hasShareTargets(CALLING_PACKAGE_1));
 
@@ -6897,7 +6900,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 +6915,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 +7140,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 +7396,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 +7465,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 +7607,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 +7712,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 +7816,7 @@
         });
     }
 
-    public void ManifestShortcuts_updateAndDisabled_notPinned() {
+    public void testManifestShortcuts_updateAndDisabled_notPinned() {
         mService.handleUnlockUser(USER_10);
 
         // First, just publish a manifest shortcut.
@@ -7853,7 +7856,7 @@
         });
     }
 
-    public void ManifestShortcuts_updateAndDisabled_pinned() {
+    public void testManifestShortcuts_updateAndDisabled_pinned() {
         mService.handleUnlockUser(USER_10);
 
         // First, just publish a manifest shortcut.
@@ -7909,7 +7912,7 @@
         });
     }
 
-    public void ManifestShortcuts_duplicateInSingleActivity() {
+    public void testManifestShortcuts_duplicateInSingleActivity() {
         mService.handleUnlockUser(USER_10);
 
         // The XML has two shortcuts with the same ID.
@@ -7934,7 +7937,7 @@
         });
     }
 
-    public void ManifestShortcuts_duplicateInTwoActivities() {
+    public void testManifestShortcuts_duplicateInTwoActivities() {
         mService.handleUnlockUser(USER_10);
 
         // ShortcutActivity has shortcut ms1
@@ -7986,7 +7989,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 +8062,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 +8155,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 +8174,7 @@
         });
     }
 
-    public void MaxShortcutCount_set() {
+    public void testMaxShortcutCount_set() {
         // Change the max number of shortcuts.
         mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3");
 
@@ -8252,7 +8255,7 @@
         });
     }
 
-    public void MaxShortcutCount_add() {
+    public void testMaxShortcutCount_add() {
         // Change the max number of shortcuts.
         mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3");
 
@@ -8379,7 +8382,7 @@
         });
     }
 
-    public void MaxShortcutCount_update() {
+    public void testMaxShortcutCount_update() {
         // Change the max number of shortcuts.
         mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3");
 
@@ -8470,7 +8473,7 @@
         });
     }
 
-    public void ShortcutsPushedOutByManifest() {
+    public void testShortcutsPushedOutByManifest() {
         // Change the max number of shortcuts.
         mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3");
 
@@ -8578,7 +8581,7 @@
         });
     }
 
-    public void ReturnedByServer() {
+    public void testReturnedByServer() {
         // Package 1 updated, with manifest shortcuts.
         addManifestShortcutResource(
                 new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
@@ -8624,7 +8627,7 @@
         });
     }
 
-    public void IsForegroundDefaultLauncher_true() {
+    public void testIsForegroundDefaultLauncher_true() {
         // random uid in the USER_10 range.
         final int uid = 1000024;
 
@@ -8635,7 +8638,7 @@
     }
 
 
-    public void IsForegroundDefaultLauncher_defaultButNotForeground() {
+    public void testIsForegroundDefaultLauncher_defaultButNotForeground() {
         // random uid in the USER_10 range.
         final int uid = 1000024;
 
@@ -8645,7 +8648,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 +8658,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 +8710,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 +8776,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 +8826,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 +8883,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 +8904,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 +8917,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/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/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 7885c9b..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);
@@ -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/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/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..23dcb65 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;
@@ -46,12 +47,14 @@
 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 +63,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 +78,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;
@@ -141,17 +146,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 +190,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);
@@ -263,8 +279,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
@@ -347,6 +363,120 @@
                 });
     }
 
+    @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"), () -> {
+                    // 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 */);
+
+                        mTarget.reportDropResult(iwindow2, true);
+                    } 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"), () -> {
+                    // 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;
+                        // 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 */);
+
+                        mTarget.reportDropResult(iwindow2, true);
+                    } finally {
+                        mTarget.mDeferDragStateClosed = false;
+                    }
+                });
+    }
+
     private DragEvent last(ArrayList<DragEvent> list) {
         return list.get(list.size() - 1);
     }
@@ -503,7 +633,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
@@ -534,7 +664,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
@@ -687,6 +817,14 @@
      * Starts a drag with the given parameters, calls Runnable `r` after drag is started.
      */
     private void startDrag(int flag, ClipData data, Runnable r) {
+        startDrag(0, 0, flag, data, r);
+    }
+
+    /**
+     * 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,
+            Runnable r) {
         final SurfaceSession appSession = new SurfaceSession();
         try {
             final SurfaceControl surface = new SurfaceControl.Builder(appSession).setName(
@@ -694,8 +832,8 @@
                     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();
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..96b11a8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -330,7 +330,7 @@
         if (horizontalReachability) {
             final Consumer<Integer> doubleClick =
                     (Integer x) -> {
-                        mActivity.mAppCompatController.getAppCompatReachabilityPolicy()
+                        mActivity.mAppCompatController.getReachabilityPolicy()
                                 .handleDoubleTap(x, displayHeight / 2);
                         mActivity.mRootWindowContainer.performSurfacePlacement();
                     };
@@ -360,7 +360,7 @@
         } else {
             final Consumer<Integer> doubleClick =
                     (Integer y) -> {
-                        mActivity.mAppCompatController.getAppCompatReachabilityPolicy()
+                        mActivity.mAppCompatController.getReachabilityPolicy()
                                 .handleDoubleTap(displayWidth / 2, y);
                         mActivity.mRootWindowContainer.performSurfacePlacement();
                     };
@@ -421,7 +421,7 @@
 
         final Consumer<Integer> doubleClick =
                 (Integer y) -> {
-                    activity.mAppCompatController.getAppCompatReachabilityPolicy()
+                    activity.mAppCompatController.getReachabilityPolicy()
                             .handleDoubleTap(dw / 2, y);
                     activity.mRootWindowContainer.performSurfacePlacement();
                 };
@@ -834,7 +834,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 +3427,7 @@
 
         setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ false);
         final AppCompatReachabilityOverrides reachabilityOverrides =
-                mActivity.mAppCompatController.getAppCompatReachabilityOverrides();
+                mActivity.mAppCompatController.getReachabilityOverrides();
         assertFalse(reachabilityOverrides.isVerticalReachabilityEnabled());
         assertFalse(reachabilityOverrides.isHorizontalReachabilityEnabled());
     }
@@ -3451,7 +3451,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 +3475,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 +3498,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 +3516,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 +3533,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 +3548,7 @@
         prepareMinAspectRatio(mActivity, OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE,
                 SCREEN_ORIENTATION_PORTRAIT);
 
-        assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides()
+        assertTrue(mActivity.mAppCompatController.getReachabilityOverrides()
                 .isHorizontalReachabilityEnabled());
     }
 
@@ -3563,7 +3563,7 @@
         prepareMinAspectRatio(mActivity, OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE,
                 SCREEN_ORIENTATION_LANDSCAPE);
 
-        assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides()
+        assertTrue(mActivity.mAppCompatController.getReachabilityOverrides()
                 .isVerticalReachabilityEnabled());
     }
 
@@ -3585,7 +3585,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 +3608,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 +3631,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 +4315,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 +4323,7 @@
         doReturn(false).when(mActivity.mDisplayContent).inTransition();
 
         assertEquals(1.0f, mActivity.mAppCompatController
-                .getAppCompatReachabilityOverrides().getVerticalPositionMultiplier(mActivity
+                .getReachabilityOverrides().getVerticalPositionMultiplier(mActivity
                         .getParent().getConfiguration()), 0);
     }
 
@@ -5028,7 +5028,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/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 513ba1d..ab9abfc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -1001,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);
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/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/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/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
index 4d7085f..d35c900 100644
--- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
@@ -830,6 +830,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 +855,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 +874,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
     )
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/tools/aapt2/cmd/Command.cpp b/tools/aapt2/cmd/Command.cpp
index 449d93d..2031556 100644
--- a/tools/aapt2/cmd/Command.cpp
+++ b/tools/aapt2/cmd/Command.cpp
@@ -53,61 +53,67 @@
 
 void Command::AddRequiredFlag(StringPiece name, StringPiece description, std::string* value,
                               uint32_t flags) {
-  auto func = [value, flags](StringPiece arg) -> bool {
+  auto func = [value, flags](StringPiece arg, std::ostream*) -> bool {
     *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 {
+  auto func = [value, flags](StringPiece arg, std::ostream*) -> bool {
     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 {
+  auto func = [value, flags](StringPiece arg, std::ostream*) -> bool {
     *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 {
+  auto func = [value, flags](StringPiece arg, std::ostream*) -> bool {
     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 {
+  auto func = [value](StringPiece arg, std::ostream* out_error) -> bool {
     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 {
+  auto func = [value](StringPiece arg, std::ostream* out_error) -> bool {
     *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 +178,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 +272,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..2a3cb2a 100644
--- a/tools/aapt2/cmd/Command_test.cpp
+++ b/tools/aapt2/cmd/Command_test.cpp
@@ -118,4 +118,45 @@
   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));
+}
+
 }  // namespace aapt
\ No newline at end of file