Merge "[Audiosharing] Add rounded background with different radius" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 1ae9ada..3790a96 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -21,6 +21,7 @@
     java_aconfig_libraries: [
         // !!! KEEP THIS LIST ALPHABETICAL !!!
         "aconfig_mediacodec_flags_java_lib",
+        "aconfig_trade_in_mode_flags_java_lib",
         "android-sdk-flags-java",
         "android.adaptiveauth.flags-aconfig-java",
         "android.app.appfunctions.flags-aconfig-java",
@@ -1559,6 +1560,10 @@
     name: "android.crashrecovery.flags-aconfig-java",
     aconfig_declarations: "android.crashrecovery.flags-aconfig",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.crashrecovery",
+    ],
 }
 
 java_aconfig_library {
diff --git a/Android.bp b/Android.bp
index 252aeef..b569df2 100644
--- a/Android.bp
+++ b/Android.bp
@@ -109,7 +109,7 @@
         ":android.hardware.radio.voice-V3-java-source",
         ":android.hardware.security.keymint-V3-java-source",
         ":android.hardware.security.secureclock-V1-java-source",
-        ":android.hardware.thermal-V2-java-source",
+        ":android.hardware.thermal-V3-java-source",
         ":android.hardware.tv.tuner-V3-java-source",
         ":android.security.apc-java-source",
         ":android.security.authorization-java-source",
diff --git a/apct-tests/perftests/tracing/Android.bp b/apct-tests/perftests/tracing/Android.bp
index 08e365b..8814216 100644
--- a/apct-tests/perftests/tracing/Android.bp
+++ b/apct-tests/perftests/tracing/Android.bp
@@ -22,6 +22,7 @@
         "apct-perftests-utils",
         "collector-device-lib",
         "platform-test-annotations",
+        "perfetto_trace_java_protos",
     ],
     test_suites: [
         "device-tests",
diff --git a/apct-tests/perftests/tracing/src/com/android/internal/protolog/ProtoLogPerfTest.java b/apct-tests/perftests/tracing/src/com/android/internal/protolog/ProtoLogPerfTest.java
index f4759b8..7ef8c53 100644
--- a/apct-tests/perftests/tracing/src/com/android/internal/protolog/ProtoLogPerfTest.java
+++ b/apct-tests/perftests/tracing/src/com/android/internal/protolog/ProtoLogPerfTest.java
@@ -17,10 +17,12 @@
 
 import android.app.Activity;
 import android.os.Bundle;
+import android.os.ServiceManager.ServiceNotFoundException;
 import android.perftests.utils.Stats;
 
 import androidx.test.InstrumentationRegistry;
 
+import com.android.internal.protolog.common.IProtoLog;
 import com.android.internal.protolog.common.IProtoLogGroup;
 import com.android.internal.protolog.common.LogLevel;
 
@@ -31,6 +33,8 @@
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameters;
 
+import perfetto.protos.ProtologCommon;
+
 import java.util.ArrayList;
 import java.util.Collection;
 
@@ -65,24 +69,48 @@
         };
     }
 
+    private IProtoLog mProcessedProtoLogger;
+    private static final String MOCK_TEST_FILE_PATH = "mock/file/path";
+    private static final perfetto.protos.Protolog.ProtoLogViewerConfig VIEWER_CONFIG =
+            perfetto.protos.Protolog.ProtoLogViewerConfig.newBuilder()
+                .addGroups(
+                        perfetto.protos.Protolog.ProtoLogViewerConfig.Group.newBuilder()
+                                .setId(1)
+                                .setName(TestProtoLogGroup.TEST_GROUP.toString())
+                                .setTag(TestProtoLogGroup.TEST_GROUP.getTag())
+                ).addMessages(
+                        perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.newBuilder()
+                                .setMessageId(123)
+                                .setMessage("My Test Debug Log Message %b")
+                                .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_DEBUG)
+                                .setGroupId(1)
+                                .setLocation("com/test/MyTestClass.java:123")
+                ).build();
+
     @BeforeClass
     public static void init() {
         ProtoLog.init(TestProtoLogGroup.values());
     }
 
     @Before
-    public void setUp() {
+    public void setUp() throws ServiceNotFoundException {
         TestProtoLogGroup.TEST_GROUP.setLogToProto(mLogToProto);
         TestProtoLogGroup.TEST_GROUP.setLogToLogcat(mLogToLogcat);
+
+        mProcessedProtoLogger = new ProcessedPerfettoProtoLogImpl(
+                MOCK_TEST_FILE_PATH,
+                () -> new AutoClosableProtoInputStream(VIEWER_CONFIG.toByteArray()),
+                () -> {},
+                TestProtoLogGroup.values()
+        );
     }
 
     @Test
     public void log_Processed_NoArgs() {
-        final var protoLog = ProtoLog.getSingleInstance();
         final var perfMonitor = new PerfMonitor();
 
         while (perfMonitor.keepRunning()) {
-            protoLog.log(
+            mProcessedProtoLogger.log(
                     LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 123,
                     0, (Object[]) null);
         }
@@ -90,11 +118,10 @@
 
     @Test
     public void log_Processed_WithArgs() {
-        final var protoLog = ProtoLog.getSingleInstance();
         final var perfMonitor = new PerfMonitor();
 
         while (perfMonitor.keepRunning()) {
-            protoLog.log(
+            mProcessedProtoLogger.log(
                     LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 123,
                     0b1110101001010100,
                     new Object[]{"test", 1, 2, 3, 0.4, 0.5, 0.6, true});
diff --git a/boot/boot-image-profile-extra.txt b/boot/boot-image-profile-extra.txt
index e3b187e..11ca1dc 100644
--- a/boot/boot-image-profile-extra.txt
+++ b/boot/boot-image-profile-extra.txt
@@ -19,3 +19,7 @@
 # methods are latency sensitive is difficult. For example, this method is executed
 # in the system server, not on the UI thread of an app.
 HSPLandroid/graphics/Color;->luminance()F
+
+# For now, compile all methods in MessageQueue to avoid performance cliffs for
+# flagged/evolving hot code paths. See: b/338098106
+HSPLandroid/os/MessageQueue;->*
diff --git a/core/api/current.txt b/core/api/current.txt
index a8b9e33..9298ca2 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -16369,6 +16369,7 @@
     field public static final int UNKNOWN = 0; // 0x0
     field public static final int Y8 = 538982489; // 0x20203859
     field public static final int YCBCR_P010 = 54; // 0x36
+    field @FlaggedApi("android.media.codec.p210_format_support") public static final int YCBCR_P210 = 60; // 0x3c
     field public static final int YUV_420_888 = 35; // 0x23
     field public static final int YUV_422_888 = 39; // 0x27
     field public static final int YUV_444_888 = 40; // 0x28
@@ -18718,6 +18719,7 @@
     field public static final long USAGE_VIDEO_ENCODE = 65536L; // 0x10000L
     field public static final int YCBCR_420_888 = 35; // 0x23
     field public static final int YCBCR_P010 = 54; // 0x36
+    field @FlaggedApi("android.media.codec.p210_format_support") public static final int YCBCR_P210 = 60; // 0x3c
   }
 
   @FlaggedApi("android.hardware.flags.overlayproperties_class_api") public final class OverlayProperties implements android.os.Parcelable {
@@ -22985,6 +22987,7 @@
     field public static final int COLOR_FormatYUV444Flexible = 2135181448; // 0x7f444888
     field @Deprecated public static final int COLOR_FormatYUV444Interleaved = 29; // 0x1d
     field public static final int COLOR_FormatYUVP010 = 54; // 0x36
+    field @FlaggedApi("android.media.codec.p210_format_support") public static final int COLOR_FormatYUVP210 = 60; // 0x3c
     field @Deprecated public static final int COLOR_QCOM_FormatYUV420SemiPlanar = 2141391872; // 0x7fa30c00
     field @Deprecated public static final int COLOR_TI_FormatYUV420PackedSemiPlanar = 2130706688; // 0x7f000100
     field public static final String FEATURE_AdaptivePlayback = "adaptive-playback";
@@ -52491,6 +52494,7 @@
     method public android.graphics.Canvas lockHardwareCanvas();
     method public void readFromParcel(android.os.Parcel);
     method public void release();
+    method @FlaggedApi("com.android.graphics.surfaceflinger.flags.arr_setframerate_api") public void setFrameRate(@NonNull android.view.Surface.FrameRateParams);
     method public void setFrameRate(@FloatRange(from=0.0) float, int, int);
     method public void setFrameRate(@FloatRange(from=0.0) float, int);
     method @Deprecated public void unlockCanvas(android.graphics.Canvas);
@@ -52507,6 +52511,22 @@
     field public static final int ROTATION_90 = 1; // 0x1
   }
 
+  @FlaggedApi("com.android.graphics.surfaceflinger.flags.arr_setframerate_api") public static class Surface.FrameRateParams {
+    method @FlaggedApi("com.android.graphics.surfaceflinger.flags.arr_setframerate_api") public int getChangeFrameRateStrategy();
+    method @FlaggedApi("com.android.graphics.surfaceflinger.flags.arr_setframerate_api") public float getDesiredMaxRate();
+    method @FlaggedApi("com.android.graphics.surfaceflinger.flags.arr_setframerate_api") public float getDesiredMinRate();
+    method @FlaggedApi("com.android.graphics.surfaceflinger.flags.arr_setframerate_api") public float getFixedSourceRate();
+    field @FlaggedApi("com.android.graphics.surfaceflinger.flags.arr_setframerate_api") public static final android.view.Surface.FrameRateParams IGNORE;
+  }
+
+  @FlaggedApi("com.android.graphics.surfaceflinger.flags.arr_setframerate_api") public static final class Surface.FrameRateParams.Builder {
+    ctor public Surface.FrameRateParams.Builder();
+    method @FlaggedApi("com.android.graphics.surfaceflinger.flags.arr_setframerate_api") @NonNull public android.view.Surface.FrameRateParams build();
+    method @FlaggedApi("com.android.graphics.surfaceflinger.flags.arr_setframerate_api") @NonNull public android.view.Surface.FrameRateParams.Builder setChangeFrameRateStrategy(int);
+    method @FlaggedApi("com.android.graphics.surfaceflinger.flags.arr_setframerate_api") @NonNull public android.view.Surface.FrameRateParams.Builder setDesiredRateRange(@FloatRange(from=0.0) float, @FloatRange(from=0.0) float);
+    method @FlaggedApi("com.android.graphics.surfaceflinger.flags.arr_setframerate_api") @NonNull public android.view.Surface.FrameRateParams.Builder setFixedSourceRate(@FloatRange(from=0.0) float);
+  }
+
   public static class Surface.OutOfResourcesException extends java.lang.RuntimeException {
     ctor public Surface.OutOfResourcesException();
     ctor public Surface.OutOfResourcesException(String);
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 207f4b5..f04df2f 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -9429,6 +9429,7 @@
     method public int getSignalStrength();
     method public int getSnr();
     method public int getSpectralInversion();
+    method @FlaggedApi("android.media.tv.flags.tuner_w_apis") @NonNull public android.media.tv.tuner.frontend.StandardExt getStandardExt();
     method @NonNull public int[] getStreamIds();
     method public int getSymbolRate();
     method @IntRange(from=0, to=65535) public int getSystemId();
@@ -9483,6 +9484,7 @@
     field public static final int FRONTEND_STATUS_TYPE_SIGNAL_STRENGTH = 6; // 0x6
     field public static final int FRONTEND_STATUS_TYPE_SNR = 1; // 0x1
     field public static final int FRONTEND_STATUS_TYPE_SPECTRAL = 10; // 0xa
+    field @FlaggedApi("android.media.tv.flags.tuner_w_apis") public static final int FRONTEND_STATUS_TYPE_STANDARD_EXT = 47; // 0x2f
     field public static final int FRONTEND_STATUS_TYPE_STREAM_IDS = 39; // 0x27
     field public static final int FRONTEND_STATUS_TYPE_SYMBOL_RATE = 7; // 0x7
     field public static final int FRONTEND_STATUS_TYPE_T2_SYSTEM_ID = 29; // 0x1d
@@ -9774,6 +9776,11 @@
     method public default void onUnlocked();
   }
 
+  @FlaggedApi("android.media.tv.flags.tuner_w_apis") public final class StandardExt {
+    method public int getDvbsStandardExt();
+    method public int getDvbtStandardExt();
+  }
+
 }
 
 package android.media.voice {
diff --git a/core/java/Android.bp b/core/java/Android.bp
index d12e1bf..9875efe 100644
--- a/core/java/Android.bp
+++ b/core/java/Android.bp
@@ -345,6 +345,13 @@
 }
 
 filegroup {
+    name: "service-crashrecovery-shared-srcs",
+    srcs: [
+        "android/util/IndentingPrintWriter.java",
+    ],
+}
+
+filegroup {
     name: "incremental_aidl",
     srcs: [
         "android/os/incremental/IIncrementalServiceConnector.aidl",
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index ed6b3d4a..b0a8b1b 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -2681,7 +2681,10 @@
                     handleUnstableProviderDied((IBinder)msg.obj, false);
                     break;
                 case REQUEST_ASSIST_CONTEXT_EXTRAS:
+                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
+                            "handleRequestAssistContextExtras");
                     handleRequestAssistContextExtras((RequestAssistContextExtras)msg.obj);
+                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                     break;
                 case TRANSLUCENT_CONVERSION_COMPLETE:
                     handleTranslucentConversionComplete((IBinder)msg.obj, msg.arg1 == 1);
diff --git a/core/java/android/appwidget/OWNERS b/core/java/android/appwidget/OWNERS
index 1910833..0e85d5b 100644
--- a/core/java/android/appwidget/OWNERS
+++ b/core/java/android/appwidget/OWNERS
@@ -3,3 +3,5 @@
 pinyaoting@google.com
 suprabh@google.com
 sunnygoyal@google.com
+zakcohen@google.com
+shamalip@google.com
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 5b38942..5f439b1 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -342,3 +342,11 @@
     bug: "292261144"
     is_fixed_read_only: true
 }
+
+flag {
+    name: "change_launcher_badging"
+    namespace: "package_manager_service"
+    description: "Feature flag to introduce a new way to change the launcher badging."
+    bug: "364760703"
+    is_fixed_read_only: true
+}
diff --git a/core/java/android/hardware/HardwareBuffer.java b/core/java/android/hardware/HardwareBuffer.java
index 9395844..44115c8 100644
--- a/core/java/android/hardware/HardwareBuffer.java
+++ b/core/java/android/hardware/HardwareBuffer.java
@@ -66,6 +66,7 @@
             DS_FP32UI8,
             S_UI8,
             YCBCR_P010,
+            YCBCR_P210,
             R_8,
             R_16,
             RG_1616,
@@ -111,6 +112,16 @@
      * little-endian value, with the lower 6 bits set to zero.
      */
     public static final int YCBCR_P010    = 0x36;
+    /**
+     * <p>Android YUV P210 format.</p>
+     *
+     * P210 is a 4:2:2 YCbCr semiplanar format comprised of a WxH Y plane
+     * followed by a WxH CbCr plane. Each sample is represented by a 16-bit
+     * little-endian value, with the lower 6 bits set to zero.
+     */
+    @FlaggedApi(android.media.codec.Flags.FLAG_P210_FORMAT_SUPPORT)
+    public static final int YCBCR_P210    = 0x3c;
+
     /** Format: 8 bits red */
     @FlaggedApi(com.android.graphics.hwui.flags.Flags.FLAG_REQUESTED_FORMATS_V)
     public static final int R_8           = 0x38;
diff --git a/core/java/android/hardware/biometrics/flags.aconfig b/core/java/android/hardware/biometrics/flags.aconfig
index 047d1fa..26ffa11 100644
--- a/core/java/android/hardware/biometrics/flags.aconfig
+++ b/core/java/android/hardware/biometrics/flags.aconfig
@@ -39,3 +39,11 @@
   description: "This flag controls whether LSKF fallback is removed from biometric prompt when the phone is outside trusted locations"
   bug: "322081563"
 }
+
+flag {
+  name: "screen_off_unlock_udfps"
+  is_exported: true
+  namespace: "biometrics_integration"
+  description: "This flag controls Whether to enable fp unlock when screen turns off on udfps devices"
+  bug: "373792870"
+}
diff --git a/core/java/android/hardware/display/AmbientDisplayConfiguration.java b/core/java/android/hardware/display/AmbientDisplayConfiguration.java
index 47541ca..59a602ca 100644
--- a/core/java/android/hardware/display/AmbientDisplayConfiguration.java
+++ b/core/java/android/hardware/display/AmbientDisplayConfiguration.java
@@ -18,6 +18,7 @@
 
 import android.annotation.TestApi;
 import android.content.Context;
+import android.hardware.biometrics.Flags;
 import android.os.Build;
 import android.os.SystemProperties;
 import android.provider.Settings;
@@ -41,6 +42,7 @@
     private final Context mContext;
     private final boolean mAlwaysOnByDefault;
     private final boolean mPickupGestureEnabledByDefault;
+    private final boolean mScreenOffUdfpsEnabledByDefault;
 
     /** Copied from android.provider.Settings.Secure since these keys are hidden. */
     private static final String[] DOZE_SETTINGS = {
@@ -68,6 +70,8 @@
         mAlwaysOnByDefault = mContext.getResources().getBoolean(R.bool.config_dozeAlwaysOnEnabled);
         mPickupGestureEnabledByDefault =
                 mContext.getResources().getBoolean(R.bool.config_dozePickupGestureEnabled);
+        mScreenOffUdfpsEnabledByDefault =
+                mContext.getResources().getBoolean(R.bool.config_screen_off_udfps_enabled);
     }
 
     /** @hide */
@@ -146,7 +150,9 @@
     /** @hide */
     public boolean screenOffUdfpsEnabled(int user) {
         return !TextUtils.isEmpty(udfpsLongPressSensorType())
-            && boolSettingDefaultOff("screen_off_udfps_enabled", user);
+                && ((mScreenOffUdfpsEnabledByDefault && Flags.screenOffUnlockUdfps())
+                ? boolSettingDefaultOn("screen_off_udfps_enabled", user)
+                : boolSettingDefaultOff("screen_off_udfps_enabled", user));
     }
 
     /** @hide */
diff --git a/core/java/android/hardware/fingerprint/FingerprintCallback.java b/core/java/android/hardware/fingerprint/FingerprintCallback.java
index 24e9f9d..e4fbe6e 100644
--- a/core/java/android/hardware/fingerprint/FingerprintCallback.java
+++ b/core/java/android/hardware/fingerprint/FingerprintCallback.java
@@ -189,7 +189,7 @@
             mEnrollmentCallback.onAcquired(acquireInfo == FINGERPRINT_ACQUIRED_GOOD);
         }
         final String msg = getAcquiredString(context, acquireInfo, vendorCode);
-        if (msg == null || msg.isEmpty()) {
+        if (msg == null) {
             return;
         }
         // emulate HAL 2.1 behavior and send real acquiredInfo
diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java
index 7f1cac0..590c4d6 100644
--- a/core/java/android/hardware/fingerprint/FingerprintManager.java
+++ b/core/java/android/hardware/fingerprint/FingerprintManager.java
@@ -1517,7 +1517,7 @@
      */
     public static String getAcquiredString(Context context, int acquireInfo, int vendorCode) {
         switch (acquireInfo) {
-            case FINGERPRINT_ACQUIRED_GOOD:
+            case FINGERPRINT_ACQUIRED_GOOD, FINGERPRINT_ACQUIRED_START:
                 return null;
             case FINGERPRINT_ACQUIRED_PARTIAL:
                 return context.getString(
@@ -1546,13 +1546,10 @@
             case FINGERPRINT_ACQUIRED_VENDOR: {
                 String[] msgArray = context.getResources().getStringArray(
                         com.android.internal.R.array.fingerprint_acquired_vendor);
-                if (vendorCode < msgArray.length) {
+                if (vendorCode < msgArray.length && !msgArray[vendorCode].isEmpty()) {
                     return msgArray[vendorCode];
                 }
             }
-                break;
-            case FINGERPRINT_ACQUIRED_START:
-                return null;
         }
         Slog.w(TAG, "Invalid acquired message: " + acquireInfo + ", " + vendorCode);
         return null;
diff --git a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
index 6afb8e0..8eaadde 100644
--- a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
+++ b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
@@ -29,6 +29,8 @@
 
 import com.android.internal.annotations.GuardedBy;
 
+import dalvik.annotation.optimization.NeverCompile;
+
 import java.io.FileDescriptor;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -1331,6 +1333,7 @@
                 mMatchAllFutureMessages, true);
     }
 
+    @NeverCompile
     private void printPriorityQueueNodes() {
         Iterator<MessageNode> iterator = mPriorityQueue.iterator();
 
@@ -1342,6 +1345,7 @@
         }
     }
 
+    @NeverCompile
     private int dumpPriorityQueue(ConcurrentSkipListSet<MessageNode> queue, Printer pw,
             String prefix, Handler h, int n) {
         int count = 0;
@@ -1357,6 +1361,7 @@
         return count;
     }
 
+    @NeverCompile
     void dump(Printer pw, String prefix, Handler h) {
         long now = SystemClock.uptimeMillis();
         int n = 0;
@@ -1387,6 +1392,7 @@
                 + ", quitting=" + (boolean) sQuitting.getVolatile(this) + ")");
     }
 
+    @NeverCompile
     private int dumpPriorityQueue(ConcurrentSkipListSet<MessageNode> queue,
             ProtoOutputStream proto) {
         int count = 0;
@@ -1399,6 +1405,7 @@
         return count;
     }
 
+    @NeverCompile
     void dumpDebug(ProtoOutputStream proto, long fieldId) {
         final long messageQueueToken = proto.start(fieldId);
 
diff --git a/core/java/android/os/ITradeInMode.aidl b/core/java/android/os/ITradeInMode.aidl
new file mode 100644
index 0000000..f15954d
--- /dev/null
+++ b/core/java/android/os/ITradeInMode.aidl
@@ -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 android.os;
+
+/** @hide */
+interface ITradeInMode {
+    /**
+     * Enable adb in limited-privilege trade-in mode. Returns true if trade-in
+     * mode was enabled.
+     *
+     * Trade-in mode can be enabled if the following conditions are all true:
+     *   ro.debuggable is 0.
+     *   Settings.Global.ADB_ENABLED is 0.
+     *   Settings.Global.USER_SETUP_COMPLETE is 0.
+     *   Settings.Secure.DEVICE_PROVISIONED is 0.
+     *
+     * It is stopped automatically when any of the following conditions become
+     * true:
+     *
+     *   Settings.Global.USER_SETUP_COMPLETE is 1.
+     *   Settings.Secure.DEVICE_PROVISIONED is 1.
+     *   A change in network configuration occurs.
+     *   An account is added.
+     *
+     * ENTER_TRADE_IN_MODE permission is required.
+     */
+    boolean start();
+
+    /**
+     * Returns whether evaluation mode is allowed on this device. It will return
+     * false if any kind of device protection (such as FRP) is detected.
+     *
+     * ENTER_TRADE_IN_MODE permission is required.
+     */
+    boolean isEvaluationModeAllowed();
+
+    /**
+     * Enable full adb access and provision the device. This forces a factory
+     * reset on the next boot.
+     *
+     * This will return false if start() was not called, if factory reset
+     * protection is active, or if trade-in mode was disabled due to any of the
+     * conditions listed above for start().
+     *
+     * ENTER_TRADE_IN_MODE permission is required.
+     */
+    boolean enterEvaluationMode();
+}
diff --git a/core/java/android/os/LegacyMessageQueue/MessageQueue.java b/core/java/android/os/LegacyMessageQueue/MessageQueue.java
index 4474e7e..9f7b0b7 100644
--- a/core/java/android/os/LegacyMessageQueue/MessageQueue.java
+++ b/core/java/android/os/LegacyMessageQueue/MessageQueue.java
@@ -28,6 +28,8 @@
 import android.util.SparseArray;
 import android.util.proto.ProtoOutputStream;
 
+import dalvik.annotation.optimization.NeverCompile;
+
 import java.io.FileDescriptor;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -1106,6 +1108,7 @@
         }
     }
 
+    @NeverCompile
     void dump(Printer pw, String prefix, Handler h) {
         synchronized (this) {
             pw.println(prefix + "(MessageQueue is using Legacy implementation)");
@@ -1122,6 +1125,7 @@
         }
     }
 
+    @NeverCompile
     void dumpDebug(ProtoOutputStream proto, long fieldId) {
         final long messageQueueToken = proto.start(fieldId);
         synchronized (this) {
diff --git a/core/java/android/os/LockedMessageQueue/MessageQueue.java b/core/java/android/os/LockedMessageQueue/MessageQueue.java
index f1affce..f3eec13 100644
--- a/core/java/android/os/LockedMessageQueue/MessageQueue.java
+++ b/core/java/android/os/LockedMessageQueue/MessageQueue.java
@@ -30,6 +30,8 @@
 
 import com.android.internal.annotations.GuardedBy;
 
+import dalvik.annotation.optimization.NeverCompile;
+
 import java.io.FileDescriptor;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -294,6 +296,7 @@
         * Keep this for manual debugging. It's easier to pepper the code with this function
         * than MessageQueue.dump()
         */
+        @NeverCompile
         void print() {
             Log.v(TAG, "heap num elem: " + mNumElements + " mHeap.length " + mHeap.length);
             for (int i = 0; i < mNumElements; i++) {
@@ -1209,6 +1212,7 @@
                 sMatchAllFutureMessages, true);
     }
 
+    @NeverCompile
     int dumpPriorityQueue(Printer pw, String prefix, Handler h, MessageHeap priorityQueue) {
         int n = 0;
         long now = SystemClock.uptimeMillis();
@@ -1222,6 +1226,7 @@
         return n;
     }
 
+    @NeverCompile
     void dumpPriorityQueue(ProtoOutputStream proto, MessageHeap priorityQueue) {
         for (int i = 0; i < priorityQueue.numElements(); i++) {
             Message m = priorityQueue.getMessageAt(i);
@@ -1229,6 +1234,7 @@
         }
     }
 
+    @NeverCompile
     void dump(Printer pw, String prefix, Handler h) {
         synchronized (this) {
             pw.println(prefix + "(MessageQueue is using Locked implementation)");
@@ -1240,6 +1246,7 @@
         }
     }
 
+    @NeverCompile
     void dumpDebug(ProtoOutputStream proto, long fieldId) {
         final long messageQueueToken = proto.start(fieldId);
         synchronized (this) {
diff --git a/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
index 02335972..db323dc 100644
--- a/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
+++ b/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
@@ -29,6 +29,8 @@
 
 import com.android.internal.annotations.GuardedBy;
 
+import dalvik.annotation.optimization.NeverCompile;
+
 import java.io.FileDescriptor;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -1249,6 +1251,7 @@
                 mMatchAllFutureMessages, true);
     }
 
+    @NeverCompile
     private void printPriorityQueueNodes() {
         Iterator<MessageNode> iterator = mPriorityQueue.iterator();
 
@@ -1260,6 +1263,7 @@
         }
     }
 
+    @NeverCompile
     private int dumpPriorityQueue(PriorityQueue<MessageNode> queue, Printer pw, String prefix,
             Handler h, int n) {
         int count = 0;
@@ -1275,6 +1279,7 @@
         return count;
     }
 
+    @NeverCompile
     void dump(Printer pw, String prefix, Handler h) {
         long now = SystemClock.uptimeMillis();
         int n = 0;
@@ -1307,6 +1312,7 @@
                 + ", quitting=" + (boolean) sQuitting.getVolatile(this) + ")");
     }
 
+    @NeverCompile
     private int dumpPriorityQueue(PriorityQueue<MessageNode> queue, ProtoOutputStream proto) {
         int count = 0;
 
@@ -1318,6 +1324,7 @@
         return count;
     }
 
+    @NeverCompile
     void dumpDebug(ProtoOutputStream proto, long fieldId) {
         final long messageQueueToken = proto.start(fieldId);
 
diff --git a/core/java/android/os/VibratorInfo.java b/core/java/android/os/VibratorInfo.java
index 9419032..9dec867 100644
--- a/core/java/android/os/VibratorInfo.java
+++ b/core/java/android/os/VibratorInfo.java
@@ -316,9 +316,7 @@
      * @return True if the hardware can control the frequency of the vibrations, otherwise false.
      */
     public boolean hasFrequencyControl() {
-        // We currently can only control frequency of the vibration using the compose PWLE method.
-        return hasCapability(
-                IVibrator.CAP_FREQUENCY_CONTROL | IVibrator.CAP_COMPOSE_PWLE_EFFECTS);
+        return hasCapability(IVibrator.CAP_FREQUENCY_CONTROL);
     }
 
     /**
@@ -481,7 +479,8 @@
      * @return True if the hardware supports creating envelope effects, false otherwise.
      */
     public boolean areEnvelopeEffectsSupported() {
-        return hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2);
+        return hasCapability(
+                IVibrator.CAP_FREQUENCY_CONTROL | IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2);
     }
 
     /**
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index c7cc653..9c83bc2 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -91,6 +91,14 @@
 }
 
 flag {
+    name: "allow_thermal_thresholds_callback"
+    is_exported: true
+    namespace: "game"
+    description: "Enable thermal threshold callback"
+    bug: "360486877"
+}
+
+flag {
     name: "android_os_build_vanilla_ice_cream"
     is_exported: true
     namespace: "build"
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index bfefba5b..3df7ff9 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -282,3 +282,12 @@
     description: "This fixed read-only flag is used to enable platform support for Skin Temperature."
     bug: "369872443"
 }
+
+flag {
+    name: "platform_oxygen_saturation_enabled"
+    is_fixed_read_only: true
+    is_exported: true
+    namespace: "android_health_services"
+    description: "This fixed read-only flag is used to enable platform support for Oxygen Saturation (SpO2)."
+    bug: "369873227"
+}
diff --git a/core/java/android/print/IPrintDocumentAdapter.aidl b/core/java/android/print/IPrintDocumentAdapter.aidl
index 8f33e0b..9d384fb 100644
--- a/core/java/android/print/IPrintDocumentAdapter.aidl
+++ b/core/java/android/print/IPrintDocumentAdapter.aidl
@@ -37,5 +37,4 @@
     void write(in PageRange[] pages, in ParcelFileDescriptor fd,
             IWriteResultCallback callback, int sequence);
     void finish();
-    void kill(String reason);
 }
diff --git a/core/java/android/print/PrintManager.java b/core/java/android/print/PrintManager.java
index ef274a5..1b1554f 100644
--- a/core/java/android/print/PrintManager.java
+++ b/core/java/android/print/PrintManager.java
@@ -946,17 +946,6 @@
         }
 
         @Override
-        public void kill(String reason) {
-            synchronized (mLock) {
-                // If destroyed the handler is null.
-                if (!isDestroyedLocked()) {
-                    mHandler.obtainMessage(MyHandler.MSG_ON_KILL,
-                            reason).sendToTarget();
-                }
-            }
-        }
-
-        @Override
         public void onActivityPaused(Activity activity) {
             /* do nothing */
         }
@@ -1118,15 +1107,6 @@
                         }
                     } break;
 
-                    case MSG_ON_KILL: {
-                        if (DEBUG) {
-                            Log.i(LOG_TAG, "onKill()");
-                        }
-
-                        String reason = (String) message.obj;
-                        throw new RuntimeException(reason);
-                    }
-
                     default: {
                         throw new IllegalArgumentException("Unknown message: "
                                 + message.what);
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 764570e..83c599e 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -5472,6 +5472,14 @@
         public static final String VOLUME_MASTER = "volume_master";
 
         /**
+         * The mapping of input device to its input gain index.
+         *
+         * @hide
+         */
+        @Readable
+        public static final String INPUT_GAIN_INDEX_SETTINGS = "input_gain_index_settings";
+
+        /**
          * Master mono (int 1 = mono, 0 = normal).
          *
          * @hide
@@ -8390,7 +8398,6 @@
         @Readable
         public static final String LOCK_SCREEN_LOCK_AFTER_TIMEOUT = "lock_screen_lock_after_timeout";
 
-
         /**
          * This preference contains the string that shows for owner info on LockScreen.
          * @hide
@@ -11281,7 +11288,8 @@
                 "assist_long_press_home_enabled";
 
         /**
-         * Whether all entrypoints can trigger search. Replaces individual settings.
+         * Whether all entrypoints (e.g. long-press home, long-press nav handle)
+         * can trigger contextual search.
          *
          * @hide
          */
diff --git a/core/java/android/telephony/PhoneStateListener.java b/core/java/android/telephony/PhoneStateListener.java
index e8ef9d6..bce51f2 100644
--- a/core/java/android/telephony/PhoneStateListener.java
+++ b/core/java/android/telephony/PhoneStateListener.java
@@ -1701,6 +1701,11 @@
         public final void onCarrierRoamingNtnEligibleStateChanged(boolean eligible) {
             // not supported on the deprecated interface - Use TelephonyCallback instead
         }
+
+        public final void onCarrierRoamingNtnAvailableServicesChanged(
+                @NetworkRegistrationInfo.ServiceType int[] availableServices) {
+            // not supported on the deprecated interface - Use TelephonyCallback instead
+        }
     }
 
     private void log(String s) {
diff --git a/core/java/android/telephony/TelephonyCallback.java b/core/java/android/telephony/TelephonyCallback.java
index 5295b60..46e27dc 100644
--- a/core/java/android/telephony/TelephonyCallback.java
+++ b/core/java/android/telephony/TelephonyCallback.java
@@ -681,6 +681,20 @@
     public static final int EVENT_CARRIER_ROAMING_NTN_ELIGIBLE_STATE_CHANGED = 43;
 
     /**
+     * Event for listening to changes in carrier roaming non-terrestrial network available services
+     * via callback onCarrierRoamingNtnAvailableServicesChanged().
+     * This callback is triggered when the available services provided by the carrier roaming
+     * satellite changes. The carrier roaming satellite is defined by the following conditions.
+     * <ul>
+     * <li>Subscription supports attaching to satellite which is defined by
+     * {@link CarrierConfigManager#KEY_SATELLITE_ATTACH_SUPPORTED_BOOL} </li>
+     * </ul>
+     *
+     * @hide
+     */
+    public static final int EVENT_CARRIER_ROAMING_NTN_AVAILABLE_SERVICES_CHANGED = 44;
+
+    /**
      * @hide
      */
     @IntDef(prefix = {"EVENT_"}, value = {
@@ -726,7 +740,8 @@
             EVENT_EMERGENCY_CALLBACK_MODE_CHANGED,
             EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED,
             EVENT_CARRIER_ROAMING_NTN_MODE_CHANGED,
-            EVENT_CARRIER_ROAMING_NTN_ELIGIBLE_STATE_CHANGED
+            EVENT_CARRIER_ROAMING_NTN_ELIGIBLE_STATE_CHANGED,
+            EVENT_CARRIER_ROAMING_NTN_AVAILABLE_SERVICES_CHANGED
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface TelephonyEvent {
@@ -1784,6 +1799,15 @@
          * </ul>
          */
         default void onCarrierRoamingNtnEligibleStateChanged(boolean eligible) {}
+
+        /**
+         * Callback invoked when carrier roaming non-terrestrial network available
+         * service changes.
+         *
+         * @param availableServices The list of the supported services.
+         */
+        default void onCarrierRoamingNtnAvailableServicesChanged(
+                @NetworkRegistrationInfo.ServiceType List<Integer> availableServices) {}
     }
 
     /**
@@ -2235,5 +2259,19 @@
             Binder.withCleanCallingIdentity(() -> mExecutor.execute(
                     () -> listener.onCarrierRoamingNtnEligibleStateChanged(eligible)));
         }
+
+        public void onCarrierRoamingNtnAvailableServicesChanged(
+                @NetworkRegistrationInfo.ServiceType int[] availableServices) {
+            if (!Flags.carrierRoamingNbIotNtn()) return;
+
+            CarrierRoamingNtnModeListener listener =
+                    (CarrierRoamingNtnModeListener) mTelephonyCallbackWeakRef.get();
+            if (listener == null) return;
+
+            List<Integer> ServiceList = Arrays.stream(availableServices).boxed()
+                    .collect(Collectors.toList());
+            Binder.withCleanCallingIdentity(() -> mExecutor.execute(
+                    () -> listener.onCarrierRoamingNtnAvailableServicesChanged(ServiceList)));
+        }
     }
 }
diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java
index 3c7e924..4d50a45 100644
--- a/core/java/android/telephony/TelephonyRegistryManager.java
+++ b/core/java/android/telephony/TelephonyRegistryManager.java
@@ -1118,6 +1118,21 @@
     }
 
     /**
+     * Notify external listeners that carrier roaming non-terrestrial available services changed.
+     * @param availableServices The list of the supported services.
+     * @hide
+     */
+    public void notifyCarrierRoamingNtnAvailableServicesChanged(
+            int subId, @NetworkRegistrationInfo.ServiceType int[] availableServices) {
+        try {
+            sRegistry.notifyCarrierRoamingNtnAvailableServicesChanged(subId, availableServices);
+        } catch (RemoteException ex) {
+            // system server crash
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Processes potential event changes from the provided {@link TelephonyCallback}.
      *
      * @param telephonyCallback callback for monitoring callback changes to the telephony state.
@@ -1272,12 +1287,9 @@
 
         if (telephonyCallback instanceof TelephonyCallback.CarrierRoamingNtnModeListener) {
             eventList.add(TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_MODE_CHANGED);
-        }
-
-        if (telephonyCallback instanceof TelephonyCallback.CarrierRoamingNtnModeListener) {
             eventList.add(TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_ELIGIBLE_STATE_CHANGED);
+            eventList.add(TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_AVAILABLE_SERVICES_CHANGED);
         }
-
         return eventList;
     }
 
diff --git a/core/java/android/view/Surface.java b/core/java/android/view/Surface.java
index 396be7b..03f9d98 100644
--- a/core/java/android/view/Surface.java
+++ b/core/java/android/view/Surface.java
@@ -18,9 +18,11 @@
 
 import static android.system.OsConstants.EINVAL;
 
+import android.annotation.FlaggedApi;
 import android.annotation.FloatRange;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.SuppressLint;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.pm.ActivityInfo;
 import android.content.res.CompatibilityInfo.Translator;
@@ -1026,6 +1028,211 @@
     }
 
     /**
+     * Parameter object for {@link #setFrameRate(FrameRateParams)}, describing the intended frame
+     * rate for the Surface that setFrameRate is called on.
+     */
+    @FlaggedApi(com.android.graphics.surfaceflinger.flags.Flags.FLAG_ARR_SETFRAMERATE_API)
+    public static class FrameRateParams {
+        private FrameRateParams() {}
+
+        /**
+         * A static FrameRateParams that can be passed directly into {@link
+         * #setFrameRate(FrameRateParams)} to indicate the surface has no preference and any frame
+         * rate is acceptable.
+         */
+        @FlaggedApi(com.android.graphics.surfaceflinger.flags.Flags.FLAG_ARR_SETFRAMERATE_API)
+        public static final FrameRateParams IGNORE =
+                new FrameRateParams.Builder().setDesiredRateRange(0f, Float.MAX_VALUE).build();
+
+        @FlaggedApi(com.android.graphics.surfaceflinger.flags.Flags.FLAG_ARR_SETFRAMERATE_API)
+        public static final class Builder {
+            private float mDesiredMinRate;
+            private float mDesiredMaxRate;
+            private float mFixedSourceRate;
+            private int mChangeFrameRateStrategy;
+
+            /**
+             * Sets the desired frame rate range (inclusive) values for the surface, specifying that
+             * the surface prefers the device render rate to be in the range [desiredMinRate,
+             * desiredMaxRate].
+
+             * Set desiredMaxRate to FLOAT.MAX_VALUE to indicate the surface prefers any value
+             * greater than or equal to desiredMinRate.
+             *
+             * Set desiredMinRate = desiredMaxRate to indicate the surface prefers an exact frame
+             * rate. Note that this is different than specifying the fixed source frame rate with
+             * {@link FrameRateParams.Builder#setFixedSourceRate}. To reiterate, this call is used
+             * to specify the surface's frame rate preference to be within the desired range.
+             *
+             * desiredMaxRate must be greater than or equal to desiredMinRate.
+             * The values should be greater than or equal to 0.
+             *
+             * If the surface has no preference and any frame rate is acceptable, use the constant
+             * {@link FrameRateParams.IGNORE} in {@link #setFrameRate(FrameRateParams)} instead of
+             * building {@link FrameRateParams.Builder}.
+             *
+             * @see FrameRateParams#getDesiredMinRate()
+             * @see FrameRateParams#getDesiredMaxRate()
+             */
+            @SuppressLint("MissingGetterMatchingBuilder")
+            @NonNull
+            @FlaggedApi(com.android.graphics.surfaceflinger.flags.Flags.FLAG_ARR_SETFRAMERATE_API)
+            public Builder setDesiredRateRange(@FloatRange(from = 0.0) float desiredMinRate,
+                    @FloatRange(from = 0.0) float desiredMaxRate) {
+                if (desiredMaxRate < desiredMinRate) {
+                    Log.e(TAG,
+                            "Failed to set desired frame rate range. desiredMaxRate should be "
+                                    + "greater than or equal to desiredMinRate");
+                    return this;
+                }
+                mDesiredMinRate = desiredMinRate;
+                mDesiredMaxRate = desiredMaxRate;
+                return this;
+            }
+
+            /**
+             * Sets the fixed frame rate of the surface when its content has a fixed frame rate,
+             * e.g. a video with a fixed frame rate.
+             *
+             * When the frame rate chosen for the surface is the {@code fixedSourceRate} or a
+             * multiple, the surface can render without frame pulldown, for optimal smoothness. For
+             * example, a 30 fps video ({@code fixedSourceRate=30}) renders just as well on 30 fps,
+             * 60 fps, 90 fps, 120 fps, and so on.
+             *
+             * This method to set the fixed source rate can also be used together with a desired
+             * frame rate range via {@link FrameRateParams.Builder#setDesiredRateRange}. This still
+             * means the surface's content has a fixed frame rate of the provided {@code
+             * fixedSourceRate}, as well as it preferring to be within the desired frame rate range.
+             * For example, a 30 fps video {@code fixedSourceRate=30} and desired frame rate range
+             * [60,90] means the surface ideally prefers 60 fps (which is 30 fps * 2) or 90 fps (30
+             * fps * 3).
+             *
+             * @see FrameRateParams#getFixedSourceRate()
+             */
+            @NonNull
+            @FlaggedApi(com.android.graphics.surfaceflinger.flags.Flags.FLAG_ARR_SETFRAMERATE_API)
+            public Builder setFixedSourceRate(@FloatRange(from = 0.0) float fixedSourceRate) {
+                mFixedSourceRate = fixedSourceRate;
+                return this;
+            }
+
+            /**
+             * Whether display refresh rate transitions caused by this surface should be seamless. A
+             * seamless transition is one that doesn't have any visual interruptions, such as a
+             * black screen for a second or two. Value is
+             * Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS, or Surface.CHANGE_FRAME_RATE_ALWAYS
+             *
+             * @see FrameRateParams#getChangeFrameRateStrategy()
+             */
+            @NonNull
+            @FlaggedApi(com.android.graphics.surfaceflinger.flags.Flags.FLAG_ARR_SETFRAMERATE_API)
+            public Builder setChangeFrameRateStrategy(
+                    @ChangeFrameRateStrategy int changeFrameRateStrategy) {
+                mChangeFrameRateStrategy = changeFrameRateStrategy;
+                return this;
+            }
+
+            /**
+             * Builds the FrameRateParams object.
+             */
+            @NonNull
+            @FlaggedApi(com.android.graphics.surfaceflinger.flags.Flags.FLAG_ARR_SETFRAMERATE_API)
+            public FrameRateParams build() {
+                FrameRateParams frameRate = new FrameRateParams();
+                frameRate.mDesiredMinRate = this.mDesiredMinRate;
+                frameRate.mDesiredMaxRate = this.mDesiredMaxRate;
+                frameRate.mFixedSourceRate = this.mFixedSourceRate;
+                frameRate.mChangeFrameRateStrategy = this.mChangeFrameRateStrategy;
+                return frameRate;
+            }
+        }
+
+        /**
+         * Gets the minimum desired frame rate.
+         * @see FrameRateParams.Builder#setDesiredRateRange()
+         */
+        @FlaggedApi(com.android.graphics.surfaceflinger.flags.Flags.FLAG_ARR_SETFRAMERATE_API)
+        public float getDesiredMinRate() {
+            return mDesiredMinRate;
+        }
+
+        /**
+         * Gets the maximum desired frame rate.
+         * @see FrameRateParams.Builder#setDesiredRateRange()
+         */
+        @FlaggedApi(com.android.graphics.surfaceflinger.flags.Flags.FLAG_ARR_SETFRAMERATE_API)
+        public float getDesiredMaxRate() {
+            return mDesiredMaxRate;
+        }
+
+        /**
+         * Gets the fixed source frame rate.
+         * @see FrameRateParams.Builder#setFixedSourceRate()
+         */
+        @FlaggedApi(com.android.graphics.surfaceflinger.flags.Flags.FLAG_ARR_SETFRAMERATE_API)
+        public float getFixedSourceRate() {
+            return mFixedSourceRate;
+        }
+
+        /**
+         * Gets the strategy when changing frame rate.
+         * @see FrameRateParams.Builder#setChangeFrameRateStrategy
+         */
+        @FlaggedApi(com.android.graphics.surfaceflinger.flags.Flags.FLAG_ARR_SETFRAMERATE_API)
+        @ChangeFrameRateStrategy
+        public int getChangeFrameRateStrategy() {
+            return mChangeFrameRateStrategy;
+        }
+
+        float mDesiredMinRate;
+        float mDesiredMaxRate;
+        float mFixedSourceRate;
+        int mChangeFrameRateStrategy;
+    }
+
+    /**
+     * Sets the intended frame rate for this surface.
+     *
+     * <p>On devices that are capable of running the display at different frame rates,
+     * the system may choose a display refresh rate to better match this surface's frame
+     * rate. Usage of this API won't introduce frame rate throttling, or affect other
+     * aspects of the application's frame production pipeline. However, because the system
+     * may change the display refresh rate, calls to this function may result in changes
+     * to Choreographer callback timings, and changes to the time interval at which the
+     * system releases buffers back to the application.</p>
+     *
+     * <p>Note that this only has an effect for surfaces presented on the display. If this
+     * surface is consumed by something other than the system compositor, e.g. a media
+     * codec, this call has no effect.</p>
+     *
+     * @param frameRateParams The parameters describing the intended frame rate of this surface.
+     *         Refer to {@link FrameRateParams} for details.
+     * @throws IllegalArgumentException If <code>frameRateParams</code> is invalid.
+     * @see #clearFrameRate()
+     */
+    @FlaggedApi(com.android.graphics.surfaceflinger.flags.Flags.FLAG_ARR_SETFRAMERATE_API)
+    public void setFrameRate(@NonNull FrameRateParams frameRateParams) {
+        synchronized (mLock) {
+            checkNotReleasedLocked();
+            // TODO(b/362798998): Logic currently incomplete: it uses fixed source rate over the
+            // desired min/max rates. Fix when plumbing is upgraded.
+            int compatibility = frameRateParams.getFixedSourceRate() == 0
+                    ? FRAME_RATE_COMPATIBILITY_DEFAULT
+                    : FRAME_RATE_COMPATIBILITY_FIXED_SOURCE;
+            float frameRate = compatibility == FRAME_RATE_COMPATIBILITY_DEFAULT
+                    ? frameRateParams.getDesiredMinRate()
+                    : frameRateParams.getFixedSourceRate();
+            int error = nativeSetFrameRate(mNativeObject, frameRate, compatibility,
+                    frameRateParams.getChangeFrameRateStrategy());
+            if (error == -EINVAL) {
+                throw new IllegalArgumentException("Invalid argument to Surface.setFrameRate()");
+            } else if (error != 0) {
+                Log.e(TAG, "Failed to set frame rate on Surface. Native error: " + error);
+            }
+        }
+    }
+
+    /**
      * Sets the intended frame rate for this surface.
      *
      * <p>On devices that are capable of running the display at different refresh rates,
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 0bcc9c1..a0d58d5 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -348,3 +348,10 @@
     description: "Enables a switch to change the concequence of dragging a window to the top edge."
     bug: "372614715"
 }
+
+flag {
+    name: "enable_task_resizing_keyboard_shortcuts"
+    namespace: "lse_desktop_experience"
+    description: "Enables keyboard shortcuts for resizing tasks in desktop mode."
+    bug: "335819608"
+}
diff --git a/core/java/com/android/internal/jank/FrameTracker.java b/core/java/com/android/internal/jank/FrameTracker.java
index 6448f10..003393c 100644
--- a/core/java/com/android/internal/jank/FrameTracker.java
+++ b/core/java/com/android/internal/jank/FrameTracker.java
@@ -230,7 +230,7 @@
         mRendererWrapper = mSurfaceOnly ? null : renderer;
         mMetricsWrapper = mSurfaceOnly ? null : metrics;
         mViewRoot = mSurfaceOnly ? null : viewRootWrapper;
-        mObserver = mSurfaceOnly
+        mObserver = mSurfaceOnly || (Flags.useSfFrameDuration() && Flags.ignoreHwuiIsFirstFrame())
                 ? null
                 : new HardwareRendererObserver(this, mMetricsWrapper.getTiming(),
                         mHandler, /* waitForPresentTime= */ false);
@@ -253,6 +253,7 @@
             mSurfaceChangedCallback = new ViewRootImpl.SurfaceChangedCallback() {
                 @Override
                 public void surfaceCreated(SurfaceControl.Transaction t) {
+                    Trace.beginSection("FrameTracker#surfaceCreated");
                     mHandler.runWithScissors(() -> {
                         if (mSurfaceControl == null) {
                             mSurfaceControl = mViewRoot.getSurfaceControl();
@@ -262,6 +263,7 @@
                             }
                         }
                     }, EXECUTOR_TASK_TIMEOUT);
+                    Trace.endSection();
                 }
 
                 @Override
@@ -464,23 +466,28 @@
     @Override
     public void onJankDataAvailable(SurfaceControl.JankData[] jankData) {
         postCallback(() -> {
-            if (mCancelled || mMetricsFinalized) {
-                return;
-            }
+            try {
+                Trace.beginSection("FrameTracker#onJankDataAvailable");
+                if (mCancelled || mMetricsFinalized) {
+                    return;
+                }
 
-            for (SurfaceControl.JankData jankStat : jankData) {
-                if (!isInRange(jankStat.frameVsyncId)) {
-                    continue;
+                for (SurfaceControl.JankData jankStat : jankData) {
+                    if (!isInRange(jankStat.frameVsyncId)) {
+                        continue;
+                    }
+                    JankInfo info = findJankInfo(jankStat.frameVsyncId);
+                    if (info != null) {
+                        info.update(jankStat);
+                    } else {
+                        mJankInfos.put((int) jankStat.frameVsyncId,
+                                JankInfo.createFromSurfaceControlCallback(jankStat));
+                    }
                 }
-                JankInfo info = findJankInfo(jankStat.frameVsyncId);
-                if (info != null) {
-                    info.update(jankStat);
-                } else {
-                    mJankInfos.put((int) jankStat.frameVsyncId,
-                            JankInfo.createFromSurfaceControlCallback(jankStat));
-                }
+                processJankInfos();
+            } finally {
+                Trace.endSection();
             }
-            processJankInfos();
         });
     }
 
@@ -507,29 +514,35 @@
     @Override
     public void onFrameMetricsAvailable(int dropCountSinceLastInvocation) {
         postCallback(() -> {
-            if (mCancelled || mMetricsFinalized) {
-                return;
-            }
+            try {
+                Trace.beginSection("FrameTracker#onFrameMetricsAvailable");
+                if (mCancelled || mMetricsFinalized) {
+                    return;
+                }
 
-            // Since this callback might come a little bit late after the end() call.
-            // We should keep tracking the begin / end timestamp that we can compare with
-            // vsync timestamp to check if the frame is in the duration of the CUJ.
-            long totalDurationNanos = mMetricsWrapper.getMetric(FrameMetrics.TOTAL_DURATION);
-            boolean isFirstFrame = mMetricsWrapper.getMetric(FrameMetrics.FIRST_DRAW_FRAME) == 1;
-            long frameVsyncId =
-                    mMetricsWrapper.getTiming()[FrameMetrics.Index.FRAME_TIMELINE_VSYNC_ID];
+                // Since this callback might come a little bit late after the end() call.
+                // We should keep tracking the begin / end timestamp that we can compare with
+                // vsync timestamp to check if the frame is in the duration of the CUJ.
+                long totalDurationNanos = mMetricsWrapper.getMetric(FrameMetrics.TOTAL_DURATION);
+                boolean isFirstFrame =
+                    mMetricsWrapper.getMetric(FrameMetrics.FIRST_DRAW_FRAME) == 1;
+                long frameVsyncId =
+                        mMetricsWrapper.getTiming()[FrameMetrics.Index.FRAME_TIMELINE_VSYNC_ID];
 
-            if (!isInRange(frameVsyncId)) {
-                return;
+                if (!isInRange(frameVsyncId)) {
+                    return;
+                }
+                JankInfo info = findJankInfo(frameVsyncId);
+                if (info != null) {
+                    info.update(totalDurationNanos, isFirstFrame);
+                } else {
+                    mJankInfos.put((int) frameVsyncId, JankInfo.createFromHwuiCallback(
+                            frameVsyncId, totalDurationNanos, isFirstFrame));
+                }
+                processJankInfos();
+            } finally {
+                Trace.endSection();
             }
-            JankInfo info = findJankInfo(frameVsyncId);
-            if (info != null) {
-                info.update(totalDurationNanos, isFirstFrame);
-            } else {
-                mJankInfos.put((int) frameVsyncId, JankInfo.createFromHwuiCallback(
-                        frameVsyncId, totalDurationNanos, isFirstFrame));
-            }
-            processJankInfos();
         });
     }
 
@@ -568,13 +581,20 @@
     }
 
     private boolean callbacksReceived(JankInfo info) {
-        return mSurfaceOnly
+        return mObserver == null
                 ? info.surfaceControlCallbackFired
                 : info.hwuiCallbackFired && info.surfaceControlCallbackFired;
     }
 
     @UiThread
     private void finish() {
+        Trace.beginSection("FrameTracker#finish");
+        finishTraced();
+        Trace.endSection();
+    }
+
+    @UiThread
+    private void finishTraced() {
         if (mMetricsFinalized || mCancelled) return;
         mMetricsFinalized = true;
 
@@ -599,7 +619,7 @@
         for (int i = 0; i < mJankInfos.size(); i++) {
             JankInfo info = mJankInfos.valueAt(i);
             final boolean isFirstDrawn = !mSurfaceOnly && info.isFirstFrame;
-            if (isFirstDrawn) {
+            if (isFirstDrawn && !Flags.ignoreHwuiIsFirstFrame()) {
                 continue;
             }
             if (info.frameVsyncId > mEndVsyncId) {
@@ -636,7 +656,7 @@
                 }
                 // TODO (b/174755489): Early latch currently gets fired way too often, so we have
                 // to ignore it for now.
-                if (!mSurfaceOnly && !info.hwuiCallbackFired) {
+                if (mObserver != null && !info.hwuiCallbackFired) {
                     markEvent("FT#MissedHWUICallback", info.frameVsyncId);
                     Log.w(TAG, "Missing HWUI jank callback for vsyncId: " + info.frameVsyncId
                             + ", CUJ=" + name);
@@ -762,7 +782,9 @@
          * @param observer observer
          */
         public void addObserver(HardwareRendererObserver observer) {
-            mRenderer.addObserver(observer);
+            if (observer != null) {
+                mRenderer.addObserver(observer);
+            }
         }
 
         /**
@@ -770,7 +792,9 @@
          * @param observer observer
          */
         public void removeObserver(HardwareRendererObserver observer) {
-            mRenderer.removeObserver(observer);
+            if (observer != null) {
+                mRenderer.removeObserver(observer);
+            }
         }
     }
 
diff --git a/core/java/com/android/internal/jank/flags.aconfig b/core/java/com/android/internal/jank/flags.aconfig
index 82f50ae..287ad18 100644
--- a/core/java/com/android/internal/jank/flags.aconfig
+++ b/core/java/com/android/internal/jank/flags.aconfig
@@ -8,3 +8,10 @@
   bug: "354763298"
   is_fixed_read_only: true
 }
+flag {
+  name: "ignore_hwui_is_first_frame"
+  namespace: "window_surfaces"
+  description: "Whether to remove the usage of the HWUI 'is first frame' flag to ignore jank"
+  bug: "354763298"
+  is_fixed_read_only: true
+}
diff --git a/core/java/com/android/internal/protolog/AutoClosableProtoInputStream.java b/core/java/com/android/internal/protolog/AutoClosableProtoInputStream.java
new file mode 100644
index 0000000..1acb34f
--- /dev/null
+++ b/core/java/com/android/internal/protolog/AutoClosableProtoInputStream.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.protolog;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.proto.ProtoInputStream;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+
+public final class AutoClosableProtoInputStream implements AutoCloseable {
+    @NonNull
+    private final ProtoInputStream mProtoInputStream;
+    @Nullable
+    private final FileInputStream mFileInputStream;
+
+    public AutoClosableProtoInputStream(@NonNull FileInputStream fileInputStream) {
+        mProtoInputStream = new ProtoInputStream(fileInputStream);
+        mFileInputStream = fileInputStream;
+    }
+
+    public AutoClosableProtoInputStream(@NonNull byte[] input) {
+        mProtoInputStream = new ProtoInputStream(input);
+        mFileInputStream = null;
+    }
+
+    /**
+     * @return the ProtoInputStream this class is wrapping
+     */
+    @NonNull
+    public ProtoInputStream get() {
+        return mProtoInputStream;
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (mFileInputStream != null) {
+            mFileInputStream.close();
+        }
+    }
+}
diff --git a/core/java/com/android/internal/protolog/NoViewerConfigProtoLogImpl.java b/core/java/com/android/internal/protolog/NoViewerConfigProtoLogImpl.java
new file mode 100644
index 0000000..15987664
--- /dev/null
+++ b/core/java/com/android/internal/protolog/NoViewerConfigProtoLogImpl.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.protolog;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.protolog.common.ILogger;
+import com.android.internal.protolog.common.IProtoLog;
+import com.android.internal.protolog.common.IProtoLogGroup;
+import com.android.internal.protolog.common.LogLevel;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Class should only be used as a temporary solution to missing viewer config file on device.
+ * In particular this class should only be initialized in Robolectric tests, if it's being used
+ * otherwise please report it.
+ *
+ * @deprecated
+ */
+@Deprecated
+public class NoViewerConfigProtoLogImpl implements IProtoLog {
+    private static final String LOG_TAG = "ProtoLog";
+
+    @Override
+    public void log(LogLevel logLevel, IProtoLogGroup group, long messageHash, int paramsMask,
+            Object[] args) {
+        Log.w(LOG_TAG, "ProtoLogging is not available due to missing viewer config file...");
+        logMessage(logLevel, group.getTag(), "PROTOLOG#" + messageHash + "("
+                + Arrays.stream(args).map(Object::toString).collect(Collectors.joining()) + ")");
+    }
+
+    @Override
+    public void log(LogLevel logLevel, IProtoLogGroup group, String messageString, Object... args) {
+        logMessage(logLevel, group.getTag(), TextUtils.formatSimple(messageString, args));
+    }
+
+    @Override
+    public boolean isProtoEnabled() {
+        return false;
+    }
+
+    @Override
+    public int startLoggingToLogcat(String[] groups, ILogger logger) {
+        return 0;
+    }
+
+    @Override
+    public int stopLoggingToLogcat(String[] groups, ILogger logger) {
+        return 0;
+    }
+
+    @Override
+    public boolean isEnabled(IProtoLogGroup group, LogLevel level) {
+        return false;
+    }
+
+    @Override
+    public List<IProtoLogGroup> getRegisteredGroups() {
+        return List.of();
+    }
+
+    private void logMessage(LogLevel logLevel, String tag, String message) {
+        switch (logLevel) {
+            case VERBOSE -> Log.v(tag, message);
+            case INFO -> Log.i(tag, message);
+            case DEBUG -> Log.d(tag, message);
+            case WARN -> Log.w(tag, message);
+            case ERROR -> Log.e(tag, message);
+            case WTF -> Log.wtf(tag, message);
+        }
+    }
+}
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index a037cb4..a1c987f 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -60,18 +60,16 @@
 import android.util.Log;
 import android.util.LongArray;
 import android.util.Slog;
-import android.util.proto.ProtoInputStream;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.ProtoLogConfigurationServiceImpl.RegisterClientArgs;
 import com.android.internal.protolog.common.ILogger;
 import com.android.internal.protolog.common.IProtoLog;
 import com.android.internal.protolog.common.IProtoLogGroup;
 import com.android.internal.protolog.common.LogDataType;
 import com.android.internal.protolog.common.LogLevel;
 
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.ArrayList;
@@ -93,26 +91,18 @@
 /**
  * A service for the ProtoLog logging system.
  */
-public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProtoLog {
+public abstract class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProtoLog {
     private static final String LOG_TAG = "ProtoLog";
     public static final String NULL_STRING = "null";
     private final AtomicInteger mTracingInstances = new AtomicInteger();
 
     @NonNull
-    private final ProtoLogDataSource mDataSource;
-    @Nullable
-    private final ProtoLogViewerConfigReader mViewerConfigReader;
-    @Deprecated
-    @Nullable
-    private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider;
+    protected final ProtoLogDataSource mDataSource;
     @NonNull
-    private final TreeMap<String, IProtoLogGroup> mLogGroups = new TreeMap<>();
+    protected final TreeMap<String, IProtoLogGroup> mLogGroups = new TreeMap<>();
     @NonNull
     private final Runnable mCacheUpdater;
 
-    @Nullable // null when the flag android.tracing.client_side_proto_logging is not flipped
-    private final IProtoLogConfigurationService mProtoLogConfigurationService;
-
     @NonNull
     private final int[] mDefaultLogLevelCounts = new int[LogLevel.values().length];
     @NonNull
@@ -121,68 +111,15 @@
     private final Map<String, Integer> mCollectStackTraceGroupCounts = new ArrayMap<>();
 
     private final Lock mBackgroundServiceLock = new ReentrantLock();
-    private ExecutorService mBackgroundLoggingService = Executors.newSingleThreadExecutor();
+    protected ExecutorService mBackgroundLoggingService = Executors.newSingleThreadExecutor();
 
-    public PerfettoProtoLogImpl(@NonNull IProtoLogGroup[] groups)
-            throws ServiceManager.ServiceNotFoundException {
-        this(null, null, null, () -> {}, groups);
-    }
+    // Set to true once this is ready to accept protolog to logcat requests.
+    private boolean mLogcatReady = false;
 
-    public PerfettoProtoLogImpl(@NonNull Runnable cacheUpdater, @NonNull IProtoLogGroup[] groups)
-            throws ServiceManager.ServiceNotFoundException {
-        this(null, null, null, cacheUpdater, groups);
-    }
-
-    public PerfettoProtoLogImpl(
-            @NonNull String viewerConfigFilePath,
+    protected PerfettoProtoLogImpl(
             @NonNull Runnable cacheUpdater,
             @NonNull IProtoLogGroup[] groups) throws ServiceManager.ServiceNotFoundException {
-        this(viewerConfigFilePath,
-                null,
-                new ProtoLogViewerConfigReader(() -> {
-                    try {
-                        return new ProtoInputStream(new FileInputStream(viewerConfigFilePath));
-                    } catch (FileNotFoundException e) {
-                        throw new RuntimeException(
-                                "Failed to load viewer config file " + viewerConfigFilePath, e);
-                    }
-                }),
-                cacheUpdater, groups);
-    }
-
-    @Deprecated
-    @VisibleForTesting
-    public PerfettoProtoLogImpl(
-            @Nullable ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
-            @Nullable ProtoLogViewerConfigReader viewerConfigReader,
-            @NonNull Runnable cacheUpdater,
-            @NonNull IProtoLogGroup[] groups,
-            @NonNull ProtoLogDataSourceBuilder dataSourceBuilder,
-            @NonNull ProtoLogConfigurationService configurationService) {
-        this(null, viewerConfigInputStreamProvider, viewerConfigReader, cacheUpdater,
-                groups, dataSourceBuilder, configurationService);
-    }
-
-    @VisibleForTesting
-    public PerfettoProtoLogImpl(
-            @Nullable String viewerConfigFilePath,
-            @Nullable ProtoLogViewerConfigReader viewerConfigReader,
-            @NonNull Runnable cacheUpdater,
-            @NonNull IProtoLogGroup[] groups,
-            @NonNull ProtoLogDataSourceBuilder dataSourceBuilder,
-            @NonNull ProtoLogConfigurationService configurationService) {
-        this(viewerConfigFilePath, null, viewerConfigReader, cacheUpdater,
-                groups, dataSourceBuilder, configurationService);
-    }
-
-    private PerfettoProtoLogImpl(
-            @Nullable String viewerConfigFilePath,
-            @Nullable ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
-            @Nullable ProtoLogViewerConfigReader viewerConfigReader,
-            @NonNull Runnable cacheUpdater,
-            @NonNull IProtoLogGroup[] groups) throws ServiceManager.ServiceNotFoundException {
-        this(viewerConfigFilePath, viewerConfigInputStreamProvider, viewerConfigReader,
-                cacheUpdater, groups,
+        this(cacheUpdater, groups,
                 ProtoLogDataSource::new,
                 android.tracing.Flags.clientSideProtoLogging() ?
                     IProtoLogConfigurationService.Stub.asInterface(
@@ -191,19 +128,11 @@
         );
     }
 
-    private PerfettoProtoLogImpl(
-            @Nullable String viewerConfigFilePath,
-            @Nullable ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
-            @Nullable ProtoLogViewerConfigReader viewerConfigReader,
+    protected PerfettoProtoLogImpl(
             @NonNull Runnable cacheUpdater,
             @NonNull IProtoLogGroup[] groups,
             @NonNull ProtoLogDataSourceBuilder dataSourceBuilder,
             @Nullable IProtoLogConfigurationService configurationService) {
-        if (viewerConfigFilePath != null && viewerConfigInputStreamProvider != null) {
-            throw new RuntimeException("Only one of viewerConfigFilePath and "
-                    + "viewerConfigInputStreamProvider can be set");
-        }
-
         mDataSource = dataSourceBuilder.build(
                 this::onTracingInstanceStart,
                 this::onTracingFlush,
@@ -219,55 +148,33 @@
         // for some messages logged right after the construction of this class.
         mDataSource.register(params);
 
-        if (viewerConfigInputStreamProvider == null && viewerConfigFilePath != null) {
-            viewerConfigInputStreamProvider = new ViewerConfigInputStreamProvider() {
-                @NonNull
-                @Override
-                public ProtoInputStream getInputStream() {
-                    try {
-                        return new ProtoInputStream(new FileInputStream(viewerConfigFilePath));
-                    } catch (FileNotFoundException e) {
-                        throw new RuntimeException(
-                                "Failed to load viewer config file " + viewerConfigFilePath, e);
-                    }
-                }
-            };
-        }
-
-        this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider;
-        this.mViewerConfigReader = viewerConfigReader;
         this.mCacheUpdater = cacheUpdater;
 
         registerGroupsLocally(groups);
 
         if (android.tracing.Flags.clientSideProtoLogging()) {
-            mProtoLogConfigurationService = configurationService;
-            Objects.requireNonNull(mProtoLogConfigurationService,
+            Objects.requireNonNull(configurationService,
                     "A null ProtoLog Configuration Service was provided!");
 
             try {
-                var args = new ProtoLogConfigurationServiceImpl.RegisterClientArgs();
-
-                if (viewerConfigFilePath != null) {
-                    args.setViewerConfigFile(viewerConfigFilePath);
-                }
+                var args = createConfigurationServiceRegisterClientArgs();
 
                 final var groupArgs = Stream.of(groups)
-                        .map(group -> new ProtoLogConfigurationServiceImpl.RegisterClientArgs
+                        .map(group -> new RegisterClientArgs
                                 .GroupConfig(group.name(), group.isLogToLogcat()))
-                        .toArray(ProtoLogConfigurationServiceImpl
-                                .RegisterClientArgs.GroupConfig[]::new);
+                        .toArray(RegisterClientArgs.GroupConfig[]::new);
                 args.setGroups(groupArgs);
 
-                mProtoLogConfigurationService.registerClient(this, args);
+                configurationService.registerClient(this, args);
             } catch (RemoteException e) {
                 throw new RuntimeException("Failed to register ProtoLog client");
             }
-        } else {
-            mProtoLogConfigurationService = null;
         }
     }
 
+    @NonNull
+    protected abstract RegisterClientArgs createConfigurationServiceRegisterClientArgs();
+
     /**
      * Main log method, do not call directly.
      */
@@ -334,9 +241,6 @@
      * @return status code
      */
     public int startLoggingToLogcat(String[] groups, @NonNull ILogger logger) {
-        if (mViewerConfigReader != null) {
-            mViewerConfigReader.loadViewerConfig(groups, logger);
-        }
         return setTextLogging(true, logger, groups);
     }
 
@@ -347,9 +251,6 @@
      * @return status code
      */
     public int stopLoggingToLogcat(String[] groups, @NonNull ILogger logger) {
-        if (mViewerConfigReader != null) {
-            mViewerConfigReader.unloadViewerConfig(groups, logger);
-        }
         return setTextLogging(false, logger, groups);
     }
 
@@ -372,21 +273,8 @@
         // we might want to manually specify an id for the group with a collision
         verifyNoCollisionsOrDuplicates(protoLogGroups);
 
-        final var groupsLoggingToLogcat = new ArrayList<String>();
         for (IProtoLogGroup protoLogGroup : protoLogGroups) {
             mLogGroups.put(protoLogGroup.name(), protoLogGroup);
-
-            if (protoLogGroup.isLogToLogcat()) {
-                groupsLoggingToLogcat.add(protoLogGroup.name());
-            }
-        }
-
-        if (mViewerConfigReader != null) {
-            // Load in background to avoid delay in boot process.
-            // The caveat is that any log message that is also logged to logcat will not be
-            // successfully decoded until this completes.
-            mBackgroundLoggingService.execute(() -> mViewerConfigReader
-                    .loadViewerConfig(groupsLoggingToLogcat.toArray(new String[0])));
         }
     }
 
@@ -403,6 +291,10 @@
         }
     }
 
+    protected void readyToLogToLogcat() {
+        mLogcatReady = true;
+    }
+
     /**
      * Responds to a shell command.
      */
@@ -499,57 +391,21 @@
     }
 
     @Deprecated
-    private void dumpViewerConfig() {
-        if (mViewerConfigInputStreamProvider == null) {
-            // No viewer config available
+    abstract void dumpViewerConfig();
+
+    @NonNull
+    abstract String getLogcatMessageString(@NonNull Message message);
+
+    private void logToLogcat(@NonNull String tag, @NonNull LogLevel level, @NonNull Message message,
+            @Nullable Object[] args) {
+        if (!mLogcatReady) {
+            Log.w(LOG_TAG, "Trying to log a protolog message with hash "
+                    + message.getMessageHash() + " to logcat before the service is ready to accept "
+                    + "such requests.");
             return;
         }
 
-        Log.d(LOG_TAG, "Dumping viewer config to trace");
-
-        Utils.dumpViewerConfig(mDataSource, mViewerConfigInputStreamProvider);
-
-        Log.d(LOG_TAG, "Dumped viewer config to trace");
-    }
-
-    private void logToLogcat(String tag, LogLevel level, Message message,
-            @Nullable Object[] args) {
-        String messageString;
-        if (mViewerConfigReader == null) {
-            messageString = message.getMessage();
-
-            if (messageString == null) {
-                Log.e(LOG_TAG, "Failed to decode message for logcat. "
-                        + "Message not available without ViewerConfig to decode the hash.");
-            }
-        } else {
-            messageString = message.getMessage(mViewerConfigReader);
-
-            if (messageString == null) {
-                Log.e(LOG_TAG, "Failed to decode message for logcat. "
-                        + "Message hash either not available in viewerConfig file or "
-                        + "not loaded into memory from file before decoding.");
-            }
-        }
-
-        if (messageString == null) {
-            StringBuilder builder = new StringBuilder("UNKNOWN MESSAGE");
-            if (args != null) {
-                builder.append(" args = (");
-                builder.append(String.join(", ", Arrays.stream(args)
-                        .map(it -> {
-                            if (it == null) {
-                                return "null";
-                            } else {
-                                return it.toString();
-                            }
-                        }).toList()));
-                builder.append(")");
-            }
-            messageString = builder.toString();
-            args = new Object[0];
-        }
-
+        String messageString = getLogcatMessageString(message);
         logToLogcat(tag, level, messageString, args);
     }
 
diff --git a/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java
new file mode 100644
index 0000000..febe1f3
--- /dev/null
+++ b/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.protolog;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.ProtoLogConfigurationServiceImpl.RegisterClientArgs;
+import com.android.internal.protolog.common.ILogger;
+import com.android.internal.protolog.common.IProtoLogGroup;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+
+public class ProcessedPerfettoProtoLogImpl extends PerfettoProtoLogImpl {
+    private static final String LOG_TAG = "PerfettoProtoLogImpl";
+
+    @NonNull
+    private final ProtoLogViewerConfigReader mViewerConfigReader;
+    @Deprecated
+    @NonNull
+    private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider;
+    @NonNull
+    private final String mViewerConfigFilePath;
+
+    public ProcessedPerfettoProtoLogImpl(
+            @NonNull String viewerConfigFilePath,
+            @NonNull Runnable cacheUpdater,
+            @NonNull IProtoLogGroup[] groups) throws ServiceManager.ServiceNotFoundException {
+        this(viewerConfigFilePath, new ViewerConfigInputStreamProvider() {
+                    @NonNull
+                    @Override
+                    public AutoClosableProtoInputStream getInputStream() {
+                        try {
+                            final var protoFileInputStream =
+                                    new FileInputStream(viewerConfigFilePath);
+                            return new AutoClosableProtoInputStream(protoFileInputStream);
+                        } catch (FileNotFoundException e) {
+                            throw new RuntimeException(
+                                    "Failed to load viewer config file " + viewerConfigFilePath, e);
+                        }
+                    }
+                },
+                cacheUpdater, groups);
+    }
+
+    @VisibleForTesting
+    public ProcessedPerfettoProtoLogImpl(
+            @NonNull String viewerConfigFilePath,
+            @NonNull ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
+            @NonNull Runnable cacheUpdater,
+            @NonNull IProtoLogGroup[] groups) throws ServiceManager.ServiceNotFoundException {
+        super(cacheUpdater, groups);
+
+        this.mViewerConfigFilePath = viewerConfigFilePath;
+
+        this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider;
+        this.mViewerConfigReader = new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider);
+
+        loadLogcatGroupsViewerConfig(groups);
+    }
+
+    @VisibleForTesting
+    public ProcessedPerfettoProtoLogImpl(
+            @NonNull String viewerConfigFilePath,
+            @NonNull ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
+            @NonNull ProtoLogViewerConfigReader viewerConfigReader,
+            @NonNull Runnable cacheUpdater,
+            @NonNull IProtoLogGroup[] groups,
+            @NonNull ProtoLogDataSourceBuilder dataSourceBuilder,
+            @Nullable IProtoLogConfigurationService configurationService)
+            throws ServiceManager.ServiceNotFoundException {
+        super(cacheUpdater, groups, dataSourceBuilder, configurationService);
+
+        this.mViewerConfigFilePath = viewerConfigFilePath;
+
+        this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider;
+        this.mViewerConfigReader = viewerConfigReader;
+
+        loadLogcatGroupsViewerConfig(groups);
+    }
+
+    @NonNull
+    @Override
+    protected RegisterClientArgs createConfigurationServiceRegisterClientArgs() {
+        return new RegisterClientArgs()
+                .setViewerConfigFile(mViewerConfigFilePath);
+    }
+
+    /**
+     * Start text logging
+     * @param groups Groups to start text logging for
+     * @param logger A logger to write status updates to
+     * @return status code
+     */
+    @Override
+    public int startLoggingToLogcat(String[] groups, @NonNull ILogger logger) {
+        mViewerConfigReader.loadViewerConfig(groups, logger);
+        return super.startLoggingToLogcat(groups, logger);
+    }
+
+    /**
+     * Stop text logging
+     * @param groups Groups to start text logging for
+     * @param logger A logger to write status updates to
+     * @return status code
+     */
+    @Override
+    public int stopLoggingToLogcat(String[] groups, @NonNull ILogger logger) {
+        mViewerConfigReader.unloadViewerConfig(groups, logger);
+        return super.stopLoggingToLogcat(groups, logger);
+    }
+
+    @Deprecated
+    @Override
+    void dumpViewerConfig() {
+        Log.d(LOG_TAG, "Dumping viewer config to trace");
+        Utils.dumpViewerConfig(mDataSource, mViewerConfigInputStreamProvider);
+        Log.d(LOG_TAG, "Dumped viewer config to trace");
+    }
+
+    @NonNull
+    @Override
+    String getLogcatMessageString(@NonNull Message message) {
+        String messageString;
+        messageString = message.getMessage(mViewerConfigReader);
+
+        if (messageString == null) {
+            throw new RuntimeException("Failed to decode message for logcat. "
+                    + "Message hash (" + message.getMessageHash() + ") either not available in "
+                    + "viewerConfig file (" +  mViewerConfigFilePath + ") or "
+                    + "not loaded into memory from file before decoding.");
+        }
+
+        return messageString;
+    }
+
+    private void loadLogcatGroupsViewerConfig(@NonNull IProtoLogGroup[] protoLogGroups) {
+        final var groupsLoggingToLogcat = new ArrayList<String>();
+        for (IProtoLogGroup protoLogGroup : protoLogGroups) {
+            if (protoLogGroup.isLogToLogcat()) {
+                groupsLoggingToLogcat.add(protoLogGroup.name());
+            }
+        }
+
+        // Load in background to avoid delay in boot process.
+        // The caveat is that any log message that is also logged to logcat will not be
+        // successfully decoded until this completes.
+        mBackgroundLoggingService.execute(() -> {
+            mViewerConfigReader.loadViewerConfig(groupsLoggingToLogcat.toArray(new String[0]));
+            readyToLogToLogcat();
+        });
+    }
+}
diff --git a/core/java/com/android/internal/protolog/ProtoLog.java b/core/java/com/android/internal/protolog/ProtoLog.java
index 60213b1..d117e93 100644
--- a/core/java/com/android/internal/protolog/ProtoLog.java
+++ b/core/java/com/android/internal/protolog/ProtoLog.java
@@ -70,16 +70,16 @@
         // directly to the generated tracing implementations.
         if (android.tracing.Flags.perfettoProtologTracing()) {
             synchronized (sInitLock) {
+                final var allGroups = new HashSet<>(Arrays.stream(groups).toList());
                 if (sProtoLogInstance != null) {
                     // The ProtoLog instance has already been initialized in this process
                     final var alreadyRegisteredGroups = sProtoLogInstance.getRegisteredGroups();
-                    final var allGroups = new HashSet<>(alreadyRegisteredGroups);
-                    allGroups.addAll(Arrays.stream(groups).toList());
-                    groups = allGroups.toArray(new IProtoLogGroup[0]);
+                    allGroups.addAll(alreadyRegisteredGroups);
                 }
 
                 try {
-                    sProtoLogInstance = new PerfettoProtoLogImpl(groups);
+                    sProtoLogInstance = new UnprocessedPerfettoProtoLogImpl(
+                            allGroups.toArray(new IProtoLogGroup[0]));
                 } catch (ServiceManager.ServiceNotFoundException e) {
                     throw new RuntimeException(e);
                 }
diff --git a/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java b/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java
index 8d37899..e9a8770 100644
--- a/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java
+++ b/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java
@@ -379,7 +379,7 @@
             @NonNull String viewerConfigFilePath) {
         Utils.dumpViewerConfig(dataSource, () -> {
             try {
-                return new ProtoInputStream(new FileInputStream(viewerConfigFilePath));
+                return new AutoClosableProtoInputStream(new FileInputStream(viewerConfigFilePath));
             } catch (FileNotFoundException e) {
                 throw new RuntimeException(
                         "Failed to load viewer config file " + viewerConfigFilePath, e);
diff --git a/core/java/com/android/internal/protolog/ProtoLogImpl.java b/core/java/com/android/internal/protolog/ProtoLogImpl.java
index 5d67534..3378d08 100644
--- a/core/java/com/android/internal/protolog/ProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/ProtoLogImpl.java
@@ -105,31 +105,10 @@
                     + "viewerConfigPath = " + sViewerConfigPath);
 
             final var groups = sLogGroups.values().toArray(new IProtoLogGroup[0]);
-
             if (android.tracing.Flags.perfettoProtologTracing()) {
-                try {
-                    File f = new File(sViewerConfigPath);
-                    if (!ProtoLog.REQUIRE_PROTOLOGTOOL && !f.exists()) {
-                        // TODO(b/353530422): Remove - temporary fix to unblock b/352290057
-                        // In some tests the viewer config file might not exist in which we don't
-                        // want to provide config path to the user
-                        Log.w(LOG_TAG, "Failed to find viewerConfigFile when setting up "
-                                + ProtoLogImpl.class.getSimpleName() + ". "
-                                + "Setting up without a viewer config instead...");
-
-                        sServiceInstance = new PerfettoProtoLogImpl(sCacheUpdater, groups);
-                    } else {
-                        sServiceInstance =
-                                new PerfettoProtoLogImpl(sViewerConfigPath, sCacheUpdater, groups);
-                    }
-                } catch (ServiceManager.ServiceNotFoundException e) {
-                    throw new RuntimeException(e);
-                }
+                sServiceInstance = createProtoLogImpl(groups);
             } else {
-                var protologImpl = new LegacyProtoLogImpl(
-                        sLegacyOutputFilePath, sLegacyViewerConfigPath, sCacheUpdater);
-                protologImpl.registerGroups(groups);
-                sServiceInstance = protologImpl;
+                sServiceInstance = createLegacyProtoLogImpl(groups);
             }
 
             sCacheUpdater.run();
@@ -137,6 +116,34 @@
         return sServiceInstance;
     }
 
+    private static IProtoLog createProtoLogImpl(IProtoLogGroup[] groups) {
+        try {
+            File f = new File(sViewerConfigPath);
+            if (!f.exists()) {
+                // TODO(b/353530422): Remove - temporary fix to unblock b/352290057
+                // In robolectric tests the viewer config file isn't current available, so we cannot
+                // use the ProcessedPerfettoProtoLogImpl.
+                Log.e(LOG_TAG, "Failed to find viewer config file " + sViewerConfigPath
+                        + " when setting up " + ProtoLogImpl.class.getSimpleName() + ". "
+                        + "ProtoLog will not work here!");
+
+                return new NoViewerConfigProtoLogImpl();
+            } else {
+                return new ProcessedPerfettoProtoLogImpl(sViewerConfigPath, sCacheUpdater, groups);
+            }
+        } catch (ServiceManager.ServiceNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static LegacyProtoLogImpl createLegacyProtoLogImpl(IProtoLogGroup[] groups) {
+        var protologImpl = new LegacyProtoLogImpl(
+                sLegacyOutputFilePath, sLegacyViewerConfigPath, sCacheUpdater);
+        protologImpl.registerGroups(groups);
+
+        return protologImpl;
+    }
+
     @VisibleForTesting
     public static synchronized void setSingleInstance(@Nullable IProtoLog instance) {
         sServiceInstance = instance;
diff --git a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
index 571fe0b..524f642 100644
--- a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
+++ b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
@@ -106,46 +106,47 @@
         long targetGroupId = loadGroupId(group);
 
         final Map<Long, String> hashesForGroup = new TreeMap<>();
-        final ProtoInputStream pis = mViewerConfigInputStreamProvider.getInputStream();
+        try (var pisWrapper = mViewerConfigInputStreamProvider.getInputStream()) {
+            final var pis = pisWrapper.get();
+            while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                if (pis.getFieldNumber() == (int) MESSAGES) {
+                    final long inMessageToken = pis.start(MESSAGES);
 
-        while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
-            if (pis.getFieldNumber() == (int) MESSAGES) {
-                final long inMessageToken = pis.start(MESSAGES);
-
-                long messageId = 0;
-                String message = null;
-                int groupId = 0;
-                while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
-                    switch (pis.getFieldNumber()) {
-                        case (int) MESSAGE_ID:
-                            messageId = pis.readLong(MESSAGE_ID);
-                            break;
-                        case (int) MESSAGE:
-                            message = pis.readString(MESSAGE);
-                            break;
-                        case (int) GROUP_ID:
-                            groupId = pis.readInt(GROUP_ID);
-                            break;
+                    long messageId = 0;
+                    String message = null;
+                    int groupId = 0;
+                    while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                        switch (pis.getFieldNumber()) {
+                            case (int) MESSAGE_ID:
+                                messageId = pis.readLong(MESSAGE_ID);
+                                break;
+                            case (int) MESSAGE:
+                                message = pis.readString(MESSAGE);
+                                break;
+                            case (int) GROUP_ID:
+                                groupId = pis.readInt(GROUP_ID);
+                                break;
+                        }
                     }
-                }
 
-                if (groupId == 0) {
-                    throw new IOException("Failed to get group id");
-                }
+                    if (groupId == 0) {
+                        throw new IOException("Failed to get group id");
+                    }
 
-                if (messageId == 0) {
-                    throw new IOException("Failed to get message id");
-                }
+                    if (messageId == 0) {
+                        throw new IOException("Failed to get message id");
+                    }
 
-                if (message == null) {
-                    throw new IOException("Failed to get message string");
-                }
+                    if (message == null) {
+                        throw new IOException("Failed to get message string");
+                    }
 
-                if (groupId == targetGroupId) {
-                    hashesForGroup.put(messageId, message);
-                }
+                    if (groupId == targetGroupId) {
+                        hashesForGroup.put(messageId, message);
+                    }
 
-                pis.end(inMessageToken);
+                    pis.end(inMessageToken);
+                }
             }
         }
 
@@ -153,30 +154,32 @@
     }
 
     private long loadGroupId(@NonNull String group) throws IOException {
-        final ProtoInputStream pis = mViewerConfigInputStreamProvider.getInputStream();
+        try (var pisWrapper = mViewerConfigInputStreamProvider.getInputStream()) {
+            final var pis = pisWrapper.get();
 
-        while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
-            if (pis.getFieldNumber() == (int) GROUPS) {
-                final long inMessageToken = pis.start(GROUPS);
+            while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                if (pis.getFieldNumber() == (int) GROUPS) {
+                    final long inMessageToken = pis.start(GROUPS);
 
-                long groupId = 0;
-                String groupName = null;
-                while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
-                    switch (pis.getFieldNumber()) {
-                        case (int) ID:
-                            groupId = pis.readInt(ID);
-                            break;
-                        case (int) NAME:
-                            groupName = pis.readString(NAME);
-                            break;
+                    long groupId = 0;
+                    String groupName = null;
+                    while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                        switch (pis.getFieldNumber()) {
+                            case (int) ID:
+                                groupId = pis.readInt(ID);
+                                break;
+                            case (int) NAME:
+                                groupName = pis.readString(NAME);
+                                break;
+                        }
                     }
-                }
 
-                if (Objects.equals(groupName, group)) {
-                    return groupId;
-                }
+                    if (Objects.equals(groupName, group)) {
+                        return groupId;
+                    }
 
-                pis.end(inMessageToken);
+                    pis.end(inMessageToken);
+                }
             }
         }
 
diff --git a/core/java/com/android/internal/protolog/UnprocessedPerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/UnprocessedPerfettoProtoLogImpl.java
new file mode 100644
index 0000000..f3fe580
--- /dev/null
+++ b/core/java/com/android/internal/protolog/UnprocessedPerfettoProtoLogImpl.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.protolog;
+
+import android.annotation.NonNull;
+import android.os.ServiceManager;
+
+import com.android.internal.protolog.ProtoLogConfigurationServiceImpl.RegisterClientArgs;
+import com.android.internal.protolog.common.IProtoLogGroup;
+
+public class UnprocessedPerfettoProtoLogImpl extends PerfettoProtoLogImpl {
+    public UnprocessedPerfettoProtoLogImpl(@NonNull IProtoLogGroup[] groups)
+            throws ServiceManager.ServiceNotFoundException {
+        this(() -> {}, groups);
+    }
+
+    public UnprocessedPerfettoProtoLogImpl(@NonNull Runnable cacheUpdater,
+            @NonNull IProtoLogGroup[] groups) throws ServiceManager.ServiceNotFoundException {
+        super(cacheUpdater, groups);
+        readyToLogToLogcat();
+    }
+
+    @NonNull
+    @Override
+    protected RegisterClientArgs createConfigurationServiceRegisterClientArgs() {
+        return new RegisterClientArgs();
+    }
+
+    @Override
+    void dumpViewerConfig() {
+        // No-op
+    }
+
+    @NonNull
+    @Override
+    String getLogcatMessageString(@NonNull Message message) {
+        String messageString;
+        messageString = message.getMessage();
+
+        if (messageString == null) {
+            throw new RuntimeException("Failed to decode message for logcat. "
+                    + "Message not available without ViewerConfig to decode the hash.");
+        }
+
+        return messageString;
+    }
+}
diff --git a/core/java/com/android/internal/protolog/Utils.java b/core/java/com/android/internal/protolog/Utils.java
index 00ef80a..629682c 100644
--- a/core/java/com/android/internal/protolog/Utils.java
+++ b/core/java/com/android/internal/protolog/Utils.java
@@ -48,8 +48,8 @@
     public static void dumpViewerConfig(@NonNull ProtoLogDataSource dataSource,
             @NonNull ViewerConfigInputStreamProvider viewerConfigInputStreamProvider) {
         dataSource.trace(ctx -> {
-            try {
-                ProtoInputStream pis = viewerConfigInputStreamProvider.getInputStream();
+            try (var pisWrapper = viewerConfigInputStreamProvider.getInputStream()) {
+                final var pis = pisWrapper.get();
 
                 final ProtoOutputStream os = ctx.newTracePacket();
 
diff --git a/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java b/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java
index 14bc8e4..60c9892 100644
--- a/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java
+++ b/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java
@@ -17,12 +17,12 @@
 package com.android.internal.protolog;
 
 import android.annotation.NonNull;
-import android.util.proto.ProtoInputStream;
 
 public interface ViewerConfigInputStreamProvider {
     /**
      * @return a ProtoInputStream.
      */
     @NonNull
-    ProtoInputStream getInputStream();
+    AutoClosableProtoInputStream getInputStream();
 }
+
diff --git a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl
index 81b885a..b5c87868 100644
--- a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl
+++ b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl
@@ -84,4 +84,5 @@
     void onSimultaneousCallingStateChanged(in int[] subIds);
     void onCarrierRoamingNtnModeChanged(in boolean active);
     void onCarrierRoamingNtnEligibleStateChanged(in boolean eligible);
+    void onCarrierRoamingNtnAvailableServicesChanged(in int[] availableServices);
 }
diff --git a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
index f836cf2..ca75abd 100644
--- a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
+++ b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
@@ -123,4 +123,5 @@
     void notifyCallbackModeStopped(int phoneId, int subId, int type, int reason);
     void notifyCarrierRoamingNtnModeChanged(int subId, in boolean active);
     void notifyCarrierRoamingNtnEligibleStateChanged(int subId, in boolean eligible);
+    void notifyCarrierRoamingNtnAvailableServicesChanged(int subId, in int[] availableServices);
 }
diff --git a/core/java/com/android/internal/widget/floatingtoolbar/OWNERS b/core/java/com/android/internal/widget/floatingtoolbar/OWNERS
index ed9425c..999ea0e 100644
--- a/core/java/com/android/internal/widget/floatingtoolbar/OWNERS
+++ b/core/java/com/android/internal/widget/floatingtoolbar/OWNERS
@@ -1 +1 @@
-include /core/java/android/view/selectiontoolbar/OWNERS
+include /core/java/android/permission/OWNERS
diff --git a/core/res/Android.bp b/core/res/Android.bp
index aa324fc..bfa1fb2 100644
--- a/core/res/Android.bp
+++ b/core/res/Android.bp
@@ -169,6 +169,7 @@
         "android.media.tv.flags-aconfig",
         "android.security.flags-aconfig",
         "com.android.hardware.input.input-aconfig",
+        "aconfig_trade_in_mode_flags",
     ],
 }
 
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 6ab6476..fb06e96 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -1494,8 +1494,8 @@
             android:description="@string/permdesc_readBasicPhoneState"
             android:protectionLevel="normal" />
 
-    <!-- Allows read access to the device's phone number(s). This is a subset of the capabilities
-         granted by {@link #READ_PHONE_STATE} but is exposed to instant applications.
+    <!-- Allows read access to the device's phone number(s),
+         which is exposed to instant applications.
          <p>Protection level: dangerous-->
     <permission android:name="android.permission.READ_PHONE_NUMBERS"
         android:permissionGroup="android.permission-group.UNDEFINED"
@@ -8464,6 +8464,14 @@
     <permission android:name="android.permission.SETUP_FSVERITY"
                 android:protectionLevel="signature|privileged"/>
 
+    <!-- Allows app to enter trade-in-mode.
+        <p>Protection level: signature|privileged
+        @hide
+    -->
+    <permission android:name="android.permission.ENTER_TRADE_IN_MODE"
+                android:protectionLevel="signature|privileged"
+                android:featureFlag="com.android.tradeinmode.flags.enable_trade_in_mode" />
+
     <!--
         @TestApi
         Signature permission reserved for testing. This should never be used to
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 42ac90dd..9c92e5c 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -7178,4 +7178,7 @@
 
     <!-- The name of the service for forensic backup transport. -->
     <string name="config_forensicBackupTransport" translatable="false"></string>
+
+    <!-- Whether to enable fp unlock when screen turns off on udfps devices -->
+    <bool name="config_screen_off_udfps_enabled">false</bool>
 </resources>
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index 9854030..b5892f6 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -460,4 +460,16 @@
     <integer name="config_satellite_location_query_throttle_interval_minutes">10</integer>
     <java-symbol type="integer" name="config_satellite_location_query_throttle_interval_minutes" />
 
+    <!-- Boolean indicating whether to enable MT SMS polling for NB IOT NTN. -->
+    <bool name="config_enabled_mt_sms_polling">true</bool>
+    <java-symbol type="bool" name="config_enabled_mt_sms_polling" />
+
+    <!-- Text to be used for MT SMS polling in NB IOT NTN. -->
+    <string name="config_mt_sms_polling_text" translatable="false">DU\\\#MMYSM€S2BIG\\\#NORED\\\!</string>
+    <java-symbol type="string" name="config_mt_sms_polling_text" />
+
+    <!-- The time duration in millis after which Telephony can send another MT SMS polling for NB IOT NTN -->
+    <integer name="config_mt_sms_polling_throttle_millis">300000</integer>
+    <java-symbol type="integer" name="config_mt_sms_polling_throttle_millis" />
+
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index dfee85a..712b994 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -5639,4 +5639,7 @@
 
   <!-- Forensic backup transport -->
   <java-symbol type="string" name="config_forensicBackupTransport" />
+
+  <!-- Fingerprint screen off unlock config -->
+  <java-symbol type="bool" name="config_screen_off_udfps_enabled" />
 </resources>
diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml
index 581dee5..bb5380e 100644
--- a/core/res/res/xml/sms_short_codes.xml
+++ b/core/res/res/xml/sms_short_codes.xml
@@ -34,7 +34,7 @@
          http://smscoin.net/software/engine/WordPress/Paid+SMS-registration/ -->
 
     <!-- Arab Emirates -->
-    <shortcode country="ae" pattern="\\d{1,5}" free="1017|1355|3214|6253" />
+    <shortcode country="ae" pattern="\\d{1,5}" free="1017|1355|3214|6253|6568" />
 
     <!-- Albania: 5 digits, known short codes listed -->
     <shortcode country="al" pattern="\\d{5}" premium="15191|55[56]00" />
@@ -70,7 +70,7 @@
     <shortcode country="bh" pattern="\\d{1,5}" free="81181|85999" />
 
     <!-- Brazil: 1-5 digits (standard system default, not country specific) -->
-    <shortcode country="br" pattern="\\d{1,5}" free="6000[012]\\d|876|5500|9963|4141|8000|2652" />
+    <shortcode country="br" pattern="\\d{1,5}" free="6000[012]\\d|876|5500|9963|4141|8000|2652|26808" />
 
     <!-- Botswana: 1-5 digits (standard system default, not country specific) -->
     <shortcode country="bw" pattern="\\d{1,5}" free="16641" />
@@ -79,7 +79,7 @@
     <shortcode country="by" pattern="\\d{4}" premium="3336|4161|444[4689]|501[34]|7781" />
 
     <!-- Canada: 5-6 digits -->
-    <shortcode country="ca" pattern="\\d{5,6}" premium="60999|88188|43030" standard="244444" free="455677" />
+    <shortcode country="ca" pattern="\\d{5,6}" premium="60999|88188|43030" standard="244444" free="455677|24470" />
 
     <!-- Switzerland: 3-5 digits: http://www.swisscom.ch/fxres/kmu/thirdpartybusiness_code_of_conduct_en.pdf -->
     <shortcode country="ch" pattern="[2-9]\\d{2,4}" premium="543|83111|30118" free="98765|30075|30047" />
@@ -123,8 +123,8 @@
          http://www.tja.ee/public/documents/Elektrooniline_side/Oigusaktid/ENG/Estonian_Numbering_Plan_annex_06_09_2010.mht -->
     <shortcode country="ee" pattern="1\\d{2,4}" premium="90\\d{5}|15330|1701[0-3]" free="116\\d{3}|95034" />
 
-    <!-- Egypt: 4 digits, known codes listed -->
-    <shortcode country="eg" pattern="\\d{4}" free="1499" />
+    <!-- Egypt: 4-5 digits, known codes listed -->
+    <shortcode country="eg" pattern="\\d{4,5}" free="1499|10020" />
 
     <!-- Spain: 5-6 digits: 25xxx, 27xxx, 280xx, 35xxx, 37xxx, 795xxx, 797xxx, 995xxx, 997xxx, plus EU.
          http://www.legallink.es/?q=en/content/which-current-regulatory-status-premium-rate-services-spain -->
@@ -147,7 +147,7 @@
     <shortcode country="ge" pattern="\\d{1,5}" premium="801[234]|888[239]" free="95201|95202|95203" />
 
     <!-- Ghana: 4 digits, known premium codes listed -->
-    <shortcode country="gh" pattern="\\d{4}" free="5041|3777|2333" />
+    <shortcode country="gh" pattern="\\d{4}" free="5041|3777|2333|6061" />
 
     <!-- Greece: 5 digits (54xxx, 19yxx, x=0-9, y=0-5): http://www.cmtelecom.com/premium-sms/greece -->
     <shortcode country="gr" pattern="\\d{5}" premium="54\\d{3}|19[0-5]\\d{2}" free="116\\d{3}|12115" />
@@ -169,7 +169,7 @@
     <shortcode country="in" pattern="\\d{1,5}" free="59336|53969" />
 
     <!-- Indonesia: 1-5 digits (standard system default, not country specific) -->
-    <shortcode country="id" pattern="\\d{1,5}" free="99477|6006|46645|363|93457|99265" />
+    <shortcode country="id" pattern="\\d{1,5}" free="99477|6006|46645|363|93457|99265|77413" />
 
     <!-- Ireland: 5 digits, 5xxxx (50xxx=free, 5[12]xxx=standard), plus EU:
          http://www.comreg.ie/_fileupload/publications/ComReg1117.pdf -->
@@ -226,13 +226,13 @@
     <shortcode country="mn" pattern="\\d{1,6}" free="44444|45678|445566" />
 
     <!-- Malawi: 1-5 digits (standard system default, not country specific) -->
-    <shortcode country="mw" pattern="\\d{1,5}" free="4276" />
+    <shortcode country="mw" pattern="\\d{1,5}" free="4276|4305" />
 
     <!-- Mozambique: 1-5 digits (standard system default, not country specific) -->
     <shortcode country="mz" pattern="\\d{1,5}" free="1714" />
 
     <!-- Mexico: 4-7 digits (not confirmed), known premium codes listed -->
-    <shortcode country="mx" pattern="\\d{4,7}" premium="53035|7766" free="26259|46645|50025|50052|5050|76551|88778|9963|91101|45453|550346|3030303|81811" />
+    <shortcode country="mx" pattern="\\d{4,7}" premium="53035|7766" free="26259|46645|50025|50052|5050|76551|88778|9963|91101|45453|550346|3030303|81811|81818" />
 
     <!-- Malaysia: 5 digits: http://www.skmm.gov.my/attachment/Consumer_Regulation/Mobile_Content_Services_FAQs.pdf -->
     <shortcode country="my" pattern="\\d{5}" premium="32298|33776" free="22099|28288|66668|66966" />
@@ -324,7 +324,7 @@
     <shortcode country="tj" pattern="\\d{4}" premium="11[3-7]1|4161|4333|444[689]" />
 
     <!-- Tanzania: 1-5 digits (standard system default, not country specific) -->
-    <shortcode country="tz" pattern="\\d{1,5}" free="15046|15234|15324" />
+    <shortcode country="tz" pattern="\\d{1,5}" free="15046|15234|15324|15610" />
 
     <!-- Tunisia: 5 digits, known premium codes listed -->
     <shortcode country="tn" pattern="\\d{5}" free="85799" />
@@ -336,11 +336,11 @@
     <shortcode country="ua" pattern="\\d{4}" premium="444[3-9]|70[579]4|7540" />
 
     <!-- Uganda(UG): 4 digits (standard system default, not country specific) -->
-    <shortcode country="ug" pattern="\\d{4}" free="8000" />
+    <shortcode country="ug" pattern="\\d{4}" free="8000|8009" />
 
     <!-- USA: 5-6 digits (premium codes from https://www.premiumsmsrefunds.com/ShortCodes.htm),
          visual voicemail code for T-Mobile: 122 -->
-    <shortcode country="us" pattern="\\d{5,6}" premium="20433|21(?:344|472)|22715|23(?:333|847)|24(?:15|28)0|25209|27(?:449|606|663)|28498|305(?:00|83)|32(?:340|941)|33(?:166|786|849)|34746|35(?:182|564)|37975|38(?:135|146|254)|41(?:366|463)|42335|43(?:355|500)|44(?:578|711|811)|45814|46(?:157|173|327)|46666|47553|48(?:221|277|669)|50(?:844|920)|51(?:062|368)|52944|54(?:723|892)|55928|56483|57370|59(?:182|187|252|342)|60339|61(?:266|982)|62478|64(?:219|898)|65(?:108|500)|69(?:208|388)|70877|71851|72(?:078|087|465)|73(?:288|588|882|909|997)|74(?:034|332|815)|76426|79213|81946|83177|84(?:103|685)|85797|86(?:234|236|666)|89616|90(?:715|842|938)|91(?:362|958)|94719|95297|96(?:040|666|835|969)|97(?:142|294|688)|99(?:689|796|807)" standard="44567|244444" free="122|87902|21696|24614|28003|30356|33669|40196|41064|41270|43753|44034|46645|52413|56139|57969|61785|66975|75136|76227|81398|83952|85140|86566|86799|95737|96684|99245|611611|96831" />
+    <shortcode country="us" pattern="\\d{5,6}" premium="20433|21(?:344|472)|22715|23(?:333|847)|24(?:15|28)0|25209|27(?:449|606|663)|28498|305(?:00|83)|32(?:340|941)|33(?:166|786|849)|34746|35(?:182|564)|37975|38(?:135|146|254)|41(?:366|463)|42335|43(?:355|500)|44(?:578|711|811)|45814|46(?:157|173|327)|46666|47553|48(?:221|277|669)|50(?:844|920)|51(?:062|368)|52944|54(?:723|892)|55928|56483|57370|59(?:182|187|252|342)|60339|61(?:266|982)|62478|64(?:219|898)|65(?:108|500)|69(?:208|388)|70877|71851|72(?:078|087|465)|73(?:288|588|882|909|997)|74(?:034|332|815)|76426|79213|81946|83177|84(?:103|685)|85797|86(?:234|236|666)|89616|90(?:715|842|938)|91(?:362|958)|94719|95297|96(?:040|666|835|969)|97(?:142|294|688)|99(?:689|796|807)" standard="44567|244444" free="122|87902|21696|24614|28003|30356|33669|40196|41064|41270|43753|44034|46645|52413|56139|57969|61785|66975|75136|76227|81398|83952|85140|86566|86799|95737|96684|99245|611611|96831|10907" />
 
     <!--Uruguay : 1-6 digits (standard system default, not country specific) -->
     <shortcode country="uy" pattern="\\d{1,6}" free="55002|191289" />
diff --git a/core/tests/vibrator/src/android/os/VibratorInfoTest.java b/core/tests/vibrator/src/android/os/VibratorInfoTest.java
index 04945f3..9099918e 100644
--- a/core/tests/vibrator/src/android/os/VibratorInfoTest.java
+++ b/core/tests/vibrator/src/android/os/VibratorInfoTest.java
@@ -71,8 +71,7 @@
         VibratorInfo noCapabilities = new VibratorInfo.Builder(TEST_VIBRATOR_ID).build();
         assertFalse(noCapabilities.hasFrequencyControl());
         VibratorInfo composeAndFrequencyControl = new VibratorInfo.Builder(TEST_VIBRATOR_ID)
-                .setCapabilities(
-                        IVibrator.CAP_FREQUENCY_CONTROL | IVibrator.CAP_COMPOSE_PWLE_EFFECTS)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .build();
         assertTrue(composeAndFrequencyControl.hasFrequencyControl());
     }
@@ -153,7 +152,8 @@
         VibratorInfo noCapabilities = new VibratorInfo.Builder(TEST_VIBRATOR_ID).build();
         assertFalse(noCapabilities.areEnvelopeEffectsSupported());
         VibratorInfo envelopeEffectCapability = new VibratorInfo.Builder(TEST_VIBRATOR_ID)
-                .setCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2)
+                .setCapabilities(
+                        IVibrator.CAP_FREQUENCY_CONTROL | IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2)
                 .build();
         assertTrue(envelopeEffectCapability.areEnvelopeEffectsSupported());
     }
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index a028e18..debd0df 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -659,5 +659,6 @@
    <privapp-permissions package="com.android.devicediagnostics">
         <permission name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>
         <permission name="android.permission.BATTERY_STATS"/>
+        <permission name="android.permission.ENTER_TRADE_IN_MODE"/>
     </privapp-permissions>
 </permissions>
diff --git a/graphics/java/android/graphics/ImageFormat.java b/graphics/java/android/graphics/ImageFormat.java
index cb3b64c..93d94c9 100644
--- a/graphics/java/android/graphics/ImageFormat.java
+++ b/graphics/java/android/graphics/ImageFormat.java
@@ -16,6 +16,7 @@
 
 package android.graphics;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 
 import java.lang.annotation.Retention;
@@ -41,6 +42,7 @@
              Y8,
              Y16,
              YCBCR_P010,
+             YCBCR_P210,
              NV16,
              NV21,
              YUY2,
@@ -206,6 +208,26 @@
     public static final int YCBCR_P010 = 0x36;
 
     /**
+     * <p>Android YUV P210 format.</p>
+     *
+     * P210 is a 4:2:2 YCbCr semiplanar format comprised of a WxH Y plane
+     * followed by a WxH CbCr plane. Each sample is represented by a 16-bit
+     * little-endian value, with the lower 6 bits set to zero.
+     *
+     * <p>For example, the {@link android.media.Image} object can provide data
+     * in this format from a {@link android.hardware.camera2.CameraDevice}
+     * through a {@link android.media.ImageReader} object if this format is
+     * supported by {@link android.hardware.camera2.CameraDevice}.</p>
+     *
+     * @see android.media.Image
+     * @see android.media.ImageReader
+     * @see android.hardware.camera2.CameraDevice
+     *
+     */
+    @FlaggedApi(android.media.codec.Flags.FLAG_P210_FORMAT_SUPPORT)
+    public static final int YCBCR_P210 = 0x3c;
+
+    /**
      * YCbCr format, used for video.
      *
      * <p>For the {@link android.hardware.camera2} API, the {@link #YUV_420_888} format is
@@ -849,6 +871,8 @@
                 return 16;
             case YCBCR_P010:
                 return 24;
+            case YCBCR_P210:
+                return 32;
             case RAW_DEPTH10:
             case RAW10:
                 return 10;
@@ -899,7 +923,9 @@
             case JPEG_R:
                 return true;
         }
-
+        if (android.media.codec.Flags.p210FormatSupport() && format == YCBCR_P210) {
+            return true;
+        }
         return false;
     }
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
index a3d2d7f..4385327 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
@@ -16,6 +16,10 @@
 
 package androidx.window.extensions.area;
 
+import static android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_DUAL_DISPLAY_INTERNAL_DEFAULT;
+import static android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_REAR_DISPLAY;
+import static android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY;
+import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
 import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER;
 
 import android.app.Activity;
@@ -23,6 +27,7 @@
 import android.hardware.devicestate.DeviceState;
 import android.hardware.devicestate.DeviceStateManager;
 import android.hardware.devicestate.DeviceStateRequest;
+import android.hardware.devicestate.feature.flags.Flags;
 import android.hardware.display.DisplayManager;
 import android.util.ArraySet;
 import android.util.DisplayMetrics;
@@ -72,18 +77,18 @@
     @GuardedBy("mLock")
     private final ArraySet<Consumer<ExtensionWindowAreaStatus>>
             mRearDisplayPresentationStatusListeners = new ArraySet<>();
-    private final int mRearDisplayState;
+    private int mRearDisplayState = INVALID_DEVICE_STATE_IDENTIFIER;
     private final int mConcurrentDisplayState;
     @NonNull
-    private final int[] mFoldedDeviceStates;
+    private int[] mFoldedDeviceStates = new int[0];
     private long mRearDisplayAddress = INVALID_DISPLAY_ADDRESS;
     @WindowAreaSessionState
     private int mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE;
 
     @GuardedBy("mLock")
-    private int mCurrentDeviceState = INVALID_DEVICE_STATE_IDENTIFIER;
+    private DeviceState mCurrentDeviceState = INVALID_DEVICE_STATE;
     @GuardedBy("mLock")
-    private int[] mCurrentSupportedDeviceStates;
+    private List<DeviceState> mCurrentSupportedDeviceStates;
 
     @GuardedBy("mLock")
     private DeviceStateRequest mRearDisplayStateRequest;
@@ -103,16 +108,25 @@
         mDisplayManager = context.getSystemService(DisplayManager.class);
         mExecutor = context.getMainExecutor();
 
-        // TODO(b/329436166): Update the usage of device state manager API's
-        mCurrentSupportedDeviceStates = getSupportedStateIdentifiers(
-                mDeviceStateManager.getSupportedDeviceStates());
-        mFoldedDeviceStates = context.getResources().getIntArray(
-                R.array.config_foldedDeviceStates);
+        mCurrentSupportedDeviceStates = mDeviceStateManager.getSupportedDeviceStates();
 
-        // TODO(b/236022708) Move rear display state to device state config file
-        mRearDisplayState = context.getResources().getInteger(
-                R.integer.config_deviceStateRearDisplay);
+        if (Flags.deviceStatePropertyMigration()) {
+            for (int i = 0; i < mCurrentSupportedDeviceStates.size(); i++) {
+                DeviceState state = mCurrentSupportedDeviceStates.get(i);
+                if (state.hasProperty(PROPERTY_FEATURE_REAR_DISPLAY)) {
+                    mRearDisplayState = state.getIdentifier();
+                    break;
+                }
+            }
+        } else {
+            mFoldedDeviceStates = context.getResources().getIntArray(
+                    R.array.config_foldedDeviceStates);
+            // TODO(b/236022708) Move rear display state to device state config file
+            mRearDisplayState = context.getResources().getInteger(
+                    R.integer.config_deviceStateRearDisplay);
+        }
 
+        // TODO(b/374351956) Use DeviceState API when the dual display state is always returned
         mConcurrentDisplayState = context.getResources().getInteger(
                 R.integer.config_deviceStateConcurrentRearDisplay);
 
@@ -147,7 +161,7 @@
             mRearDisplayStatusListeners.add(consumer);
 
             // If current device state is still invalid, the initial value has not been provided.
-            if (mCurrentDeviceState == INVALID_DEVICE_STATE_IDENTIFIER) {
+            if (mCurrentDeviceState.getIdentifier() == INVALID_DEVICE_STATE_IDENTIFIER) {
                 return;
             }
             consumer.accept(getCurrentRearDisplayModeStatus());
@@ -312,7 +326,7 @@
             mRearDisplayPresentationStatusListeners.add(consumer);
 
             // If current device state is still invalid, the initial value has not been provided
-            if (mCurrentDeviceState == INVALID_DEVICE_STATE_IDENTIFIER) {
+            if (mCurrentDeviceState.getIdentifier() == INVALID_DEVICE_STATE_IDENTIFIER) {
                 return;
             }
             @WindowAreaStatus int currentStatus = getCurrentRearDisplayPresentationModeStatus();
@@ -452,8 +466,7 @@
     @Override
     public void onSupportedStatesChanged(@NonNull List<DeviceState> supportedStates) {
         synchronized (mLock) {
-            // TODO(b/329436166): Update the usage of device state manager API's
-            mCurrentSupportedDeviceStates = getSupportedStateIdentifiers(supportedStates);
+            mCurrentSupportedDeviceStates = supportedStates;
             updateRearDisplayStatusListeners(getCurrentRearDisplayModeStatus());
             updateRearDisplayPresentationStatusListeners(
                     getCurrentRearDisplayPresentationModeStatus());
@@ -463,8 +476,7 @@
     @Override
     public void onDeviceStateChanged(@NonNull DeviceState state) {
         synchronized (mLock) {
-            // TODO(b/329436166): Update the usage of device state manager API's
-            mCurrentDeviceState = state.getIdentifier();
+            mCurrentDeviceState = state;
             updateRearDisplayStatusListeners(getCurrentRearDisplayModeStatus());
             updateRearDisplayPresentationStatusListeners(
                     getCurrentRearDisplayPresentationModeStatus());
@@ -477,7 +489,8 @@
             return WindowAreaComponent.STATUS_UNSUPPORTED;
         }
 
-        if (!ArrayUtils.contains(mCurrentSupportedDeviceStates, mRearDisplayState)) {
+        if (!deviceStateListContainsIdentifier(mCurrentSupportedDeviceStates,
+                mRearDisplayState)) {
             return WindowAreaComponent.STATUS_UNAVAILABLE;
         }
 
@@ -488,15 +501,6 @@
         return WindowAreaComponent.STATUS_AVAILABLE;
     }
 
-    // TODO(b/329436166): Remove and update the usage of device state manager API's
-    private int[] getSupportedStateIdentifiers(@NonNull List<DeviceState> states) {
-        int[] identifiers = new int[states.size()];
-        for (int i = 0; i < states.size(); i++) {
-            identifiers[i] = states.get(i).getIdentifier();
-        }
-        return identifiers;
-    }
-
     /**
      * Helper method to determine if a rear display session is currently active by checking
      * if the current device state is that which corresponds to {@code mRearDisplayState}.
@@ -505,7 +509,31 @@
      */
     @GuardedBy("mLock")
     private boolean isRearDisplayActive() {
-        return mCurrentDeviceState == mRearDisplayState;
+        if (Flags.deviceStatePropertyApi()) {
+            return mCurrentDeviceState.hasProperty(PROPERTY_FEATURE_REAR_DISPLAY);
+        } else {
+            return mCurrentDeviceState.getIdentifier() == mRearDisplayState;
+        }
+    }
+
+    @GuardedBy("mLock")
+    private boolean isRearDisplayPresentationModeActive() {
+        if (Flags.deviceStatePropertyApi()) {
+            return mCurrentDeviceState.hasProperty(PROPERTY_FEATURE_DUAL_DISPLAY_INTERNAL_DEFAULT);
+        } else {
+            return mCurrentDeviceState.getIdentifier() == mConcurrentDisplayState;
+        }
+    }
+
+    @GuardedBy("mLock")
+    private boolean deviceStateListContainsIdentifier(List<DeviceState> deviceStates,
+            int identifier) {
+        for (int i = 0; i < deviceStates.size(); i++) {
+            if (deviceStates.get(i).getIdentifier() == identifier) {
+                return true;
+            }
+        }
+        return false;
     }
 
     @GuardedBy("mLock")
@@ -526,12 +554,12 @@
             return WindowAreaComponent.STATUS_UNSUPPORTED;
         }
 
-        if (mCurrentDeviceState == mConcurrentDisplayState) {
+        if (isRearDisplayPresentationModeActive()) {
             return WindowAreaComponent.STATUS_ACTIVE;
         }
 
-        if (!ArrayUtils.contains(mCurrentSupportedDeviceStates, mConcurrentDisplayState)
-                || isDeviceFolded()) {
+        if (!deviceStateListContainsIdentifier(mCurrentSupportedDeviceStates,
+                mConcurrentDisplayState) || isDeviceFolded()) {
             return WindowAreaComponent.STATUS_UNAVAILABLE;
         }
         return WindowAreaComponent.STATUS_AVAILABLE;
@@ -539,7 +567,12 @@
 
     @GuardedBy("mLock")
     private boolean isDeviceFolded() {
-        return ArrayUtils.contains(mFoldedDeviceStates, mCurrentDeviceState);
+        if (Flags.deviceStatePropertyApi()) {
+            return mCurrentDeviceState.hasProperty(
+                    PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY);
+        } else {
+            return ArrayUtils.contains(mFoldedDeviceStates, mCurrentDeviceState.getIdentifier());
+        }
     }
 
     @GuardedBy("mLock")
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/ManageWindowsViewContainer.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/ManageWindowsViewContainer.kt
index 79becb0..0e8e904 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/ManageWindowsViewContainer.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/ManageWindowsViewContainer.kt
@@ -122,7 +122,8 @@
                     snapshot.hardwareBuffer,
                     snapshot.colorSpace
                 )
-                val scaledSnapshotBitmap = snapshotBitmap?.let {
+                val croppedBitmap = snapshotBitmap?.let { cropBitmap(it) }
+                val scaledSnapshotBitmap = croppedBitmap?.let {
                     Bitmap.createScaledBitmap(
                         it, instanceIconWidth.toInt(), instanceIconHeight.toInt(), true /* filter */
                     )
@@ -160,6 +161,35 @@
             menuHeight += iconMargin.toInt()
         }
 
+        private fun cropBitmap(
+            bitmapToCrop: Bitmap
+        ): Bitmap {
+            val ratioToMatch = ICON_WIDTH_DP / ICON_HEIGHT_DP
+            val bitmapWidth = bitmapToCrop.width
+            val bitmapHeight = bitmapToCrop.height
+            if (bitmapWidth > bitmapHeight * ratioToMatch) {
+                // Crop based on height
+                val newWidth = bitmapHeight * ratioToMatch
+                return Bitmap.createBitmap(
+                    bitmapToCrop,
+                    ((bitmapWidth - newWidth) / 2).toInt(),
+                    0,
+                    newWidth.toInt(),
+                    bitmapHeight
+                )
+            } else {
+                // Crop based on width
+                val newHeight = bitmapWidth / ratioToMatch
+                return Bitmap.createBitmap(
+                    bitmapToCrop,
+                    0,
+                    ((bitmapHeight - newHeight) / 2).toInt(),
+                    bitmapWidth,
+                    newHeight.toInt()
+                )
+            }
+        }
+
         companion object {
             private const val MENU_RADIUS_DP = 26f
             private const val ICON_WIDTH_DP = 204f
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
index 7a56979..52391d2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
@@ -16,12 +16,15 @@
 
 package com.android.wm.shell.back;
 
+import static android.view.MotionEvent.ACTION_MOVE;
 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
 import static android.window.BackEvent.EDGE_RIGHT;
 
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_CROSS_TASK;
+import static com.android.window.flags.Flags.predictiveBackTimestampApi;
 import static com.android.wm.shell.back.BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD;
+import static com.android.wm.shell.back.CrossActivityBackAnimationKt.scaleCentered;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
 
 import android.animation.Animator;
@@ -36,11 +39,14 @@
 import android.graphics.RectF;
 import android.os.Handler;
 import android.os.RemoteException;
+import android.util.TimeUtils;
 import android.view.Choreographer;
 import android.view.IRemoteAnimationFinishedCallback;
 import android.view.IRemoteAnimationRunner;
+import android.view.MotionEvent;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
+import android.view.VelocityTracker;
 import android.view.animation.DecelerateInterpolator;
 import android.view.animation.Interpolator;
 import android.window.BackEvent;
@@ -48,6 +54,9 @@
 import android.window.BackProgressAnimator;
 import android.window.IOnBackInvokedCallback;
 
+import com.android.internal.dynamicanimation.animation.FloatValueHolder;
+import com.android.internal.dynamicanimation.animation.SpringAnimation;
+import com.android.internal.dynamicanimation.animation.SpringForce;
 import com.android.internal.policy.ScreenDecorationsUtils;
 import com.android.internal.policy.SystemBarUtils;
 import com.android.internal.protolog.ProtoLog;
@@ -81,6 +90,11 @@
     /** Duration of post animation after gesture committed. */
     private static final int POST_ANIMATION_DURATION_MS = 500;
 
+    private static final float SPRING_SCALE = 100f;
+    private static final float DEFAULT_FLING_VELOCITY = 320f;
+    private static final float MAX_FLING_VELOCITY = 1000f;
+    private static final float FLING_SPRING_STIFFNESS = 320f;
+
     private final Rect mStartTaskRect = new Rect();
     private float mCornerRadius;
     private int mStatusbarHeight;
@@ -114,6 +128,14 @@
     private float mInterWindowMargin;
     private float mVerticalMargin;
 
+    private final FloatValueHolder mPostCommitFlingScale = new FloatValueHolder(SPRING_SCALE);
+    private final SpringForce mPostCommitFlingSpring = new SpringForce(SPRING_SCALE)
+            .setStiffness(FLING_SPRING_STIFFNESS)
+            .setDampingRatio(1f);
+    private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
+    private float mGestureProgress = 0f;
+    private long mDownTime = 0L;
+
     @Inject
     public CrossTaskBackAnimation(Context context, BackAnimationBackground background,
             @ShellMainThread Handler handler) {
@@ -168,6 +190,7 @@
         if (mEnteringTarget == null || mClosingTarget == null) {
             return;
         }
+        mGestureProgress = progress;
 
         float touchY = event.getTouchY();
 
@@ -229,6 +252,8 @@
         }
 
         mClosingCurrentRect.set(left, top, left + width, top + height);
+
+        applyFlingScale(mClosingCurrentRect);
         applyTransform(mClosingTarget.leash, mClosingCurrentRect, mCornerRadius);
     }
 
@@ -239,9 +264,19 @@
         float height = mapRange(progress, mEnteringStartRect.height(), mStartTaskRect.height());
 
         mEnteringCurrentRect.set(left, top, left + width, top + height);
+
+        applyFlingScale(mEnteringCurrentRect);
         applyTransform(mEnteringTarget.leash, mEnteringCurrentRect, mCornerRadius);
     }
 
+    private void applyFlingScale(RectF rect) {
+        // apply a scale to the rect to account for fling velocity
+        final float flingScale = Math.min(mPostCommitFlingScale.getValue() / SPRING_SCALE, 1f);
+        if (flingScale >= 1f) return;
+        scaleCentered(rect, flingScale, /* pivotX */ rect.right,
+                /* pivotY */ rect.top + rect.height() / 2);
+    }
+
     /** Transform the target window to match the target rect. */
     private void applyTransform(SurfaceControl leash, RectF targetRect, float cornerRadius) {
         if (leash == null || !leash.isValid()) {
@@ -280,6 +315,9 @@
         mTransformMatrix.reset();
         mClosingCurrentRect.setEmpty();
         mInitialTouchPos.set(0, 0);
+        mGestureProgress = 0;
+        mDownTime = 0;
+        mVelocityTracker.clear();
 
         if (mFinishCallback != null) {
             try {
@@ -295,10 +333,24 @@
     private void onGestureProgress(@NonNull BackEvent backEvent) {
         if (!mBackInProgress) {
             mBackInProgress = true;
+            mDownTime = backEvent.getFrameTime();
         }
         float progress = backEvent.getProgress();
         mTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
-        updateGestureBackProgress(getInterpolatedProgress(progress), backEvent);
+        float interpolatedProgress = getInterpolatedProgress(progress);
+        if (predictiveBackTimestampApi()) {
+            mVelocityTracker.addMovement(
+                    MotionEvent.obtain(
+                            /* downTime */ mDownTime,
+                            /* eventTime */ backEvent.getFrameTime(),
+                            /* action */ ACTION_MOVE,
+                            /* x */ interpolatedProgress * SPRING_SCALE,
+                            /* y */ 0f,
+                            /* metaState */ 0
+                    )
+            );
+        }
+        updateGestureBackProgress(interpolatedProgress, backEvent);
     }
 
     private void onGestureCommitted() {
@@ -307,6 +359,25 @@
             return;
         }
 
+        if (predictiveBackTimestampApi()) {
+            // kick off spring animation with the current velocity from the pre-commit phase, this
+            // affects the scaling of the closing and/or opening task during post-commit
+            mVelocityTracker.computeCurrentVelocity(1000);
+            float startVelocity = mGestureProgress < 0.1f
+                    ? -DEFAULT_FLING_VELOCITY : -mVelocityTracker.getXVelocity();
+            SpringAnimation flingAnimation =
+                    new SpringAnimation(mPostCommitFlingScale, SPRING_SCALE)
+                    .setStartVelocity(Math.max(-MAX_FLING_VELOCITY, Math.min(0f, startVelocity)))
+                    .setStartValue(SPRING_SCALE)
+                    .setMinimumVisibleChange(0.1f)
+                    .setSpring(mPostCommitFlingSpring);
+            flingAnimation.start();
+            // do an animation-frame immediately to prevent idle frame
+            flingAnimation.doAnimationFrame(
+                    Choreographer.getInstance().getLastFrameTimeNanos() / TimeUtils.NANOS_PER_MS
+            );
+        }
+
         // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current
         // coordinate of the gesture driven phase.
         mEnteringCurrentRect.round(mEnteringStartRect);
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 427df17..b700a54 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -67,7 +67,7 @@
 import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler;
 import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler;
 import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler;
-import com.android.wm.shell.desktopmode.DesktopFullImmersiveTransitionHandler;
+import com.android.wm.shell.desktopmode.DesktopImmersiveController;
 import com.android.wm.shell.desktopmode.DesktopMixedTransitionHandler;
 import com.android.wm.shell.desktopmode.DesktopModeDragAndDropTransitionHandler;
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
@@ -267,7 +267,8 @@
             AppHandleEducationController appHandleEducationController,
             WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
             Optional<DesktopActivityOrientationChangeHandler> desktopActivityOrientationHandler,
-            FocusTransitionObserver focusTransitionObserver) {
+            FocusTransitionObserver focusTransitionObserver,
+            DesktopModeEventLogger desktopModeEventLogger) {
         if (DesktopModeStatus.canEnterDesktopMode(context)) {
             return new DesktopModeWindowDecorViewModel(
                     context,
@@ -295,7 +296,8 @@
                     appHandleEducationController,
                     windowDecorCaptionHandleRepository,
                     desktopActivityOrientationHandler,
-                    focusTransitionObserver);
+                    focusTransitionObserver,
+                    desktopModeEventLogger);
         }
         return new CaptionWindowDecorViewModel(
                 context,
@@ -395,12 +397,12 @@
             Context context,
             ShellInit shellInit,
             Transitions transitions,
-            Optional<DesktopFullImmersiveTransitionHandler> desktopImmersiveTransitionHandler,
+            Optional<DesktopImmersiveController> desktopImmersiveController,
             WindowDecorViewModel windowDecorViewModel,
             Optional<TaskChangeListener> taskChangeListener,
             FocusTransitionObserver focusTransitionObserver) {
         return new FreeformTaskTransitionObserver(
-                context, shellInit, transitions, desktopImmersiveTransitionHandler,
+                context, shellInit, transitions, desktopImmersiveController,
                 windowDecorViewModel, taskChangeListener, focusTransitionObserver);
     }
 
@@ -636,7 +638,7 @@
             ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler,
             DragToDesktopTransitionHandler dragToDesktopTransitionHandler,
             @DynamicOverride DesktopRepository desktopRepository,
-            Optional<DesktopFullImmersiveTransitionHandler> desktopFullImmersiveTransitionHandler,
+            Optional<DesktopImmersiveController> desktopImmersiveController,
             DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver,
             LaunchAdjacentController launchAdjacentController,
             RecentsTransitionHandler recentsTransitionHandler,
@@ -647,19 +649,21 @@
             Optional<RecentTasksController> recentTasksController,
             InteractionJankMonitor interactionJankMonitor,
             InputManager inputManager,
-            FocusTransitionObserver focusTransitionObserver) {
+            FocusTransitionObserver focusTransitionObserver,
+            DesktopModeEventLogger desktopModeEventLogger) {
         return new DesktopTasksController(context, shellInit, shellCommandHandler, shellController,
                 displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer,
                 dragAndDropController, transitions, keyguardManager,
                 returnToDragStartAnimator, enterDesktopTransitionHandler,
                 exitDesktopTransitionHandler, desktopModeDragAndDropTransitionHandler,
                 toggleResizeDesktopTaskTransitionHandler,
-                dragToDesktopTransitionHandler, desktopFullImmersiveTransitionHandler.get(),
+                dragToDesktopTransitionHandler, desktopImmersiveController.get(),
                 desktopRepository,
                 desktopModeLoggerTransitionObserver, launchAdjacentController,
                 recentsTransitionHandler, multiInstanceHelper, mainExecutor, desktopTasksLimiter,
                 recentTasksController.orElse(null), interactionJankMonitor, mainHandler,
-                inputManager, focusTransitionObserver);
+                inputManager, focusTransitionObserver,
+                desktopModeEventLogger);
     }
 
     @WMSingleton
@@ -701,7 +705,7 @@
 
     @WMSingleton
     @Provides
-    static Optional<DesktopFullImmersiveTransitionHandler> provideDesktopImmersiveHandler(
+    static Optional<DesktopImmersiveController> provideDesktopImmersiveController(
             Context context,
             Transitions transitions,
             @DynamicOverride DesktopRepository desktopRepository,
@@ -709,7 +713,7 @@
             ShellTaskOrganizer shellTaskOrganizer) {
         if (DesktopModeStatus.canEnterDesktopMode(context)) {
             return Optional.of(
-                    new DesktopFullImmersiveTransitionHandler(
+                    new DesktopImmersiveController(
                             transitions,
                             desktopRepository,
                             displayController,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt
similarity index 80%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt
rename to libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt
index 9d4926b..d0bc5f0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt
@@ -36,20 +36,21 @@
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.transition.Transitions.TransitionHandler
+import com.android.wm.shell.transition.Transitions.TransitionObserver
 import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
 
 /**
- * A [TransitionHandler] to move a task in/out of desktop's full immersive state where the task
+ * A controller to move tasks in/out of desktop's full immersive state where the task
  * remains freeform while being able to take fullscreen bounds and have its App Header visibility
  * be transient below the status bar like in fullscreen immersive mode.
  */
-class DesktopFullImmersiveTransitionHandler(
+class DesktopImmersiveController(
     private val transitions: Transitions,
     private val desktopRepository: DesktopRepository,
     private val displayController: DisplayController,
     private val shellTaskOrganizer: ShellTaskOrganizer,
     private val transactionSupplier: () -> SurfaceControl.Transaction,
-) : TransitionHandler {
+) : TransitionHandler, TransitionObserver {
 
     constructor(
         transitions: Transitions,
@@ -67,7 +68,7 @@
     private var state: TransitionState? = null
 
     @VisibleForTesting
-    val pendingExternalExitTransitions = mutableSetOf<ExternalPendingExit>()
+    val pendingExternalExitTransitions = mutableListOf<ExternalPendingExit>()
 
     /** Whether there is an immersive transition that hasn't completed yet. */
     private val inProgress: Boolean
@@ -137,14 +138,19 @@
      *
      * @param wct that will apply these changes
      * @param displayId of the display that should exit immersive mode
+     * @param excludeTaskId of the task to ignore (not exit) if it is the immersive one
      * @return a function to apply once the transition that will apply these changes is started
      */
     fun exitImmersiveIfApplicable(
         wct: WindowContainerTransaction,
-        displayId: Int
+        displayId: Int,
+        excludeTaskId: Int? = null,
     ): ((IBinder) -> Unit)? {
         if (!Flags.enableFullyImmersiveInDesktop()) return null
         val immersiveTask = desktopRepository.getTaskInFullImmersiveState(displayId) ?: return null
+        if (immersiveTask == excludeTaskId) {
+            return null
+        }
         val taskInfo = shellTaskOrganizer.getRunningTaskInfo(immersiveTask) ?: return null
         logV("Appending immersive exit for task: $immersiveTask in display: $displayId")
         wct.setBounds(taskInfo.token, getExitDestinationBounds(taskInfo))
@@ -179,6 +185,17 @@
         return null
     }
 
+
+    /** Whether the [change] in the [transition] is a known immersive change. */
+    fun isImmersiveChange(
+        transition: IBinder,
+        change: TransitionInfo.Change,
+    ): Boolean {
+        return pendingExternalExitTransitions.any {
+            it.transition == transition && it.taskId == change.taskInfo?.taskId
+        }
+    }
+
     private fun addPendingImmersiveExit(taskId: Int, displayId: Int, transition: IBinder) {
         pendingExternalExitTransitions.add(
             ExternalPendingExit(
@@ -196,10 +213,11 @@
         finishTransaction: SurfaceControl.Transaction,
         finishCallback: Transitions.TransitionFinishCallback
     ): Boolean {
+        logD("startAnimation transition=%s", transition)
         val state = requireState()
         if (transition != state.transition) return false
         animateResize(
-            transitionState = state,
+            targetTaskId = state.taskId,
             info = info,
             startTransaction = startTransaction,
             finishTransaction = finishTransaction,
@@ -209,40 +227,55 @@
     }
 
     private fun animateResize(
-        transitionState: TransitionState,
+        targetTaskId: Int,
         info: TransitionInfo,
         startTransaction: SurfaceControl.Transaction,
         finishTransaction: SurfaceControl.Transaction,
         finishCallback: Transitions.TransitionFinishCallback
     ) {
+        logD("animateResize for task#%d", targetTaskId)
         val change = info.changes.first { c ->
             val taskInfo = c.taskInfo
-            return@first taskInfo != null && taskInfo.taskId == transitionState.taskId
+            return@first taskInfo != null && taskInfo.taskId == targetTaskId
         }
+        animateResizeChange(change, startTransaction, finishTransaction, finishCallback)
+    }
+
+    /**
+     *  Animate an immersive change.
+     *
+     *  As of now, both enter and exit transitions have the same animation, a veiled resize.
+     */
+    fun animateResizeChange(
+        change: TransitionInfo.Change,
+        startTransaction: SurfaceControl.Transaction,
+        finishTransaction: SurfaceControl.Transaction,
+        finishCallback: Transitions.TransitionFinishCallback,
+    ) {
+        val taskId = change.taskInfo!!.taskId
         val leash = change.leash
         val startBounds = change.startAbsBounds
         val endBounds = change.endAbsBounds
+        logD("Animating resize change for task#%d from %s to %s", taskId, startBounds, endBounds)
 
+        startTransaction
+            .setPosition(leash, startBounds.left.toFloat(), startBounds.top.toFloat())
+            .setWindowCrop(leash, startBounds.width(), startBounds.height())
+            .show(leash)
+        onTaskResizeAnimationListener
+            ?.onAnimationStart(taskId, startTransaction, startBounds)
+            ?: startTransaction.apply()
         val updateTransaction = transactionSupplier()
         ValueAnimator.ofObject(rectEvaluator, startBounds, endBounds).apply {
             duration = FULL_IMMERSIVE_ANIM_DURATION_MS
             interpolator = DecelerateInterpolator()
             addListener(
-                onStart = {
-                    startTransaction
-                        .setPosition(leash, startBounds.left.toFloat(), startBounds.top.toFloat())
-                        .setWindowCrop(leash, startBounds.width(), startBounds.height())
-                        .show(leash)
-                    onTaskResizeAnimationListener
-                        ?.onAnimationStart(transitionState.taskId, startTransaction, startBounds)
-                        ?: startTransaction.apply()
-                },
                 onEnd = {
                     finishTransaction
                         .setPosition(leash, endBounds.left.toFloat(), endBounds.top.toFloat())
                         .setWindowCrop(leash, endBounds.width(), endBounds.height())
                         .apply()
-                    onTaskResizeAnimationListener?.onAnimationEnd(transitionState.taskId)
+                    onTaskResizeAnimationListener?.onAnimationEnd(taskId)
                     finishCallback.onTransitionFinished(null /* wct */)
                     clearState()
                 }
@@ -254,7 +287,7 @@
                     .setWindowCrop(leash, rect.width(), rect.height())
                     .apply()
                 onTaskResizeAnimationListener
-                    ?.onBoundsChange(transitionState.taskId, updateTransaction, rect)
+                    ?.onBoundsChange(taskId, updateTransaction, rect)
                     ?: updateTransaction.apply()
             }
             start()
@@ -284,15 +317,20 @@
      * |onTransitionReady|, before this transition actually animates) because drawing decorations
      * depends on whether the task is in full immersive state or not.
      */
-    fun onTransitionReady(transition: IBinder, info: TransitionInfo) {
+    override fun onTransitionReady(
+        transition: IBinder,
+        info: TransitionInfo,
+        startTransaction: SurfaceControl.Transaction,
+        finishTransaction: SurfaceControl.Transaction,
+    ) {
+        logD("onTransitionReady transition=%s", transition)
         // Check if this is a pending external exit transition.
         val pendingExit = pendingExternalExitTransitions
             .firstOrNull { pendingExit -> pendingExit.transition == transition }
         if (pendingExit != null) {
-            pendingExternalExitTransitions.remove(pendingExit)
             if (info.hasTaskChange(taskId = pendingExit.taskId)) {
                 if (desktopRepository.isTaskInFullImmersiveState(pendingExit.taskId)) {
-                    logV("Pending external exit for task ${pendingExit.taskId} verified")
+                    logV("Pending external exit for task#%d verified", pendingExit.taskId)
                     desktopRepository.setTaskInFullImmersiveState(
                         displayId = pendingExit.displayId,
                         taskId = pendingExit.taskId,
@@ -311,7 +349,7 @@
             val state = requireState()
             val startBounds = info.changes.first { c -> c.taskInfo?.taskId == state.taskId }
                 .startAbsBounds
-            logV("Direct move for task ${state.taskId} in ${state.direction} direction verified")
+            logV("Direct move for task#%d in %s direction verified", state.taskId, state.direction)
             when (state.direction) {
                 Direction.ENTER -> {
                     desktopRepository.setTaskInFullImmersiveState(
@@ -343,7 +381,7 @@
             .filter { c -> desktopRepository.isTaskInFullImmersiveState(c.taskInfo!!.taskId) }
             .filter { c -> c.startRotation != c.endRotation }
             .forEach { c ->
-                logV("Detected immersive exit due to rotation for task: ${c.taskInfo!!.taskId}")
+                logV("Detected immersive exit due to rotation for task#%d", c.taskInfo!!.taskId)
                 desktopRepository.setTaskInFullImmersiveState(
                     displayId = c.taskInfo!!.displayId,
                     taskId = c.taskInfo!!.taskId,
@@ -352,6 +390,32 @@
             }
     }
 
+    override fun onTransitionMerged(merged: IBinder, playing: IBinder) {
+        logD("onTransitionMerged merged=%s playing=%s", merged, playing)
+        val pendingExit = pendingExternalExitTransitions
+            .firstOrNull { pendingExit -> pendingExit.transition == merged }
+        if (pendingExit != null) {
+            logV(
+                "Pending exit transition %s for task#%s merged into %s",
+                merged, pendingExit.taskId, playing
+            )
+            pendingExit.transition = playing
+        }
+    }
+
+    override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {
+        logD("onTransitionFinished transition=%s aborted=%b", transition, aborted)
+        val pendingExit = pendingExternalExitTransitions
+            .firstOrNull { pendingExit -> pendingExit.transition == transition }
+        if (pendingExit != null) {
+            logV(
+                "Pending exit transition %s for task#%s finished",
+                transition, pendingExit
+            )
+            pendingExternalExitTransitions.remove(pendingExit)
+        }
+    }
+
     private fun clearState() {
         state = null
     }
@@ -394,7 +458,7 @@
     data class ExternalPendingExit(
         val taskId: Int,
         val displayId: Int,
-        val transition: IBinder,
+        var transition: IBinder,
     )
 
     private enum class Direction {
@@ -405,6 +469,10 @@
         ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
     }
 
+    private fun logD(msg: String, vararg arguments: Any?) {
+        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+    }
+
     private companion object {
         private const val TAG = "DesktopImmersive"
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
index 8ebe503..255ca6e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
@@ -16,11 +16,20 @@
 
 package com.android.wm.shell.desktopmode
 
+import android.app.ActivityManager.RunningTaskInfo
+import android.util.Size
+import android.view.InputDevice.SOURCE_MOUSE
+import android.view.InputDevice.SOURCE_TOUCHSCREEN
+import android.view.MotionEvent
+import android.view.MotionEvent.TOOL_TYPE_FINGER
+import android.view.MotionEvent.TOOL_TYPE_MOUSE
+import android.view.MotionEvent.TOOL_TYPE_STYLUS
 import com.android.internal.annotations.VisibleForTesting
 import com.android.internal.protolog.ProtoLog
 import com.android.internal.util.FrameworkStatsLog
 import com.android.window.flags.Flags
 import com.android.wm.shell.EventLogTags
+import com.android.wm.shell.common.DisplayController
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
 import java.security.SecureRandom
 import java.util.Random
@@ -176,7 +185,13 @@
      * Logs that a task resize event is starting with [taskSizeUpdate] within a Desktop mode
      * session.
      */
-    fun logTaskResizingStarted(taskSizeUpdate: TaskSizeUpdate) {
+    fun logTaskResizingStarted(
+        resizeTrigger: ResizeTrigger,
+        motionEvent: MotionEvent?,
+        taskInfo: RunningTaskInfo,
+        displayController: DisplayController? = null,
+        displayLayoutSize: Size? = null,
+    ) {
         if (!Flags.enableResizingMetrics()) return
 
         val sessionId = currentSessionId.get()
@@ -188,11 +203,19 @@
             return
         }
 
+        val taskSizeUpdate = createTaskSizeUpdate(
+            resizeTrigger,
+            motionEvent,
+            taskInfo,
+            displayController = displayController,
+            displayLayoutSize = displayLayoutSize,
+        )
+
         ProtoLog.v(
             WM_SHELL_DESKTOP_MODE,
-            "DesktopModeLogger: Logging task resize is starting, session: %s taskId: %s",
+            "DesktopModeLogger: Logging task resize is starting, session: %s, taskSizeUpdate: %s",
             sessionId,
-            taskSizeUpdate.instanceId
+            taskSizeUpdate
         )
         logTaskSizeUpdated(
             FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__START_RESIZING_STAGE,
@@ -203,7 +226,15 @@
     /**
      * Logs that a task resize event is ending with [taskSizeUpdate] within a Desktop mode session.
      */
-    fun logTaskResizingEnded(taskSizeUpdate: TaskSizeUpdate) {
+    fun logTaskResizingEnded(
+        resizeTrigger: ResizeTrigger,
+        motionEvent: MotionEvent?,
+        taskInfo: RunningTaskInfo,
+        taskHeight: Int? = null,
+        taskWidth: Int? = null,
+        displayController: DisplayController? = null,
+        displayLayoutSize: Size? = null,
+    ) {
         if (!Flags.enableResizingMetrics()) return
 
         val sessionId = currentSessionId.get()
@@ -215,18 +246,61 @@
             return
         }
 
+        val taskSizeUpdate = createTaskSizeUpdate(
+            resizeTrigger,
+            motionEvent,
+            taskInfo,
+            taskHeight,
+            taskWidth,
+            displayController,
+            displayLayoutSize,
+        )
+
         ProtoLog.v(
             WM_SHELL_DESKTOP_MODE,
-            "DesktopModeLogger: Logging task resize is ending, session: %s taskId: %s",
+            "DesktopModeLogger: Logging task resize is ending, session: %s, taskSizeUpdate: %s",
             sessionId,
-            taskSizeUpdate.instanceId
+            taskSizeUpdate
         )
+
         logTaskSizeUpdated(
             FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__END_RESIZING_STAGE,
             sessionId, taskSizeUpdate
         )
     }
 
+    private fun createTaskSizeUpdate(
+        resizeTrigger: ResizeTrigger,
+        motionEvent: MotionEvent?,
+        taskInfo: RunningTaskInfo,
+        taskHeight: Int? = null,
+        taskWidth: Int? = null,
+        displayController: DisplayController? = null,
+        displayLayoutSize: Size? = null,
+    ): TaskSizeUpdate {
+        val taskBounds = taskInfo.configuration.windowConfiguration.bounds
+
+        val height = taskHeight ?: taskBounds.height()
+        val width = taskWidth ?: taskBounds.width()
+
+        val displaySize = when {
+            displayLayoutSize != null -> displayLayoutSize.height * displayLayoutSize.width
+            displayController != null -> displayController.getDisplayLayout(taskInfo.displayId)
+                ?.let { it.height() * it.width() }
+            else -> null
+        }
+
+        return TaskSizeUpdate(
+            resizeTrigger,
+            getInputMethodFromMotionEvent(motionEvent),
+            taskInfo.taskId,
+            taskInfo.effectiveUid,
+            height,
+            width,
+            displaySize,
+        )
+    }
+
     fun logTaskInfoStateInit() {
         logTaskUpdate(
             FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INIT_STATSD,
@@ -238,7 +312,8 @@
                 taskHeight = 0,
                 taskWidth = 0,
                 taskX = 0,
-                taskY = 0)
+                taskY = 0
+            )
         )
     }
 
@@ -314,7 +389,7 @@
             /* task_width */
             taskSizeUpdate.taskWidth,
             /* display_area */
-            taskSizeUpdate.displayArea
+            taskSizeUpdate.displayArea ?: -1
         )
     }
 
@@ -364,9 +439,24 @@
             val uid: Int,
             val taskHeight: Int,
             val taskWidth: Int,
-            val displayArea: Int,
+            val displayArea: Int?,
         )
 
+        private fun getInputMethodFromMotionEvent(e: MotionEvent?): InputMethod {
+            if (e == null) return InputMethod.UNKNOWN_INPUT_METHOD
+
+            val toolType = e.getToolType(
+                e.findPointerIndex(e.getPointerId(0))
+            )
+            return when {
+                toolType == TOOL_TYPE_STYLUS -> InputMethod.STYLUS
+                toolType == TOOL_TYPE_MOUSE -> InputMethod.MOUSE
+                toolType == TOOL_TYPE_FINGER && e.source == SOURCE_MOUSE -> InputMethod.TOUCHPAD
+                toolType == TOOL_TYPE_FINGER && e.source == SOURCE_TOUCHSCREEN -> InputMethod.TOUCH
+                else -> InputMethod.UNKNOWN_INPUT_METHOD
+            }
+        }
+
         // Default value used when the task was not minimized.
         @VisibleForTesting
         const val UNSET_MINIMIZE_REASON =
@@ -499,6 +589,10 @@
                 FrameworkStatsLog
                     .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__SNAP_RIGHT_MENU_RESIZE_TRIGGER
             ),
+            MAXIMIZE_MENU(
+                FrameworkStatsLog
+                    .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__MAXIMIZE_MENU_RESIZE_TRIGGER
+            ),
         }
 
         /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
index 443e417..85a3126 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
@@ -30,7 +30,6 @@
 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.desktopmode.persistence.DesktopTask
 import com.android.wm.shell.desktopmode.persistence.DesktopTaskState
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
 import com.android.wm.shell.shared.annotations.ShellMainThread
@@ -124,7 +123,8 @@
         if (!Flags.enableDesktopWindowingPersistence()) return
         //  TODO: b/365962554 - Handle the case that user moves to desktop before it's initialized
         mainCoroutineScope.launch {
-            val desktop = persistentRepository.readDesktop()
+            val desktop = persistentRepository.readDesktop() ?: return@launch
+
             val maxTasks =
                 DesktopModeStatus.getMaxTaskLimit(context).takeIf { it > 0 }
                     ?: desktop.zOrderedTasksCount
@@ -132,13 +132,11 @@
             desktop.zOrderedTasksList
                 // Reverse it so we initialize the repo from bottom to top.
                 .reversed()
-                .map { taskId ->
-                    desktop.tasksByTaskIdMap.getOrDefault(
-                        taskId,
-                        DesktopTask.getDefaultInstance()
-                    )
+                .mapNotNull { taskId ->
+                    desktop.tasksByTaskIdMap[taskId]?.takeIf {
+                        it.desktopTaskState == DesktopTaskState.VISIBLE
+                    }
                 }
-                .filter { task -> task.desktopTaskState == DesktopTaskState.VISIBLE }
                 .take(maxTasks)
                 .forEach { task ->
                     addOrMoveFreeformTaskToTop(desktop.displayId, task.taskId)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index b505bee..781aee0 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
@@ -46,7 +46,9 @@
 import android.view.Display.DEFAULT_DISPLAY
 import android.view.DragEvent
 import android.view.KeyEvent
+import android.view.MotionEvent
 import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
 import android.view.WindowManager.TRANSIT_CHANGE
 import android.view.WindowManager.TRANSIT_CLOSE
 import android.view.WindowManager.TRANSIT_NONE
@@ -58,6 +60,7 @@
 import android.window.DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS
 import android.window.RemoteTransition
 import android.window.TransitionInfo
+import android.window.TransitionInfo.Change
 import android.window.TransitionRequestInfo
 import android.window.WindowContainerTransaction
 import androidx.annotation.BinderThread
@@ -114,6 +117,7 @@
 import com.android.wm.shell.transition.FocusTransitionObserver
 import com.android.wm.shell.transition.OneShotRemoteHandler
 import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TransitionFinishCallback
 import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility
 import com.android.wm.shell.windowdecor.MoveToDesktopAnimator
 import com.android.wm.shell.windowdecor.OnTaskRepositionAnimationListener
@@ -125,7 +129,7 @@
 import java.util.Optional
 import java.util.concurrent.Executor
 import java.util.function.Consumer
-
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger
 /** Handles moving tasks in and out of desktop */
 class DesktopTasksController(
     private val context: Context,
@@ -145,7 +149,7 @@
     private val desktopModeDragAndDropTransitionHandler: DesktopModeDragAndDropTransitionHandler,
     private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler,
     private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler,
-    private val immersiveTransitionHandler: DesktopFullImmersiveTransitionHandler,
+    private val desktopImmersiveController: DesktopImmersiveController,
     private val taskRepository: DesktopRepository,
     private val desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver,
     private val launchAdjacentController: LaunchAdjacentController,
@@ -158,6 +162,7 @@
     @ShellMainThread private val handler: Handler,
     private val inputManager: InputManager,
     private val focusTransitionObserver: FocusTransitionObserver,
+    private val desktopModeEventLogger: DesktopModeEventLogger,
 ) :
     RemoteCallable<DesktopTasksController>,
     Transitions.TransitionHandler,
@@ -250,7 +255,7 @@
         toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
         enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
         dragToDesktopTransitionHandler.onTaskResizeAnimationListener = listener
-        immersiveTransitionHandler.onTaskResizeAnimationListener = listener
+        desktopImmersiveController.onTaskResizeAnimationListener = listener
     }
 
     fun setOnTaskRepositionAnimationListener(listener: OnTaskRepositionAnimationListener) {
@@ -370,8 +375,11 @@
         // TODO(342378842): Instead of using default display, support multiple displays
         val taskToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask(
             DEFAULT_DISPLAY, wct, taskId)
-        val runOnTransit = immersiveTransitionHandler
-            .exitImmersiveIfApplicable(wct, DEFAULT_DISPLAY)
+        val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(
+            wct = wct,
+            displayId = DEFAULT_DISPLAY,
+            excludeTaskId = taskId,
+        )
         wct.startTask(
             taskId,
             ActivityOptions.makeBasic().apply {
@@ -398,7 +406,11 @@
         }
         logV("moveRunningTaskToDesktop taskId=%d", task.taskId)
         exitSplitIfApplicable(wct, task)
-        val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(wct, task.displayId)
+        val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(
+            wct = wct,
+            displayId = task.displayId,
+            excludeTaskId = task.taskId,
+        )
         // Bring other apps to front first
         val taskToMinimize =
             bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId)
@@ -443,7 +455,7 @@
         val taskToMinimize =
             bringDesktopAppsToFrontBeforeShowingNewTask(taskInfo.displayId, wct, taskInfo.taskId)
         addMoveToDesktopChanges(wct, taskInfo)
-        val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(
+        val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(
             wct, taskInfo.displayId)
         val transition = dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct)
         transition?.let {
@@ -490,7 +502,7 @@
                 taskId
             )
         )
-        return immersiveTransitionHandler.exitImmersiveIfApplicable(wct, taskInfo)
+        return desktopImmersiveController.exitImmersiveIfApplicable(wct, taskInfo)
     }
 
     fun minimizeTask(taskInfo: RunningTaskInfo) {
@@ -503,7 +515,7 @@
             removeWallpaperActivity(wct)
         }
         // Notify immersive handler as it might need to exit immersive state.
-        val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(wct, taskInfo)
+        val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(wct, taskInfo)
 
         wct.reorder(taskInfo.token, false)
         val transition = freeformTaskTransitionStarter.startMinimizedModeTransition(wct)
@@ -607,8 +619,11 @@
         logV("moveBackgroundTaskToFront taskId=%s", taskId)
         val wct = WindowContainerTransaction()
         // TODO: b/342378842 - Instead of using default display, support multiple displays
-        val runOnTransit = immersiveTransitionHandler
-            .exitImmersiveIfApplicable(wct, DEFAULT_DISPLAY)
+        val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(
+            wct = wct,
+            displayId = DEFAULT_DISPLAY,
+            excludeTaskId = taskId,
+        )
         wct.startTask(
             taskId,
             ActivityOptions.makeBasic().apply {
@@ -630,8 +645,11 @@
         logV("moveTaskToFront taskId=%s", taskInfo.taskId)
         val wct = WindowContainerTransaction()
         wct.reorder(taskInfo.token, true /* onTop */, true /* includingParents */)
-        val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(
-            wct, taskInfo.displayId)
+        val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(
+            wct = wct,
+            displayId = taskInfo.displayId,
+            excludeTaskId = taskInfo.taskId,
+        )
         val transition =
             startLaunchTransition(TRANSIT_TO_FRONT, wct, taskInfo.taskId, remoteTransition)
         runOnTransit?.invoke(transition)
@@ -734,12 +752,12 @@
 
     private fun moveDesktopTaskToFullImmersive(taskInfo: RunningTaskInfo) {
         check(taskInfo.isFreeform) { "Task must already be in freeform" }
-        immersiveTransitionHandler.moveTaskToImmersive(taskInfo)
+        desktopImmersiveController.moveTaskToImmersive(taskInfo)
     }
 
     private fun exitDesktopTaskFromFullImmersive(taskInfo: RunningTaskInfo) {
         check(taskInfo.isFreeform) { "Task must already be in freeform" }
-        immersiveTransitionHandler.moveTaskToNonImmersive(taskInfo)
+        desktopImmersiveController.moveTaskToNonImmersive(taskInfo)
     }
 
     /**
@@ -747,7 +765,11 @@
      * bounds) and a free floating state (either the last saved bounds if available or the default
      * bounds otherwise).
      */
-    fun toggleDesktopTaskSize(taskInfo: RunningTaskInfo) {
+    fun toggleDesktopTaskSize(
+        taskInfo: RunningTaskInfo,
+        resizeTrigger: ResizeTrigger,
+        motionEvent: MotionEvent?,
+    ) {
         val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
 
         val stableBounds = Rect().apply { displayLayout.getStableBounds(this) }
@@ -794,7 +816,10 @@
 
         taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding)
         val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds)
-
+        desktopModeEventLogger.logTaskResizingEnded(
+            resizeTrigger, motionEvent, taskInfo, destinationBounds.height(),
+            destinationBounds.width(), displayController
+        )
         toggleResizeDesktopTaskTransitionHandler.startTransition(wct)
     }
 
@@ -884,9 +909,19 @@
         taskInfo: RunningTaskInfo,
         taskSurface: SurfaceControl,
         currentDragBounds: Rect,
-        position: SnapPosition
+        position: SnapPosition,
+        resizeTrigger: ResizeTrigger,
+        motionEvent: MotionEvent?,
     ) {
         val destinationBounds = getSnapBounds(taskInfo, position)
+        desktopModeEventLogger.logTaskResizingEnded(
+            resizeTrigger,
+            motionEvent,
+            taskInfo,
+            destinationBounds.height(),
+            destinationBounds.width(),
+            displayController,
+        )
         if (destinationBounds == taskInfo.configuration.windowConfiguration.bounds) {
             // Handle the case where we attempt to snap resize when already snap resized: the task
             // position won't need to change but we want to animate the surface going back to the
@@ -915,7 +950,8 @@
         position: SnapPosition,
         taskSurface: SurfaceControl,
         currentDragBounds: Rect,
-        dragStartBounds: Rect
+        dragStartBounds: Rect,
+        motionEvent: MotionEvent,
     ) {
         releaseVisualIndicator()
         if (!taskInfo.isResizeable && DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE.isTrue()) {
@@ -932,10 +968,25 @@
                 isResizable = taskInfo.isResizeable,
             )
         } else {
+            val resizeTrigger = if (position == SnapPosition.LEFT) {
+                ResizeTrigger.DRAG_LEFT
+            } else {
+                ResizeTrigger.DRAG_RIGHT
+            }
+            desktopModeEventLogger.logTaskResizingStarted(
+                resizeTrigger, motionEvent, taskInfo, displayController
+            )
             interactionJankMonitor.begin(
                 taskSurface, context, handler, CUJ_DESKTOP_MODE_SNAP_RESIZE, "drag_resizable"
             )
-            snapToHalfScreen(taskInfo, taskSurface, currentDragBounds, position)
+            snapToHalfScreen(
+                taskInfo,
+                taskSurface,
+                currentDragBounds,
+                position,
+                resizeTrigger,
+                motionEvent,
+            )
         }
     }
 
@@ -1185,6 +1236,67 @@
         return result
     }
 
+    /** Whether the given [change] in the [transition] is a known desktop change. */
+    fun isDesktopChange(
+        transition: IBinder,
+        change: TransitionInfo.Change,
+    ): Boolean {
+        // Only the immersive controller is currently involved in mixed transitions.
+        return Flags.enableFullyImmersiveInDesktop()
+                && desktopImmersiveController.isImmersiveChange(transition, change)
+    }
+
+    /**
+     * Whether the given transition [info] will potentially include a desktop change, in which
+     * case the transition should be treated as mixed so that the change is in part animated by
+     * one of the desktop transition handlers.
+     */
+    fun shouldPlayDesktopAnimation(info: TransitionRequestInfo): Boolean {
+        // Only immersive mixed transition are currently supported.
+        if (!Flags.enableFullyImmersiveInDesktop()) return false
+        val triggerTask = info.triggerTask ?: return false
+        if (!isDesktopModeShowing(triggerTask.displayId)) {
+            return false
+        }
+        if (!TransitionUtil.isOpeningType(info.type)) {
+            return false
+        }
+        taskRepository.getTaskInFullImmersiveState(displayId = triggerTask.displayId)
+            ?: return false
+        return when {
+            triggerTask.isFullscreen -> {
+                // Trigger fullscreen task will enter desktop, so any existing immersive task
+                // should exit.
+                shouldFullscreenTaskLaunchSwitchToDesktop(triggerTask)
+            }
+            triggerTask.isFreeform -> {
+                // Trigger freeform task will enter desktop, so any existing immersive task should
+                // exit.
+                !shouldFreeformTaskLaunchSwitchToFullscreen(triggerTask)
+            }
+            else -> false
+        }
+    }
+
+    /** Animate a desktop change found in a mixed transitions. */
+    fun animateDesktopChange(
+        transition: IBinder,
+        change: Change,
+        startTransaction: Transaction,
+        finishTransaction: Transaction,
+        finishCallback: TransitionFinishCallback,
+    ) {
+        if (!desktopImmersiveController.isImmersiveChange(transition, change)) {
+            throw IllegalStateException("Only immersive changes support desktop mixed transitions")
+        }
+        desktopImmersiveController.animateResizeChange(
+            change,
+            startTransaction,
+            finishTransaction,
+            finishCallback
+        )
+    }
+
     private fun taskContainsDragAndDropCookie(taskInfo: RunningTaskInfo?) =
         taskInfo?.launchCookies?.any { it == dragAndDropFullscreenCookie } ?: false
 
@@ -1231,8 +1343,11 @@
             wct.startTask(requestedTaskId, options.toBundle())
             val taskToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask(
                 callingTask.displayId, wct, requestedTaskId)
-            val runOnTransit = immersiveTransitionHandler
-                .exitImmersiveIfApplicable(wct, callingTask.displayId)
+            val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(
+                wct = wct,
+                displayId = callingTask.displayId,
+                excludeTaskId = requestedTaskId,
+            )
             val transition = transitions.startTransition(TRANSIT_OPEN, wct, null)
             addPendingMinimizeTransition(transition, taskToMinimize)
             runOnTransit?.invoke(transition)
@@ -1341,7 +1456,7 @@
             return null
         }
         val wct = WindowContainerTransaction()
-        if (!isDesktopModeShowing(task.displayId)) {
+        if (shouldFreeformTaskLaunchSwitchToFullscreen(task)) {
             logD("Bring desktop tasks to front on transition=taskId=%d", task.taskId)
             if (taskRepository.isActiveTask(task.taskId) && !forceEnterDesktop(task.displayId)) {
                 // We are outside of desktop mode and already existing desktop task is being
@@ -1372,7 +1487,7 @@
         }
         // Desktop Mode is showing and we're launching a new Task:
         // 1) Exit immersive if needed.
-        immersiveTransitionHandler.exitImmersiveIfApplicable(transition, wct, task.displayId)
+        desktopImmersiveController.exitImmersiveIfApplicable(transition, wct, task.displayId)
         // 2) minimize a Task if needed.
         val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task.taskId)
         if (taskToMinimize != null) {
@@ -1387,7 +1502,7 @@
         transition: IBinder
     ): WindowContainerTransaction? {
         logV("handleFullscreenTaskLaunch")
-        if (isDesktopModeShowing(task.displayId) || forceEnterDesktop(task.displayId)) {
+        if (shouldFullscreenTaskLaunchSwitchToDesktop(task)) {
             logD("Switch fullscreen task to freeform on transition: taskId=%d", task.taskId)
             return WindowContainerTransaction().also { wct ->
                 addMoveToDesktopChanges(wct, task)
@@ -1403,7 +1518,7 @@
                 val taskToMinimize =
                     addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task.taskId)
                 addPendingMinimizeTransition(transition, taskToMinimize)
-                immersiveTransitionHandler.exitImmersiveIfApplicable(
+                desktopImmersiveController.exitImmersiveIfApplicable(
                     transition, wct, task.displayId
                 )
             }
@@ -1411,6 +1526,12 @@
         return null
     }
 
+    private fun shouldFreeformTaskLaunchSwitchToFullscreen(task: RunningTaskInfo): Boolean =
+        !isDesktopModeShowing(task.displayId)
+
+    private fun shouldFullscreenTaskLaunchSwitchToDesktop(task: RunningTaskInfo): Boolean =
+        isDesktopModeShowing(task.displayId) || forceEnterDesktop(task.displayId)
+
     /**
      * If a task is not compatible with desktop mode freeform, it should always be launched in
      * fullscreen.
@@ -1735,6 +1856,7 @@
         currentDragBounds: Rect,
         validDragArea: Rect,
         dragStartBounds: Rect,
+        motionEvent: MotionEvent,
     ) {
         if (taskInfo.configuration.windowConfiguration.windowingMode != WINDOWING_MODE_FREEFORM) {
             return
@@ -1755,12 +1877,22 @@
             }
             IndicatorType.TO_SPLIT_LEFT_INDICATOR -> {
                 handleSnapResizingTask(
-                    taskInfo, SnapPosition.LEFT, taskSurface, currentDragBounds, dragStartBounds
+                    taskInfo,
+                    SnapPosition.LEFT,
+                    taskSurface,
+                    currentDragBounds,
+                    dragStartBounds,
+                    motionEvent,
                 )
             }
             IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> {
                 handleSnapResizingTask(
-                    taskInfo, SnapPosition.RIGHT, taskSurface, currentDragBounds, dragStartBounds
+                    taskInfo,
+                    SnapPosition.RIGHT,
+                    taskSurface,
+                    currentDragBounds,
+                    dragStartBounds,
+                    motionEvent,
                 )
             }
             IndicatorType.NO_INDICATOR -> {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt
index 3f41d7c..2d11e02 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt
@@ -73,15 +73,14 @@
      */
     private suspend fun getDesktopRepositoryState(
         userId: Int = DEFAULT_USER_ID
-    ): DesktopRepositoryState =
+    ): DesktopRepositoryState? =
         try {
             dataStoreFlow
                 .first()
-                .desktopRepoByUserMap
-                .getOrDefault(userId, DesktopRepositoryState.getDefaultInstance())
+                .desktopRepoByUserMap[userId]
         } catch (e: Exception) {
             Log.e(TAG, "Unable to read from datastore", e)
-            DesktopRepositoryState.getDefaultInstance()
+            null
         }
 
     /**
@@ -91,13 +90,13 @@
     suspend fun readDesktop(
         userId: Int = DEFAULT_USER_ID,
         desktopId: Int = DEFAULT_DESKTOP_ID,
-    ): Desktop =
+    ): Desktop? =
         try {
             val repository = getDesktopRepositoryState(userId)
-            repository.getDesktopOrThrow(desktopId)
+            repository?.getDesktopOrThrow(desktopId)
         } catch (e: Exception) {
             Log.e(TAG, "Unable to get desktop info from persistent repository", e)
-            Desktop.getDefaultInstance()
+            null
         }
 
     /** Adds or updates a desktop stored in the datastore */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
index 771573d..7631ece 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
@@ -28,7 +28,7 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.window.flags.Flags;
-import com.android.wm.shell.desktopmode.DesktopFullImmersiveTransitionHandler;
+import com.android.wm.shell.desktopmode.DesktopImmersiveController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.FocusTransitionObserver;
 import com.android.wm.shell.transition.Transitions;
@@ -48,7 +48,7 @@
  */
 public class FreeformTaskTransitionObserver implements Transitions.TransitionObserver {
     private final Transitions mTransitions;
-    private final Optional<DesktopFullImmersiveTransitionHandler> mImmersiveTransitionHandler;
+    private final Optional<DesktopImmersiveController> mDesktopImmersiveController;
     private final WindowDecorViewModel mWindowDecorViewModel;
     private final Optional<TaskChangeListener> mTaskChangeListener;
     private final FocusTransitionObserver mFocusTransitionObserver;
@@ -60,12 +60,12 @@
             Context context,
             ShellInit shellInit,
             Transitions transitions,
-            Optional<DesktopFullImmersiveTransitionHandler> immersiveTransitionHandler,
+            Optional<DesktopImmersiveController> desktopImmersiveController,
             WindowDecorViewModel windowDecorViewModel,
             Optional<TaskChangeListener> taskChangeListener,
             FocusTransitionObserver focusTransitionObserver) {
         mTransitions = transitions;
-        mImmersiveTransitionHandler = immersiveTransitionHandler;
+        mDesktopImmersiveController = desktopImmersiveController;
         mWindowDecorViewModel = windowDecorViewModel;
         mTaskChangeListener = taskChangeListener;
         mFocusTransitionObserver = focusTransitionObserver;
@@ -89,7 +89,8 @@
             // TODO(b/367268953): Remove when DesktopTaskListener is introduced and the repository
             //  is updated from there **before** the |mWindowDecorViewModel| methods are invoked.
             //  Otherwise window decoration relayout won't run with the immersive state up to date.
-            mImmersiveTransitionHandler.ifPresent(h -> h.onTransitionReady(transition, info));
+            mDesktopImmersiveController.ifPresent(h ->
+                    h.onTransitionReady(transition, info, startT, finishT));
         }
         // Update focus state first to ensure the correct state can be queried from listeners.
         // TODO(371503964): Remove this once the unified task repository is ready.
@@ -194,10 +195,20 @@
     }
 
     @Override
-    public void onTransitionStarting(@NonNull IBinder transition) {}
+    public void onTransitionStarting(@NonNull IBinder transition) {
+        if (Flags.enableFullyImmersiveInDesktop()) {
+            // TODO(b/367268953): Remove when DesktopTaskListener is introduced.
+            mDesktopImmersiveController.ifPresent(h -> h.onTransitionStarting(transition));
+        }
+    }
 
     @Override
     public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {
+        if (Flags.enableFullyImmersiveInDesktop()) {
+            // TODO(b/367268953): Remove when DesktopTaskListener is introduced.
+            mDesktopImmersiveController.ifPresent(h -> h.onTransitionMerged(merged, playing));
+        }
+
         final List<ActivityManager.RunningTaskInfo> infoOfMerged =
                 mTransitionToTaskInfo.get(merged);
         if (infoOfMerged == null) {
@@ -218,6 +229,11 @@
 
     @Override
     public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) {
+        if (Flags.enableFullyImmersiveInDesktop()) {
+            // TODO(b/367268953): Remove when DesktopTaskListener is introduced.
+            mDesktopImmersiveController.ifPresent(h -> h.onTransitionFinished(transition, aborted));
+        }
+
         final List<ActivityManager.RunningTaskInfo> taskInfo =
                 mTransitionToTaskInfo.getOrDefault(transition, Collections.emptyList());
         mTransitionToTaskInfo.remove(transition);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index 766a6b3..0d89f75 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -83,8 +83,11 @@
         /** Both the display and split-state (enter/exit) is changing */
         static final int TYPE_DISPLAY_AND_SPLIT_CHANGE = 2;
 
-        /** Pip was entered while handling an intent with its own remoteTransition. */
-        static final int TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE = 3;
+        /**
+         * While handling an intent with its own remoteTransition, a PIP enter or Desktop immersive
+         * exit change is found.
+         */
+        static final int TYPE_OPTIONS_REMOTE_AND_PIP_OR_DESKTOP_CHANGE = 3;
 
         /** Recents transition while split-screen foreground. */
         static final int TYPE_RECENTS_DURING_SPLIT = 4;
@@ -110,6 +113,9 @@
         /** The display changes when pip is entering. */
         static final int TYPE_ENTER_PIP_WITH_DISPLAY_CHANGE = 11;
 
+        /** Open transition during a desktop session. */
+        static final int TYPE_OPEN_IN_DESKTOP = 12;
+
         /** The default animation for this mixed transition. */
         static final int ANIM_TYPE_DEFAULT = 0;
 
@@ -296,7 +302,7 @@
                 return null;
             }
             final MixedTransition mixed = createDefaultMixedTransition(
-                    MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE, transition);
+                    MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_OR_DESKTOP_CHANGE, transition);
             mixed.mLeftoversHandler = handler.first;
             mActiveTransitions.add(mixed);
             if (mixed.mLeftoversHandler != mPlayer.getRemoteTransitionHandler()) {
@@ -334,6 +340,20 @@
                         MixedTransition.TYPE_UNFOLD, transition));
             }
             return wct;
+        } else if (mDesktopTasksController != null
+                && mDesktopTasksController.shouldPlayDesktopAnimation(request)) {
+            final Pair<Transitions.TransitionHandler, WindowContainerTransaction> handler =
+                    mPlayer.dispatchRequest(transition, request, /* skip= */ this);
+            if (handler == null) {
+                return null;
+            }
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a desktop request, so"
+                    + " treat it as Mixed. handler=%s", handler.first);
+            final MixedTransition mixed = createDefaultMixedTransition(
+                    MixedTransition.TYPE_OPEN_IN_DESKTOP, transition);
+            mixed.mLeftoversHandler = handler.first;
+            mActiveTransitions.add(mixed);
+            return handler.second;
         }
         return null;
     }
@@ -341,7 +361,7 @@
     private DefaultMixedTransition createDefaultMixedTransition(int type, IBinder transition) {
         return new DefaultMixedTransition(
                 type, transition, mPlayer, this, mPipHandler, mSplitHandler, mKeyguardHandler,
-                mUnfoldHandler, mActivityEmbeddingController);
+                mUnfoldHandler, mActivityEmbeddingController, mDesktopTasksController);
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java
index c8921d2..3d3de88 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java
@@ -30,6 +30,7 @@
 
 import com.android.internal.protolog.ProtoLog;
 import com.android.wm.shell.activityembedding.ActivityEmbeddingController;
+import com.android.wm.shell.desktopmode.DesktopTasksController;
 import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
@@ -39,15 +40,19 @@
 class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition {
     private final UnfoldTransitionHandler mUnfoldHandler;
     private final ActivityEmbeddingController mActivityEmbeddingController;
+    @Nullable
+    private final DesktopTasksController mDesktopTasksController;
 
     DefaultMixedTransition(int type, IBinder transition, Transitions player,
             MixedTransitionHandler mixedHandler, PipTransitionController pipHandler,
             StageCoordinator splitHandler, KeyguardTransitionHandler keyguardHandler,
             UnfoldTransitionHandler unfoldHandler,
-            ActivityEmbeddingController activityEmbeddingController) {
+            ActivityEmbeddingController activityEmbeddingController,
+            @Nullable DesktopTasksController desktopTasksController) {
         super(type, transition, player, mixedHandler, pipHandler, splitHandler, keyguardHandler);
         mUnfoldHandler = unfoldHandler;
         mActivityEmbeddingController = activityEmbeddingController;
+        mDesktopTasksController = desktopTasksController;
 
         switch (type) {
             case TYPE_UNFOLD:
@@ -57,7 +62,8 @@
             case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
             case TYPE_ENTER_PIP_FROM_SPLIT:
             case TYPE_KEYGUARD:
-            case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
+            case TYPE_OPTIONS_REMOTE_AND_PIP_OR_DESKTOP_CHANGE:
+            case TYPE_OPEN_IN_DESKTOP:
             default:
                 break;
         }
@@ -85,11 +91,14 @@
             case TYPE_KEYGUARD ->
                     animateKeyguard(this, info, startTransaction, finishTransaction, finishCallback,
                             mKeyguardHandler, mPipHandler);
-            case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE ->
-                    animateOpenIntentWithRemoteAndPip(transition, info, startTransaction,
+            case TYPE_OPTIONS_REMOTE_AND_PIP_OR_DESKTOP_CHANGE ->
+                    animateOpenIntentWithRemoteAndPipOrDesktop(transition, info, startTransaction,
                             finishTransaction, finishCallback);
             case TYPE_UNFOLD ->
                     animateUnfold(info, startTransaction, finishTransaction, finishCallback);
+            case TYPE_OPEN_IN_DESKTOP ->
+                    animateOpenInDesktop(
+                            transition, info, startTransaction, finishTransaction, finishCallback);
             default -> throw new IllegalStateException(
                     "Starting default mixed animation with unknown or illegal type: " + mType);
         };
@@ -146,31 +155,34 @@
         return true;
     }
 
-    private boolean animateOpenIntentWithRemoteAndPip(
+    private boolean animateOpenIntentWithRemoteAndPipOrDesktop(
             @NonNull IBinder transition, @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Mixed transition for opening an intent"
-                + " with a remote transition and PIP #%d", info.getDebugId());
-        boolean handledToPip = tryAnimateOpenIntentWithRemoteAndPip(
+                + " with a remote transition and PIP or Desktop #%d", info.getDebugId());
+        boolean handledToPipOrDesktop = tryAnimateOpenIntentWithRemoteAndPipOrDesktop(
                 info, startTransaction, finishTransaction, finishCallback);
         // Consume the transition on remote handler if the leftover handler already handle this
         // transition. And if it cannot, the transition will be handled by remote handler, so don't
         // consume here.
-        // Need to check leftOverHandler as it may change in #animateOpenIntentWithRemoteAndPip
-        if (handledToPip && mHasRequestToRemote
+        // Need to check leftOverHandler as it may change in
+        // #animateOpenIntentWithRemoteAndPipOrDesktop
+        if (handledToPipOrDesktop && mHasRequestToRemote
                 && mLeftoversHandler != mPlayer.getRemoteTransitionHandler()) {
             mPlayer.getRemoteTransitionHandler().onTransitionConsumed(transition, false, null);
         }
-        return handledToPip;
+        return handledToPipOrDesktop;
     }
 
-    private boolean tryAnimateOpenIntentWithRemoteAndPip(
+    private boolean tryAnimateOpenIntentWithRemoteAndPipOrDesktop(
             @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+                "tryAnimateOpenIntentWithRemoteAndPipOrDesktop");
         TransitionInfo.Change pipChange = null;
         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
             TransitionInfo.Change change = info.getChanges().get(i);
@@ -183,13 +195,31 @@
                 info.getChanges().remove(i);
             }
         }
+        TransitionInfo.Change desktopChange = null;
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            TransitionInfo.Change change = info.getChanges().get(i);
+            if (mDesktopTasksController != null
+                    && mDesktopTasksController.isDesktopChange(mTransition, change)) {
+                if (desktopChange != null) {
+                    throw new IllegalStateException("More than 1 desktop changes in one"
+                            + " transition? " + info);
+                }
+                desktopChange = change;
+                info.getChanges().remove(i);
+            }
+        }
         Transitions.TransitionFinishCallback finishCB = (wct) -> {
             --mInFlightSubAnimations;
             joinFinishArgs(wct);
             if (mInFlightSubAnimations > 0) return;
             finishCallback.onTransitionFinished(mFinishWCT);
         };
-        if (pipChange == null) {
+        if ((pipChange == null && desktopChange == null)
+                || (pipChange != null && desktopChange != null)) {
+            // Don't split the transition. Let the leftovers handler handle it all.
+            // TODO: b/? - split the transition into three pieces when there's both a PIP and a
+            //  desktop change are present. For example, during remote intent open over a desktop
+            //  with both a PIP capable task and an immersive task.
             if (mLeftoversHandler != null) {
                 mInFlightSubAnimations = 1;
                 if (mLeftoversHandler.startAnimation(
@@ -198,27 +228,52 @@
                 }
             }
             return false;
-        }
-        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Splitting PIP into a separate"
-                + " animation because remote-animation likely doesn't support it #%d",
-                info.getDebugId());
-        // Split the transition into 2 parts: the pip part and the rest.
-        mInFlightSubAnimations = 2;
-        // make a new startTransaction because pip's startEnterAnimation "consumes" it so
-        // we need a separate one to send over to launcher.
-        SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
+        } else if (pipChange != null && desktopChange == null) {
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Splitting PIP into a separate"
+                            + " animation because remote-animation likely doesn't support it #%d",
+                    info.getDebugId());
+            // Split the transition into 2 parts: the pip part and the rest.
+            mInFlightSubAnimations = 2;
+            // make a new startTransaction because pip's startEnterAnimation "consumes" it so
+            // we need a separate one to send over to launcher.
+            SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
 
-        mPipHandler.startEnterAnimation(pipChange, otherStartT, finishTransaction, finishCB);
+            mPipHandler.startEnterAnimation(pipChange, otherStartT, finishTransaction, finishCB);
 
-        // Dispatch the rest of the transition normally.
-        if (mLeftoversHandler != null
-                && mLeftoversHandler.startAnimation(mTransition, info,
-                startTransaction, finishTransaction, finishCB)) {
+            // Dispatch the rest of the transition normally.
+            if (mLeftoversHandler != null
+                    && mLeftoversHandler.startAnimation(mTransition, info,
+                    startTransaction, finishTransaction, finishCB)) {
+                return true;
+            }
+            mLeftoversHandler = mPlayer.dispatchTransition(
+                    mTransition, info, startTransaction, finishTransaction, finishCB,
+                    mMixedHandler);
             return true;
+        } else if (pipChange == null && desktopChange != null) {
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Splitting desktop change into a"
+                            + "separate animation because remote-animation likely doesn't support"
+                            + "it #%d", info.getDebugId());
+            mInFlightSubAnimations = 2;
+            SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
+
+            mDesktopTasksController.animateDesktopChange(
+                            mTransition, desktopChange, otherStartT, finishTransaction, finishCB);
+
+            // Dispatch the rest of the transition normally.
+            if (mLeftoversHandler != null
+                    && mLeftoversHandler.startAnimation(mTransition, info,
+                    startTransaction, finishTransaction, finishCB)) {
+                return true;
+            }
+            mLeftoversHandler = mPlayer.dispatchTransition(
+                    mTransition, info, startTransaction, finishTransaction, finishCB,
+                    mMixedHandler);
+            return true;
+        } else {
+            throw new IllegalStateException(
+                    "All PIP and Immersive combinations should've been handled");
         }
-        mLeftoversHandler = mPlayer.dispatchTransition(
-                mTransition, info, startTransaction, finishTransaction, finishCB, mMixedHandler);
-        return true;
     }
 
     private boolean animateUnfold(
@@ -246,6 +301,51 @@
                 mTransition, info, startTransaction, finishTransaction, finishCB);
     }
 
+    private boolean animateOpenInDesktop(
+            @NonNull IBinder transition,
+            @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "animateOpenInDesktop");
+        TransitionInfo.Change desktopChange = null;
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            TransitionInfo.Change change = info.getChanges().get(i);
+            if (mDesktopTasksController.isDesktopChange(mTransition, change)) {
+                if (desktopChange != null) {
+                    throw new IllegalStateException("More than 1 desktop changes in one"
+                            + " transition? " + info);
+                }
+                desktopChange = change;
+                info.getChanges().remove(i);
+            }
+        }
+        final Transitions.TransitionFinishCallback finishCB = (wct) -> {
+            --mInFlightSubAnimations;
+            joinFinishArgs(wct);
+            if (mInFlightSubAnimations > 0) return;
+            finishCallback.onTransitionFinished(mFinishWCT);
+        };
+        if (desktopChange == null) {
+            if (mLeftoversHandler != null) {
+                mInFlightSubAnimations = 1;
+                if (mLeftoversHandler.startAnimation(
+                        mTransition, info, startTransaction, finishTransaction, finishCB)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Splitting desktop change into a"
+                + "separate animation #%d", info.getDebugId());
+        mInFlightSubAnimations = 2;
+        mDesktopTasksController.animateDesktopChange(
+                transition, desktopChange, startTransaction, finishTransaction, finishCB);
+        mLeftoversHandler = mPlayer.dispatchTransition(
+                mTransition, info, startTransaction, finishTransaction, finishCB, mMixedHandler);
+        return true;
+    }
+
     @Override
     void mergeAnimation(
             @NonNull IBinder transition, @NonNull TransitionInfo info,
@@ -279,7 +379,7 @@
             case TYPE_KEYGUARD:
                 mKeyguardHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
                 return;
-            case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
+            case TYPE_OPTIONS_REMOTE_AND_PIP_OR_DESKTOP_CHANGE:
                 mPipHandler.end();
                 if (mLeftoversHandler != null) {
                     mLeftoversHandler.mergeAnimation(
@@ -289,6 +389,10 @@
             case TYPE_UNFOLD:
                 mUnfoldHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
                 return;
+            case TYPE_OPEN_IN_DESKTOP:
+                mDesktopTasksController.mergeAnimation(
+                        transition, info, t, mergeTarget, finishCallback);
+                return;
             default:
                 throw new IllegalStateException("Playing a default mixed transition with unknown or"
                         + " illegal type: " + mType);
@@ -310,12 +414,14 @@
             case TYPE_KEYGUARD:
                 mKeyguardHandler.onTransitionConsumed(transition, aborted, finishT);
                 break;
-            case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
+            case TYPE_OPTIONS_REMOTE_AND_PIP_OR_DESKTOP_CHANGE:
                 mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT);
                 break;
             case TYPE_UNFOLD:
                 mUnfoldHandler.onTransitionConsumed(transition, aborted, finishT);
                 break;
+            case TYPE_OPEN_IN_DESKTOP:
+                mDesktopTasksController.onTransitionConsumed(transition, aborted, finishT);
             default:
                 break;
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 509cb85..fde01ee 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -274,6 +274,7 @@
             closeDragResizeListener();
             mDragResizeListener = new DragResizeInputListener(
                     mContext,
+                    mTaskInfo,
                     mHandler,
                     mChoreographer,
                     mDisplay.getDisplayId(),
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 a06b4a2..a775cbc 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
@@ -32,6 +32,7 @@
 
 import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU;
 import static com.android.wm.shell.compatui.AppCompatUtils.isTopActivityExemptFromDesktopWindowing;
+import static com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger;
 import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR;
 import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR;
 import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR;
@@ -103,6 +104,7 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler;
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
 import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator;
 import com.android.wm.shell.desktopmode.DesktopRepository;
 import com.android.wm.shell.desktopmode.DesktopTasksController;
@@ -221,6 +223,7 @@
             };
     private final TaskPositionerFactory mTaskPositionerFactory;
     private final FocusTransitionObserver mFocusTransitionObserver;
+    private final DesktopModeEventLogger mDesktopModeEventLogger;
 
     public DesktopModeWindowDecorViewModel(
             Context context,
@@ -248,7 +251,8 @@
             AppHandleEducationController appHandleEducationController,
             WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
             Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler,
-            FocusTransitionObserver focusTransitionObserver) {
+            FocusTransitionObserver focusTransitionObserver,
+            DesktopModeEventLogger desktopModeEventLogger) {
         this(
                 context,
                 shellExecutor,
@@ -281,7 +285,8 @@
                 windowDecorCaptionHandleRepository,
                 activityOrientationChangeHandler,
                 new TaskPositionerFactory(),
-                focusTransitionObserver);
+                focusTransitionObserver,
+                desktopModeEventLogger);
     }
 
     @VisibleForTesting
@@ -317,7 +322,8 @@
             WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
             Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler,
             TaskPositionerFactory taskPositionerFactory,
-            FocusTransitionObserver focusTransitionObserver) {
+            FocusTransitionObserver focusTransitionObserver,
+            DesktopModeEventLogger desktopModeEventLogger) {
         mContext = context;
         mMainExecutor = shellExecutor;
         mMainHandler = mainHandler;
@@ -378,6 +384,7 @@
         };
         mTaskPositionerFactory = taskPositionerFactory;
         mFocusTransitionObserver = focusTransitionObserver;
+        mDesktopModeEventLogger = desktopModeEventLogger;
 
         shellInit.addInitCallback(this::onInit, this);
     }
@@ -547,15 +554,20 @@
                 >= MANAGE_WINDOWS_MINIMUM_INSTANCES);
     }
 
-    private void onMaximizeOrRestore(int taskId, String source) {
+    private void onMaximizeOrRestore(int taskId, String source, ResizeTrigger resizeTrigger,
+            MotionEvent motionEvent) {
         final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
         if (decoration == null) {
             return;
         }
+        mDesktopModeEventLogger.logTaskResizingStarted(resizeTrigger, motionEvent,
+                decoration.mTaskInfo,
+                mDisplayController, /* displayLayoutSize= */ null);
         mInteractionJankMonitor.begin(
                 decoration.mTaskSurface, mContext, mMainHandler,
                 Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, source);
-        mDesktopTasksController.toggleDesktopTaskSize(decoration.mTaskInfo);
+        mDesktopTasksController.toggleDesktopTaskSize(decoration.mTaskInfo, resizeTrigger,
+                motionEvent);
         decoration.closeHandleMenu();
         decoration.closeMaximizeMenu();
     }
@@ -568,7 +580,7 @@
         mDesktopTasksController.toggleDesktopTaskFullImmersiveState(decoration.mTaskInfo);
     }
 
-    private void onSnapResize(int taskId, boolean left) {
+    private void onSnapResize(int taskId, boolean left, MotionEvent motionEvent) {
         final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
         if (decoration == null) {
             return;
@@ -579,13 +591,20 @@
             Toast.makeText(mContext,
                     R.string.desktop_mode_non_resizable_snap_text, Toast.LENGTH_SHORT).show();
         } else {
+            ResizeTrigger resizeTrigger =
+                    left ? ResizeTrigger.SNAP_LEFT_MENU : ResizeTrigger.SNAP_RIGHT_MENU;
+            mDesktopModeEventLogger.logTaskResizingStarted(resizeTrigger, motionEvent,
+                    decoration.mTaskInfo,
+                    mDisplayController, /* displayLayoutSize= */ null);
             mInteractionJankMonitor.begin(decoration.mTaskSurface, mContext, mMainHandler,
                     Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE, "maximize_menu_resizable");
             mDesktopTasksController.snapToHalfScreen(
                     decoration.mTaskInfo,
                     decoration.mTaskSurface,
                     decoration.mTaskInfo.configuration.windowConfiguration.getBounds(),
-                    left ? SnapPosition.LEFT : SnapPosition.RIGHT);
+                    left ? SnapPosition.LEFT : SnapPosition.RIGHT,
+                    resizeTrigger,
+                    motionEvent);
         }
 
         decoration.closeHandleMenu();
@@ -737,6 +756,7 @@
         private boolean mTouchscreenInUse;
         private boolean mHasLongClicked;
         private int mDragPointerId = -1;
+        private MotionEvent mMotionEvent;
 
         private DesktopModeTouchEventListener(
                 RunningTaskInfo taskInfo,
@@ -798,7 +818,8 @@
                 } else {
                     // Full immersive is disabled or task doesn't request/support it, so just
                     // toggle between maximize/restore states.
-                    onMaximizeOrRestore(decoration.mTaskInfo.taskId, "caption_bar_button");
+                    onMaximizeOrRestore(decoration.mTaskInfo.taskId, "caption_bar_button",
+                            ResizeTrigger.MAXIMIZE_BUTTON, mMotionEvent);
                 }
             } else if (id == R.id.minimize_window) {
                 mDesktopTasksController.minimizeTask(decoration.mTaskInfo);
@@ -807,6 +828,7 @@
 
         @Override
         public boolean onTouch(View v, MotionEvent e) {
+            mMotionEvent = e;
             final int id = v.getId();
             final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
             if ((e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN) {
@@ -897,6 +919,7 @@
          */
         @Override
         public boolean onGenericMotion(View v, MotionEvent ev) {
+            mMotionEvent = ev;
             final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
             final int id = v.getId();
             if (ev.getAction() == ACTION_HOVER_ENTER && id == R.id.maximize_window) {
@@ -1040,7 +1063,7 @@
                             taskInfo, decoration.mTaskSurface, position,
                             new PointF(e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)),
                             newTaskBounds, decoration.calculateValidDragArea(),
-                            new Rect(mOnDragStartInitialBounds));
+                            new Rect(mOnDragStartInitialBounds), e);
                     if (touchingButton && !mHasLongClicked) {
                         // We need the input event to not be consumed here to end the ripple
                         // effect on the touched button. We will reset drag state in the ensuing
@@ -1087,7 +1110,7 @@
                 // Disallow double-tap to resize when in full immersive.
                 return false;
             }
-            onMaximizeOrRestore(mTaskId, "double_tap");
+            onMaximizeOrRestore(mTaskId, "double_tap", ResizeTrigger.DOUBLE_TAP_APP_HEADER, e);
             return true;
         }
     }
@@ -1484,7 +1507,8 @@
                         mGenericLinksParser,
                         mAssistContentRequester,
                         mMultiInstanceHelper,
-                        mWindowDecorCaptionHandleRepository);
+                        mWindowDecorCaptionHandleRepository,
+                        mDesktopModeEventLogger);
         mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration);
 
         final TaskPositioner taskPositioner = mTaskPositionerFactory.create(
@@ -1501,15 +1525,16 @@
         final DesktopModeTouchEventListener touchEventListener =
                 new DesktopModeTouchEventListener(taskInfo, taskPositioner);
         windowDecoration.setOnMaximizeOrRestoreClickListener(() -> {
-            onMaximizeOrRestore(taskInfo.taskId, "maximize_menu");
+            onMaximizeOrRestore(taskInfo.taskId, "maximize_menu", ResizeTrigger.MAXIMIZE_MENU,
+                    touchEventListener.mMotionEvent);
             return Unit.INSTANCE;
         });
         windowDecoration.setOnLeftSnapClickListener(() -> {
-            onSnapResize(taskInfo.taskId, true /* isLeft */);
+            onSnapResize(taskInfo.taskId, /* isLeft= */ true, touchEventListener.mMotionEvent);
             return Unit.INSTANCE;
         });
         windowDecoration.setOnRightSnapClickListener(() -> {
-            onSnapResize(taskInfo.taskId, false /* isLeft */);
+            onSnapResize(taskInfo.taskId, /* isLeft= */ false, touchEventListener.mMotionEvent);
             return Unit.INSTANCE;
         });
         windowDecoration.setOnToDesktopClickListener(desktopModeTransitionSource -> {
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 6eb20b9..d94f3a9 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
@@ -94,6 +94,7 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.desktopmode.CaptionState;
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
 import com.android.wm.shell.desktopmode.DesktopRepository;
 import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
 import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
@@ -216,7 +217,8 @@
             AppToWebGenericLinksParser genericLinksParser,
             AssistContentRequester assistContentRequester,
             MultiInstanceHelper multiInstanceHelper,
-            WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository) {
+            WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
+            DesktopModeEventLogger desktopModeEventLogger) {
         this (context, userContext, displayController, splitScreenController, desktopRepository,
                 taskOrganizer, taskInfo, taskSurface, handler, bgExecutor, choreographer, syncQueue,
                 appHeaderViewHolderFactory, rootTaskDisplayAreaOrganizer, genericLinksParser,
@@ -227,7 +229,7 @@
                 new SurfaceControlViewHostFactory() {},
                 DefaultMaximizeMenuFactory.INSTANCE,
                 DefaultHandleMenuFactory.INSTANCE, multiInstanceHelper,
-                windowDecorCaptionHandleRepository);
+                windowDecorCaptionHandleRepository, desktopModeEventLogger);
     }
 
     DesktopModeWindowDecoration(
@@ -256,11 +258,12 @@
             MaximizeMenuFactory maximizeMenuFactory,
             HandleMenuFactory handleMenuFactory,
             MultiInstanceHelper multiInstanceHelper,
-            WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository) {
+            WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
+            DesktopModeEventLogger desktopModeEventLogger) {
         super(context, userContext, displayController, taskOrganizer, taskInfo, taskSurface,
                 surfaceControlBuilderSupplier, surfaceControlTransactionSupplier,
                 windowContainerTransactionSupplier, surfaceControlSupplier,
-                surfaceControlViewHostFactory);
+                surfaceControlViewHostFactory, desktopModeEventLogger);
         mSplitScreenController = splitScreenController;
         mHandler = handler;
         mBgExecutor = bgExecutor;
@@ -605,6 +608,7 @@
             Trace.beginSection("DesktopModeWindowDecoration#relayout-DragResizeInputListener");
             mDragResizeListener = new DragResizeInputListener(
                     mContext,
+                    mTaskInfo,
                     mHandler,
                     mChoreographer,
                     mDisplay.getDisplayId(),
@@ -612,7 +616,8 @@
                     mDragPositioningCallback,
                     mSurfaceControlBuilderSupplier,
                     mSurfaceControlTransactionSupplier,
-                    mDisplayController);
+                    mDisplayController,
+                    mDesktopModeEventLogger);
             Trace.endSection();
         }
 
@@ -1700,7 +1705,8 @@
                 AppToWebGenericLinksParser genericLinksParser,
                 AssistContentRequester assistContentRequester,
                 MultiInstanceHelper multiInstanceHelper,
-                WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository) {
+                WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
+                DesktopModeEventLogger desktopModeEventLogger) {
             return new DesktopModeWindowDecoration(
                     context,
                     userContext,
@@ -1719,7 +1725,8 @@
                     genericLinksParser,
                     assistContentRequester,
                     multiInstanceHelper,
-                    windowDecorCaptionHandleRepository);
+                    windowDecorCaptionHandleRepository,
+                    desktopModeEventLogger);
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
index 60c9222..78e7962 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
@@ -29,8 +29,6 @@
 import android.view.SurfaceControl;
 import android.window.DesktopModeFlags;
 
-import androidx.annotation.NonNull;
-
 import com.android.wm.shell.R;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
@@ -129,13 +127,15 @@
 
         // If width or height are negative or exceeding the width or height constraints, revert the
         // respective bounds to use previous bound dimensions.
-        if (isExceedingWidthConstraint(repositionTaskBounds, stableBounds, displayController,
+        if (isExceedingWidthConstraint(repositionTaskBounds.width(),
+                /* startingWidth= */ oldRight - oldLeft, stableBounds, displayController,
                 windowDecoration)) {
             repositionTaskBounds.right = oldRight;
             repositionTaskBounds.left = oldLeft;
             isAspectRatioMaintained = false;
         }
-        if (isExceedingHeightConstraint(repositionTaskBounds, stableBounds, displayController,
+        if (isExceedingHeightConstraint(repositionTaskBounds.height(),
+                /* startingHeight= */oldBottom - oldTop, stableBounds, displayController,
                 windowDecoration)) {
             repositionTaskBounds.top = oldTop;
             repositionTaskBounds.bottom = oldBottom;
@@ -208,28 +208,34 @@
         return result;
     }
 
-    private static boolean isExceedingWidthConstraint(@NonNull Rect repositionTaskBounds,
+    private static boolean isExceedingWidthConstraint(int repositionedWidth, int startingWidth,
             Rect maxResizeBounds, DisplayController displayController,
             WindowDecoration windowDecoration) {
+        boolean isSizeIncreasing = (repositionedWidth - startingWidth) > 0;
         // Check if width is less than the minimum width constraint.
-        if (repositionTaskBounds.width() < getMinWidth(displayController, windowDecoration)) {
-            return true;
+        if (repositionedWidth < getMinWidth(displayController, windowDecoration)) {
+            // Only allow width to be increased if it is already below minimum.
+            return !isSizeIncreasing;
         }
         // Check if width is more than the maximum resize bounds on desktop windowing mode.
+        // Only allow width to be decreased if it already exceeds maximum.
         return isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext)
-                && repositionTaskBounds.width() > maxResizeBounds.width();
+                && repositionedWidth > maxResizeBounds.width() && isSizeIncreasing;
     }
 
-    private static boolean isExceedingHeightConstraint(@NonNull Rect repositionTaskBounds,
+    private static boolean isExceedingHeightConstraint(int repositionedHeight, int startingHeight,
             Rect maxResizeBounds, DisplayController displayController,
             WindowDecoration windowDecoration) {
+        boolean isSizeIncreasing = (repositionedHeight - startingHeight) > 0;
         // Check if height is less than the minimum height constraint.
-        if (repositionTaskBounds.height() < getMinHeight(displayController, windowDecoration)) {
-            return true;
+        if (repositionedHeight < getMinHeight(displayController, windowDecoration)) {
+            // Only allow height to be increased if it is already below minimum.
+            return !isSizeIncreasing;
         }
         // Check if height is more than the maximum resize bounds on desktop windowing mode.
+        // Only allow height to be decreased if it already exceeds maximum.
         return isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext)
-                && repositionTaskBounds.height() > maxResizeBounds.height();
+                && repositionedHeight > maxResizeBounds.height() && isSizeIncreasing;
     }
 
     private static float getMinWidth(DisplayController displayController,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
index 4ff394e..4204097 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
@@ -29,10 +29,12 @@
 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT;
 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT;
 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP;
+import static com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger;
 import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.isEdgeResizePermitted;
 import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.isEventFromTouchscreen;
 
 import android.annotation.NonNull;
+import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -59,6 +61,7 @@
 import com.android.internal.protolog.ProtoLog;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
 
 import java.util.function.Consumer;
 import java.util.function.Supplier;
@@ -83,14 +86,17 @@
     private final TaskResizeInputEventReceiver mInputEventReceiver;
 
     private final Context mContext;
+    private final RunningTaskInfo mTaskInfo;
     private final SurfaceControl mInputSinkSurface;
     private final IBinder mSinkClientToken;
     private final InputChannel mSinkInputChannel;
     private final DisplayController mDisplayController;
+    private final DesktopModeEventLogger mDesktopModeEventLogger;
     private final Region mTouchRegion = new Region();
 
     DragResizeInputListener(
             Context context,
+            RunningTaskInfo taskInfo,
             Handler handler,
             Choreographer choreographer,
             int displayId,
@@ -98,12 +104,15 @@
             DragPositioningCallback callback,
             Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier,
             Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier,
-            DisplayController displayController) {
+            DisplayController displayController,
+            DesktopModeEventLogger desktopModeEventLogger) {
         mContext = context;
+        mTaskInfo = taskInfo;
         mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier;
         mDisplayId = displayId;
         mDecorationSurface = decorationSurface;
         mDisplayController = displayController;
+        mDesktopModeEventLogger = desktopModeEventLogger;
         mClientToken = new Binder();
         final InputTransferToken inputTransferToken = new InputTransferToken();
         mInputChannel = new InputChannel();
@@ -125,11 +134,12 @@
             e.rethrowFromSystemServer();
         }
 
-        mInputEventReceiver = new TaskResizeInputEventReceiver(context, mInputChannel, callback,
+        mInputEventReceiver = new TaskResizeInputEventReceiver(context, mTaskInfo, mInputChannel,
+                callback,
                 handler, choreographer, () -> {
             final DisplayLayout layout = mDisplayController.getDisplayLayout(mDisplayId);
             return new Size(layout.width(), layout.height());
-        }, this::updateSinkInputChannel);
+        }, this::updateSinkInputChannel, mDesktopModeEventLogger);
         mInputEventReceiver.setTouchSlop(ViewConfiguration.get(context).getScaledTouchSlop());
 
         mInputSinkSurface = surfaceControlBuilderSupplier.get()
@@ -163,6 +173,22 @@
         }
     }
 
+    DragResizeInputListener(
+            Context context,
+            RunningTaskInfo taskInfo,
+            Handler handler,
+            Choreographer choreographer,
+            int displayId,
+            SurfaceControl decorationSurface,
+            DragPositioningCallback callback,
+            Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier,
+            Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier,
+            DisplayController displayController) {
+        this(context, taskInfo, handler, choreographer, displayId, decorationSurface, callback,
+                surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, displayController,
+                new DesktopModeEventLogger());
+    }
+
     /**
      * Updates the geometry (the touch region) of this drag resize handler.
      *
@@ -274,6 +300,7 @@
     private static class TaskResizeInputEventReceiver extends InputEventReceiver implements
             DragDetector.MotionEventHandler {
         @NonNull private final Context mContext;
+        @NonNull private final RunningTaskInfo mTaskInfo;
         private final InputManager mInputManager;
         @NonNull private final InputChannel mInputChannel;
         @NonNull private final DragPositioningCallback mCallback;
@@ -282,6 +309,7 @@
         @NonNull private final DragDetector mDragDetector;
         @NonNull private final Supplier<Size> mDisplayLayoutSizeSupplier;
         @NonNull private final Consumer<Region> mTouchRegionConsumer;
+        @NonNull private final DesktopModeEventLogger mDesktopModeEventLogger;
         private final Rect mTmpRect = new Rect();
         private boolean mConsumeBatchEventScheduled;
         private DragResizeWindowGeometry mDragResizeWindowGeometry;
@@ -293,15 +321,24 @@
         // resize events. For example, if multiple fingers are touching the screen, then each one
         // has a separate pointer id, but we only accept drag input from one.
         private int mDragPointerId = -1;
+        // The type of resizing that is currently being done. Used to track the same resize trigger
+        // on start and end of the resizing action.
+        private ResizeTrigger mResizeTrigger = ResizeTrigger.UNKNOWN_RESIZE_TRIGGER;
+        // The last MotionEvent on ACTION_DOWN, used to track the input tool type and source for
+        // logging the start and end of the resizing action.
+        private MotionEvent mLastMotionEventOnDown;
 
         private TaskResizeInputEventReceiver(@NonNull Context context,
+                @NonNull RunningTaskInfo taskInfo,
                 @NonNull InputChannel inputChannel,
                 @NonNull DragPositioningCallback callback, @NonNull Handler handler,
                 @NonNull Choreographer choreographer,
                 @NonNull Supplier<Size> displayLayoutSizeSupplier,
-                @NonNull Consumer<Region> touchRegionConsumer) {
+                @NonNull Consumer<Region> touchRegionConsumer,
+                @NonNull DesktopModeEventLogger desktopModeEventLogger) {
             super(inputChannel, handler.getLooper());
             mContext = context;
+            mTaskInfo = taskInfo;
             mInputManager = context.getSystemService(InputManager.class);
             mInputChannel = inputChannel;
             mCallback = callback;
@@ -322,6 +359,7 @@
                     ViewConfiguration.get(mContext).getScaledTouchSlop());
             mDisplayLayoutSizeSupplier = displayLayoutSizeSupplier;
             mTouchRegionConsumer = touchRegionConsumer;
+            mDesktopModeEventLogger = desktopModeEventLogger;
         }
 
         /**
@@ -395,6 +433,7 @@
         @Override
         public boolean handleMotionEvent(View v, MotionEvent e) {
             boolean result = false;
+
             // Check if this is a touch event vs mouse event.
             // Touch events are tracked in four corners. Other events are tracked in resize edges.
             switch (e.getActionMasked()) {
@@ -416,6 +455,13 @@
                                 "%s: Handling action down, update ctrlType to %d", TAG, ctrlType);
                         mDragStartTaskBounds = mCallback.onDragPositioningStart(ctrlType,
                                 rawX, rawY);
+                        mLastMotionEventOnDown = e;
+                        mResizeTrigger = (ctrlType == CTRL_TYPE_BOTTOM || ctrlType == CTRL_TYPE_TOP
+                                || ctrlType == CTRL_TYPE_RIGHT || ctrlType == CTRL_TYPE_LEFT)
+                                ? ResizeTrigger.EDGE : ResizeTrigger.CORNER;
+                        mDesktopModeEventLogger.logTaskResizingStarted(mResizeTrigger,
+                                e, mTaskInfo, /* displayController= */ null,
+                                /* displayLayoutSize= */ mDisplayLayoutSizeSupplier.get());
                         // Increase the input sink region to cover the whole screen; this is to
                         // prevent input and focus from going to other tasks during a drag resize.
                         updateInputSinkRegionForDrag(mDragStartTaskBounds);
@@ -464,6 +510,12 @@
                         if (taskBounds.equals(mDragStartTaskBounds)) {
                             mTouchRegionConsumer.accept(mTouchRegion);
                         }
+
+                        mDesktopModeEventLogger.logTaskResizingEnded(mResizeTrigger,
+                                mLastMotionEventOnDown, mTaskInfo, taskBounds.height(),
+                                taskBounds.width(),
+                                /* displayController= */ null,
+                                /* displayLayoutSize= */ mDisplayLayoutSizeSupplier.get());
                     }
                     mShouldHandleEvents = false;
                     mDragPointerId = -1;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
index 33d1c26..844ceb3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
@@ -181,7 +181,7 @@
     }
 
     private boolean isInEdgeResizeBounds(float x, float y) {
-        return calculateEdgeResizeCtrlType(x, y) != 0;
+        return calculateEdgeResizeCtrlType(x, y) != CTRL_TYPE_UNDEFINED;
     }
 
     /**
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 6b3b357..34cc098 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
@@ -58,6 +58,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams.OccludingCaptionElement;
 import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer;
@@ -111,6 +112,7 @@
     final Context mContext;
     final @NonNull Context mUserContext;
     final @NonNull DisplayController mDisplayController;
+    final @NonNull DesktopModeEventLogger mDesktopModeEventLogger;
     final ShellTaskOrganizer mTaskOrganizer;
     final Supplier<SurfaceControl.Builder> mSurfaceControlBuilderSupplier;
     final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier;
@@ -163,7 +165,7 @@
         this(context, userContext, displayController, taskOrganizer, taskInfo, taskSurface,
                 SurfaceControl.Builder::new, SurfaceControl.Transaction::new,
                 WindowContainerTransaction::new, SurfaceControl::new,
-                new SurfaceControlViewHostFactory() {});
+                new SurfaceControlViewHostFactory() {}, new DesktopModeEventLogger());
     }
 
     WindowDecoration(
@@ -177,13 +179,16 @@
             Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier,
             Supplier<WindowContainerTransaction> windowContainerTransactionSupplier,
             Supplier<SurfaceControl> surfaceControlSupplier,
-            SurfaceControlViewHostFactory surfaceControlViewHostFactory) {
+            SurfaceControlViewHostFactory surfaceControlViewHostFactory,
+            @NonNull DesktopModeEventLogger desktopModeEventLogger
+    ) {
         mContext = context;
         mUserContext = userContext;
         mDisplayController = displayController;
         mTaskOrganizer = taskOrganizer;
         mTaskInfo = taskInfo;
         mTaskSurface = cloneSurfaceControl(taskSurface, surfaceControlSupplier);
+        mDesktopModeEventLogger = desktopModeEventLogger;
         mSurfaceControlBuilderSupplier = surfaceControlBuilderSupplier;
         mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier;
         mWindowContainerTransactionSupplier = windowContainerTransactionSupplier;
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt
index b7ddfd1..4fe66f3 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt
@@ -298,12 +298,18 @@
             FlickerConfigEntry(
                 scenarioId = ScenarioId("MAXIMIZE_APP"),
                 extractor =
-                TaggedScenarioExtractorBuilder()
-                    .setTargetTag(CujType.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW)
-                    .setTransitionMatcher(
-                        TaggedCujTransitionMatcher(associatedTransitionRequired = false)
-                    )
-                    .build(),
+                ShellTransitionScenarioExtractor(
+                    transitionMatcher =
+                    object : ITransitionMatcher {
+                        override fun findAll(
+                            transitions: Collection<Transition>
+                        ): Collection<Transition> {
+                            return transitions.filter {
+                                it.type == TransitionType.DESKTOP_MODE_TOGGLE_RESIZE
+                            }
+                        }
+                    }
+                ),
                 assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS +
                         listOf(
                             AppLayerIncreasesInSize(DESKTOP_MODE_APP),
@@ -316,12 +322,18 @@
             FlickerConfigEntry(
                 scenarioId = ScenarioId("MAXIMIZE_APP_NON_RESIZABLE"),
                 extractor =
-                TaggedScenarioExtractorBuilder()
-                    .setTargetTag(CujType.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW)
-                    .setTransitionMatcher(
-                        TaggedCujTransitionMatcher(associatedTransitionRequired = false)
-                    )
-                    .build(),
+                ShellTransitionScenarioExtractor(
+                    transitionMatcher =
+                    object : ITransitionMatcher {
+                        override fun findAll(
+                            transitions: Collection<Transition>
+                        ): Collection<Transition> {
+                            return transitions.filter {
+                                it.type == TransitionType.DESKTOP_MODE_TOGGLE_RESIZE
+                            }
+                        }
+                    }
+                ),
                 assertions =
                 AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS +
                         listOf(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java
index e6bd05b..f935ac7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java
@@ -40,6 +40,8 @@
 
     private WindowContainerToken mToken = createMockWCToken();
     private int mParentTaskId = INVALID_TASK_ID;
+    private int mUid = INVALID_TASK_ID;
+    private int mTaskId = INVALID_TASK_ID;
     private Intent mBaseIntent = new Intent();
     private @WindowConfiguration.ActivityType int mActivityType = ACTIVITY_TYPE_STANDARD;
     private @WindowConfiguration.WindowingMode int mWindowingMode = WINDOWING_MODE_UNDEFINED;
@@ -73,6 +75,18 @@
         return this;
     }
 
+    /** Sets the task info's effective UID. */
+    public TestRunningTaskInfoBuilder setUid(int uid) {
+        mUid = uid;
+        return this;
+    }
+
+    /** Sets the task info's UID. */
+    public TestRunningTaskInfoBuilder setTaskId(int taskId) {
+        mTaskId = taskId;
+        return this;
+    }
+
     /**
      * Set {@link ActivityManager.RunningTaskInfo#baseIntent} for the task info, by default
      * an empty intent is assigned
@@ -132,7 +146,8 @@
 
     public ActivityManager.RunningTaskInfo build() {
         final ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo();
-        info.taskId = sNextTaskId++;
+        info.taskId = (mTaskId == INVALID_TASK_ID) ? sNextTaskId++ : mTaskId;
+        info.effectiveUid = mUid;
         info.baseIntent = mBaseIntent;
         info.parentTaskId = mParentTaskId;
         info.displayId = mDisplayId;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt
similarity index 69%
rename from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt
rename to libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt
index ef99b00..e83f5c7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt
@@ -15,6 +15,7 @@
  */
 package com.android.wm.shell.desktopmode
 
+import android.app.ActivityManager.RunningTaskInfo
 import android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS
 import android.graphics.Rect
 import android.os.Binder
@@ -58,13 +59,13 @@
 import org.mockito.kotlin.whenever
 
 /**
- * Tests for [DesktopFullImmersiveTransitionHandler].
+ * Tests for [DesktopImmersiveController].
  *
- * Usage: atest WMShellUnitTests:DesktopFullImmersiveTransitionHandlerTest
+ * Usage: atest WMShellUnitTests:DesktopImmersiveControllerTest
  */
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
-class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() {
+class DesktopImmersiveControllerTest : ShellTestCase() {
 
     @JvmField @Rule val setFlagsRule = SetFlagsRule()
 
@@ -75,7 +76,7 @@
     @Mock private lateinit var mockDisplayLayout: DisplayLayout
     private val transactionSupplier = { SurfaceControl.Transaction() }
 
-    private lateinit var immersiveHandler: DesktopFullImmersiveTransitionHandler
+    private lateinit var controller: DesktopImmersiveController
 
     @Before
     fun setUp() {
@@ -87,7 +88,7 @@
         whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { invocation ->
             (invocation.getArgument(0) as Rect).set(STABLE_BOUNDS)
         }
-        immersiveHandler = DesktopFullImmersiveTransitionHandler(
+        controller = DesktopImmersiveController(
             transitions = mockTransitions,
             desktopRepository = desktopRepository,
             displayController = mockDisplayController,
@@ -100,7 +101,7 @@
     fun enterImmersive_transitionReady_updatesRepository() {
         val task = createFreeformTask()
         val mockBinder = mock(IBinder::class.java)
-        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)))
+        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller)))
             .thenReturn(mockBinder)
         desktopRepository.setTaskInFullImmersiveState(
             displayId = task.displayId,
@@ -108,16 +109,14 @@
             immersive = false
         )
 
-        immersiveHandler.moveTaskToImmersive(task)
-        immersiveHandler.onTransitionReady(
+        controller.moveTaskToImmersive(task)
+        controller.onTransitionReady(
             transition = mockBinder,
             info = createTransitionInfo(
-                changes = listOf(
-                    TransitionInfo.Change(task.token, SurfaceControl()).apply {
-                        taskInfo = task
-                    }
-                )
-            )
+                changes = listOf(createChange(task))
+            ),
+            startTransaction = SurfaceControl.Transaction(),
+            finishTransaction = SurfaceControl.Transaction(),
         )
 
         assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isTrue()
@@ -128,7 +127,7 @@
     fun enterImmersive_savesPreImmersiveBounds() {
         val task = createFreeformTask()
         val mockBinder = mock(IBinder::class.java)
-        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)))
+        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller)))
             .thenReturn(mockBinder)
         desktopRepository.setTaskInFullImmersiveState(
             displayId = task.displayId,
@@ -137,16 +136,14 @@
         )
         assertThat(desktopRepository.removeBoundsBeforeFullImmersive(task.taskId)).isNull()
 
-        immersiveHandler.moveTaskToImmersive(task)
-        immersiveHandler.onTransitionReady(
+        controller.moveTaskToImmersive(task)
+        controller.onTransitionReady(
             transition = mockBinder,
             info = createTransitionInfo(
-                changes = listOf(
-                    TransitionInfo.Change(task.token, SurfaceControl()).apply {
-                        taskInfo = task
-                    }
-                )
-            )
+                changes = listOf(createChange(task))
+            ),
+            startTransaction = SurfaceControl.Transaction(),
+            finishTransaction = SurfaceControl.Transaction(),
         )
 
         assertThat(desktopRepository.removeBoundsBeforeFullImmersive(task.taskId)).isNotNull()
@@ -156,7 +153,7 @@
     fun exitImmersive_transitionReady_updatesRepository() {
         val task = createFreeformTask()
         val mockBinder = mock(IBinder::class.java)
-        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)))
+        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller)))
             .thenReturn(mockBinder)
         desktopRepository.setTaskInFullImmersiveState(
             displayId = task.displayId,
@@ -164,16 +161,14 @@
             immersive = true
         )
 
-        immersiveHandler.moveTaskToNonImmersive(task)
-        immersiveHandler.onTransitionReady(
+        controller.moveTaskToNonImmersive(task)
+        controller.onTransitionReady(
             transition = mockBinder,
             info = createTransitionInfo(
-                changes = listOf(
-                    TransitionInfo.Change(task.token, SurfaceControl()).apply {
-                        taskInfo = task
-                    }
-                )
-            )
+                changes = listOf(createChange(task))
+            ),
+            startTransaction = SurfaceControl.Transaction(),
+            finishTransaction = SurfaceControl.Transaction(),
         )
 
         assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isFalse()
@@ -184,7 +179,7 @@
     fun exitImmersive_onTransitionReady_removesBoundsBeforeImmersive() {
         val task = createFreeformTask()
         val mockBinder = mock(IBinder::class.java)
-        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)))
+        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller)))
             .thenReturn(mockBinder)
         desktopRepository.setTaskInFullImmersiveState(
             displayId = task.displayId,
@@ -193,16 +188,14 @@
         )
         desktopRepository.saveBoundsBeforeFullImmersive(task.taskId, Rect(100, 100, 600, 600))
 
-        immersiveHandler.moveTaskToNonImmersive(task)
-        immersiveHandler.onTransitionReady(
+        controller.moveTaskToNonImmersive(task)
+        controller.onTransitionReady(
             transition = mockBinder,
             info = createTransitionInfo(
-                changes = listOf(
-                    TransitionInfo.Change(task.token, SurfaceControl()).apply {
-                        taskInfo = task
-                    }
-                )
-            )
+                changes = listOf(createChange(task))
+            ),
+            startTransaction = SurfaceControl.Transaction(),
+            finishTransaction = SurfaceControl.Transaction(),
         )
 
         assertThat(desktopRepository.removeBoundsBeforeMaximize(task.taskId)).isNull()
@@ -217,16 +210,15 @@
             immersive = true
         )
 
-        immersiveHandler.onTransitionReady(
+        controller.onTransitionReady(
             transition = mock(IBinder::class.java),
             info = createTransitionInfo(
-                changes = listOf(
-                    TransitionInfo.Change(task.token, SurfaceControl()).apply {
-                        taskInfo = task
-                        setRotation(/* start= */ Surface.ROTATION_0, /* end= */ Surface.ROTATION_90)
-                    }
-                )
-            )
+                changes = listOf(createChange(task).apply {
+                    setRotation(/* start= */ Surface.ROTATION_0, /* end= */ Surface.ROTATION_90)
+                })
+            ),
+            startTransaction = SurfaceControl.Transaction(),
+            finishTransaction = SurfaceControl.Transaction(),
         )
 
         assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isFalse()
@@ -236,28 +228,28 @@
     fun enterImmersive_inProgress_ignores() {
         val task = createFreeformTask()
         val mockBinder = mock(IBinder::class.java)
-        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)))
+        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller)))
             .thenReturn(mockBinder)
 
-        immersiveHandler.moveTaskToImmersive(task)
-        immersiveHandler.moveTaskToImmersive(task)
+        controller.moveTaskToImmersive(task)
+        controller.moveTaskToImmersive(task)
 
         verify(mockTransitions, times(1))
-            .startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler))
+            .startTransition(eq(TRANSIT_CHANGE), any(), eq(controller))
     }
 
     @Test
     fun exitImmersive_inProgress_ignores() {
         val task = createFreeformTask()
         val mockBinder = mock(IBinder::class.java)
-        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)))
+        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller)))
             .thenReturn(mockBinder)
 
-        immersiveHandler.moveTaskToNonImmersive(task)
-        immersiveHandler.moveTaskToNonImmersive(task)
+        controller.moveTaskToNonImmersive(task)
+        controller.moveTaskToNonImmersive(task)
 
         verify(mockTransitions, times(1))
-            .startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler))
+            .startTransition(eq(TRANSIT_CHANGE), any(), eq(controller))
     }
 
     @Test
@@ -273,9 +265,9 @@
             immersive = true
         )
 
-        immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
 
-        assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+        assertThat(controller.pendingExternalExitTransitions.any { exit ->
             exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
                     && exit.taskId == task.taskId
         }).isTrue()
@@ -294,9 +286,9 @@
             immersive = false
         )
 
-        immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
 
-        assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+        assertThat(controller.pendingExternalExitTransitions.any { exit ->
             exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
                     && exit.taskId == task.taskId
         }).isFalse()
@@ -315,7 +307,7 @@
             immersive = true
         )
 
-        immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
 
         assertThat(wct.hasBoundsChange(task.token)).isTrue()
     }
@@ -333,13 +325,38 @@
             immersive = false
         )
 
-        immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
 
         assertThat(wct.hasBoundsChange(task.token)).isFalse()
     }
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun exitImmersiveIfApplicable_byDisplay_withExcludeTask_doesNotExit() {
+        val task = createFreeformTask()
+        whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+        val wct = WindowContainerTransaction()
+        val transition = Binder()
+        desktopRepository.setTaskInFullImmersiveState(
+            displayId = DEFAULT_DISPLAY,
+            taskId = task.taskId,
+            immersive = true
+        )
+
+        controller.exitImmersiveIfApplicable(
+            wct = wct,
+            displayId = DEFAULT_DISPLAY,
+            excludeTaskId = task.taskId
+        )?.invoke(transition)
+
+        assertThat(controller.pendingExternalExitTransitions.any { exit ->
+            exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
+                    && exit.taskId == task.taskId
+        }).isFalse()
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
     fun exitImmersiveIfApplicable_byTask_inImmersive_changesTaskBounds() {
         val task = createFreeformTask()
         whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
@@ -350,7 +367,7 @@
             immersive = true
         )
 
-        immersiveHandler.exitImmersiveIfApplicable(wct = wct, taskInfo = task)
+        controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task)
 
         assertThat(wct.hasBoundsChange(task.token)).isTrue()
     }
@@ -367,7 +384,7 @@
             immersive = false
         )
 
-        immersiveHandler.exitImmersiveIfApplicable(wct, task)
+        controller.exitImmersiveIfApplicable(wct, task)
 
         assertThat(wct.hasBoundsChange(task.token)).isFalse()
     }
@@ -385,9 +402,9 @@
             immersive = true
         )
 
-        immersiveHandler.exitImmersiveIfApplicable(wct, task)?.invoke(transition)
+        controller.exitImmersiveIfApplicable(wct, task)?.invoke(transition)
 
-        assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+        assertThat(controller.pendingExternalExitTransitions.any { exit ->
             exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
                     && exit.taskId == task.taskId
         }).isTrue()
@@ -406,9 +423,9 @@
             immersive = false
         )
 
-        immersiveHandler.exitImmersiveIfApplicable(wct, task)?.invoke(transition)
+        controller.exitImmersiveIfApplicable(wct, task)?.invoke(transition)
 
-        assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+        assertThat(controller.pendingExternalExitTransitions.any { exit ->
             exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
                     && exit.taskId == task.taskId
         }).isFalse()
@@ -416,7 +433,7 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
-    fun onTransitionReady_pendingExit_removesPendingExit() {
+    fun onTransitionReady_pendingExit_removesPendingExitOnFinish() {
         val task = createFreeformTask()
         whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
         val wct = WindowContainerTransaction()
@@ -426,18 +443,19 @@
             taskId = task.taskId,
             immersive = true
         )
-        immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
 
-        immersiveHandler.onTransitionReady(
+        controller.onTransitionReady(
             transition = transition,
             info = createTransitionInfo(
-                changes = listOf(
-                    TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task }
-                )
-            )
+                changes = listOf(createChange(task))
+            ),
+            startTransaction = SurfaceControl.Transaction(),
+            finishTransaction = SurfaceControl.Transaction(),
         )
+        controller.onTransitionFinished(transition, aborted = false)
 
-        assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+        assertThat(controller.pendingExternalExitTransitions.any { exit ->
             exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
                     && exit.taskId == task.taskId
         }).isFalse()
@@ -445,6 +463,42 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun onTransitionReady_pendingExit_withMerge_removesPendingExitOnFinish() {
+        val task = createFreeformTask()
+        whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+        val wct = WindowContainerTransaction()
+        val transition = Binder()
+        val mergedToTransition = Binder()
+        desktopRepository.setTaskInFullImmersiveState(
+            displayId = DEFAULT_DISPLAY,
+            taskId = task.taskId,
+            immersive = true
+        )
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+
+        controller.onTransitionReady(
+            transition = transition,
+            info = createTransitionInfo(
+                changes = listOf(createChange(task))
+            ),
+            startTransaction = SurfaceControl.Transaction(),
+            finishTransaction = SurfaceControl.Transaction(),
+        )
+        controller.onTransitionMerged(transition, mergedToTransition)
+        controller.onTransitionFinished(mergedToTransition, aborted = false)
+
+        assertThat(controller.pendingExternalExitTransitions.any { exit ->
+            exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
+                    && exit.taskId == task.taskId
+        }).isFalse()
+        assertThat(controller.pendingExternalExitTransitions.any { exit ->
+            exit.transition == mergedToTransition && exit.displayId == DEFAULT_DISPLAY
+                    && exit.taskId == task.taskId
+        }).isFalse()
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
     fun onTransitionReady_pendingExit_updatesRepository() {
         val task = createFreeformTask()
         whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
@@ -455,15 +509,15 @@
             taskId = task.taskId,
             immersive = true
         )
-        immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
 
-        immersiveHandler.onTransitionReady(
+        controller.onTransitionReady(
             transition = transition,
             info = createTransitionInfo(
-                changes = listOf(
-                    TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task }
-                )
-            )
+                changes = listOf(createChange(task))
+            ),
+            startTransaction = SurfaceControl.Transaction(),
+            finishTransaction = SurfaceControl.Transaction(),
         )
 
         assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isFalse()
@@ -485,15 +539,15 @@
             immersive = true
         )
         desktopRepository.saveBoundsBeforeFullImmersive(task.taskId, Rect(100, 100, 600, 600))
-        immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
 
-        immersiveHandler.onTransitionReady(
+        controller.onTransitionReady(
             transition = transition,
             info = createTransitionInfo(
-                changes = listOf(
-                    TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task }
-                )
-            )
+                changes = listOf(createChange(task))
+            ),
+            startTransaction = SurfaceControl.Transaction(),
+            finishTransaction = SurfaceControl.Transaction(),
         )
 
         assertThat(desktopRepository.removeBoundsBeforeMaximize(task.taskId)).isNull()
@@ -512,7 +566,7 @@
             immersive = true
         )
 
-        immersiveHandler.exitImmersiveIfApplicable(wct = wct, taskInfo = task)
+        controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task)
 
         assertThat(
             wct.hasBoundsChange(task.token, calculateMaximizeBounds(mockDisplayLayout, task))
@@ -536,7 +590,7 @@
         val preImmersiveBounds = Rect(100, 100, 500, 500)
         desktopRepository.saveBoundsBeforeFullImmersive(task.taskId, preImmersiveBounds)
 
-        immersiveHandler.exitImmersiveIfApplicable(wct = wct, taskInfo = task)
+        controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task)
 
         assertThat(
             wct.hasBoundsChange(task.token, preImmersiveBounds)
@@ -559,7 +613,7 @@
             immersive = true
         )
 
-        immersiveHandler.exitImmersiveIfApplicable(wct = wct, taskInfo = task)
+        controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task)
 
         assertThat(
             wct.hasBoundsChange(task.token, calculateInitialBounds(mockDisplayLayout, task))
@@ -577,13 +631,32 @@
             taskId = task.taskId,
             immersive = true
         )
-        immersiveHandler.exitImmersiveIfApplicable(wct, task)?.invoke(Binder())
+        controller.exitImmersiveIfApplicable(wct, task)?.invoke(Binder())
 
-        immersiveHandler.moveTaskToNonImmersive(task)
+        controller.moveTaskToNonImmersive(task)
 
         verify(mockTransitions, never()).startTransition(any(), any(), any())
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun exitImmersiveIfApplicable_inImmersive_isImmersiveChange() {
+        val task = createFreeformTask()
+        whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+        val wct = WindowContainerTransaction()
+        val transition = Binder()
+        val change = createChange(task)
+        desktopRepository.setTaskInFullImmersiveState(
+            displayId = DEFAULT_DISPLAY,
+            taskId = task.taskId,
+            immersive = true
+        )
+
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+
+        assertThat(controller.isImmersiveChange(transition, change)).isTrue()
+    }
+
     private fun createTransitionInfo(
         @TransitionType type: Int = TRANSIT_CHANGE,
         @TransitionFlags flags: Int = 0,
@@ -592,6 +665,11 @@
         changes.forEach { change -> addChange(change) }
     }
 
+    private fun createChange(task: RunningTaskInfo): TransitionInfo.Change =
+        TransitionInfo.Change(task.token, SurfaceControl()).apply {
+            taskInfo = task
+        }
+
     private fun WindowContainerTransaction.hasBoundsChange(token: WindowContainerToken): Boolean =
         this.changes.any { change ->
             change.key == token.asBinder()
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 0825b6b..2a82e6e 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
@@ -16,9 +16,12 @@
 
 package com.android.wm.shell.desktopmode
 
+import android.app.ActivityManager.RunningTaskInfo
+import android.graphics.Rect
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.SetFlagsRule
 import com.android.dx.mockito.inline.extended.ExtendedMockito.clearInvocations
+import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
 import com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker
 import com.android.dx.mockito.inline.extended.ExtendedMockito.verify
 import com.android.dx.mockito.inline.extended.ExtendedMockito.verifyZeroInteractions
@@ -27,6 +30,9 @@
 import com.android.window.flags.Flags
 import com.android.wm.shell.EventLogTags
 import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestRunningTaskInfoBuilder
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayLayout
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.InputMethod
@@ -39,9 +45,13 @@
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UNSET_UNMINIMIZE_REASON
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UnminimizeReason
 import com.google.common.truth.Truth.assertThat
+import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
 
 /**
  * Tests for [DesktopModeEventLogger].
@@ -49,6 +59,8 @@
 class DesktopModeEventLoggerTest : ShellTestCase() {
 
     private val desktopModeEventLogger = DesktopModeEventLogger()
+    val displayController = mock<DisplayController>()
+    val displayLayout = mock<DisplayLayout>()
 
     @JvmField
     @Rule(order = 0)
@@ -60,6 +72,13 @@
     @Rule(order = 1)
     val setFlagsRule = SetFlagsRule()
 
+    @Before
+    fun setUp() {
+        doReturn(displayLayout).whenever(displayController).getDisplayLayout(anyInt())
+        doReturn(DISPLAY_WIDTH).whenever(displayLayout).width()
+        doReturn(DISPLAY_HEIGHT).whenever(displayLayout).height()
+    }
+
     @Test
     fun logSessionEnter_logsEnterReasonWithNewSessionId() {
         desktopModeEventLogger.logSessionEnter(EnterReason.KEYBOARD_SHORTCUT_ENTER)
@@ -467,7 +486,8 @@
 
     @Test
     fun logTaskResizingStarted_noOngoingSession_doesNotLog() {
-        desktopModeEventLogger.logTaskResizingStarted(TASK_SIZE_UPDATE)
+        desktopModeEventLogger.logTaskResizingStarted(ResizeTrigger.CORNER,
+            null, createTaskInfo())
 
         verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
@@ -478,13 +498,14 @@
     fun logTaskResizingStarted_logsTaskSizeUpdatedWithStartResizingStage() {
         val sessionId = startDesktopModeSession()
 
-        desktopModeEventLogger.logTaskResizingStarted(TASK_SIZE_UPDATE)
+        desktopModeEventLogger.logTaskResizingStarted(ResizeTrigger.CORNER,
+            null, createTaskInfo(), displayController)
 
         verify {
             FrameworkStatsLog.write(
                 eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED),
                 /* resize_trigger */
-                eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__UNKNOWN_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 */
@@ -500,7 +521,7 @@
                 /* task_width */
                 eq(TASK_SIZE_UPDATE.taskWidth),
                 /* display_area */
-                eq(TASK_SIZE_UPDATE.displayArea),
+                eq(DISPLAY_AREA),
             )
         }
         verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
@@ -508,7 +529,8 @@
 
     @Test
     fun logTaskResizingEnded_noOngoingSession_doesNotLog() {
-        desktopModeEventLogger.logTaskResizingEnded(TASK_SIZE_UPDATE)
+        desktopModeEventLogger.logTaskResizingEnded(ResizeTrigger.CORNER,
+            null, createTaskInfo())
 
         verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
         verifyZeroInteractions(staticMockMarker(EventLogTags::class.java))
@@ -519,13 +541,14 @@
     fun logTaskResizingEnded_logsTaskSizeUpdatedWithEndResizingStage() {
         val sessionId = startDesktopModeSession()
 
-        desktopModeEventLogger.logTaskResizingEnded(TASK_SIZE_UPDATE)
+        desktopModeEventLogger.logTaskResizingEnded(ResizeTrigger.CORNER,
+            null, createTaskInfo(), displayController = displayController)
 
         verify {
             FrameworkStatsLog.write(
                 eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED),
                 /* resize_trigger */
-                eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__UNKNOWN_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 */
@@ -541,7 +564,7 @@
                 /* task_width */
                 eq(TASK_SIZE_UPDATE.taskWidth),
                 /* display_area */
-                eq(TASK_SIZE_UPDATE.displayArea),
+                eq(DISPLAY_AREA),
             )
         }
         verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java))
@@ -585,8 +608,14 @@
         }
     }
 
+    private fun createTaskInfo(): RunningTaskInfo {
+        return TestRunningTaskInfoBuilder().setTaskId(TASK_ID)
+            .setUid(TASK_UID)
+            .setBounds(Rect(TASK_X, TASK_Y, TASK_WIDTH, TASK_HEIGHT))
+            .build()
+    }
+
     private companion object {
-        private const val sessionId = 1
         private const val TASK_ID = 1
         private const val TASK_UID = 1
         private const val TASK_X = 0
@@ -594,7 +623,9 @@
         private const val TASK_HEIGHT = 100
         private const val TASK_WIDTH = 100
         private const val TASK_COUNT = 1
-        private const val DISPLAY_AREA = 1000
+        private const val DISPLAY_WIDTH = 500
+        private const val DISPLAY_HEIGHT = 500
+        private const val DISPLAY_AREA = DISPLAY_HEIGHT * DISPLAY_WIDTH
 
         private val TASK_UPDATE = TaskUpdate(
             TASK_ID, TASK_UID, TASK_HEIGHT, TASK_WIDTH, TASK_X, TASK_Y,
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 55b44ac..bc2b36c 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
@@ -54,6 +54,7 @@
 import android.view.DragEvent
 import android.view.Gravity
 import android.view.KeyEvent
+import android.view.MotionEvent
 import android.view.SurfaceControl
 import android.view.WindowInsets
 import android.view.WindowManager
@@ -99,6 +100,7 @@
 import com.android.wm.shell.common.MultiInstanceHelper
 import com.android.wm.shell.common.ShellExecutor
 import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger
 import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
 import com.android.wm.shell.desktopmode.DesktopTasksController.TaskbarDesktopTaskListener
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
@@ -199,7 +201,7 @@
   lateinit var toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler
   @Mock lateinit var dragToDesktopTransitionHandler: DragToDesktopTransitionHandler
   @Mock
-  lateinit var mockDesktopFullImmersiveTransitionHandler: DesktopFullImmersiveTransitionHandler
+  lateinit var mMockDesktopImmersiveController: DesktopImmersiveController
   @Mock lateinit var launchAdjacentController: LaunchAdjacentController
   @Mock lateinit var splitScreenController: SplitScreenController
   @Mock lateinit var recentsTransitionHandler: RecentsTransitionHandler
@@ -214,9 +216,11 @@
   @Mock private lateinit var taskbarDesktopTaskListener: TaskbarDesktopTaskListener
   @Mock private lateinit var freeformTaskTransitionStarter: FreeformTaskTransitionStarter
   @Mock private lateinit var mockHandler: Handler
+  @Mock private lateinit var desktopModeEventLogger: DesktopModeEventLogger
   @Mock lateinit var persistentRepository: DesktopPersistentRepository
   @Mock private lateinit var mockInputManager: InputManager
   @Mock private lateinit var mockFocusTransitionObserver: FocusTransitionObserver
+  @Mock lateinit var motionEvent: MotionEvent
 
   private lateinit var mockitoSession: StaticMockitoSession
   private lateinit var controller: DesktopTasksController
@@ -295,6 +299,8 @@
     recentsTransitionStateListener = captor.value
 
     controller.taskbarDesktopTaskListener = taskbarDesktopTaskListener
+
+    assumeTrue(ENABLE_SHELL_TRANSITIONS)
   }
 
   private fun createController(): DesktopTasksController {
@@ -316,7 +322,7 @@
         dragAndDropTransitionHandler,
         toggleResizeDesktopTaskTransitionHandler,
         dragToDesktopTransitionHandler,
-        mockDesktopFullImmersiveTransitionHandler,
+        mMockDesktopImmersiveController,
         taskRepository,
         desktopModeLoggerTransitionObserver,
         launchAdjacentController,
@@ -329,6 +335,7 @@
         mockHandler,
         mockInputManager,
         mockFocusTransitionObserver,
+        desktopModeEventLogger,
       )
   }
 
@@ -357,9 +364,17 @@
     val task1 = setUpFreeformTask()
 
     val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java)
-    controller.toggleDesktopTaskSize(task1)
-    verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(argumentCaptor.capture())
+    controller.toggleDesktopTaskSize(task1, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
 
+    verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(argumentCaptor.capture())
+    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
+      ResizeTrigger.MAXIMIZE_BUTTON,
+      motionEvent,
+      task1,
+      STABLE_BOUNDS.height(),
+      STABLE_BOUNDS.width(),
+      displayController
+    )
     assertThat(argumentCaptor.value).isTrue()
   }
 
@@ -376,9 +391,17 @@
     val task1 = setUpFreeformTask(bounds = stableBounds, active = true)
 
     val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java)
-    controller.toggleDesktopTaskSize(task1)
-    verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(argumentCaptor.capture())
+    controller.toggleDesktopTaskSize(task1, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
 
+    verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(argumentCaptor.capture())
+    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
+      ResizeTrigger.MAXIMIZE_BUTTON,
+      motionEvent,
+      task1,
+      0,
+      0,
+      displayController
+    )
     assertThat(argumentCaptor.value).isFalse()
   }
 
@@ -754,7 +777,6 @@
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
   fun handleRequest_newFreeformTaskLaunch_cascadeApplied() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
     setUpLandscapeDisplay()
     val stableBounds = Rect()
     displayLayout.getStableBoundsForDesktopMode(stableBounds)
@@ -773,7 +795,6 @@
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
   fun handleRequest_freeformTaskAlreadyExistsInDesktopMode_cascadeNotApplied() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
     setUpLandscapeDisplay()
     val stableBounds = Rect()
     displayLayout.getStableBoundsForDesktopMode(stableBounds)
@@ -1752,7 +1773,7 @@
 
     controller.minimizeTask(task)
 
-    verify(mockDesktopFullImmersiveTransitionHandler).exitImmersiveIfApplicable(any(), eq(task))
+    verify(mMockDesktopImmersiveController).exitImmersiveIfApplicable(any(), eq(task))
   }
 
   @Test
@@ -1762,7 +1783,7 @@
     val runOnTransit = RunOnStartTransitionCallback()
     whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
       .thenReturn(transition)
-    whenever(mockDesktopFullImmersiveTransitionHandler.exitImmersiveIfApplicable(any(), eq(task)))
+    whenever(mMockDesktopImmersiveController.exitImmersiveIfApplicable(any(), eq(task)))
       .thenReturn(runOnTransit)
 
     controller.minimizeTask(task)
@@ -1773,8 +1794,6 @@
 
   @Test
   fun handleRequest_fullscreenTask_freeformVisible_returnSwitchToFreeformWCT() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val homeTask = setUpHomeTask()
     val freeformTask = setUpFreeformTask()
     markTaskVisible(freeformTask)
@@ -1791,8 +1810,6 @@
 
   @Test
   fun handleRequest_fullscreenTaskWithTaskOnHome_freeformVisible_returnSwitchToFreeformWCT() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val homeTask = setUpHomeTask()
     val freeformTask = setUpFreeformTask()
     markTaskVisible(freeformTask)
@@ -1818,8 +1835,6 @@
 
   @Test
   fun handleRequest_fullscreenTaskToFreeform_underTaskLimit_dontMinimize() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val freeformTask = setUpFreeformTask()
     markTaskVisible(freeformTask)
     val fullscreenTask = createFullscreenTask()
@@ -1833,8 +1848,6 @@
 
   @Test
   fun handleRequest_fullscreenTaskToFreeform_bringsTasksOverLimit_otherTaskIsMinimized() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
     freeformTasks.forEach { markTaskVisible(it) }
     val fullscreenTask = createFullscreenTask()
@@ -1849,8 +1862,6 @@
 
   @Test
   fun handleRequest_fullscreenTaskWithTaskOnHome_bringsTasksOverLimit_otherTaskIsMinimized() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
     freeformTasks.forEach { markTaskVisible(it) }
     val fullscreenTask = createFullscreenTask()
@@ -1866,8 +1877,6 @@
 
   @Test
   fun handleRequest_fullscreenTaskWithTaskOnHome_beyondLimit_existingAndNewTasksAreMinimized() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val minimizedTask = setUpFreeformTask()
     taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = minimizedTask.taskId)
     val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
@@ -1888,7 +1897,6 @@
 
   @Test
   fun handleRequest_fullscreenTask_noTasks_enforceDesktop_freeformDisplay_returnFreeformWCT() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
     whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
     val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
     tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
@@ -1905,7 +1913,6 @@
 
   @Test
   fun handleRequest_fullscreenTask_noTasks_enforceDesktop_fullscreenDisplay_returnNull() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
     whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
     val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
     tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
@@ -1918,8 +1925,6 @@
 
   @Test
   fun handleRequest_fullscreenTask_freeformNotVisible_returnNull() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val freeformTask = setUpFreeformTask()
     markTaskHidden(freeformTask)
     val fullscreenTask = createFullscreenTask()
@@ -1928,16 +1933,12 @@
 
   @Test
   fun handleRequest_fullscreenTask_noOtherTasks_returnNull() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val fullscreenTask = createFullscreenTask()
     assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull()
   }
 
   @Test
   fun handleRequest_fullscreenTask_freeformTaskOnOtherDisplay_returnNull() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val fullscreenTaskDefaultDisplay = createFullscreenTask(displayId = DEFAULT_DISPLAY)
     createFreeformTask(displayId = SECOND_DISPLAY)
 
@@ -1947,8 +1948,6 @@
 
   @Test
   fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_minimize() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
     freeformTasks.forEach { markTaskVisible(it) }
     val newFreeformTask = createFreeformTask()
@@ -1961,8 +1960,6 @@
 
   @Test
   fun handleRequest_freeformTask_relaunchActiveTask_taskBecomesUndefined() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val freeformTask = setUpFreeformTask()
     markTaskHidden(freeformTask)
 
@@ -1977,7 +1974,6 @@
 
   @Test
   fun handleRequest_freeformTask_relaunchTask_enforceDesktop_freeformDisplay_noWinModeChange() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
     whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
     val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
     tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
@@ -1992,7 +1988,6 @@
 
   @Test
   fun handleRequest_freeformTask_relaunchTask_enforceDesktop_fullscreenDisplay_becomesUndefined() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
     whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
     val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
     tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
@@ -2009,8 +2004,6 @@
   @Test
   @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   fun handleRequest_freeformTask_desktopWallpaperDisabled_freeformNotVisible_reorderedToTop() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val freeformTask1 = setUpFreeformTask()
     val freeformTask2 = createFreeformTask()
 
@@ -2026,8 +2019,6 @@
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_reorderedToTop() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val freeformTask1 = setUpFreeformTask()
     val freeformTask2 = createFreeformTask()
 
@@ -2048,8 +2039,6 @@
   @Test
   @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   fun handleRequest_freeformTask_desktopWallpaperDisabled_noOtherTasks_reorderedToTop() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val task = createFreeformTask()
     val result = controller.handleRequest(Binder(), createTransition(task))
 
@@ -2061,8 +2050,6 @@
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   fun handleRequest_freeformTask_desktopWallpaperEnabled_noOtherTasks_reorderedToTop() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val task = createFreeformTask()
     val result = controller.handleRequest(Binder(), createTransition(task))
 
@@ -2077,8 +2064,6 @@
   @Test
   @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   fun handleRequest_freeformTask_dskWallpaperDisabled_freeformOnOtherDisplayOnly_reorderedToTop() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY)
     // Second display task
     createFreeformTask(displayId = SECOND_DISPLAY)
@@ -2093,8 +2078,6 @@
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   fun handleRequest_freeformTask_dskWallpaperEnabled_freeformOnOtherDisplayOnly_reorderedToTop() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY)
     // Second display task
     createFreeformTask(displayId = SECOND_DISPLAY)
@@ -2111,7 +2094,6 @@
 
   @Test
   fun handleRequest_freeformTask_alreadyInDesktop_noOverrideDensity_noConfigDensityChange() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
     whenever(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(false)
 
     val freeformTask1 = setUpFreeformTask()
@@ -2125,7 +2107,6 @@
 
   @Test
   fun handleRequest_freeformTask_alreadyInDesktop_overrideDensity_hasConfigDensityChange() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
     whenever(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(true)
 
     val freeformTask1 = setUpFreeformTask()
@@ -2139,7 +2120,6 @@
 
   @Test
   fun handleRequest_freeformTask_keyguardLocked_returnNull() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
     whenever(keyguardManager.isKeyguardLocked).thenReturn(true)
     val freeformTask = createFreeformTask(displayId = DEFAULT_DISPLAY)
 
@@ -2150,8 +2130,6 @@
 
   @Test
   fun handleRequest_notOpenOrToFrontTransition_returnNull() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val task =
         TestRunningTaskInfoBuilder()
             .setActivityType(ACTIVITY_TYPE_STANDARD)
@@ -2164,21 +2142,17 @@
 
   @Test
   fun handleRequest_noTriggerTask_returnNull() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
     assertThat(controller.handleRequest(Binder(), createTransition(task = null))).isNull()
   }
 
   @Test
   fun handleRequest_triggerTaskNotStandard_returnNull() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
     val task = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build()
     assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull()
   }
 
   @Test
   fun handleRequest_triggerTaskNotFullscreenOrFreeform_returnNull() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
     val task =
         TestRunningTaskInfoBuilder()
             .setActivityType(ACTIVITY_TYPE_STANDARD)
@@ -2860,7 +2834,8 @@
         PointF(200f, -200f), /* inputCoordinate */
         Rect(100, -100, 500, 1000), /* currentDragBounds */
         Rect(0, 50, 2000, 2000), /* validDragArea */
-        Rect() /* dragStartBounds */ )
+        Rect() /* dragStartBounds */,
+        motionEvent)
     val rectAfterEnd = Rect(100, 50, 500, 1150)
     verify(transitions)
         .startTransition(
@@ -2895,7 +2870,8 @@
       PointF(200f, 300f), /* inputCoordinate */
       currentDragBounds, /* currentDragBounds */
       Rect(0, 50, 2000, 2000) /* validDragArea */,
-      Rect() /* dragStartBounds */)
+      Rect() /* dragStartBounds */,
+      motionEvent)
 
 
     verify(transitions)
@@ -3116,13 +3092,14 @@
     val transition = Binder()
     whenever(transitions.startTransition(eq(TRANSIT_OPEN), any(), anyOrNull()))
       .thenReturn(transition)
-    whenever(mockDesktopFullImmersiveTransitionHandler
-      .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId))).thenReturn(runOnStartTransit)
+    whenever(mMockDesktopImmersiveController
+      .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId), eq(freeformTask.taskId)))
+      .thenReturn(runOnStartTransit)
 
     runOpenInstance(immersiveTask, freeformTask.taskId)
 
-    verify(mockDesktopFullImmersiveTransitionHandler)
-      .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId))
+    verify(mMockDesktopImmersiveController)
+      .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId), eq(freeformTask.taskId))
     runOnStartTransit.assertOnlyInvocation(transition)
   }
 
@@ -3142,10 +3119,19 @@
     val bounds = Rect(0, 0, 100, 100)
     val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds)
 
-    controller.toggleDesktopTaskSize(task)
+    controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
+
     // Assert bounds set to stable bounds
     val wct = getLatestToggleResizeDesktopTaskWct()
     assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS)
+    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
+      ResizeTrigger.MAXIMIZE_BUTTON,
+      motionEvent,
+      task,
+      STABLE_BOUNDS.height(),
+      STABLE_BOUNDS.width(),
+      displayController
+    )
   }
 
   @Test
@@ -3164,15 +3150,22 @@
       STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom
     )
 
-    controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT)
+    controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, motionEvent)
     // Assert bounds set to stable bounds
     val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds)
     assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds)
+    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
+      ResizeTrigger.SNAP_LEFT_MENU,
+      motionEvent,
+      task,
+      expectedBounds.height(),
+      expectedBounds.width(),
+      displayController
+    )
   }
 
   @Test
   fun snapToHalfScreen_snapBoundsWhenAlreadySnapped_animatesSurfaceWithoutWCT() {
-    assumeTrue(ENABLE_SHELL_TRANSITIONS)
     // Set up task to already be in snapped-left bounds
     val bounds = Rect(
       STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom
@@ -3187,7 +3180,7 @@
 
     // Attempt to snap left again
     val currentDragBounds = Rect(bounds).apply { offset(-100, 0) }
-    controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT)
+    controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, motionEvent)
 
     // Assert that task is NOT updated via WCT
     verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any())
@@ -3200,6 +3193,14 @@
       eq(bounds),
       eq(true)
     )
+    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
+      ResizeTrigger.SNAP_LEFT_MENU,
+      motionEvent,
+      task,
+      bounds.height(),
+      bounds.width(),
+      displayController
+    )
   }
 
   @Test
@@ -3210,12 +3211,22 @@
     }
     val preDragBounds = Rect(100, 100, 400, 500)
     val currentDragBounds = Rect(0, 100, 300, 500)
+    val expectedBounds =
+      Rect(STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom)
 
     controller.handleSnapResizingTask(
-      task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds)
+      task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds, motionEvent
+    )
     val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds)
     assertThat(findBoundsChange(wct, task)).isEqualTo(
-      Rect(STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom))
+      expectedBounds
+    )
+    verify(desktopModeEventLogger, times(1)).logTaskResizingStarted(
+      ResizeTrigger.DRAG_LEFT,
+      motionEvent,
+      task,
+      displayController
+    )
   }
 
   @Test
@@ -3228,7 +3239,7 @@
     val currentDragBounds = Rect(0, 100, 300, 500)
 
     controller.handleSnapResizingTask(
-      task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds)
+      task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds, motionEvent)
     verify(mReturnToDragStartAnimator).start(
       eq(task.taskId),
       eq(mockSurface),
@@ -3236,6 +3247,13 @@
       eq(preDragBounds),
       eq(false)
     )
+    verify(desktopModeEventLogger, never()).logTaskResizingStarted(
+      any(),
+      any(),
+      any(),
+      any(),
+      any()
+    )
   }
 
   @Test
@@ -3254,10 +3272,19 @@
     // Bounds should be 1000 x 500, vertically centered in the 1000 x 1000 stable bounds
     val expectedBounds = Rect(STABLE_BOUNDS.left, 250, STABLE_BOUNDS.right, 750)
 
-    controller.toggleDesktopTaskSize(task)
+    controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
+
     // Assert bounds set to stable bounds
     val wct = getLatestToggleResizeDesktopTaskWct()
     assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds)
+    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
+      ResizeTrigger.MAXIMIZE_BUTTON,
+      motionEvent,
+      task,
+      expectedBounds.height(),
+      expectedBounds.width(),
+      displayController
+    )
   }
 
   @Test
@@ -3265,8 +3292,12 @@
     val bounds = Rect(0, 0, 100, 100)
     val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds)
 
-    controller.toggleDesktopTaskSize(task)
+    controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
     assertThat(taskRepository.removeBoundsBeforeMaximize(task.taskId)).isEqualTo(bounds)
+    verify(desktopModeEventLogger, never()).logTaskResizingEnded(
+      any(), any(), any(), any(),
+      any(), any(), any()
+    )
   }
 
   @Test
@@ -3275,15 +3306,23 @@
     val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize)
 
     // Maximize
-    controller.toggleDesktopTaskSize(task)
+    controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
     task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS)
 
     // Restore
-    controller.toggleDesktopTaskSize(task)
+    controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
 
     // Assert bounds set to last bounds before maximize
     val wct = getLatestToggleResizeDesktopTaskWct()
     assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize)
+    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
+      ResizeTrigger.MAXIMIZE_BUTTON,
+      motionEvent,
+      task,
+      boundsBeforeMaximize.height(),
+      boundsBeforeMaximize.width(),
+      displayController
+    )
   }
 
   @Test
@@ -3294,16 +3333,24 @@
     }
 
     // Maximize
-    controller.toggleDesktopTaskSize(task)
+    controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
     task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS.left,
       boundsBeforeMaximize.top, STABLE_BOUNDS.right, boundsBeforeMaximize.bottom)
 
     // Restore
-    controller.toggleDesktopTaskSize(task)
+    controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
 
     // Assert bounds set to last bounds before maximize
     val wct = getLatestToggleResizeDesktopTaskWct()
     assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize)
+    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
+      ResizeTrigger.MAXIMIZE_BUTTON,
+      motionEvent,
+      task,
+      boundsBeforeMaximize.height(),
+      boundsBeforeMaximize.width(),
+      displayController
+    )
   }
 
   @Test
@@ -3314,16 +3361,24 @@
     }
 
     // Maximize
-    controller.toggleDesktopTaskSize(task)
+    controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
     task.configuration.windowConfiguration.bounds.set(boundsBeforeMaximize.left,
       STABLE_BOUNDS.top, boundsBeforeMaximize.right, STABLE_BOUNDS.bottom)
 
     // Restore
-    controller.toggleDesktopTaskSize(task)
+    controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
 
     // Assert bounds set to last bounds before maximize
     val wct = getLatestToggleResizeDesktopTaskWct()
     assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize)
+    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
+      ResizeTrigger.MAXIMIZE_BUTTON,
+      motionEvent,
+      task,
+      boundsBeforeMaximize.height(),
+      boundsBeforeMaximize.width(),
+      displayController
+    )
   }
 
   @Test
@@ -3332,14 +3387,22 @@
     val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize)
 
     // Maximize
-    controller.toggleDesktopTaskSize(task)
+    controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
     task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS)
 
     // Restore
-    controller.toggleDesktopTaskSize(task)
+    controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent)
 
     // Assert last bounds before maximize removed after use
     assertThat(taskRepository.removeBoundsBeforeMaximize(task.taskId)).isNull()
+    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
+      ResizeTrigger.MAXIMIZE_BUTTON,
+      motionEvent,
+      task,
+      boundsBeforeMaximize.height(),
+      boundsBeforeMaximize.width(),
+      displayController
+    )
   }
 
 
@@ -3383,7 +3446,7 @@
 
     controller.toggleDesktopTaskFullImmersiveState(task)
 
-    verify(mockDesktopFullImmersiveTransitionHandler).moveTaskToImmersive(task)
+    verify(mMockDesktopImmersiveController).moveTaskToImmersive(task)
   }
 
   @Test
@@ -3393,7 +3456,7 @@
 
     controller.toggleDesktopTaskFullImmersiveState(task)
 
-    verify(mockDesktopFullImmersiveTransitionHandler).moveTaskToNonImmersive(task)
+    verify(mMockDesktopImmersiveController).moveTaskToNonImmersive(task)
   }
 
   @Test
@@ -3405,7 +3468,7 @@
     task.requestedVisibleTypes = WindowInsets.Type.statusBars()
     controller.onTaskInfoChanged(task)
 
-    verify(mockDesktopFullImmersiveTransitionHandler).moveTaskToNonImmersive(task)
+    verify(mMockDesktopImmersiveController).moveTaskToNonImmersive(task)
   }
 
   @Test
@@ -3417,7 +3480,7 @@
     task.requestedVisibleTypes = WindowInsets.Type.statusBars()
     controller.onTaskInfoChanged(task)
 
-    verify(mockDesktopFullImmersiveTransitionHandler, never()).moveTaskToNonImmersive(task)
+    verify(mMockDesktopImmersiveController, never()).moveTaskToNonImmersive(task)
   }
 
   @Test
@@ -3426,13 +3489,14 @@
     val wct = WindowContainerTransaction()
     val runOnStartTransit = RunOnStartTransitionCallback()
     val transition = Binder()
-    whenever(mockDesktopFullImmersiveTransitionHandler
-      .exitImmersiveIfApplicable(wct, task.displayId)).thenReturn(runOnStartTransit)
+    whenever(mMockDesktopImmersiveController
+      .exitImmersiveIfApplicable(wct, task.displayId, task.taskId)).thenReturn(runOnStartTransit)
     whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition)
 
     controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN)
 
-    verify(mockDesktopFullImmersiveTransitionHandler).exitImmersiveIfApplicable(wct, task.displayId)
+    verify(mMockDesktopImmersiveController)
+      .exitImmersiveIfApplicable(wct, task.displayId, task.taskId)
     runOnStartTransit.assertOnlyInvocation(transition)
   }
 
@@ -3442,13 +3506,14 @@
     val wct = WindowContainerTransaction()
     val runOnStartTransit = RunOnStartTransitionCallback()
     val transition = Binder()
-    whenever(mockDesktopFullImmersiveTransitionHandler
-      .exitImmersiveIfApplicable(wct, task.displayId)).thenReturn(runOnStartTransit)
+    whenever(mMockDesktopImmersiveController
+      .exitImmersiveIfApplicable(wct, task.displayId, task.taskId)).thenReturn(runOnStartTransit)
     whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition)
 
     controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN)
 
-    verify(mockDesktopFullImmersiveTransitionHandler).exitImmersiveIfApplicable(wct, task.displayId)
+    verify(mMockDesktopImmersiveController)
+      .exitImmersiveIfApplicable(wct, task.displayId, task.taskId)
     runOnStartTransit.assertOnlyInvocation(transition)
   }
 
@@ -3457,14 +3522,15 @@
     val task = setUpFreeformTask(background = true)
     val runOnStartTransit = RunOnStartTransitionCallback()
     val transition = Binder()
-    whenever(mockDesktopFullImmersiveTransitionHandler
-      .exitImmersiveIfApplicable(any(), eq(task.displayId))).thenReturn(runOnStartTransit)
+    whenever(mMockDesktopImmersiveController
+      .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId)))
+      .thenReturn(runOnStartTransit)
     whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition)
 
     controller.moveTaskToFront(task.taskId, remoteTransition = null)
 
-    verify(mockDesktopFullImmersiveTransitionHandler)
-      .exitImmersiveIfApplicable(any(), eq(task.displayId))
+    verify(mMockDesktopImmersiveController)
+      .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId))
     runOnStartTransit.assertOnlyInvocation(transition)
   }
 
@@ -3473,14 +3539,15 @@
     val task = setUpFreeformTask(background = false)
     val runOnStartTransit = RunOnStartTransitionCallback()
     val transition = Binder()
-    whenever(mockDesktopFullImmersiveTransitionHandler
-      .exitImmersiveIfApplicable(any(), eq(task.displayId))).thenReturn(runOnStartTransit)
+    whenever(mMockDesktopImmersiveController
+      .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId)))
+      .thenReturn(runOnStartTransit)
     whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition)
 
     controller.moveTaskToFront(task.taskId, remoteTransition = null)
 
-    verify(mockDesktopFullImmersiveTransitionHandler)
-      .exitImmersiveIfApplicable(any(), eq(task.displayId))
+    verify(mMockDesktopImmersiveController)
+      .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId))
     runOnStartTransit.assertOnlyInvocation(transition)
   }
 
@@ -3493,7 +3560,7 @@
 
     controller.handleRequest(binder, createTransition(task))
 
-    verify(mockDesktopFullImmersiveTransitionHandler)
+    verify(mMockDesktopImmersiveController)
       .exitImmersiveIfApplicable(eq(binder), any(), eq(task.displayId))
   }
 
@@ -3505,10 +3572,117 @@
 
     controller.handleRequest(binder, createTransition(task))
 
-    verify(mockDesktopFullImmersiveTransitionHandler)
+    verify(mMockDesktopImmersiveController)
       .exitImmersiveIfApplicable(eq(binder), any(), eq(task.displayId))
   }
 
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+  fun shouldPlayDesktopAnimation_notShowingDesktop_doesNotPlay() {
+    val triggerTask = setUpFullscreenTask(displayId = 5)
+    taskRepository.setTaskInFullImmersiveState(
+      displayId = triggerTask.displayId,
+      taskId = triggerTask.taskId,
+      immersive = true
+    )
+
+    assertThat(controller.shouldPlayDesktopAnimation(
+      TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null)
+    )).isFalse()
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+  fun shouldPlayDesktopAnimation_notOpening_doesNotPlay() {
+    val triggerTask = setUpFreeformTask(displayId = 5)
+    taskRepository.setTaskInFullImmersiveState(
+      displayId = triggerTask.displayId,
+      taskId = triggerTask.taskId,
+      immersive = true
+    )
+
+    assertThat(controller.shouldPlayDesktopAnimation(
+      TransitionRequestInfo(TRANSIT_CHANGE, triggerTask, /* remoteTransition= */ null)
+    )).isFalse()
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+  fun shouldPlayDesktopAnimation_notImmersive_doesNotPlay() {
+    val triggerTask = setUpFreeformTask(displayId = 5)
+    taskRepository.setTaskInFullImmersiveState(
+      displayId = triggerTask.displayId,
+      taskId = triggerTask.taskId,
+      immersive = false
+    )
+
+    assertThat(controller.shouldPlayDesktopAnimation(
+      TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null)
+    )).isFalse()
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+  fun shouldPlayDesktopAnimation_fullscreenEntersDesktop_plays() {
+    // At least one freeform task to be in a desktop.
+    val existingTask = setUpFreeformTask(displayId = 5)
+    val triggerTask = setUpFullscreenTask(displayId = 5)
+    assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isTrue()
+    taskRepository.setTaskInFullImmersiveState(
+      displayId = existingTask.displayId,
+      taskId = existingTask.taskId,
+      immersive = true
+    )
+
+    assertThat(
+      controller.shouldPlayDesktopAnimation(
+        TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null)
+      )
+    ).isTrue()
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+  fun shouldPlayDesktopAnimation_fullscreenStaysFullscreen_doesNotPlay() {
+    val triggerTask = setUpFullscreenTask(displayId = 5)
+    assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse()
+
+    assertThat(controller.shouldPlayDesktopAnimation(
+      TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null)
+    )).isFalse()
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+  fun shouldPlayDesktopAnimation_freeformStaysInDesktop_plays() {
+    // At least one freeform task to be in a desktop.
+    val existingTask = setUpFreeformTask(displayId = 5)
+    val triggerTask = setUpFreeformTask(displayId = 5, active = false)
+    assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isTrue()
+    taskRepository.setTaskInFullImmersiveState(
+      displayId = existingTask.displayId,
+      taskId = existingTask.taskId,
+      immersive = true
+    )
+
+    assertThat(
+      controller.shouldPlayDesktopAnimation(
+        TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null)
+      )
+    ).isTrue()
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+  fun shouldPlayDesktopAnimation_freeformExitsDesktop_doesNotPlay() {
+    val triggerTask = setUpFreeformTask(displayId = 5, active = false)
+    assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse()
+
+    assertThat(controller.shouldPlayDesktopAnimation(
+      TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null)
+    )).isFalse()
+  }
+
   private class RunOnStartTransitionCallback : ((IBinder) -> Unit) {
     var invocations = 0
       private set
@@ -3738,14 +3912,11 @@
       handlerClass: Class<out TransitionHandler>? = null
   ): WindowContainerTransaction {
     val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    if (ENABLE_SHELL_TRANSITIONS) {
-      if (handlerClass == null) {
-        verify(transitions).startTransition(eq(type), arg.capture(), isNull())
-      } else {
-        verify(transitions).startTransition(eq(type), arg.capture(), isA(handlerClass))
-      }
+
+    if (handlerClass == null) {
+      verify(transitions).startTransition(eq(type), arg.capture(), isNull())
     } else {
-      verify(shellTaskOrganizer).applyTransaction(arg.capture())
+      verify(transitions).startTransition(eq(type), arg.capture(), isA(handlerClass))
     }
     return arg.value
   }
@@ -3755,43 +3926,27 @@
   ): WindowContainerTransaction {
     val arg: ArgumentCaptor<WindowContainerTransaction> =
         ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    if (ENABLE_SHELL_TRANSITIONS) {
-      verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce())
+    verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce())
         .startTransition(capture(arg), eq(currentBounds))
-    } else {
-      verify(shellTaskOrganizer).applyTransaction(capture(arg))
-    }
     return arg.value
   }
 
   private fun getLatestEnterDesktopWct(): WindowContainerTransaction {
     val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    if (ENABLE_SHELL_TRANSITIONS) {
-      verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture(), any())
-    } else {
-      verify(shellTaskOrganizer).applyTransaction(arg.capture())
-    }
+    verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture(), any())
     return arg.value
   }
 
   private fun getLatestDragToDesktopWct(): WindowContainerTransaction {
     val arg: ArgumentCaptor<WindowContainerTransaction> =
         ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    if (ENABLE_SHELL_TRANSITIONS) {
-      verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(capture(arg))
-    } else {
-      verify(shellTaskOrganizer).applyTransaction(capture(arg))
-    }
+    verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(capture(arg))
     return arg.value
   }
 
   private fun getLatestExitDesktopWct(): WindowContainerTransaction {
     val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
-    if (ENABLE_SHELL_TRANSITIONS) {
-      verify(exitDesktopTransitionHandler).startTransition(any(), arg.capture(), any(), any())
-    } else {
-      verify(shellTaskOrganizer).applyTransaction(arg.capture())
-    }
+    verify(exitDesktopTransitionHandler).startTransition(any(), arg.capture(), any(), any())
     return arg.value
   }
 
@@ -3799,27 +3954,15 @@
       wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds
 
   private fun verifyWCTNotExecuted() {
-    if (ENABLE_SHELL_TRANSITIONS) {
-      verify(transitions, never()).startTransition(anyInt(), any(), isNull())
-    } else {
-      verify(shellTaskOrganizer, never()).applyTransaction(any())
-    }
+    verify(transitions, never()).startTransition(anyInt(), any(), isNull())
   }
 
   private fun verifyExitDesktopWCTNotExecuted() {
-    if (ENABLE_SHELL_TRANSITIONS) {
-      verify(exitDesktopTransitionHandler, never()).startTransition(any(), any(), any(), any())
-    } else {
-      verify(shellTaskOrganizer, never()).applyTransaction(any())
-    }
+    verify(exitDesktopTransitionHandler, never()).startTransition(any(), any(), any(), any())
   }
 
   private fun verifyEnterDesktopWCTNotExecuted() {
-    if (ENABLE_SHELL_TRANSITIONS) {
-      verify(enterDesktopTransitionHandler, never()).moveToDesktop(any(), any())
-    } else {
-      verify(shellTaskOrganizer, never()).applyTransaction(any())
-    }
+    verify(enterDesktopTransitionHandler, never()).moveToDesktop(any(), any())
   }
 
   private fun createTransition(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt
index 9b9703f..8495580 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt
@@ -115,8 +115,8 @@
                 freeformTasksInZOrder = freeformTasksInZOrder)
 
             val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID)
-            assertThat(actualDesktop.tasksByTaskIdMap).hasSize(2)
-            assertThat(actualDesktop.getZOrderedTasks(0)).isEqualTo(2)
+            assertThat(actualDesktop?.tasksByTaskIdMap).hasSize(2)
+            assertThat(actualDesktop?.getZOrderedTasks(0)).isEqualTo(2)
         }
     }
 
@@ -138,7 +138,7 @@
                 freeformTasksInZOrder = freeformTasksInZOrder)
 
             val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID)
-            assertThat(actualDesktop.tasksByTaskIdMap[task.taskId]?.desktopTaskState)
+            assertThat(actualDesktop?.tasksByTaskIdMap?.get(task.taskId)?.desktopTaskState)
                 .isEqualTo(DesktopTaskState.MINIMIZED)
         }
     }
@@ -161,8 +161,8 @@
                 freeformTasksInZOrder = freeformTasksInZOrder)
 
             val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID)
-            assertThat(actualDesktop.tasksByTaskIdMap).isEmpty()
-            assertThat(actualDesktop.zOrderedTasksList).isEmpty()
+            assertThat(actualDesktop?.tasksByTaskIdMap).isEmpty()
+            assertThat(actualDesktop?.zOrderedTasksList).isEmpty()
         }
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
index 7ae0bcd..90ab2b8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
@@ -43,7 +43,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.window.flags.Flags;
-import com.android.wm.shell.desktopmode.DesktopFullImmersiveTransitionHandler;
+import com.android.wm.shell.desktopmode.DesktopImmersiveController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.FocusTransitionObserver;
 import com.android.wm.shell.transition.TransitionInfoBuilder;
@@ -70,7 +70,7 @@
     @Mock
     private Transitions mTransitions;
     @Mock
-    private DesktopFullImmersiveTransitionHandler mDesktopFullImmersiveTransitionHandler;
+    private DesktopImmersiveController mDesktopImmersiveController;
     @Mock
     private WindowDecorViewModel mWindowDecorViewModel;
     @Mock
@@ -92,7 +92,7 @@
 
         mTransitionObserver = new FreeformTaskTransitionObserver(
                 context, mShellInit, mTransitions,
-                Optional.of(mDesktopFullImmersiveTransitionHandler),
+                Optional.of(mDesktopImmersiveController),
                 mWindowDecorViewModel, Optional.of(mTaskChangeListener), mFocusTransitionObserver);
 
         final ArgumentCaptor<Runnable> initRunnableCaptor = ArgumentCaptor.forClass(
@@ -321,7 +321,7 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
-    public void onTransitionReady_forwardsToDesktopImmersiveHandler() {
+    public void onTransitionReady_forwardsToDesktopImmersiveController() {
         final IBinder transition = mock(IBinder.class);
         final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CHANGE, 0).build();
         final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
@@ -329,7 +329,38 @@
 
         mTransitionObserver.onTransitionReady(transition, info, startT, finishT);
 
-        verify(mDesktopFullImmersiveTransitionHandler).onTransitionReady(transition, info);
+        verify(mDesktopImmersiveController).onTransitionReady(transition, info, startT, finishT);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    public void onTransitionMerged_forwardsToDesktopImmersiveController() {
+        final IBinder merged = mock(IBinder.class);
+        final IBinder playing = mock(IBinder.class);
+
+        mTransitionObserver.onTransitionMerged(merged, playing);
+
+        verify(mDesktopImmersiveController).onTransitionMerged(merged, playing);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    public void onTransitionStarting_forwardsToDesktopImmersiveController() {
+        final IBinder transition = mock(IBinder.class);
+
+        mTransitionObserver.onTransitionStarting(transition);
+
+        verify(mDesktopImmersiveController).onTransitionStarting(transition);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    public void onTransitionFinished_forwardsToDesktopImmersiveController() {
+        final IBinder transition = mock(IBinder.class);
+
+        mTransitionObserver.onTransitionFinished(transition, /* aborted= */ false);
+
+        verify(mDesktopImmersiveController).onTransitionFinished(transition, /* aborted= */ false);
     }
 
     private static TransitionInfo.Change createChange(int mode, int taskId, int windowingMode) {
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 175fbd2..1839b8a 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
@@ -87,6 +87,8 @@
 import com.android.wm.shell.common.ShellExecutor
 import com.android.wm.shell.common.SyncTransactionQueue
 import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger
 import com.android.wm.shell.desktopmode.DesktopRepository
 import com.android.wm.shell.desktopmode.DesktopTasksController
 import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
@@ -194,7 +196,11 @@
     @Mock private lateinit var mockAppHandleEducationController: AppHandleEducationController
     @Mock private lateinit var mockFocusTransitionObserver: FocusTransitionObserver
     @Mock private lateinit var mockCaptionHandleRepository: WindowDecorCaptionHandleRepository
+    @Mock private lateinit var motionEvent: MotionEvent
+    @Mock lateinit var displayController: DisplayController
+    @Mock lateinit var displayLayout: DisplayLayout
     private lateinit var spyContext: TestableContext
+    private lateinit var desktopModeEventLogger: DesktopModeEventLogger
 
     private val transactionFactory = Supplier<SurfaceControl.Transaction> {
         SurfaceControl.Transaction()
@@ -224,6 +230,7 @@
         shellInit = ShellInit(mockShellExecutor)
         windowDecorByTaskIdSpy.clear()
         spyContext.addMockSystemService(InputManager::class.java, mockInputManager)
+        desktopModeEventLogger = mock<DesktopModeEventLogger>()
         desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel(
                 spyContext,
                 mockShellExecutor,
@@ -256,7 +263,8 @@
                 mockCaptionHandleRepository,
                 Optional.of(mockActivityOrientationChangeHandler),
                 mockTaskPositionerFactory,
-                mockFocusTransitionObserver
+                mockFocusTransitionObserver,
+                desktopModeEventLogger
         )
         desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController)
         whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout)
@@ -299,6 +307,10 @@
             argumentCaptor<DesktopModeKeyguardChangeListener>()
         verify(mockShellController).addKeyguardChangeListener(keyguardChangedCaptor.capture())
         desktopModeOnKeyguardChangedListener = keyguardChangedCaptor.firstValue
+        whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout)
+        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+            (i.arguments.first() as Rect).set(STABLE_BOUNDS)
+        }
     }
 
     @After
@@ -612,7 +624,11 @@
 
         maxOrRestoreListenerCaptor.value.invoke()
 
-        verify(mockDesktopTasksController).toggleDesktopTaskSize(decor.mTaskInfo)
+        verify(mockDesktopTasksController).toggleDesktopTaskSize(
+            decor.mTaskInfo,
+            ResizeTrigger.MAXIMIZE_MENU,
+            null
+        )
     }
 
     @Test
@@ -647,7 +663,9 @@
             eq(decor.mTaskInfo),
             taskSurfaceCaptor.capture(),
             eq(currentBounds),
-            eq(SnapPosition.LEFT)
+            eq(SnapPosition.LEFT),
+            eq(ResizeTrigger.SNAP_LEFT_MENU),
+            eq(null)
         )
         assertEquals(taskSurfaceCaptor.firstValue, decor.mTaskSurface)
     }
@@ -685,7 +703,9 @@
             eq(decor.mTaskInfo),
             taskSurfaceCaptor.capture(),
             eq(currentBounds),
-            eq(SnapPosition.LEFT)
+            eq(SnapPosition.LEFT),
+            eq(ResizeTrigger.SNAP_LEFT_MENU),
+            eq(null)
         )
         assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue)
     }
@@ -704,7 +724,9 @@
         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(null))
         verify(mockToast).show()
     }
 
@@ -725,7 +747,9 @@
             eq(decor.mTaskInfo),
             taskSurfaceCaptor.capture(),
             eq(currentBounds),
-            eq(SnapPosition.RIGHT)
+            eq(SnapPosition.RIGHT),
+            eq(ResizeTrigger.SNAP_RIGHT_MENU),
+            eq(null)
         )
         assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue)
     }
@@ -763,7 +787,9 @@
             eq(decor.mTaskInfo),
             taskSurfaceCaptor.capture(),
             eq(currentBounds),
-            eq(SnapPosition.RIGHT)
+            eq(SnapPosition.RIGHT),
+            eq(ResizeTrigger.SNAP_RIGHT_MENU),
+            eq(null)
         )
         assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue)
     }
@@ -782,7 +808,9 @@
         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(null))
         verify(mockToast).show()
     }
 
@@ -1247,7 +1275,7 @@
         onClickListenerCaptor.value.onClick(view)
 
         verify(mockDesktopTasksController)
-            .toggleDesktopTaskSize(decor.mTaskInfo)
+            .toggleDesktopTaskSize(decor.mTaskInfo, ResizeTrigger.MAXIMIZE_BUTTON, null)
     }
 
     private fun createOpenTaskDecoration(
@@ -1337,7 +1365,7 @@
         whenever(
             mockDesktopModeWindowDecorFactory.create(
                 any(), any(), any(), any(), any(), any(), eq(task), any(), any(), any(), any(),
-                any(), any(), any(), any(), any(), any(), any())
+                any(), any(), any(), any(), any(), any(), any(), any())
         ).thenReturn(decoration)
         decoration.mTaskInfo = task
         whenever(decoration.user).thenReturn(mockUserHandle)
@@ -1378,5 +1406,6 @@
         private const val TAG = "DesktopModeWindowDecorViewModelTests"
         private val STABLE_INSETS = Rect(0, 100, 0, 0)
         private val INITIAL_BOUNDS = Rect(0, 0, 100, 100)
+        private val STABLE_BOUNDS = Rect(0, 0, 1000, 1000)
     }
 }
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 3208872..0afb6c1 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
@@ -106,6 +106,7 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.desktopmode.CaptionState;
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
 import com.android.wm.shell.desktopmode.DesktopRepository;
 import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
@@ -210,6 +211,8 @@
     private MultiInstanceHelper mMockMultiInstanceHelper;
     @Mock
     private WindowDecorCaptionHandleRepository mMockCaptionHandleRepository;
+    @Mock
+    private DesktopModeEventLogger mDesktopModeEventLogger;
     @Captor
     private ArgumentCaptor<Function1<Boolean, Unit>> mOnMaxMenuHoverChangeListener;
     @Captor
@@ -1400,7 +1403,7 @@
                 mMockTransactionSupplier, WindowContainerTransaction::new, SurfaceControl::new,
                 new WindowManagerWrapper(mMockWindowManager), mMockSurfaceControlViewHostFactory,
                 maximizeMenuFactory, mMockHandleMenuFactory,
-                mMockMultiInstanceHelper, mMockCaptionHandleRepository);
+                mMockMultiInstanceHelper, mMockCaptionHandleRepository, mDesktopModeEventLogger);
         windowDecor.setCaptionListeners(mMockTouchEventListener, mMockTouchEventListener,
                 mMockTouchEventListener, mMockTouchEventListener);
         windowDecor.setExclusionRegionListener(mMockExclusionRegionListener);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
index 24f6bec..a20a89c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
@@ -36,6 +36,7 @@
 import com.android.wm.shell.common.DisplayLayout
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM
+import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT
 import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT
 import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP
 import com.google.common.truth.Truth.assertThat
@@ -48,9 +49,9 @@
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito.any
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 import org.mockito.quality.Strictness
+import org.mockito.Mockito.`when` as whenever
 
 /**
  * Tests for [DragPositioningCallbackUtility].
@@ -193,6 +194,62 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_SCALED_RESIZING)
+    fun testChangeBounds_unresizeableApp_initialHeightLessThanMin_increasingBounds_resizeAllowed() {
+        mockWindowDecoration.mTaskInfo.isResizeable = false
+        val startingPoint = PointF(BELOW_MIN_HEIGHT_BOUNDS.right.toFloat(),
+            BELOW_MIN_HEIGHT_BOUNDS.bottom.toFloat())
+        val repositionTaskBounds = Rect(BELOW_MIN_HEIGHT_BOUNDS)
+
+        // Resize to increased bounds
+        val newX = BELOW_MIN_HEIGHT_BOUNDS.right.toFloat() + 20
+        val newY = BELOW_MIN_HEIGHT_BOUNDS.bottom.toFloat() + 10
+        val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+        // Resize should be allowed as drag is in direction of desired range
+        assertTrue(
+            DragPositioningCallbackUtility.changeBounds(
+                CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM,
+                repositionTaskBounds, BELOW_MIN_HEIGHT_BOUNDS, STABLE_BOUNDS, delta,
+                mockDisplayController, mockWindowDecoration
+            )
+        )
+
+        assertThat(repositionTaskBounds.left).isEqualTo(BELOW_MIN_HEIGHT_BOUNDS.left)
+        assertThat(repositionTaskBounds.top).isEqualTo(BELOW_MIN_HEIGHT_BOUNDS.top)
+        assertThat(repositionTaskBounds.right).isEqualTo(BELOW_MIN_HEIGHT_BOUNDS.right + 20)
+        assertThat(repositionTaskBounds.bottom).isEqualTo(BELOW_MIN_HEIGHT_BOUNDS.bottom + 10)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_SCALED_RESIZING)
+    fun testChangeBounds_unresizeableApp_initialHeightMoreThanMax_decreasingBounds_resizeAllowed() {
+        mockWindowDecoration.mTaskInfo.isResizeable = false
+        val startingPoint = PointF(EXCEEDS_MAX_HEIGHT_BOUNDS.right.toFloat(),
+            EXCEEDS_MAX_HEIGHT_BOUNDS.top.toFloat())
+        val repositionTaskBounds = Rect(EXCEEDS_MAX_HEIGHT_BOUNDS)
+
+        // Resize to decreased bounds.
+        val newX = EXCEEDS_MAX_HEIGHT_BOUNDS.right.toFloat() - 10
+        val newY = EXCEEDS_MAX_HEIGHT_BOUNDS.top.toFloat() + 20
+        val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+        // Resize should be allowed as drag is in direction of desired range.
+        assertTrue(
+            DragPositioningCallbackUtility.changeBounds(
+                CTRL_TYPE_RIGHT or CTRL_TYPE_TOP,
+                repositionTaskBounds, EXCEEDS_MAX_HEIGHT_BOUNDS, STABLE_BOUNDS, delta,
+                mockDisplayController, mockWindowDecoration
+            )
+        )
+
+        assertThat(repositionTaskBounds.left).isEqualTo(EXCEEDS_MAX_HEIGHT_BOUNDS.left)
+        assertThat(repositionTaskBounds.top).isEqualTo(EXCEEDS_MAX_HEIGHT_BOUNDS.top + 20)
+        assertThat(repositionTaskBounds.right).isEqualTo(EXCEEDS_MAX_HEIGHT_BOUNDS.right - 10)
+        assertThat(repositionTaskBounds.bottom).isEqualTo(EXCEEDS_MAX_HEIGHT_BOUNDS.bottom)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_SCALED_RESIZING)
     fun testChangeBounds_unresizeableApp_widthLessThanMin_resetToStartingBounds() {
         mockWindowDecoration.mTaskInfo.isResizeable = false
         val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat())
@@ -211,13 +268,68 @@
             )
         )
 
-
         assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left)
         assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top)
         assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right)
         assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom)
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_SCALED_RESIZING)
+    fun testChangeBounds_unresizeableApp_initialWidthLessThanMin_increasingBounds_resizeAllowed() {
+        mockWindowDecoration.mTaskInfo.isResizeable = false
+        val startingPoint = PointF(BELOW_MIN_WIDTH_BOUNDS.right.toFloat(),
+            BELOW_MIN_WIDTH_BOUNDS.bottom.toFloat())
+        val repositionTaskBounds = Rect(BELOW_MIN_WIDTH_BOUNDS)
+
+        // Resize to increased bounds.
+        val newX = BELOW_MIN_WIDTH_BOUNDS.right.toFloat() + 10
+        val newY = BELOW_MIN_WIDTH_BOUNDS.bottom.toFloat() + 20
+        val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+        // Resize should be allowed as drag is in direction of desired range.
+        assertTrue(
+            DragPositioningCallbackUtility.changeBounds(
+                CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM,
+                repositionTaskBounds, BELOW_MIN_WIDTH_BOUNDS, STABLE_BOUNDS, delta,
+                mockDisplayController, mockWindowDecoration
+            )
+        )
+
+        assertThat(repositionTaskBounds.left).isEqualTo(BELOW_MIN_WIDTH_BOUNDS.left)
+        assertThat(repositionTaskBounds.top).isEqualTo(BELOW_MIN_WIDTH_BOUNDS.top)
+        assertThat(repositionTaskBounds.right).isEqualTo(BELOW_MIN_WIDTH_BOUNDS.right + 10)
+        assertThat(repositionTaskBounds.bottom).isEqualTo(BELOW_MIN_WIDTH_BOUNDS.bottom + 20)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_SCALED_RESIZING)
+    fun testChangeBounds_unresizeableApp_initialWidthMoreThanMax_decreasingBounds_resizeAllowed() {
+        mockWindowDecoration.mTaskInfo.isResizeable = false
+        val startingPoint = PointF(EXCEEDS_MAX_WIDTH_BOUNDS.left.toFloat(),
+            EXCEEDS_MAX_WIDTH_BOUNDS.top.toFloat())
+        val repositionTaskBounds = Rect(EXCEEDS_MAX_WIDTH_BOUNDS)
+
+        // Resize to decreased bounds.
+        val newX = EXCEEDS_MAX_WIDTH_BOUNDS.left.toFloat() + 20
+        val newY = EXCEEDS_MAX_WIDTH_BOUNDS.top.toFloat() + 10
+        val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+        // Resize should be allowed as drag is in direction of desired range.
+        assertTrue(
+            DragPositioningCallbackUtility.changeBounds(
+                CTRL_TYPE_LEFT or CTRL_TYPE_TOP,
+                repositionTaskBounds, EXCEEDS_MAX_WIDTH_BOUNDS, STABLE_BOUNDS, delta,
+                mockDisplayController, mockWindowDecoration
+            )
+        )
+
+        assertThat(repositionTaskBounds.left).isEqualTo(EXCEEDS_MAX_WIDTH_BOUNDS.left + 20)
+        assertThat(repositionTaskBounds.top).isEqualTo(EXCEEDS_MAX_WIDTH_BOUNDS.top + 10)
+        assertThat(repositionTaskBounds.right).isEqualTo(EXCEEDS_MAX_WIDTH_BOUNDS.right)
+        assertThat(repositionTaskBounds.bottom).isEqualTo(EXCEEDS_MAX_WIDTH_BOUNDS.bottom)
+    }
+
 
     @Test
     fun testChangeBoundsDoesNotChangeHeightWhenNegative() {
@@ -427,6 +539,60 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS)
+    fun testMinHeight_initialHeightLessThanMin_increasingHeight_resizeAllowed() {
+        val startingPoint = PointF(BELOW_MIN_HEIGHT_BOUNDS.right.toFloat(),
+            BELOW_MIN_HEIGHT_BOUNDS.bottom.toFloat())
+        val repositionTaskBounds = Rect(BELOW_MIN_HEIGHT_BOUNDS)
+
+        // Attempt to increase height.
+        val newX = BELOW_MIN_HEIGHT_BOUNDS.right.toFloat()
+        val newY = BELOW_MIN_HEIGHT_BOUNDS.bottom.toFloat() + 10
+        val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+        // Resize should be allowed as drag is increasing height closer to valid region.
+        assertTrue(
+            DragPositioningCallbackUtility.changeBounds(
+                CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM,
+                repositionTaskBounds, BELOW_MIN_HEIGHT_BOUNDS, STABLE_BOUNDS, delta,
+                mockDisplayController, mockWindowDecoration
+            )
+        )
+
+        assertThat(repositionTaskBounds.left).isEqualTo(BELOW_MIN_HEIGHT_BOUNDS.left)
+        assertThat(repositionTaskBounds.top).isEqualTo(BELOW_MIN_HEIGHT_BOUNDS.top)
+        assertThat(repositionTaskBounds.right).isEqualTo(BELOW_MIN_HEIGHT_BOUNDS.right)
+        assertThat(repositionTaskBounds.bottom).isEqualTo(BELOW_MIN_HEIGHT_BOUNDS.bottom + 10)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS)
+    fun testMinWidth_initialWidthLessThanMin_increasingBounds_resizeAllowed() {
+        val startingPoint = PointF(BELOW_MIN_WIDTH_BOUNDS.right.toFloat(),
+            BELOW_MIN_WIDTH_BOUNDS.bottom.toFloat())
+        val repositionTaskBounds = Rect(BELOW_MIN_WIDTH_BOUNDS)
+
+        // Attempt to increase width.
+        val newX = BELOW_MIN_WIDTH_BOUNDS.right.toFloat() + 10
+        val newY = BELOW_MIN_WIDTH_BOUNDS.bottom.toFloat()
+        val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+        // Resize should be allowed as drag is increasing width closer to valid region.
+        assertTrue(
+            DragPositioningCallbackUtility.changeBounds(
+                CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM,
+                repositionTaskBounds, BELOW_MIN_WIDTH_BOUNDS, STABLE_BOUNDS, delta,
+                mockDisplayController, mockWindowDecoration
+            )
+        )
+
+        assertThat(repositionTaskBounds.left).isEqualTo(BELOW_MIN_WIDTH_BOUNDS.left)
+        assertThat(repositionTaskBounds.top).isEqualTo(BELOW_MIN_WIDTH_BOUNDS.top)
+        assertThat(repositionTaskBounds.right).isEqualTo(BELOW_MIN_WIDTH_BOUNDS.right + 10)
+        assertThat(repositionTaskBounds.bottom).isEqualTo(BELOW_MIN_WIDTH_BOUNDS.bottom)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS)
     fun taskMinWidthHeightUndefined_changeBoundsInDesktopModeAllowedSize_shouldChangeBounds() {
         doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(mockContext) }
         initializeTaskInfo(taskMinWidth = -1, taskMinHeight = -1)
@@ -547,6 +713,61 @@
         assertThat(repositionTaskBounds.height()).isLessThan(STABLE_BOUNDS.bottom)
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS)
+    fun testMaxHeight_initialHeightMoreThanMax_decreasingHeight_resizeAllowed() {
+        mockWindowDecoration.mTaskInfo.isResizeable = false
+        val startingPoint = PointF(EXCEEDS_MAX_HEIGHT_BOUNDS.right.toFloat(),
+            EXCEEDS_MAX_HEIGHT_BOUNDS.top.toFloat())
+        val repositionTaskBounds = Rect(EXCEEDS_MAX_HEIGHT_BOUNDS)
+
+        // Attempt to decrease height
+        val newX = EXCEEDS_MAX_HEIGHT_BOUNDS.right.toFloat() - 10
+        val newY = EXCEEDS_MAX_HEIGHT_BOUNDS.top.toFloat()
+        val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+        // Resize should be allowed as drag is decreasing height closer to valid region.
+        assertTrue(
+            DragPositioningCallbackUtility.changeBounds(
+                CTRL_TYPE_RIGHT or CTRL_TYPE_TOP,
+                repositionTaskBounds, EXCEEDS_MAX_HEIGHT_BOUNDS, STABLE_BOUNDS, delta,
+                mockDisplayController, mockWindowDecoration
+            )
+        )
+
+        assertThat(repositionTaskBounds.left).isEqualTo(EXCEEDS_MAX_HEIGHT_BOUNDS.left)
+        assertThat(repositionTaskBounds.top).isEqualTo(EXCEEDS_MAX_HEIGHT_BOUNDS.top)
+        assertThat(repositionTaskBounds.right).isEqualTo(EXCEEDS_MAX_HEIGHT_BOUNDS.right - 10)
+        assertThat(repositionTaskBounds.bottom).isEqualTo(EXCEEDS_MAX_HEIGHT_BOUNDS.bottom )
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS)
+    fun testMaxHeight_initialWidthMoreThanMax_decreasingBounds_resizeAllowed() {
+        val startingPoint = PointF(EXCEEDS_MAX_WIDTH_BOUNDS.left.toFloat(),
+            EXCEEDS_MAX_WIDTH_BOUNDS.top.toFloat())
+        val repositionTaskBounds = Rect(EXCEEDS_MAX_WIDTH_BOUNDS)
+
+        // Attempt to decrease width.
+        val newX = EXCEEDS_MAX_WIDTH_BOUNDS.left.toFloat() + 20
+        val newY = EXCEEDS_MAX_WIDTH_BOUNDS.top.toFloat()
+        val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+        // Resize should be allowed as drag is decreasing width closer to valid region.
+        assertTrue(
+            DragPositioningCallbackUtility.changeBounds(
+                CTRL_TYPE_LEFT or CTRL_TYPE_TOP,
+                repositionTaskBounds, EXCEEDS_MAX_WIDTH_BOUNDS, STABLE_BOUNDS, delta,
+                mockDisplayController, mockWindowDecoration
+            )
+        )
+
+        assertThat(repositionTaskBounds.left).isEqualTo(EXCEEDS_MAX_WIDTH_BOUNDS.left + 20)
+        assertThat(repositionTaskBounds.top).isEqualTo(EXCEEDS_MAX_WIDTH_BOUNDS.top)
+        assertThat(repositionTaskBounds.right).isEqualTo(EXCEEDS_MAX_WIDTH_BOUNDS.right)
+        assertThat(repositionTaskBounds.bottom).isEqualTo(EXCEEDS_MAX_WIDTH_BOUNDS.bottom)
+    }
+
     private fun initializeTaskInfo(taskMinWidth: Int = MIN_WIDTH, taskMinHeight: Int = MIN_HEIGHT) {
         mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply {
             taskId = TASK_ID
@@ -571,6 +792,10 @@
         private const val NAVBAR_HEIGHT = 50
         private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600)
         private val STARTING_BOUNDS = Rect(0, 0, 100, 100)
+        private val BELOW_MIN_WIDTH_BOUNDS = Rect(0, 0, 50, 100)
+        private val BELOW_MIN_HEIGHT_BOUNDS = Rect(0, 0, 100, 50)
+        private val EXCEEDS_MAX_WIDTH_BOUNDS = Rect(0, 0, 3000, 1500)
+        private val EXCEEDS_MAX_HEIGHT_BOUNDS = Rect(0, 0, 1000, 2000)
         private val OFF_CENTER_STARTING_BOUNDS = Rect(-100, -100, 10, 10)
         private val DISALLOWED_RESIZE_AREA = Rect(
             DISPLAY_BOUNDS.left,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index fb17ae9..cb7fade 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -83,6 +83,7 @@
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestRunningTaskInfoBuilder;
 import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.tests.R;
 import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer;
@@ -138,6 +139,8 @@
     private SurfaceSyncGroup mMockSurfaceSyncGroup;
     @Mock
     private SurfaceControl mMockTaskSurface;
+    @Mock
+    private DesktopModeEventLogger mDesktopModeEventLogger;
 
     private final List<SurfaceControl.Transaction> mMockSurfaceControlTransactions =
             new ArrayList<>();
@@ -1014,7 +1017,7 @@
                 new MockObjectSupplier<>(mMockSurfaceControlTransactions,
                         () -> mock(SurfaceControl.Transaction.class)),
                 () -> mMockWindowContainerTransaction, () -> mMockTaskSurface,
-                mMockSurfaceControlViewHostFactory);
+                mMockSurfaceControlViewHostFactory, mDesktopModeEventLogger);
     }
 
     private class MockObjectSupplier<T> implements Supplier<T> {
@@ -1054,11 +1057,12 @@
                 Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier,
                 Supplier<WindowContainerTransaction> windowContainerTransactionSupplier,
                 Supplier<SurfaceControl> surfaceControlSupplier,
-                SurfaceControlViewHostFactory surfaceControlViewHostFactory) {
+                SurfaceControlViewHostFactory surfaceControlViewHostFactory,
+                DesktopModeEventLogger desktopModeEventLogger) {
             super(context, userContext, displayController, taskOrganizer, taskInfo, taskSurface,
                     surfaceControlBuilderSupplier, surfaceControlTransactionSupplier,
                     windowContainerTransactionSupplier, surfaceControlSupplier,
-                    surfaceControlViewHostFactory);
+                    surfaceControlViewHostFactory, desktopModeEventLogger);
         }
 
         @Override
diff --git a/libs/hwui/utils/Color.cpp b/libs/hwui/utils/Color.cpp
index 23097f6..c7a7ed2 100644
--- a/libs/hwui/utils/Color.cpp
+++ b/libs/hwui/utils/Color.cpp
@@ -17,7 +17,6 @@
 #include "Color.h"
 
 #include <Properties.h>
-#include <aidl/android/hardware/graphics/common/Dataspace.h>
 #include <android/hardware_buffer.h>
 #include <android/native_window.h>
 #include <ui/ColorSpace.h>
@@ -222,8 +221,7 @@
         if (nearlyEqual(fn, SkNamedTransferFn::kRec2020)) {
             return HAL_DATASPACE_BT2020;
         } else if (nearlyEqual(fn, SkNamedTransferFn::kSRGB)) {
-            return static_cast<android_dataspace>(
-                    ::aidl::android::hardware::graphics::common::Dataspace::DISPLAY_BT2020);
+            return static_cast<android_dataspace>(HAL_DATASPACE_DISPLAY_BT2020);
         }
     }
 
diff --git a/libs/protoutil/Android.bp b/libs/protoutil/Android.bp
index 28856c8..8af4b7e 100644
--- a/libs/protoutil/Android.bp
+++ b/libs/protoutil/Android.bp
@@ -60,6 +60,7 @@
         "//apex_available:platform",
         "com.android.os.statsd",
         "test_com.android.os.statsd",
+        "com.android.uprobestats",
     ],
 }
 
diff --git a/media/java/android/media/AudioDeviceVolumeManager.java b/media/java/android/media/AudioDeviceVolumeManager.java
index 13876ad..e1fbfea 100644
--- a/media/java/android/media/AudioDeviceVolumeManager.java
+++ b/media/java/android/media/AudioDeviceVolumeManager.java
@@ -16,8 +16,11 @@
 
 package android.media;
 
+import static com.android.media.flags.Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL;
+
 import android.Manifest;
 import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -434,6 +437,83 @@
 
     /**
      * @hide
+     * Sets the input gain index for a particular AudioDeviceAttributes.
+     * TODO(b/364923030): create InputVolumeInfo on top of VolumeInfo rather than using index to
+     * handle volume information, to solve issues e.g. gain index ranges might be different for
+     * different categories of devices.
+     */
+    @FlaggedApi(FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+    @RequiresPermission(Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    public void setInputGainIndex(@NonNull AudioDeviceAttributes ada, int index) {
+        try {
+            getService().setInputGainIndex(ada, index);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @hide
+     * Gets the input gain index for a particular AudioDeviceAttributes.
+     */
+    @FlaggedApi(FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+    @RequiresPermission(Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    public int getInputGainIndex(@NonNull AudioDeviceAttributes ada) {
+        try {
+            return getService().getInputGainIndex(ada);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @hide
+     * Gets the maximum input gain index for input device.
+     */
+    @FlaggedApi(FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+    @RequiresPermission(Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    public int getMaxInputGainIndex() {
+        try {
+            return getService().getMaxInputGainIndex();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @hide
+     * Gets the minimum input gain index for input device.
+     */
+    @FlaggedApi(FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+    @RequiresPermission(Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    public int getMinInputGainIndex() {
+        try {
+            return getService().getMinInputGainIndex();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @hide
+     * Indicates if an input device does not support input gain control.
+     *     <p>The following APIs have no effect when input gain is fixed:
+     *     <ul>
+     *       <li>{@link #setInputGainIndex(AudioDeviceAttributes, int)}
+     *     </ul>
+     */
+    @FlaggedApi(FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+    @RequiresPermission(Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    public boolean isInputGainFixed(@NonNull AudioDeviceAttributes ada) {
+        try {
+            return getService().isInputGainFixed(ada);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @hide
      * Return human-readable name for volume behavior
      * @param behavior one of the volume behaviors defined in AudioManager
      * @return a string for the given behavior
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index c22b674..9fd3f5b 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -189,6 +189,21 @@
 
     void setMicrophoneMute(boolean on, String callingPackage, int userId, in String attributionTag);
 
+    @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+    void setInputGainIndex(in AudioDeviceAttributes ada, int index);
+
+    @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+    int getInputGainIndex(in AudioDeviceAttributes ada);
+
+    @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+    int getMaxInputGainIndex();
+
+    @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+    int getMinInputGainIndex();
+
+    @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+    boolean isInputGainFixed(in AudioDeviceAttributes ada);
+
     oneway void setMicrophoneMuteFromSwitch(boolean on);
 
     void setRingerModeExternal(int ringerMode, String caller);
diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java
index 8ff4305..3a19f46 100644
--- a/media/java/android/media/MediaCodecInfo.java
+++ b/media/java/android/media/MediaCodecInfo.java
@@ -462,6 +462,33 @@
         @SuppressLint("AllUpper")
         public static final int COLOR_FormatYUVP010                 = 54;
 
+        /**
+         * P210 is 10-bit-per component 4:2:2 YCbCr semiplanar format.
+         * <p>
+         * This format uses 32 allocated bits per pixel with 20 bits of
+         * data per pixel. Chroma planes are subsampled by 2 both
+         * horizontally. Each chroma and luma component
+         * has 16 allocated bits in little-endian configuration with 10
+         * MSB of actual data.
+         *
+         * <pre>
+         *            byte                   byte
+         *  <--------- i --------> | <------ i + 1 ------>
+         * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+         * |     UNUSED      |      Y/Cb/Cr                |
+         * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+         *  0               5 6   7 0                    7
+         * bit
+         * </pre>
+         *
+         * Use this format with {@link Image}. This format corresponds
+         * to {@link android.graphics.ImageFormat#YCBCR_P210}.
+         * <p>
+         */
+        @SuppressLint("AllUpper")
+        @FlaggedApi(android.media.codec.Flags.FLAG_P210_FORMAT_SUPPORT)
+        public static final int COLOR_FormatYUVP210                 = 60;
+
         /** @deprecated Use {@link #COLOR_FormatYUV420Flexible}. */
         public static final int COLOR_TI_FormatYUV420PackedSemiPlanar = 0x7f000100;
         // COLOR_FormatSurface indicates that the data will be a GraphicBuffer metadata reference.
diff --git a/media/java/android/media/Utils.java b/media/java/android/media/Utils.java
index 41e9b65..11bd221 100644
--- a/media/java/android/media/Utils.java
+++ b/media/java/android/media/Utils.java
@@ -719,6 +719,9 @@
      * @return {@code true} if the Uri has vibration parameter
      */
     public static boolean hasVibration(Uri ringtoneUri) {
+        if (ringtoneUri == null) {
+            return false;
+        }
         final String vibrationUriString = ringtoneUri.getQueryParameter(VIBRATION_URI_PARAM);
         return vibrationUriString != null;
     }
@@ -730,7 +733,10 @@
      * @return parsed {@link Uri} of vibration parameter, {@code null} if the vibration parameter
      * is not found.
      */
-    public static Uri getVibrationUri(Uri ringtoneUri) {
+    public static @Nullable Uri getVibrationUri(Uri ringtoneUri) {
+        if (ringtoneUri == null) {
+            return null;
+        }
         final String vibrationUriString = ringtoneUri.getQueryParameter(VIBRATION_URI_PARAM);
         if (vibrationUriString == null) {
             return null;
diff --git a/media/java/android/media/tv/tuner/frontend/FrontendStatus.java b/media/java/android/media/tv/tuner/frontend/FrontendStatus.java
index fd677ac..898a8bf 100644
--- a/media/java/android/media/tv/tuner/frontend/FrontendStatus.java
+++ b/media/java/android/media/tv/tuner/frontend/FrontendStatus.java
@@ -16,11 +16,13 @@
 
 package android.media.tv.tuner.frontend;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
+import android.media.tv.flags.Flags;
 import android.media.tv.tuner.Lnb;
 import android.media.tv.tuner.TunerVersionChecker;
 
@@ -61,7 +63,7 @@
             FRONTEND_STATUS_TYPE_DVBT_CELL_IDS, FRONTEND_STATUS_TYPE_ATSC3_ALL_PLP_INFO,
             FRONTEND_STATUS_TYPE_IPTV_CONTENT_URL, FRONTEND_STATUS_TYPE_IPTV_PACKETS_LOST,
             FRONTEND_STATUS_TYPE_IPTV_PACKETS_RECEIVED, FRONTEND_STATUS_TYPE_IPTV_WORST_JITTER_MS,
-            FRONTEND_STATUS_TYPE_IPTV_AVERAGE_JITTER_MS})
+            FRONTEND_STATUS_TYPE_IPTV_AVERAGE_JITTER_MS, FRONTEND_STATUS_TYPE_STANDARD_EXT})
     @Retention(RetentionPolicy.SOURCE)
     public @interface FrontendStatusType {}
 
@@ -311,6 +313,13 @@
     public static final int FRONTEND_STATUS_TYPE_ATSC3_ALL_PLP_INFO =
             android.hardware.tv.tuner.FrontendStatusType.ATSC3_ALL_PLP_INFO;
 
+    /**
+     * Standard extension.
+     */
+    @FlaggedApi(Flags.FLAG_TUNER_W_APIS)
+    public static final int FRONTEND_STATUS_TYPE_STANDARD_EXT =
+            android.hardware.tv.tuner.FrontendStatusType.STANDARD_EXT;
+
     /** @hide */
     @IntDef(value = {
             AtscFrontendSettings.MODULATION_UNDEFINED,
@@ -558,6 +567,7 @@
     private Long mIptvPacketsReceived;
     private Integer mIptvWorstJitterMs;
     private Integer mIptvAverageJitterMs;
+    private StandardExt mStandardExt;
 
     // Constructed and fields set by JNI code.
     private FrontendStatus() {
@@ -1273,4 +1283,27 @@
         }
         return mIptvAverageJitterMs;
     }
+    /**
+     * Gets the standard extension.
+     *
+     * <p>The tuner standard DVB-T has the extension DVB-T2, and the standard DVB-S has the
+     * extensions DVB-S2 and DVB-S2X. This method returns the current standard extension within the
+     * same standard series. This frontend status is reported when the standard extension
+     * transitions to another during playback.
+     *
+     * <p>This query is supported only by Tuner HAL 4.0 or higher. Use
+     * {@link TunerVersionChecker#getTunerVersion()} to check the version.
+     *
+     * @return The current standard extension.
+     */
+    @NonNull
+    @FlaggedApi(Flags.FLAG_TUNER_W_APIS)
+    public StandardExt getStandardExt() {
+        TunerVersionChecker.checkHigherOrEqualVersionTo(
+                TunerVersionChecker.TUNER_VERSION_4_0, "StandardExt status");
+        if (mStandardExt == null) {
+            throw new IllegalStateException("StandardExt status is empty");
+        }
+        return mStandardExt;
+    }
 }
diff --git a/media/java/android/media/tv/tuner/frontend/StandardExt.java b/media/java/android/media/tv/tuner/frontend/StandardExt.java
new file mode 100644
index 0000000..4907272
--- /dev/null
+++ b/media/java/android/media/tv/tuner/frontend/StandardExt.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.tuner.frontend;
+
+import android.annotation.FlaggedApi;
+import android.annotation.SystemApi;
+import android.hardware.tv.tuner.FrontendDvbsStandard;
+import android.hardware.tv.tuner.FrontendDvbtStandard;
+import android.media.tv.flags.Flags;
+
+/**
+ * Standard extension for the standard DVB-T and DVB-S series.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_TUNER_W_APIS)
+public final class StandardExt {
+    private final int mDvbsStandardExt;
+    private final int mDvbtStandardExt;
+
+    /**
+     * Private constructor called by JNI only.
+     */
+    private StandardExt(int dvbsStandardExt, int dvbtStandardExt) {
+        mDvbsStandardExt = dvbsStandardExt;
+        mDvbtStandardExt = dvbtStandardExt;
+    }
+
+    /**
+     * Gets the DVB-S standard extension within the DVB-S standard series.
+     *
+     * @return An integer representing the standard, such as
+     * {@link DvbsFrontendSettings#STANDARD_S}.
+     *
+     * @see android.media.tv.tuner.frontend.DvbsFrontendSettings
+     */
+    @DvbsFrontendSettings.Standard
+    public int getDvbsStandardExt() {
+        if (mDvbsStandardExt == FrontendDvbsStandard.UNDEFINED) {
+            throw new IllegalStateException("No DVB-S standard transition");
+        }
+        return mDvbsStandardExt;
+    }
+
+    /**
+     * Gets the DVB-T standard extension within the DVB-T standard series.
+     *
+     * @return An integer representing the standard, such as
+     * {@link DvbtFrontendSettings#STANDARD_T}.
+     *
+     * @see android.media.tv.tuner.frontend.DvbtFrontendSettings
+     */
+    @DvbtFrontendSettings.Standard
+    public int getDvbtStandardExt() {
+        if (mDvbtStandardExt == FrontendDvbtStandard.UNDEFINED) {
+            throw new IllegalStateException("No DVB-T standard transition");
+        }
+        return mDvbtStandardExt;
+    }
+}
diff --git a/media/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp
index 9e1e2c3..80ca4f2 100644
--- a/media/jni/android_media_tv_Tuner.cpp
+++ b/media/jni/android_media_tv_Tuner.cpp
@@ -144,6 +144,7 @@
 #include <aidl/android/hardware/tv/tuner/FrontendScanAtsc3PlpInfo.h>
 #include <aidl/android/hardware/tv/tuner/FrontendScanMessageStandard.h>
 #include <aidl/android/hardware/tv/tuner/FrontendSpectralInversion.h>
+#include <aidl/android/hardware/tv/tuner/FrontendStandardExt.h>
 #include <aidl/android/hardware/tv/tuner/FrontendStatus.h>
 #include <aidl/android/hardware/tv/tuner/FrontendStatusAtsc3PlpInfo.h>
 #include <aidl/android/hardware/tv/tuner/FrontendStatusType.h>
@@ -302,6 +303,7 @@
 using ::aidl::android::hardware::tv::tuner::FrontendScanAtsc3PlpInfo;
 using ::aidl::android::hardware::tv::tuner::FrontendScanMessageStandard;
 using ::aidl::android::hardware::tv::tuner::FrontendSpectralInversion;
+using ::aidl::android::hardware::tv::tuner::FrontendStandardExt;
 using ::aidl::android::hardware::tv::tuner::FrontendStatus;
 using ::aidl::android::hardware::tv::tuner::FrontendStatusAtsc3PlpInfo;
 using ::aidl::android::hardware::tv::tuner::FrontendStatusType;
@@ -2937,6 +2939,33 @@
                 env->SetObjectField(statusObj, field, newIntegerObj.get());
                 break;
             }
+            case FrontendStatus::Tag::standardExt: {
+                jfieldID field = env->GetFieldID(clazz, "mStandardExt",
+                        "Landroid/media/tv/tuner/frontend/StandardExt;");
+                ScopedLocalRef standardExtClazz(env,
+                        env->FindClass("android/media/tv/tuner/frontend/StandardExt"));
+                jmethodID initStandardExt = env->GetMethodID(standardExtClazz.get(), "<init>",
+                        "(II)V");
+
+                jint dvbsStandardExt = static_cast<jint>(FrontendDvbsStandard::UNDEFINED);
+                jint dvbtStandardExt = static_cast<jint>(FrontendDvbtStandard::UNDEFINED);
+                FrontendStandardExt standardExt = s.get<FrontendStatus::Tag::standardExt>();
+                switch (standardExt.getTag()) {
+                    case FrontendStandardExt::Tag::dvbsStandardExt: {
+                        dvbsStandardExt = static_cast<jint>(standardExt
+                                .get<FrontendStandardExt::Tag::dvbsStandardExt>());
+                        break;
+                    }
+                    case FrontendStandardExt::Tag::dvbtStandardExt: {
+                        dvbtStandardExt = static_cast<jint>(standardExt
+                                .get<FrontendStandardExt::Tag::dvbtStandardExt>());
+                        break;
+                    }
+                }
+                ScopedLocalRef standardExtObj(env, env->NewObject(standardExtClazz.get(),
+                        initStandardExt, dvbsStandardExt, dvbtStandardExt));
+                env->SetObjectField(statusObj, field, standardExtObj.get());
+            }
         }
     }
     return statusObj;
diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp
index 095d7d1..15f77ce 100644
--- a/native/android/performance_hint.cpp
+++ b/native/android/performance_hint.cpp
@@ -39,6 +39,7 @@
 #include <utils/SystemClock.h>
 
 #include <chrono>
+#include <future>
 #include <set>
 #include <utility>
 #include <vector>
@@ -104,6 +105,7 @@
     size_t mAvailableSlots GUARDED_BY(sHintMutex) = 0;
     bool mHalSupported = true;
     HalMessageQueue::MemTransaction mFmqTransaction GUARDED_BY(sHintMutex);
+    std::future<bool> mChannelCreationFinished;
 };
 
 struct APerformanceHintManager {
@@ -218,6 +220,8 @@
 }
 
 APerformanceHintManager* APerformanceHintManager::getInstance() {
+    static std::once_flag creationFlag;
+    static APerformanceHintManager* instance = nullptr;
     if (gHintManagerForTesting) {
         return gHintManagerForTesting.get();
     }
@@ -226,7 +230,7 @@
                 std::shared_ptr<APerformanceHintManager>(create(*gIHintManagerForTesting));
         return gHintManagerForTesting.get();
     }
-    static APerformanceHintManager* instance = create(nullptr);
+    std::call_once(creationFlag, []() { instance = create(nullptr); });
     return instance;
 }
 
@@ -522,25 +526,28 @@
 }
 
 bool FMQWrapper::startChannel(IHintManager* manager) {
-    if (isSupported() && !isActive()) {
-        std::optional<hal::ChannelConfig> config;
-        auto ret = manager->getSessionChannel(mToken, &config);
-        if (ret.isOk() && config.has_value()) {
-            std::scoped_lock lock{sHintMutex};
-            mQueue = std::make_shared<HalMessageQueue>(config->channelDescriptor, true);
-            if (config->eventFlagDescriptor.has_value()) {
-                mFlagQueue = std::make_shared<HalFlagQueue>(*config->eventFlagDescriptor, true);
-                android::hardware::EventFlag::createEventFlag(mFlagQueue->getEventFlagWord(),
-                                                              &mEventFlag);
-                mWriteMask = config->writeFlagBitmask;
+    if (isSupported() && !isActive() && manager->isRemote()) {
+        mChannelCreationFinished = std::async(std::launch::async, [&, this, manager]() {
+            std::optional<hal::ChannelConfig> config;
+            auto ret = manager->getSessionChannel(mToken, &config);
+            if (ret.isOk() && config.has_value()) {
+                std::scoped_lock lock{sHintMutex};
+                mQueue = std::make_shared<HalMessageQueue>(config->channelDescriptor, true);
+                if (config->eventFlagDescriptor.has_value()) {
+                    mFlagQueue = std::make_shared<HalFlagQueue>(*config->eventFlagDescriptor, true);
+                    android::hardware::EventFlag::createEventFlag(mFlagQueue->getEventFlagWord(),
+                                                                  &mEventFlag);
+                    mWriteMask = config->writeFlagBitmask;
+                }
+                updatePersistentTransaction();
+            } else if (ret.isOk() && !config.has_value()) {
+                ALOGV("FMQ channel enabled but unsupported.");
+                setUnsupported();
+            } else {
+                ALOGE("%s: FMQ channel initialization failed: %s", __FUNCTION__, ret.getMessage());
             }
-            updatePersistentTransaction();
-        } else if (ret.isOk() && !config.has_value()) {
-            ALOGV("FMQ channel enabled but unsupported.");
-            setUnsupported();
-        } else {
-            ALOGE("%s: FMQ channel initialization failed: %s", __FUNCTION__, ret.getMessage());
-        }
+            return true;
+        });
     }
     return isActive();
 }
diff --git a/omapi/aidl/vts/functional/AccessControlApp/Android.bp b/omapi/aidl/vts/functional/AccessControlApp/Android.bp
index f03c3f6..57d75f5 100644
--- a/omapi/aidl/vts/functional/AccessControlApp/Android.bp
+++ b/omapi/aidl/vts/functional/AccessControlApp/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_nfc",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/omapi/aidl/vts/functional/omapi/Android.bp b/omapi/aidl/vts/functional/omapi/Android.bp
index c41479f..8ee55ff 100644
--- a/omapi/aidl/vts/functional/omapi/Android.bp
+++ b/omapi/aidl/vts/functional/omapi/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_fwk_nfc",
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
diff --git a/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java b/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java
index 9a8261c..8e5ae20 100644
--- a/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java
+++ b/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java
@@ -24,9 +24,12 @@
 
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -94,6 +97,8 @@
  * be notified.
  * @hide
  */
+@FlaggedApi(Flags.FLAG_ENABLE_CRASHRECOVERY)
+@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
 public class PackageWatchdog {
     private static final String TAG = "PackageWatchdog";
 
@@ -351,7 +356,7 @@
      *
      * <p>If monitoring a package supporting explicit health check, at the end of the monitoring
      * duration if {@link #onHealthCheckPassed} was never called,
-     * {@link PackageHealthObserver#execute} will be called as if the package failed.
+     * {@link PackageHealthObserver#onExecuteHealthCheckMitigation} will be called as if the package failed.
      *
      * <p>If {@code observer} is already monitoring a package in {@code packageNames},
      * the monitoring window of that package will be reset to {@code durationMs} and the health
@@ -514,8 +519,8 @@
                                 maybeExecute(currentObserverToNotify, versionedPackage,
                                         failureReason, currentObserverImpact, mitigationCount);
                             } else {
-                                currentObserverToNotify.execute(versionedPackage,
-                                        failureReason, mitigationCount);
+                                currentObserverToNotify.onExecuteHealthCheckMitigation(
+                                        versionedPackage, failureReason, mitigationCount);
                             }
                         }
                     }
@@ -550,7 +555,8 @@
                 maybeExecute(currentObserverToNotify, failingPackage, failureReason,
                         currentObserverImpact, /*mitigationCount=*/ 1);
             } else {
-                currentObserverToNotify.execute(failingPackage,  failureReason, 1);
+                currentObserverToNotify.onExecuteHealthCheckMitigation(failingPackage,
+                        failureReason, 1);
             }
         }
     }
@@ -564,7 +570,8 @@
             synchronized (mLock) {
                 mLastMitigation = mSystemClock.uptimeMillis();
             }
-            currentObserverToNotify.execute(versionedPackage, failureReason, mitigationCount);
+            currentObserverToNotify.onExecuteHealthCheckMitigation(versionedPackage, failureReason,
+                    mitigationCount);
         }
     }
 
@@ -626,12 +633,12 @@
                         currentObserverInternal.setBootMitigationCount(
                                 currentObserverMitigationCount);
                         saveAllObserversBootMitigationCountToMetadata(METADATA_FILE);
-                        currentObserverToNotify.executeBootLoopMitigation(
+                        currentObserverToNotify.onExecuteBootLoopMitigation(
                                 currentObserverMitigationCount);
                     } else {
                         mBootThreshold.setMitigationCount(mitigationCount);
                         mBootThreshold.saveMitigationCountToMetadata();
-                        currentObserverToNotify.executeBootLoopMitigation(mitigationCount);
+                        currentObserverToNotify.onExecuteBootLoopMitigation(mitigationCount);
                     }
                 }
             }
@@ -717,7 +724,9 @@
         return mPackagesExemptFromImpactLevelThreshold;
     }
 
-    /** Possible severity values of the user impact of a {@link PackageHealthObserver#execute}.
+    /**
+     * Possible severity values of the user impact of a
+     * {@link PackageHealthObserver#onExecuteHealthCheckMitigation}.
      * @hide
      */
     @Retention(SOURCE)
@@ -753,6 +762,7 @@
     }
 
     /** Register instances of this interface to receive notifications on package failure. */
+    @SuppressLint({"CallbackName"})
     public interface PackageHealthObserver {
         /**
          * Called when health check fails for the {@code versionedPackage}.
@@ -765,7 +775,7 @@
          *
          *
          * @return any one of {@link PackageHealthObserverImpact} to express the impact
-         * to the user on {@link #execute}
+         * to the user on {@link #onExecuteHealthCheckMitigation}
          */
         @PackageHealthObserverImpact int onHealthCheckFailed(
                 @Nullable VersionedPackage versionedPackage,
@@ -773,7 +783,10 @@
                 int mitigationCount);
 
         /**
-         * Executes mitigation for {@link #onHealthCheckFailed}.
+         * This would be called after {@link #onHealthCheckFailed}.
+         * This is called only if current observer returned least
+         * {@link PackageHealthObserverImpact} mitigation for failed health
+         * check.
          *
          * @param versionedPackage the package that is failing. This may be null if a native
          *                          service is crashing.
@@ -782,7 +795,7 @@
          *                        (including this time).
          * @return {@code true} if action was executed successfully, {@code false} otherwise
          */
-        boolean execute(@Nullable VersionedPackage versionedPackage,
+        boolean onExecuteHealthCheckMitigation(@Nullable VersionedPackage versionedPackage,
                 @FailureReasons int failureReason, int mitigationCount);
 
 
@@ -798,11 +811,14 @@
         }
 
         /**
-         * Executes mitigation for {@link #onBootLoop}
+         * This would be called after {@link #onBootLoop}.
+         * This is called only if current observer returned least
+         * {@link PackageHealthObserverImpact} mitigation for fixing boot loop
+         *
          * @param mitigationCount the number of times mitigation has been attempted for this
          *                        boot loop (including this time).
          */
-        default boolean executeBootLoopMitigation(int mitigationCount) {
+        default boolean onExecuteBootLoopMitigation(int mitigationCount) {
             return false;
         }
 
@@ -1083,7 +1099,7 @@
                         if (versionedPkg != null) {
                             Slog.i(TAG,
                                     "Explicit health check failed for package " + versionedPkg);
-                            registeredObserver.execute(versionedPkg,
+                            registeredObserver.onExecuteHealthCheckMitigation(versionedPkg,
                                     PackageWatchdog.FAILURE_REASON_EXPLICIT_HEALTH_CHECK, 1);
                         }
                     }
diff --git a/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java b/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java
index f1b2f6b..f1103e1 100644
--- a/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java
+++ b/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java
@@ -728,7 +728,7 @@
         }
 
         @Override
-        public boolean execute(@Nullable VersionedPackage failedPackage,
+        public boolean onExecuteHealthCheckMitigation(@Nullable VersionedPackage failedPackage,
                 @FailureReasons int failureReason, int mitigationCount) {
             if (isDisabled()) {
                 return false;
@@ -796,7 +796,7 @@
         }
 
         @Override
-        public boolean executeBootLoopMitigation(int mitigationCount) {
+        public boolean onExecuteBootLoopMitigation(int mitigationCount) {
             if (isDisabled()) {
                 return false;
             }
diff --git a/packages/CrashRecovery/services/module/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/packages/CrashRecovery/services/module/java/com/android/server/rollback/RollbackPackageHealthObserver.java
index 2931652..8277e57 100644
--- a/packages/CrashRecovery/services/module/java/com/android/server/rollback/RollbackPackageHealthObserver.java
+++ b/packages/CrashRecovery/services/module/java/com/android/server/rollback/RollbackPackageHealthObserver.java
@@ -19,8 +19,11 @@
 import static com.android.server.crashrecovery.CrashRecoveryUtils.logCrashRecoveryEvent;
 
 import android.annotation.AnyThread;
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
 import android.annotation.WorkerThread;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
@@ -75,6 +78,9 @@
  *
  * @hide
  */
+@FlaggedApi(Flags.FLAG_ENABLE_CRASHRECOVERY)
+@SuppressLint({"CallbackName"})
+@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
 public final class RollbackPackageHealthObserver implements PackageHealthObserver {
     private static final String TAG = "RollbackPackageHealthObserver";
     private static final String NAME = "rollback-observer";
@@ -148,7 +154,7 @@
                 // Note: For non-native crashes the rollback-all step has higher impact
                 impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30;
             } else if (getAvailableRollback(failedPackage) != null) {
-                // Rollback is available, we may get a callback into #execute
+                // Rollback is available, we may get a callback into #onExecuteHealthCheckMitigation
                 impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30;
             } else if (anyRollbackAvailable) {
                 // If any rollbacks are available, we will commit them
@@ -165,7 +171,7 @@
     }
 
     @Override
-    public boolean execute(@Nullable VersionedPackage failedPackage,
+    public boolean onExecuteHealthCheckMitigation(@Nullable VersionedPackage failedPackage,
             @FailureReasons int rollbackReason, int mitigationCount) {
         Slog.i(TAG, "Executing remediation."
                 + " failedPackage: "
@@ -219,7 +225,7 @@
     }
 
     @Override
-    public boolean executeBootLoopMitigation(int mitigationCount) {
+    public boolean onExecuteBootLoopMitigation(int mitigationCount) {
         if (Flags.recoverabilityDetection()) {
             List<RollbackInfo> availableRollbacks = getAvailableRollbacks();
 
diff --git a/packages/PrintSpooler/src/com/android/printspooler/model/RemotePrintDocument.java b/packages/PrintSpooler/src/com/android/printspooler/model/RemotePrintDocument.java
index bfc00bb..b48c55d 100644
--- a/packages/PrintSpooler/src/com/android/printspooler/model/RemotePrintDocument.java
+++ b/packages/PrintSpooler/src/com/android/printspooler/model/RemotePrintDocument.java
@@ -329,18 +329,6 @@
         disconnectFromRemoteDocument();
     }
 
-    public void kill(String reason) {
-        if (DEBUG) {
-            Log.i(LOG_TAG, "[CALLED] kill()");
-        }
-
-        try {
-            mPrintDocumentAdapter.kill(reason);
-        } catch (RemoteException re) {
-            Log.e(LOG_TAG, "Error calling kill()", re);
-        }
-    }
-
     public boolean isUpdating() {
         return mState == STATE_UPDATING || mState == STATE_CANCELING;
     }
diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java
index c4173ed..bd2b5ec 100644
--- a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java
+++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java
@@ -514,8 +514,12 @@
         ensureErrorUiShown(null, PrintErrorFragment.ACTION_RETRY);
 
         setState(STATE_UPDATE_FAILED);
-
-        mPrintedDocument.kill(message);
+        if (DEBUG) {
+            Log.i(LOG_TAG, "PrintJob state[" +  PrintJobInfo.STATE_FAILED + "] reason: " + message);
+        }
+        PrintSpoolerService spooler = mSpoolerProvider.getSpooler();
+        spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_FAILED, message);
+        mPrintedDocument.finish();
     }
 
     @Override
diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout-v31/settingslib_main_switch_bar.xml b/packages/SettingsLib/MainSwitchPreference/res/layout-v31/settingslib_main_switch_bar.xml
index 2e3ee32..e3f8fbb 100644
--- a/packages/SettingsLib/MainSwitchPreference/res/layout-v31/settingslib_main_switch_bar.xml
+++ b/packages/SettingsLib/MainSwitchPreference/res/layout-v31/settingslib_main_switch_bar.xml
@@ -20,6 +20,8 @@
     android:layout_height="wrap_content"
     android:layout_width="match_parent"
     android:minHeight="?android:attr/listPreferredItemHeight"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
     android:paddingTop="@dimen/settingslib_switchbar_margin"
     android:paddingBottom="@dimen/settingslib_switchbar_margin"
     android:orientation="vertical">
diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout-v33/settingslib_main_switch_bar.xml b/packages/SettingsLib/MainSwitchPreference/res/layout-v33/settingslib_main_switch_bar.xml
index 3e0e184..255b2c9 100644
--- a/packages/SettingsLib/MainSwitchPreference/res/layout-v33/settingslib_main_switch_bar.xml
+++ b/packages/SettingsLib/MainSwitchPreference/res/layout-v33/settingslib_main_switch_bar.xml
@@ -20,6 +20,8 @@
     android:layout_height="wrap_content"
     android:layout_width="match_parent"
     android:minHeight="?android:attr/listPreferredItemHeight"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
     android:paddingTop="@dimen/settingslib_switchbar_margin"
     android:paddingBottom="@dimen/settingslib_switchbar_margin"
     android:orientation="vertical">
diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout-v35/settingslib_expressive_main_switch_layout.xml b/packages/SettingsLib/MainSwitchPreference/res/layout-v35/settingslib_expressive_main_switch_layout.xml
new file mode 100644
index 0000000..94c6924
--- /dev/null
+++ b/packages/SettingsLib/MainSwitchPreference/res/layout-v35/settingslib_expressive_main_switch_layout.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_height="wrap_content"
+    android:layout_width="match_parent"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:importantForAccessibility="no">
+
+    <com.android.settingslib.widget.MainSwitchBar
+        android:id="@+id/settingslib_main_switch_bar"
+        android:visibility="gone"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent" />
+
+</FrameLayout>
+
+
diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_bar.xml b/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_bar.xml
index 7c0eaea..bf34db9 100644
--- a/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_bar.xml
+++ b/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_bar.xml
@@ -18,7 +18,11 @@
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_height="wrap_content"
-    android:layout_width="match_parent">
+    android:layout_width="match_parent"
+    android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
 
     <TextView
         android:id="@+id/switch_text"
diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_layout.xml b/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_layout.xml
index fa908a4..bef6e35 100644
--- a/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_layout.xml
+++ b/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_layout.xml
@@ -18,10 +18,6 @@
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_height="wrap_content"
     android:layout_width="match_parent"
-    android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
-    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
-    android:paddingRight="?android:attr/listPreferredItemPaddingRight"
-    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
     android:importantForAccessibility="no">
 
     <com.android.settingslib.widget.MainSwitchBar
diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
index 3394874..83858d9 100644
--- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
+++ b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
@@ -81,7 +81,11 @@
     }
 
     private void init(Context context, AttributeSet attrs) {
-        setLayoutResource(R.layout.settingslib_main_switch_layout);
+        boolean isExpressive = SettingsThemeHelper.isExpressiveTheme(context);
+        int resId = isExpressive
+                ? R.layout.settingslib_expressive_main_switch_layout
+                : R.layout.settingslib_main_switch_layout;
+        setLayoutResource(resId);
         mSwitchChangeListeners.add(this);
         if (attrs != null) {
             final TypedArray a = context.obtainStyledAttributes(attrs,
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
index a33fcc6..c16366e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
@@ -279,7 +279,7 @@
                             getService(intent, IDeviceSettingsProviderService.Stub::asInterface)
                                 .stateIn(
                                     coroutineScope.plus(backgroundCoroutineContext),
-                                    SharingStarted.WhileSubscribed(),
+                                    SharingStarted.WhileSubscribed(stopTimeoutMillis = SERVICE_CONNECTION_STOP_MILLIS),
                                     ServiceConnectionStatus.Connecting,
                                 )
                         },
@@ -370,5 +370,6 @@
         const val CONFIG_SERVICE_PACKAGE_NAME = "DEVICE_SETTINGS_CONFIG_PACKAGE_NAME"
         const val CONFIG_SERVICE_CLASS_NAME = "DEVICE_SETTINGS_CONFIG_CLASS"
         const val CONFIG_SERVICE_INTENT_ACTION = "DEVICE_SETTINGS_CONFIG_ACTION"
+        const val SERVICE_CONNECTION_STOP_MILLIS = 1000L
     }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
index 63661f6..4f315a2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
@@ -74,23 +74,7 @@
             new AudioDeviceCallback() {
                 @Override
                 public void onAudioDevicesAdded(@NonNull AudioDeviceInfo[] addedDevices) {
-                    // Activate the last hot plugged valid input device, to match the output device
-                    // behavior.
-                    @AudioDeviceType int deviceTypeToActivate = mSelectedInputDeviceType;
-                    for (AudioDeviceInfo info : addedDevices) {
-                        if (InputMediaDevice.isSupportedInputDevice(info.getType())) {
-                            deviceTypeToActivate = info.getType();
-                        }
-                    }
-
-                    // Only activate if we find a different valid input device. e.g. if none of the
-                    // addedDevices is supported input device, we don't need to activate anything.
-                    if (mSelectedInputDeviceType != deviceTypeToActivate) {
-                        mSelectedInputDeviceType = deviceTypeToActivate;
-                        AudioDeviceAttributes deviceAttributes =
-                                createInputDeviceAttributes(mSelectedInputDeviceType);
-                        setPreferredDeviceForAllPresets(deviceAttributes);
-                    }
+                    applyDefaultSelectedTypeToAllPresets();
                 }
 
                 @Override
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
index d808a25..782cee2 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
@@ -24,7 +24,6 @@
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -139,18 +138,6 @@
                 /* address= */ "");
     }
 
-    private AudioDeviceAttributes getUsbHeadsetDeviceAttributes() {
-        return new AudioDeviceAttributes(
-                AudioDeviceAttributes.ROLE_INPUT,
-                AudioDeviceInfo.TYPE_USB_HEADSET,
-                /* address= */ "");
-    }
-
-    private AudioDeviceAttributes getHdmiDeviceAttributes() {
-        return new AudioDeviceAttributes(
-                AudioDeviceAttributes.ROLE_INPUT, AudioDeviceInfo.TYPE_HDMI, /* address= */ "");
-    }
-
     private void onPreferredDevicesForCapturePresetChanged(InputRouteManager inputRouteManager) {
         final List<AudioDeviceAttributes> audioDeviceAttributesList =
                 new ArrayList<AudioDeviceAttributes>();
@@ -316,47 +303,21 @@
     }
 
     @Test
-    public void onAudioDevicesAdded_shouldActivateAddedDevice() {
+    public void onAudioDevicesAdded_shouldApplyDefaultSelectedDeviceToAllPresets() {
         final AudioManager audioManager = mock(AudioManager.class);
+        AudioDeviceAttributes wiredHeadsetDeviceAttributes = getWiredHeadsetDeviceAttributes();
+        when(audioManager.getDevicesForAttributes(INPUT_ATTRIBUTES))
+                .thenReturn(Collections.singletonList(wiredHeadsetDeviceAttributes));
+
         InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
         AudioDeviceInfo[] devices = {mockWiredHeadsetInfo()};
         inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
 
-        // The only added wired headset will be activated.
+        // Called twice, one after initiation, the other after onAudioDevicesAdded call.
+        verify(audioManager, atLeast(2)).getDevicesForAttributes(INPUT_ATTRIBUTES);
         for (@MediaRecorder.Source int preset : PRESETS) {
-            verify(audioManager, atLeast(1))
-                    .setPreferredDeviceForCapturePreset(preset, getWiredHeadsetDeviceAttributes());
-        }
-    }
-
-    @Test
-    public void onAudioDevicesAdded_shouldActivateLastAddedDevice() {
-        final AudioManager audioManager = mock(AudioManager.class);
-        InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
-        AudioDeviceInfo[] devices = {mockWiredHeadsetInfo(), mockUsbHeadsetInfo()};
-        inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
-
-        // When adding multiple valid input devices, the last added device (usb headset in this
-        // case) will be activated.
-        for (@MediaRecorder.Source int preset : PRESETS) {
-            verify(audioManager, never())
-                    .setPreferredDeviceForCapturePreset(preset, getWiredHeadsetDeviceAttributes());
-            verify(audioManager, atLeast(1))
-                    .setPreferredDeviceForCapturePreset(preset, getUsbHeadsetDeviceAttributes());
-        }
-    }
-
-    @Test
-    public void onAudioDevicesAdded_doNotActivateInvalidAddedDevice() {
-        final AudioManager audioManager = mock(AudioManager.class);
-        InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
-        AudioDeviceInfo[] devices = {mockHdmiInfo()};
-        inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
-
-        // Do not activate since HDMI is not a valid input device.
-        for (@MediaRecorder.Source int preset : PRESETS) {
-            verify(audioManager, never())
-                    .setPreferredDeviceForCapturePreset(preset, getHdmiDeviceAttributes());
+            verify(audioManager, atLeast(2))
+                    .setPreferredDeviceForCapturePreset(preset, wiredHeadsetDeviceAttributes);
         }
     }
 
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index d39b564..1659c9e 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -903,8 +903,8 @@
                         Settings.System.EGG_MODE, // I am the lolrus
                         Settings.System.END_BUTTON_BEHAVIOR, // bug?
                         Settings.System.DEFAULT_DEVICE_FONT_SCALE, // Non configurable
-                        Settings.System
-                                .HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY,
+                        Settings.System.HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY,
+                        Settings.System.INPUT_GAIN_INDEX_SETTINGS,
                         // candidate for backup?
                         Settings.System.LOCKSCREEN_DISABLED, // ?
                         Settings.System.MEDIA_BUTTON_RECEIVER, // candidate for backup?
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index a984b7d..1c29db1 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1559,6 +1559,16 @@
 }
 
 flag {
+   name: "hide_ringer_button_in_single_volume_mode"
+   namespace: "systemui"
+   description: "When the device is in single volume mode, hide the ringer button because it doesn't work"
+   bug: "374870615"
+   metadata {
+       purpose: PURPOSE_BUGFIX
+   }
+}
+
+flag {
     name: "qs_tile_detailed_view"
     namespace: "systemui"
     description: "Enables the tile detailed view UI."
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/FlingOnBackAnimationCallback.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/FlingOnBackAnimationCallback.kt
new file mode 100644
index 0000000..6da06d0
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/FlingOnBackAnimationCallback.kt
@@ -0,0 +1,166 @@
+/*
+ * 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.animation.back
+
+import android.util.TimeUtils
+import android.view.Choreographer
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.VelocityTracker
+import android.view.animation.Interpolator
+import android.window.BackEvent
+import android.window.OnBackAnimationCallback
+import com.android.app.animation.Interpolators
+import com.android.internal.dynamicanimation.animation.DynamicAnimation
+import com.android.internal.dynamicanimation.animation.FlingAnimation
+import com.android.internal.dynamicanimation.animation.FloatValueHolder
+import com.android.window.flags.Flags.predictiveBackTimestampApi
+
+private const val FLING_FRICTION = 6f
+private const val SCALE_FACTOR = 100f
+
+/**
+ * Enhanced [OnBackAnimationCallback] with automatic fling animation and interpolated progress.
+ *
+ * Simplifies back gesture handling by animating flings and emitting processed events through
+ * `compat` functions. Customize progress interpolation with an optional [Interpolator].
+ *
+ * @param progressInterpolator [Interpolator] for progress, defaults to
+ *   [Interpolators.BACK_GESTURE].
+ */
+abstract class FlingOnBackAnimationCallback(
+    val progressInterpolator: Interpolator = Interpolators.BACK_GESTURE
+) : OnBackAnimationCallback {
+
+    private val velocityTracker = VelocityTracker.obtain()
+    private var lastBackEvent: BackEvent? = null
+    private var downTime: Long? = null
+
+    private var backInvokedFlingAnim: FlingAnimation? = null
+    private val backInvokedFlingUpdateListener =
+        DynamicAnimation.OnAnimationUpdateListener { _, progress: Float, _ ->
+            lastBackEvent?.let {
+                val backEvent =
+                    BackEvent(
+                        it.touchX,
+                        it.touchY,
+                        progress / SCALE_FACTOR,
+                        it.swipeEdge,
+                        it.frameTime,
+                    )
+                onBackProgressedCompat(backEvent)
+            }
+        }
+    private val backInvokedFlingEndListener =
+        DynamicAnimation.OnAnimationEndListener { _, _, _, _ ->
+            onBackInvokedCompat()
+            reset()
+        }
+
+    abstract fun onBackStartedCompat(backEvent: BackEvent)
+
+    abstract fun onBackProgressedCompat(backEvent: BackEvent)
+
+    abstract fun onBackInvokedCompat()
+
+    abstract fun onBackCancelledCompat()
+
+    final override fun onBackStarted(backEvent: BackEvent) {
+        if (backInvokedFlingAnim != null) {
+            // This should never happen but let's call onBackInvokedCompat() just in case there is
+            // still a fling animation in progress
+            onBackInvokedCompat()
+        }
+        reset()
+        if (predictiveBackTimestampApi()) {
+            downTime = backEvent.frameTime
+        }
+        onBackStartedCompat(backEvent)
+    }
+
+    final override fun onBackProgressed(backEvent: BackEvent) {
+        val interpolatedProgress = progressInterpolator.getInterpolation(backEvent.progress)
+        if (predictiveBackTimestampApi()) {
+            velocityTracker.addMovement(
+                MotionEvent.obtain(
+                    /* downTime */ downTime!!,
+                    /* eventTime */ backEvent.frameTime,
+                    /* action */ ACTION_MOVE,
+                    /* x */ interpolatedProgress * SCALE_FACTOR,
+                    /* y */ 0f,
+                    /* metaState */ 0,
+                )
+            )
+            lastBackEvent =
+                BackEvent(
+                    backEvent.touchX,
+                    backEvent.touchY,
+                    interpolatedProgress,
+                    backEvent.swipeEdge,
+                    backEvent.frameTime,
+                )
+        } else {
+            lastBackEvent =
+                BackEvent(
+                    backEvent.touchX,
+                    backEvent.touchY,
+                    interpolatedProgress,
+                    backEvent.swipeEdge,
+                )
+        }
+        lastBackEvent?.let { onBackProgressedCompat(it) }
+    }
+
+    final override fun onBackInvoked() {
+        if (predictiveBackTimestampApi() && lastBackEvent != null) {
+            velocityTracker.computeCurrentVelocity(1000)
+            backInvokedFlingAnim =
+                FlingAnimation(FloatValueHolder())
+                    .setStartValue((lastBackEvent?.progress ?: 0f) * SCALE_FACTOR)
+                    .setFriction(FLING_FRICTION)
+                    .setStartVelocity(velocityTracker.xVelocity)
+                    .setMinValue(0f)
+                    .setMaxValue(SCALE_FACTOR)
+                    .also {
+                        it.addUpdateListener(backInvokedFlingUpdateListener)
+                        it.addEndListener(backInvokedFlingEndListener)
+                        it.start()
+                        // do an animation-frame immediately to prevent idle frame
+                        it.doAnimationFrame(
+                            Choreographer.getInstance().lastFrameTimeNanos / TimeUtils.NANOS_PER_MS
+                        )
+                    }
+        } else {
+            onBackInvokedCompat()
+            reset()
+        }
+    }
+
+    final override fun onBackCancelled() {
+        onBackCancelledCompat()
+        reset()
+    }
+
+    private fun reset() {
+        velocityTracker.clear()
+        backInvokedFlingAnim?.removeEndListener(backInvokedFlingEndListener)
+        backInvokedFlingAnim?.removeUpdateListener(backInvokedFlingUpdateListener)
+        lastBackEvent = null
+        backInvokedFlingAnim = null
+        downTime = null
+    }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt
index 8740d14..f708de3 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt
@@ -23,6 +23,7 @@
 import android.window.OnBackAnimationCallback
 import android.window.OnBackInvokedDispatcher
 import android.window.OnBackInvokedDispatcher.Priority
+import com.android.app.animation.Interpolators
 
 /**
  * Generates an [OnBackAnimationCallback] given a [backAnimationSpec]. [onBackProgressed] will be
@@ -40,16 +41,16 @@
     onBackInvoked: () -> Unit = {},
     onBackCancelled: () -> Unit = {},
 ): OnBackAnimationCallback {
-    return object : OnBackAnimationCallback {
+    return object : FlingOnBackAnimationCallback(progressInterpolator = Interpolators.LINEAR) {
         private var initialY = 0f
         private val lastTransformation = BackTransformation()
 
-        override fun onBackStarted(backEvent: BackEvent) {
+        override fun onBackStartedCompat(backEvent: BackEvent) {
             initialY = backEvent.touchY
             onBackStarted(backEvent)
         }
 
-        override fun onBackProgressed(backEvent: BackEvent) {
+        override fun onBackProgressedCompat(backEvent: BackEvent) {
             val progressY = (backEvent.touchY - initialY) / displayMetrics.heightPixels
 
             backAnimationSpec.getBackTransformation(
@@ -61,11 +62,11 @@
             onBackProgressed(lastTransformation)
         }
 
-        override fun onBackInvoked() {
+        override fun onBackInvokedCompat() {
             onBackInvoked()
         }
 
-        override fun onBackCancelled() {
+        override fun onBackCancelledCompat() {
             onBackCancelled()
         }
     }
@@ -86,7 +87,7 @@
             override fun onViewAttachedToWindow(v: View) {
                 onBackInvokedDispatcher.registerOnBackInvokedCallback(
                     priority,
-                    onBackAnimationCallback
+                    onBackAnimationCallback,
                 )
             }
 
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt
new file mode 100644
index 0000000..3115191
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.ui.graphics
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.drawscope.clipPath
+import androidx.compose.ui.graphics.drawscope.translate
+import androidx.compose.ui.graphics.layer.GraphicsLayer
+import androidx.compose.ui.graphics.layer.drawLayer
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.positionInWindow
+import androidx.compose.ui.modifier.ModifierLocalModifierNode
+import androidx.compose.ui.node.DrawModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.requireDensity
+import androidx.compose.ui.node.requireGraphicsContext
+import androidx.compose.ui.node.requireLayoutCoordinates
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.util.fastForEach
+
+/**
+ * Define this as a container into which other composables can be drawn using [drawInContainer].
+ *
+ * The elements redirected to this container will be drawn above the content of this composable.
+ */
+fun Modifier.container(state: ContainerState): Modifier {
+    return layout { measurable, constraints ->
+            val p = measurable.measure(constraints)
+            layout(p.width, p.height) {
+                val coords = coordinates
+                if (coords != null && !isLookingAhead) {
+                    state.lastCoords = coords
+                }
+
+                p.place(0, 0)
+            }
+        }
+        .drawWithContent {
+            drawContent()
+            state.drawInOverlay(this)
+        }
+}
+
+/**
+ * Draw this composable into the container associated to [state].
+ *
+ * @param state the state of the container into which we should draw this composable.
+ * @param enabled whether the redirection of the drawing to the container is enabled.
+ * @param zIndex the z-index of the composable in the container.
+ * @param clipPath the clip path applied when drawing this composable into the container.
+ */
+fun Modifier.drawInContainer(
+    state: ContainerState,
+    enabled: () -> Boolean = { true },
+    zIndex: Float = 0f,
+    clipPath: (LayoutDirection, Density) -> Path? = { _, _ -> null },
+): Modifier {
+    return this.then(
+        DrawInContainerElement(
+            state = state,
+            enabled = enabled,
+            zIndex = zIndex,
+            clipPath = clipPath,
+        )
+    )
+}
+
+class ContainerState {
+    private var renderers = mutableStateListOf<LayerRenderer>()
+    internal var lastCoords: LayoutCoordinates? = null
+
+    internal fun onLayerRendererAttached(renderer: LayerRenderer) {
+        renderers.add(renderer)
+        renderers.sortBy { it.zIndex }
+    }
+
+    internal fun onLayerRendererDetached(renderer: LayerRenderer) {
+        renderers.remove(renderer)
+    }
+
+    internal fun drawInOverlay(drawScope: DrawScope) {
+        renderers.fastForEach { it.drawInOverlay(drawScope) }
+    }
+}
+
+internal interface LayerRenderer {
+    val zIndex: Float
+
+    fun drawInOverlay(drawScope: DrawScope)
+}
+
+private data class DrawInContainerElement(
+    var state: ContainerState,
+    var enabled: () -> Boolean,
+    val zIndex: Float,
+    val clipPath: (LayoutDirection, Density) -> Path?,
+) : ModifierNodeElement<DrawInContainerNode>() {
+    override fun create(): DrawInContainerNode {
+        return DrawInContainerNode(state, enabled, zIndex, clipPath)
+    }
+
+    override fun update(node: DrawInContainerNode) {
+        node.state = state
+        node.enabled = enabled
+        node.zIndex = zIndex
+        node.clipPath = clipPath
+    }
+}
+
+/**
+ * The implementation of [drawInContainer].
+ *
+ * Note: this was forked from AndroidX RenderInTransitionOverlayNodeElement.kt
+ * (http://shortn/_3dfSFPbm8f).
+ */
+internal class DrawInContainerNode(
+    var state: ContainerState,
+    var enabled: () -> Boolean = { true },
+    zIndex: Float = 0f,
+    var clipPath: (LayoutDirection, Density) -> Path? = { _, _ -> null },
+) : Modifier.Node(), DrawModifierNode, ModifierLocalModifierNode {
+    var zIndex by mutableFloatStateOf(zIndex)
+
+    private inner class LayerWithRenderer(val layer: GraphicsLayer) : LayerRenderer {
+        override val zIndex: Float
+            get() = this@DrawInContainerNode.zIndex
+
+        override fun drawInOverlay(drawScope: DrawScope) {
+            if (enabled()) {
+                with(drawScope) {
+                    val containerCoords =
+                        checkNotNull(state.lastCoords) { "container is not placed" }
+                    val (x, y) =
+                        requireLayoutCoordinates().positionInWindow() -
+                            containerCoords.positionInWindow()
+                    val clipPath = clipPath(layoutDirection, requireDensity())
+                    if (clipPath != null) {
+                        clipPath(clipPath) { translate(x, y) { drawLayer(layer) } }
+                    } else {
+                        translate(x, y) { drawLayer(layer) }
+                    }
+                }
+            }
+        }
+    }
+
+    // Render in-place logic. Depending on the result of `renderInOverlay()`, the content will
+    // either render in-place or in the overlay, but never in both places.
+    override fun ContentDrawScope.draw() {
+        val layer = requireNotNull(layer) { "Error: layer never initialized" }
+        layer.record { this@draw.drawContent() }
+        if (!enabled()) {
+            drawLayer(layer)
+        }
+    }
+
+    val layer: GraphicsLayer?
+        get() = layerWithRenderer?.layer
+
+    private var layerWithRenderer: LayerWithRenderer? = null
+
+    override fun onAttach() {
+        LayerWithRenderer(requireGraphicsContext().createGraphicsLayer()).let {
+            state.onLayerRendererAttached(it)
+            layerWithRenderer = it
+        }
+    }
+
+    override fun onDetach() {
+        layerWithRenderer?.let {
+            state.onLayerRendererDetached(it)
+            requireGraphicsContext().releaseGraphicsLayer(it.layer)
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInOverlay.kt b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInOverlay.kt
new file mode 100644
index 0000000..f5c3a83
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInOverlay.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.ui.graphics
+
+import android.view.View
+import android.view.ViewGroupOverlay
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCompositionContext
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.unit.IntSize
+import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.lifecycle.findViewTreeViewModelStoreOwner
+import androidx.lifecycle.setViewTreeLifecycleOwner
+import androidx.lifecycle.setViewTreeViewModelStoreOwner
+import androidx.savedstate.findViewTreeSavedStateRegistryOwner
+import androidx.savedstate.setViewTreeSavedStateRegistryOwner
+
+/**
+ * Draw this composable in the [overlay][ViewGroupOverlay] of the [current ComposeView][LocalView].
+ */
+@Composable
+fun Modifier.drawInOverlay(): Modifier {
+    val containerState = remember { ContainerState() }
+    val context = LocalContext.current
+    val localView = LocalView.current
+    val compositionContext = rememberCompositionContext()
+    val displayMetrics = context.resources.displayMetrics
+    val displaySize = IntSize(displayMetrics.widthPixels, displayMetrics.heightPixels)
+
+    DisposableEffect(containerState, context, localView, compositionContext, displaySize) {
+        val overlay = localView.rootView.overlay as ViewGroupOverlay
+        val view =
+            ComposeView(context).apply {
+                setParentCompositionContext(compositionContext)
+
+                // Set the owners.
+                setViewTreeLifecycleOwner(localView.findViewTreeLifecycleOwner())
+                setViewTreeViewModelStoreOwner(localView.findViewTreeViewModelStoreOwner())
+                setViewTreeSavedStateRegistryOwner(localView.findViewTreeSavedStateRegistryOwner())
+
+                setContent { Box(Modifier.fillMaxSize().container(containerState)) }
+            }
+
+        overlay.add(view)
+
+        // Make the ComposeView as big as the display. We have to manually measure and layout the
+        // View given that there is no layout pass in Android overlays.
+        view.measure(
+            View.MeasureSpec.makeSafeMeasureSpec(displaySize.width, View.MeasureSpec.EXACTLY),
+            View.MeasureSpec.makeSafeMeasureSpec(displaySize.height, View.MeasureSpec.EXACTLY),
+        )
+        view.layout(0, 0, displaySize.width, displaySize.height)
+
+        onDispose { overlay.remove(view) }
+    }
+
+    return this.drawInContainer(containerState, enabled = { true })
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt
index 8b9e927..e4c60e1 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt
@@ -18,6 +18,8 @@
 
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.util.fastCoerceAtLeast
+import androidx.compose.ui.util.fastCoerceAtMost
 import com.android.compose.nestedscroll.PriorityNestedScrollConnection
 
 /**
@@ -44,7 +46,7 @@
         orientation = Orientation.Vertical,
         // scrolling up and inner content is taller than the scrim, so scrim needs to
         // expand; content can scroll once scrim is at the minScrimOffset.
-        canStartPreScroll = { offsetAvailable, offsetBeforeStart ->
+        canStartPreScroll = { offsetAvailable, offsetBeforeStart, _ ->
             offsetAvailable < 0 &&
                 offsetBeforeStart == 0f &&
                 contentHeight() > minVisibleScrimHeight() &&
@@ -52,36 +54,38 @@
         },
         // scrolling down and content is done scrolling to top. After that, the scrim
         // needs to collapse; collapse the scrim until it is at the maxScrimOffset.
-        canStartPostScroll = { offsetAvailable, _ ->
+        canStartPostScroll = { offsetAvailable, _, _ ->
             offsetAvailable > 0 && (scrimOffset() < maxScrimOffset || isCurrentGestureOverscroll())
         },
         canStartPostFling = { false },
-        canContinueScroll = {
-            val currentHeight = scrimOffset()
-            minScrimOffset() < currentHeight && currentHeight < maxScrimOffset
-        },
-        canScrollOnFling = true,
+        canStopOnPreFling = { false },
         onStart = { offsetAvailable -> onStart(offsetAvailable) },
-        onScroll = { offsetAvailable ->
+        onScroll = { offsetAvailable, _ ->
             val currentHeight = scrimOffset()
             val amountConsumed =
                 if (offsetAvailable > 0) {
                     val amountLeft = maxScrimOffset - currentHeight
-                    offsetAvailable.coerceAtMost(amountLeft)
+                    offsetAvailable.fastCoerceAtMost(amountLeft)
                 } else {
                     val amountLeft = minScrimOffset() - currentHeight
-                    offsetAvailable.coerceAtLeast(amountLeft)
+                    offsetAvailable.fastCoerceAtLeast(amountLeft)
                 }
             snapScrimOffset(currentHeight + amountConsumed)
             amountConsumed
         },
-        // Don't consume the velocity on pre/post fling
         onStop = { velocityAvailable ->
             onStop(velocityAvailable)
             if (scrimOffset() < minScrimOffset()) {
                 animateScrimOffset(minScrimOffset())
             }
-            { 0f }
+            // Don't consume the velocity on pre/post fling
+            0f
+        },
+        onCancel = {
+            onStop(0f)
+            if (scrimOffset() < minScrimOffset()) {
+                animateScrimOffset(minScrimOffset())
+            }
         },
     )
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt
index a706585..edb05eb 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt
@@ -28,6 +28,7 @@
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastCoerceAtLeast
 import com.android.compose.nestedscroll.PriorityNestedScrollConnection
 import kotlin.math.max
 import kotlin.math.roundToInt
@@ -86,21 +87,25 @@
 ): PriorityNestedScrollConnection {
     return PriorityNestedScrollConnection(
         orientation = Orientation.Vertical,
-        canStartPreScroll = { _, _ -> false },
-        canStartPostScroll = { offsetAvailable, offsetBeforeStart ->
+        canStartPreScroll = { _, _, _ -> false },
+        canStartPostScroll = { offsetAvailable, offsetBeforeStart, _ ->
             offsetAvailable < 0f && offsetBeforeStart < 0f && !canScrollForward()
         },
         canStartPostFling = { velocityAvailable -> velocityAvailable < 0f && !canScrollForward() },
-        canContinueScroll = { stackOffset() > 0f },
-        canScrollOnFling = true,
+        canStopOnPreFling = { false },
         onStart = { offsetAvailable -> onStart(offsetAvailable) },
-        onScroll = { offsetAvailable ->
-            onScroll(offsetAvailable)
-            offsetAvailable
+        onScroll = { offsetAvailable, _ ->
+            val minOffset = 0f
+            val consumed = offsetAvailable.fastCoerceAtLeast(minOffset - stackOffset())
+            if (consumed != 0f) {
+                onScroll(consumed)
+            }
+            consumed
         },
         onStop = { velocityAvailable ->
             onStop(velocityAvailable)
-            suspend { velocityAvailable }
+            velocityAvailable
         },
+        onCancel = { onStop(0f) },
     )
 }
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 e5ef79b..8469007 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
@@ -27,9 +27,10 @@
 import com.android.compose.animation.scene.content.state.TransitionState
 import com.android.compose.animation.scene.content.state.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified
 import com.android.compose.nestedscroll.PriorityNestedScrollConnection
-import com.android.compose.nestedscroll.SuspendedValue
 import kotlin.math.absoluteValue
 
+internal typealias SuspendedValue<T> = suspend () -> T
+
 internal interface DraggableHandler {
     /**
      * Start a drag in the given [startedPosition], with the given [overSlop] and number of
@@ -612,7 +613,7 @@
 
         return PriorityNestedScrollConnection(
             orientation = orientation,
-            canStartPreScroll = { offsetAvailable, offsetBeforeStart ->
+            canStartPreScroll = { offsetAvailable, offsetBeforeStart, _ ->
                 canChangeScene =
                     if (isExternalOverscrollGesture()) false else offsetBeforeStart == 0f
 
@@ -644,7 +645,7 @@
                 isIntercepting = true
                 true
             },
-            canStartPostScroll = { offsetAvailable, offsetBeforeStart ->
+            canStartPostScroll = { offsetAvailable, offsetBeforeStart, _ ->
                 val behavior: NestedScrollBehavior =
                     when {
                         offsetAvailable > 0f -> topOrLeftBehavior
@@ -709,8 +710,10 @@
 
                 canStart
             },
-            canContinueScroll = { true },
-            canScrollOnFling = false,
+            // We need to maintain scroll priority even if the scene transition can no longer
+            // consume the scroll gesture to allow us to return to the previous scene.
+            canStopOnScroll = { _, _ -> false },
+            canStopOnPreFling = { true },
             onStart = { offsetAvailable ->
                 val pointersInfo = pointersInfo()
                 dragController =
@@ -720,7 +723,7 @@
                         overSlop = if (isIntercepting) 0f else offsetAvailable,
                     )
             },
-            onScroll = { offsetAvailable ->
+            onScroll = { offsetAvailable, _ ->
                 val controller = dragController ?: error("Should be called after onStart")
 
                 val pointersInfo = pointersInfoOwner.pointersInfo()
@@ -735,10 +738,23 @@
             },
             onStop = { velocityAvailable ->
                 val controller = dragController ?: error("Should be called after onStart")
-
-                controller
-                    .onStop(velocity = velocityAvailable, canChangeContent = canChangeScene)
-                    .also { dragController = null }
+                try {
+                    controller
+                        .onStop(velocity = velocityAvailable, canChangeContent = canChangeScene)
+                        .invoke()
+                } finally {
+                    // onStop might still be running when a new gesture begins.
+                    // To prevent conflicts, we should only remove the drag controller if it's the
+                    // same one that was active initially.
+                    if (dragController == controller) {
+                        dragController = null
+                    }
+                }
+            },
+            onCancel = {
+                val controller = dragController ?: error("Should be called after onStart")
+                controller.onStop(velocity = 0f, canChangeContent = canChangeScene)
+                dragController = null
             },
         )
     }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
index 205267d..f0043e1 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
@@ -27,7 +27,6 @@
 import androidx.compose.ui.unit.IntSize
 import com.android.compose.animation.scene.content.state.TransitionState
 import com.android.compose.animation.scene.content.state.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified
-import com.android.compose.nestedscroll.SuspendedValue
 import kotlin.math.absoluteValue
 import kotlinx.coroutines.CompletableDeferred
 
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt
index 4ae3235..ecf64b7 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt
@@ -18,6 +18,8 @@
 
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.util.fastCoerceAtLeast
+import androidx.compose.ui.util.fastCoerceAtMost
 
 /**
  * A [NestedScrollConnection] that listens for all vertical scroll events and responds in the
@@ -43,35 +45,32 @@
         orientation = Orientation.Vertical,
         // When swiping up, the LargeTopAppBar will shrink (to [minHeight]) and the content will
         // expand. Then, you can then scroll down the content.
-        canStartPreScroll = { offsetAvailable, offsetBeforeStart ->
+        canStartPreScroll = { offsetAvailable, offsetBeforeStart, _ ->
             offsetAvailable < 0 && offsetBeforeStart == 0f && height() > minHeight()
         },
         // When swiping down, the content will scroll up until it reaches the top. Then, the
         // LargeTopAppBar will expand until it reaches its [maxHeight].
-        canStartPostScroll = { offsetAvailable, _ ->
+        canStartPostScroll = { offsetAvailable, _, _ ->
             offsetAvailable > 0 && height() < maxHeight()
         },
         canStartPostFling = { false },
-        canContinueScroll = {
-            val currentHeight = height()
-            minHeight() < currentHeight && currentHeight < maxHeight()
-        },
-        canScrollOnFling = true,
+        canStopOnPreFling = { false },
         onStart = { /* do nothing */ },
-        onScroll = { offsetAvailable ->
+        onScroll = { offsetAvailable, _ ->
             val currentHeight = height()
             val amountConsumed =
                 if (offsetAvailable > 0) {
                     val amountLeft = maxHeight() - currentHeight
-                    offsetAvailable.coerceAtMost(amountLeft)
+                    offsetAvailable.fastCoerceAtMost(amountLeft)
                 } else {
                     val amountLeft = minHeight() - currentHeight
-                    offsetAvailable.coerceAtLeast(amountLeft)
+                    offsetAvailable.fastCoerceAtLeast(amountLeft)
                 }
             onHeightChanged(currentHeight + amountConsumed)
             amountConsumed
         },
         // Don't consume the velocity on pre/post fling
-        onStop = { { 0f } },
+        onStop = { 0f },
+        onCancel = { /* do nothing */ },
     )
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt
index a3641e6..57d236b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt
@@ -16,37 +16,64 @@
 
 package com.android.compose.nestedscroll
 
+import androidx.compose.animation.core.AnimationState
+import androidx.compose.animation.core.DecayAnimationSpec
+import androidx.compose.animation.core.animateDecay
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
 import androidx.compose.ui.unit.Velocity
 import com.android.compose.ui.util.SpaceVectorConverter
+import kotlin.math.abs
 import kotlin.math.sign
-
-internal typealias SuspendedValue<T> = suspend () -> T
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
 
 /**
- * This [NestedScrollConnection] waits for a child to scroll ([onPreScroll] or [onPostScroll]), and
- * then decides (via [canStartPreScroll] or [canStartPostScroll]) if it should take over scrolling.
- * If it does, it will scroll before its children, until [canContinueScroll] allows it.
+ * A [NestedScrollConnection] that intercepts scroll events in priority mode.
  *
- * Note: Call [reset] before destroying this object to make sure you always get a call to [onStop]
- * after [onStart].
+ * Priority mode allows this connection to take control over scroll events within a nested scroll
+ * hierarchy. When in priority mode, this connection consumes scroll events before its children,
+ * enabling custom scrolling behaviors like sticky headers.
  *
+ * @param orientation The orientation of the scroll.
+ * @param canStartPreScroll lambda that returns true if the connection can start consuming scroll
+ *   events in pre-scroll mode.
+ * @param canStartPostScroll lambda that returns true if the connection can start consuming scroll
+ *   events in post-scroll mode.
+ * @param canStartPostFling lambda that returns true if the connection can start consuming scroll
+ *   events in post-fling mode.
+ * @param canStopOnScroll lambda that returns true if the connection can stop consuming scroll
+ *   events in scroll mode.
+ * @param canStopOnPreFling lambda that returns true if the connection can stop consuming scroll
+ *   events in pre-fling (i.e. as soon as the user lifts their fingers).
+ * @param onStart lambda that is called when the connection starts consuming scroll events.
+ * @param onScroll lambda that is called when the connection consumes a scroll event and returns the
+ *   consumed amount.
+ * @param onStop lambda that is called when the connection stops consuming scroll events and returns
+ *   the consumed velocity.
+ * @param onCancel lambda that is called when the connection is cancelled.
  * @sample LargeTopAppBarNestedScrollConnection
  * @sample com.android.compose.animation.scene.NestedScrollHandlerImpl.nestedScrollConnection
  */
 class PriorityNestedScrollConnection(
     orientation: Orientation,
-    private val canStartPreScroll: (offsetAvailable: Float, offsetBeforeStart: Float) -> Boolean,
-    private val canStartPostScroll: (offsetAvailable: Float, offsetBeforeStart: Float) -> Boolean,
+    private val canStartPreScroll:
+        (offsetAvailable: Float, offsetBeforeStart: Float, source: NestedScrollSource) -> Boolean,
+    private val canStartPostScroll:
+        (offsetAvailable: Float, offsetBeforeStart: Float, source: NestedScrollSource) -> Boolean,
     private val canStartPostFling: (velocityAvailable: Float) -> Boolean,
-    private val canContinueScroll: (source: NestedScrollSource) -> Boolean,
-    private val canScrollOnFling: Boolean,
+    private val canStopOnScroll: (available: Float, consumed: Float) -> Boolean = { _, consumed ->
+        consumed == 0f
+    },
+    private val canStopOnPreFling: () -> Boolean,
     private val onStart: (offsetAvailable: Float) -> Unit,
-    private val onScroll: (offsetAvailable: Float) -> Float,
-    private val onStop: (velocityAvailable: Float) -> SuspendedValue<Float>,
+    private val onScroll: (offsetAvailable: Float, source: NestedScrollSource) -> Float,
+    private val onStop: suspend (velocityAvailable: Float) -> Float,
+    private val onCancel: () -> Unit,
 ) : NestedScrollConnection, SpaceVectorConverter by SpaceVectorConverter(orientation) {
 
     /** In priority mode [onPreScroll] events are first consumed by the parent, via [onScroll]. */
@@ -54,6 +81,9 @@
 
     private var offsetScrolledBeforePriorityMode = 0f
 
+    /** This job allows us to interrupt the onStop animation */
+    private var onStopJob: Deferred<Float> = CompletableDeferred(0f)
+
     override fun onPostScroll(
         consumed: Offset,
         available: Offset,
@@ -64,62 +94,48 @@
         // the beginning or from the last fling gesture.
         val offsetBeforeStart = offsetScrolledBeforePriorityMode - availableFloat
 
-        if (
-            isPriorityMode ||
-                (source == NestedScrollSource.SideEffect && !canScrollOnFling) ||
-                !canStartPostScroll(availableFloat, offsetBeforeStart)
-        ) {
+        if (isPriorityMode || !canStartPostScroll(availableFloat, offsetBeforeStart, source)) {
             // The priority mode cannot start so we won't consume the available offset.
             return Offset.Zero
         }
 
-        return onPriorityStart(availableFloat).toOffset()
+        return start(availableFloat, source).toOffset()
     }
 
     override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
         if (!isPriorityMode) {
-            if (source == NestedScrollSource.UserInput || canScrollOnFling) {
-                val availableFloat = available.toFloat()
-                if (canStartPreScroll(availableFloat, offsetScrolledBeforePriorityMode)) {
-                    return onPriorityStart(availableFloat).toOffset()
-                }
-                // We want to track the amount of offset consumed before entering priority mode
-                offsetScrolledBeforePriorityMode += availableFloat
+            val availableFloat = available.toFloat()
+            if (canStartPreScroll(availableFloat, offsetScrolledBeforePriorityMode, source)) {
+                return start(availableFloat, source).toOffset()
             }
-
-            return Offset.Zero
-        }
-
-        val availableFloat = available.toFloat()
-        if (!canContinueScroll(source)) {
-            // Step 3a: We have lost priority and we no longer need to intercept scroll events.
-            onPriorityStop(velocity = 0f)
-
-            // We've just reset offsetScrolledBeforePriorityMode to 0f
             // We want to track the amount of offset consumed before entering priority mode
             offsetScrolledBeforePriorityMode += availableFloat
-
             return Offset.Zero
         }
 
-        // Step 2: We have the priority and can consume the scroll events.
-        return onScroll(availableFloat).toOffset()
+        return scroll(available.toFloat(), source).toOffset()
     }
 
     override suspend fun onPreFling(available: Velocity): Velocity {
-        if (isPriorityMode && canScrollOnFling) {
-            // We don't want to consume the velocity, we prefer to continue receiving scroll events.
+        if (!isPriorityMode) {
+            resetOffsetTracker()
             return Velocity.Zero
         }
-        // Step 3b: The finger is lifted, we can stop intercepting scroll events and use the speed
-        // of the fling gesture.
-        return onPriorityStop(velocity = available.toFloat()).invoke().toVelocity()
+
+        if (canStopOnPreFling()) {
+            // Step 3b: The finger is lifted, we can stop intercepting scroll events and use the
+            // velocity of the fling gesture.
+            return stop(velocityAvailable = available.toFloat()).toVelocity()
+        }
+
+        // We don't want to consume the velocity, we prefer to continue receiving scroll events.
+        return Velocity.Zero
     }
 
     override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
         val availableFloat = available.toFloat()
         if (isPriorityMode) {
-            return onPriorityStop(velocity = availableFloat).invoke().toVelocity()
+            return stop(velocityAvailable = availableFloat).toVelocity()
         }
 
         if (!canStartPostFling(availableFloat)) {
@@ -131,10 +147,14 @@
         // TODO(b/291053278): Remove canStartPostFling() and instead make it possible to define the
         // overscroll behavior on the Scene level.
         val smallOffset = availableFloat.sign
-        onPriorityStart(availableOffset = smallOffset)
+        start(
+            availableOffset = smallOffset,
+            source = NestedScrollSource.SideEffect,
+            skipScroll = true,
+        )
 
         // This is the last event of a scroll gesture.
-        return onPriorityStop(availableFloat).invoke().toVelocity()
+        return stop(availableFloat).toVelocity()
     }
 
     /**
@@ -143,36 +163,72 @@
      * TODO(b/303224944) This method should be removed.
      */
     fun reset() {
-        // Step 3c: To ensure that an onStop is always called for every onStart.
-        onPriorityStop(velocity = 0f)
+        if (isPriorityMode) {
+            // Step 3c: To ensure that an onStop (or onCancel) is always called for every onStart.
+            cancel()
+        } else {
+            resetOffsetTracker()
+        }
     }
 
-    private fun onPriorityStart(availableOffset: Float): Float {
-        if (isPriorityMode) {
-            error("This should never happen, onPriorityStart() was called when isPriorityMode")
+    private fun start(
+        availableOffset: Float,
+        source: NestedScrollSource,
+        skipScroll: Boolean = false,
+    ): Float {
+        check(!isPriorityMode) {
+            "This should never happen, start() was called when isPriorityMode"
         }
 
         // Step 1: It's our turn! We start capturing scroll events when one of our children has an
         // available offset following a scroll event.
         isPriorityMode = true
 
+        onStopJob.cancel()
+
         // Note: onStop will be called if we cannot continue to scroll (step 3a), or the finger is
         // lifted (step 3b), or this object has been destroyed (step 3c).
         onStart(availableOffset)
 
-        return onScroll(availableOffset)
+        return if (skipScroll) 0f else scroll(availableOffset, source)
     }
 
-    private fun onPriorityStop(velocity: Float): SuspendedValue<Float> {
-        // We can restart tracking the consumed offsets from scratch.
-        offsetScrolledBeforePriorityMode = 0f
+    private fun scroll(offsetAvailable: Float, source: NestedScrollSource): Float {
+        // Step 2: We have the priority and can consume the scroll events.
+        val consumedByScroll = onScroll(offsetAvailable, source)
 
-        if (!isPriorityMode) {
-            return { 0f }
+        if (canStopOnScroll(offsetAvailable, consumedByScroll)) {
+            // Step 3a: We have lost priority and we no longer need to intercept scroll events.
+            cancel()
+
+            // We've just reset offsetScrolledBeforePriorityMode to 0f
+            // We want to track the amount of offset consumed before entering priority mode
+            offsetScrolledBeforePriorityMode += offsetAvailable - consumedByScroll
         }
 
-        isPriorityMode = false
+        return consumedByScroll
+    }
 
-        return onStop(velocity)
+    /** Reset the tracking of consumed offsets before entering in priority mode. */
+    private fun resetOffsetTracker() {
+        offsetScrolledBeforePriorityMode = 0f
+    }
+
+    private suspend fun stop(velocityAvailable: Float): Float {
+        check(isPriorityMode) { "This should never happen, stop() was called before start()" }
+        isPriorityMode = false
+        resetOffsetTracker()
+
+        return coroutineScope {
+            onStopJob = async { onStop(velocityAvailable) }
+            onStopJob.await()
+        }
+    }
+
+    private fun cancel() {
+        check(isPriorityMode) { "This should never happen, cancel() was called before start()" }
+        isPriorityMode = false
+        resetOffsetTracker()
+        onCancel()
     }
 }
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 fd02148..f24d93f 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
@@ -39,7 +39,6 @@
 import com.android.compose.animation.scene.content.state.TransitionState
 import com.android.compose.animation.scene.content.state.TransitionState.Transition
 import com.android.compose.animation.scene.subjects.assertThat
-import com.android.compose.nestedscroll.SuspendedValue
 import com.android.compose.test.MonotonicClockTestScope
 import com.android.compose.test.runMonotonicClockTest
 import com.android.compose.test.transition
@@ -850,6 +849,34 @@
     }
 
     @Test
+    fun duringATransition_aNewScrollGesture_shouldTakeControl() = runGestureTest {
+        val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview)
+        // First gesture
+        nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f))
+        assertTransition(currentScene = SceneA)
+        nestedScroll.preFling(available = Velocity.Zero)
+        assertTransition(currentScene = SceneA)
+
+        // Second gesture, it starts during onStop() animation
+        nestedScroll.scroll(downOffset(0.1f))
+        assertTransition(currentScene = SceneA)
+
+        // Allows onStop() to complete or cancel
+        advanceUntilIdle()
+
+        // Second gesture continues
+        nestedScroll.scroll(downOffset(0.1f))
+        assertTransition(currentScene = SceneA)
+
+        // Second gesture ends
+        nestedScroll.preFling(available = Velocity.Zero)
+        assertTransition(currentScene = SceneA)
+
+        advanceUntilIdle()
+        assertIdle(currentScene = SceneA)
+    }
+
+    @Test
     fun onPreFling_velocityLowerThanThreshold_remainSameScene() = runGestureTest {
         val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview)
         nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f))
@@ -1296,6 +1323,26 @@
     }
 
     @Test
+    fun scrollKeepPriorityEvenIfWeCanNoLongerScrollOnThatDirection() = runGestureTest {
+        // Overscrolling on scene B does nothing.
+        layoutState.transitions = transitions { overscrollDisabled(SceneB, Orientation.Vertical) }
+        val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways)
+
+        // Overscroll is disabled, it will scroll up to 100%
+        nestedScroll.scroll(available = upOffset(fractionOfScreen = 2f))
+        assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f)
+
+        // We need to maintain scroll priority even if the scene transition can no longer consume
+        // the scroll gesture.
+        nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
+        assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f)
+
+        // A scroll gesture in the opposite direction allows us to return to the previous scene.
+        nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.5f))
+        assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.5f)
+    }
+
+    @Test
     fun overscroll_releaseBetween0And100Percent_up() = runGestureTest {
         // Make scene B overscrollable.
         layoutState.transitions = transitions {
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 c8f6e6d..3df6087 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
@@ -46,7 +46,6 @@
 import androidx.compose.ui.unit.Velocity
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.compose.modifiers.thenIf
-import com.android.compose.nestedscroll.SuspendedValue
 import com.google.common.truth.Truth.assertThat
 import kotlin.properties.Delegates
 import kotlinx.coroutines.coroutineScope
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 3001505..2bc9b38 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
@@ -938,6 +938,71 @@
     }
 
     @Test
+    fun scrollKeepPriorityEvenIfWeCanNoLongerScrollOnThatDirection() {
+        val swipeDistance = 100.dp
+        val state =
+            rule.runOnUiThread {
+                MutableSceneTransitionLayoutState(
+                    SceneA,
+                    transitions {
+                        from(SceneA, to = SceneB) { distance = FixedDistance(swipeDistance) }
+                        from(SceneB, to = SceneC) { distance = FixedDistance(swipeDistance) }
+                        overscrollDisabled(SceneB, Orientation.Vertical)
+                    },
+                )
+            }
+        val layoutSize = 200.dp
+        var touchSlop = 0f
+        rule.setContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            SceneTransitionLayout(state, Modifier.size(layoutSize)) {
+                scene(SceneA, userActions = mapOf(Swipe.Down to SceneB, Swipe.Right to SceneC)) {
+                    Box(
+                        Modifier.fillMaxSize()
+                            // A scrollable that does not consume the scroll gesture
+                            .scrollable(rememberScrollableState { 0f }, Orientation.Vertical)
+                    )
+                }
+                scene(SceneB, userActions = mapOf(Swipe.Right to SceneC)) {
+                    Box(Modifier.element(TestElements.Foo).fillMaxSize())
+                }
+                scene(SceneC) { Box(Modifier.fillMaxSize()) }
+            }
+        }
+
+        fun assertTransition(from: SceneKey, to: SceneKey, progress: Float) {
+            val transition = assertThat(state.transitionState).isSceneTransition()
+            assertThat(transition).hasFromScene(from)
+            assertThat(transition).hasToScene(to)
+            assertThat(transition.progress).isEqualTo(progress)
+        }
+
+        // Vertical scroll 100%
+        rule.onRoot().performTouchInput {
+            val middle = (layoutSize / 2).toPx()
+            down(Offset(middle, middle))
+            moveBy(Offset(0f, y = touchSlop + swipeDistance.toPx()), delayMillis = 1_000)
+        }
+        assertTransition(from = SceneA, to = SceneB, progress = 1f)
+
+        // Continue vertical scroll, should be ignored (overscrollDisabled)
+        rule.onRoot().performTouchInput { moveBy(Offset(0f, y = touchSlop), delayMillis = 1_000) }
+        assertTransition(from = SceneA, to = SceneB, progress = 1f)
+
+        // Horizontal scroll, should be ignored
+        rule.onRoot().performTouchInput {
+            moveBy(Offset(x = touchSlop + swipeDistance.toPx(), 0f), delayMillis = 1_000)
+        }
+        assertTransition(from = SceneA, to = SceneB, progress = 1f)
+
+        // Vertical scroll, in the opposite direction
+        rule.onRoot().performTouchInput {
+            moveBy(Offset(0f, -swipeDistance.toPx()), delayMillis = 1_000)
+        }
+        assertTransition(from = SceneA, to = SceneB, progress = 0f)
+    }
+
+    @Test
     fun sceneWithoutSwipesDoesNotConsumeGestures() {
         val buttonTag = "button"
 
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/PriorityNestedScrollConnectionTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/PriorityNestedScrollConnectionTest.kt
index badc43b..1a3b86b 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/PriorityNestedScrollConnectionTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/PriorityNestedScrollConnectionTest.kt
@@ -34,30 +34,31 @@
     private var canStartPreScroll = false
     private var canStartPostScroll = false
     private var canStartPostFling = false
-    private var canContinueScroll = false
+    private var canStopOnPreFling = true
     private var isStarted = false
     private var lastScroll: Float? = null
-    private var returnOnScroll = 0f
+    private var consumeScroll = true
     private var lastStop: Float? = null
-    private var returnOnStop = 0f
+    private var isCancelled: Boolean = false
+    private var consumeStop = true
 
     private val scrollConnection =
         PriorityNestedScrollConnection(
             orientation = Orientation.Vertical,
-            canStartPreScroll = { _, _ -> canStartPreScroll },
-            canStartPostScroll = { _, _ -> canStartPostScroll },
+            canStartPreScroll = { _, _, _ -> canStartPreScroll },
+            canStartPostScroll = { _, _, _ -> canStartPostScroll },
             canStartPostFling = { canStartPostFling },
-            canContinueScroll = { canContinueScroll },
-            canScrollOnFling = false,
+            canStopOnPreFling = { canStopOnPreFling },
             onStart = { isStarted = true },
-            onScroll = {
-                lastScroll = it
-                returnOnScroll
+            onScroll = { offsetAvailable, _ ->
+                lastScroll = offsetAvailable
+                if (consumeScroll) offsetAvailable else 0f
             },
             onStop = {
                 lastStop = it
-                { returnOnStop }
+                if (consumeStop) it else 0f
             },
+            onCancel = { isCancelled = true },
         )
 
     @Test
@@ -85,7 +86,7 @@
         canStartPostScroll = true
         scrollConnection.onPostScroll(
             consumed = Offset.Zero,
-            available = Offset.Zero,
+            available = Offset(1f, 1f),
             source = UserInput,
         )
     }
@@ -136,45 +137,55 @@
     @Test
     fun step2_onPriorityMode_shouldContinueIfAllowed() {
         startPriorityModePostScroll()
-        canContinueScroll = true
 
-        scrollConnection.onPreScroll(available = Offset(1f, 1f), source = UserInput)
+        val scroll1 = scrollConnection.onPreScroll(available = Offset(0f, 1f), source = UserInput)
         assertThat(lastScroll).isEqualTo(1f)
+        assertThat(scroll1.y).isEqualTo(1f)
 
-        canContinueScroll = false
-        scrollConnection.onPreScroll(available = Offset(2f, 2f), source = UserInput)
-        assertThat(lastScroll).isNotEqualTo(2f)
-        assertThat(lastScroll).isEqualTo(1f)
+        consumeScroll = false
+        val scroll2 = scrollConnection.onPreScroll(available = Offset(0f, 2f), source = UserInput)
+        assertThat(lastScroll).isEqualTo(2f)
+        assertThat(scroll2.y).isEqualTo(0f)
     }
 
     @Test
-    fun step3a_onPriorityMode_shouldStopIfCannotContinue() {
+    fun step3a_onPriorityMode_shouldCancelIfCannotContinue() {
         startPriorityModePostScroll()
-        canContinueScroll = false
+        consumeScroll = false
 
-        scrollConnection.onPreScroll(available = Offset.Zero, source = UserInput)
+        scrollConnection.onPreScroll(available = Offset(0f, 1f), source = UserInput)
 
-        assertThat(lastStop).isNotNull()
+        assertThat(isCancelled).isTrue()
     }
 
     @Test
     fun step3b_onPriorityMode_shouldStopOnFling() = runTest {
         startPriorityModePostScroll()
-        canContinueScroll = true
 
         scrollConnection.onPreFling(available = Velocity.Zero)
 
-        assertThat(lastStop).isNotNull()
+        assertThat(lastStop).isEqualTo(0f)
     }
 
     @Test
-    fun step3c_onPriorityMode_shouldStopOnReset() {
+    fun ifCannotStopOnPreFling_shouldStopOnPostFling() = runTest {
         startPriorityModePostScroll()
-        canContinueScroll = true
+        canStopOnPreFling = false
+
+        scrollConnection.onPreFling(available = Velocity.Zero)
+        assertThat(lastStop).isNull()
+
+        scrollConnection.onPostFling(consumed = Velocity.Zero, available = Velocity.Zero)
+        assertThat(lastStop).isEqualTo(0f)
+    }
+
+    @Test
+    fun step3c_onPriorityMode_shouldCancelOnReset() {
+        startPriorityModePostScroll()
 
         scrollConnection.reset()
 
-        assertThat(lastStop).isNotNull()
+        assertThat(isCancelled).isTrue()
     }
 
     @Test
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index 900971b..2a85823 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -39,7 +39,7 @@
     val resources: Resources,
     private val hasStepClockAnimation: Boolean = false,
     private val migratedClocks: Boolean = false,
-    private val clockReactiveVariants: Boolean = false,
+    private val isClockReactiveVariantsEnabled: Boolean = false,
 ) : ClockProvider {
     private var messageBuffers: ClockMessageBuffers? = null
 
@@ -54,7 +54,7 @@
             throw IllegalArgumentException("${settings.clockId} is unsupported by $TAG")
         }
 
-        return if (clockReactiveVariants) {
+        return if (isClockReactiveVariantsEnabled) {
             val buffer =
                 messageBuffers?.infraMessageBuffer ?: LogcatOnlyMessageBuffer(LogLevel.INFO)
             val assets = AssetLoader(ctx, ctx, "clocks/", buffer)
@@ -84,7 +84,7 @@
             // TODO(b/352049256): Update placeholder to actual resource
             resources.getDrawable(R.drawable.clock_default_thumbnail, null),
             isReactiveToTone = true,
-            isReactiveToTouch = clockReactiveVariants,
+            isReactiveToTouch = isClockReactiveVariantsEnabled,
             axes = listOf(), // TODO: Ater some picker definition
         )
     }
@@ -103,7 +103,6 @@
                                     timespec = DigitalTimespec.FIRST_DIGIT,
                                     style =
                                         FontTextStyle(
-                                            fontFamily = "google_sans_flex.ttf",
                                             lineHeight = 147.25f,
                                             fontVariation =
                                                 "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100",
@@ -112,7 +111,6 @@
                                         FontTextStyle(
                                             fontVariation =
                                                 "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100",
-                                            fontFamily = "google_sans_flex.ttf",
                                             fillColorLight = "#FFFFFFFF",
                                             outlineColor = "#00000000",
                                             renderType = RenderType.CHANGE_WEIGHT,
@@ -131,7 +129,6 @@
                                     timespec = DigitalTimespec.SECOND_DIGIT,
                                     style =
                                         FontTextStyle(
-                                            fontFamily = "google_sans_flex.ttf",
                                             lineHeight = 147.25f,
                                             fontVariation =
                                                 "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100",
@@ -140,7 +137,6 @@
                                         FontTextStyle(
                                             fontVariation =
                                                 "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100",
-                                            fontFamily = "google_sans_flex.ttf",
                                             fillColorLight = "#FFFFFFFF",
                                             outlineColor = "#00000000",
                                             renderType = RenderType.CHANGE_WEIGHT,
@@ -159,7 +155,6 @@
                                     timespec = DigitalTimespec.FIRST_DIGIT,
                                     style =
                                         FontTextStyle(
-                                            fontFamily = "google_sans_flex.ttf",
                                             lineHeight = 147.25f,
                                             fontVariation =
                                                 "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100",
@@ -168,7 +163,6 @@
                                         FontTextStyle(
                                             fontVariation =
                                                 "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100",
-                                            fontFamily = "google_sans_flex.ttf",
                                             fillColorLight = "#FFFFFFFF",
                                             outlineColor = "#00000000",
                                             renderType = RenderType.CHANGE_WEIGHT,
@@ -187,7 +181,6 @@
                                     timespec = DigitalTimespec.SECOND_DIGIT,
                                     style =
                                         FontTextStyle(
-                                            fontFamily = "google_sans_flex.ttf",
                                             lineHeight = 147.25f,
                                             fontVariation =
                                                 "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100",
@@ -196,7 +189,6 @@
                                         FontTextStyle(
                                             fontVariation =
                                                 "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100",
-                                            fontFamily = "google_sans_flex.ttf",
                                             fillColorLight = "#FFFFFFFF",
                                             outlineColor = "#00000000",
                                             renderType = RenderType.CHANGE_WEIGHT,
@@ -221,13 +213,11 @@
                         timespec = DigitalTimespec.TIME_FULL_FORMAT,
                         style =
                             FontTextStyle(
-                                fontFamily = "google_sans_flex.ttf",
                                 fontVariation = "'wght' 600, 'wdth' 100, 'opsz' 144, 'ROND' 100",
                                 fontSizeScale = 0.98f,
                             ),
                         aodStyle =
                             FontTextStyle(
-                                fontFamily = "google_sans_flex.ttf",
                                 fontVariation = "'wght' 133, 'wdth' 43, 'opsz' 144, 'ROND' 100",
                                 fillColorLight = "#FFFFFFFF",
                                 outlineColor = "#00000000",
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/back/FlingOnBackAnimationCallbackTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/back/FlingOnBackAnimationCallbackTest.kt
new file mode 100644
index 0000000..75a5768
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/back/FlingOnBackAnimationCallbackTest.kt
@@ -0,0 +1,127 @@
+/*
+ * 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.animation.back
+
+import android.platform.test.annotations.RequiresFlagsDisabled
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import android.view.animation.Interpolator
+import android.window.BackEvent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.app.animation.Interpolators
+import com.android.systemui.SysuiTestCase
+import com.android.window.flags.Flags.FLAG_PREDICTIVE_BACK_TIMESTAMP_API
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FlingOnBackAnimationCallbackTest : SysuiTestCase() {
+
+    @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
+    @Test
+    fun testProgressInterpolation() {
+        val mockInterpolator = Mockito.mock(Interpolator::class.java)
+        val backEvent = backEventOf(0.5f)
+        Mockito.`when`(mockInterpolator.getInterpolation(0.5f)).thenReturn(0.8f)
+        val callback = TestFlingOnBackAnimationCallback(mockInterpolator)
+        callback.onBackStarted(backEvent)
+        assertTrue("Assert onBackStartedCompat called", callback.backStartedCalled)
+        callback.onBackProgressed(backEvent)
+        assertTrue("Assert onBackProgressedCompat called", callback.backProgressedCalled)
+        assertEquals("Assert interpolated progress", 0.8f, callback.progressEvent?.progress)
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_PREDICTIVE_BACK_TIMESTAMP_API)
+    fun testFling() {
+        val callback = TestFlingOnBackAnimationCallback(Interpolators.LINEAR)
+        callback.onBackStarted(backEventOf(progress = 0f, frameTime = 0))
+        assertTrue("Assert onBackStartedCompat called", callback.backStartedCalled)
+        callback.onBackProgressed(backEventOf(0f, 8))
+        callback.onBackProgressed(backEventOf(0.2f, 16))
+        callback.onBackProgressed(backEventOf(0.4f, 24))
+        callback.onBackProgressed(backEventOf(0.6f, 32))
+        assertTrue("Assert onBackProgressedCompat called", callback.backProgressedCalled)
+        assertEquals("Assert interpolated progress", 0.6f, callback.progressEvent?.progress)
+        getInstrumentation().runOnMainSync { callback.onBackInvoked() }
+        // Assert that onBackInvoked is not called immediately...
+        assertFalse(callback.backInvokedCalled)
+        // Instead the fling animation is played and eventually onBackInvoked is called.
+        callback.backInvokedLatch.await(1000, TimeUnit.MILLISECONDS)
+        assertTrue(callback.backInvokedCalled)
+    }
+
+    @Test
+    @RequiresFlagsDisabled(FLAG_PREDICTIVE_BACK_TIMESTAMP_API)
+    fun testCallbackWithoutTimestampApi() {
+        // Assert that all callback methods are immediately forwarded
+        val callback = TestFlingOnBackAnimationCallback(Interpolators.LINEAR)
+        callback.onBackStarted(backEventOf(progress = 0f, frameTime = 0))
+        assertTrue("Assert onBackStartedCompat called", callback.backStartedCalled)
+        callback.onBackProgressed(backEventOf(0f, 8))
+        assertTrue("Assert onBackProgressedCompat called", callback.backProgressedCalled)
+        callback.onBackInvoked()
+        assertTrue("Assert onBackInvoked called", callback.backInvokedCalled)
+        callback.onBackCancelled()
+        assertTrue("Assert onBackCancelled called", callback.backCancelledCalled)
+    }
+
+    private fun backEventOf(progress: Float, frameTime: Long = 0): BackEvent {
+        return BackEvent(10f, 10f, progress, 0, frameTime)
+    }
+
+    /** Helper class to expose the compat functions for testing */
+    private class TestFlingOnBackAnimationCallback(progressInterpolator: Interpolator) :
+        FlingOnBackAnimationCallback(progressInterpolator) {
+        var backStartedCalled = false
+        var backProgressedCalled = false
+        var backInvokedCalled = false
+        val backInvokedLatch = CountDownLatch(1)
+        var backCancelledCalled = false
+        var progressEvent: BackEvent? = null
+
+        override fun onBackStartedCompat(backEvent: BackEvent) {
+            backStartedCalled = true
+        }
+
+        override fun onBackProgressedCompat(backEvent: BackEvent) {
+            backProgressedCalled = true
+            progressEvent = backEvent
+        }
+
+        override fun onBackInvokedCompat() {
+            backInvokedCalled = true
+            backInvokedLatch.countDown()
+        }
+
+        override fun onBackCancelledCompat() {
+            backCancelledCalled = true
+        }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractorTest.kt
index 5bd3645..99d2da67 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractorTest.kt
@@ -30,9 +30,11 @@
 import com.android.systemui.qs.pipeline.domain.interactor.panelInteractor
 import com.android.systemui.qs.tiles.base.interactor.QSTileInput
 import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import com.android.systemui.recordissue.IssueRecordingState
 import com.android.systemui.recordissue.RecordIssueDialogDelegate
 import com.android.systemui.screenrecord.RecordingController
 import com.android.systemui.settings.UserContextProvider
+import com.android.systemui.settings.userFileManager
 import com.android.systemui.settings.userTracker
 import com.android.systemui.statusbar.phone.KeyguardDismissUtil
 import com.android.systemui.statusbar.policy.keyguardStateController
@@ -81,10 +83,11 @@
             underTest =
                 IssueRecordingUserActionInteractor(
                     testDispatcher,
+                    IssueRecordingState(userTracker, userFileManager),
                     KeyguardDismissUtil(
                         keyguardStateController,
                         statusBarStateController,
-                        activityStarter
+                        activityStarter,
                     ),
                     keyguardStateController,
                     dialogTransitionAnimator,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt
index a1edfc1..9dbcf18 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt
@@ -60,6 +60,7 @@
     private val iActivityManager = mock<IActivityManager>()
     private val notificationManager = mock<NotificationManager>()
     private val panelInteractor = mock<PanelInteractor>()
+    private val screenRecordingStartTimeStore = mock<ScreenRecordingStartTimeStore>()
 
     private lateinit var underTest: IssueRecordingServiceSession
 
@@ -76,6 +77,7 @@
                 iActivityManager,
                 notificationManager,
                 userContextProvider,
+                screenRecordingStartTimeStore,
             )
     }
 
@@ -90,7 +92,7 @@
 
     @Test
     fun stopsTracing_afterReceivingStopTracingCommand() {
-        underTest.stop(mContext.contentResolver)
+        underTest.stop()
         bgExecutor.runAllReady()
 
         Truth.assertThat(issueRecordingState.isRecording).isFalse()
@@ -107,7 +109,7 @@
 
     @Test
     fun requestBugreport_afterReceivingShareCommand_withTakeBugreportTrue() {
-        issueRecordingState.takeBugreport = true
+        underTest.takeBugReport = true
         val uri = mock<Uri>()
 
         underTest.share(0, uri)
@@ -118,13 +120,13 @@
 
     @Test
     fun sharesTracesDirectly_afterReceivingShareCommand_withTakeBugreportFalse() {
-        issueRecordingState.takeBugreport = false
+        underTest.takeBugReport = false
         val uri = mock<Uri>()
 
         underTest.share(0, uri)
         bgExecutor.runAllReady()
 
-        verify(traceurConnection).shareTraces(uri)
+        verify(traceurConnection).shareTraces(any())
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/ScreenRecordingStartTimeStoreTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/ScreenRecordingStartTimeStoreTest.kt
new file mode 100644
index 0000000..737b101
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/ScreenRecordingStartTimeStoreTest.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.recordissue
+
+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.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.settings.userTracker
+import com.google.common.truth.Truth
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class ScreenRecordingStartTimeStoreTest : SysuiTestCase() {
+    private val userTracker: UserTracker = Kosmos().also { it.testCase = this }.userTracker
+
+    private lateinit var underTest: ScreenRecordingStartTimeStore
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        underTest = ScreenRecordingStartTimeStore(userTracker)
+    }
+
+    @Test
+    fun markStartTime_correctlyStoresValues_inSharedPreferences() {
+        underTest.markStartTime()
+
+        val startTimeMetadata = underTest.userIdToScreenRecordingStartTime.get(userTracker.userId)
+        Truth.assertThat(startTimeMetadata).isNotNull()
+        Truth.assertThat(startTimeMetadata!!.getLong(ELAPSED_REAL_TIME_NANOS_KEY)).isNotNull()
+        Truth.assertThat(startTimeMetadata.getLong(REAL_TO_ELAPSED_TIME_OFFSET_NANOS_KEY))
+            .isNotNull()
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/RecordingServiceTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
index 0d5ddae..bff3903 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
@@ -52,6 +52,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.mediaprojection.MediaProjectionCaptureTarget;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.recordissue.ScreenRecordingStartTimeStore;
 import com.android.systemui.settings.UserContextProvider;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
@@ -95,6 +96,8 @@
     private SysuiStatusBarStateController mStatusBarStateController;
     @Mock
     private ActivityStarter mActivityStarter;
+    @Mock
+    private ScreenRecordingStartTimeStore mScreenRecordingStartTimeStore;
 
     private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
 
@@ -108,9 +111,10 @@
                 RecordingController controller, Executor executor,
                 Handler handler, UiEventLogger uiEventLogger,
                 NotificationManager notificationManager,
-                UserContextProvider userContextTracker, KeyguardDismissUtil keyguardDismissUtil) {
-            super(controller, executor, handler,
-                    uiEventLogger, notificationManager, userContextTracker, keyguardDismissUtil);
+                UserContextProvider userContextTracker, KeyguardDismissUtil keyguardDismissUtil,
+                ScreenRecordingStartTimeStore screenRecordingStartTimeStore) {
+            super(controller, executor, handler, uiEventLogger, notificationManager,
+                    userContextTracker, keyguardDismissUtil, screenRecordingStartTimeStore);
             attachBaseContext(mContext);
         }
     }
@@ -120,7 +124,7 @@
         MockitoAnnotations.initMocks(this);
         mRecordingService = Mockito.spy(new RecordingServiceTestable(mController, mExecutor,
                 mHandler, mUiEventLogger, mNotificationManager,
-                mUserContextTracker, mKeyguardDismissUtil));
+                mUserContextTracker, mKeyguardDismissUtil, mScreenRecordingStartTimeStore));
 
         // Return actual context info
         doReturn(mContext).when(mRecordingService).getApplicationContext();
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/scroll/ScrollCaptureControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/scroll/ScrollCaptureControllerTest.java
index f8de714..a831e63 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/scroll/ScrollCaptureControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/scroll/ScrollCaptureControllerTest.java
@@ -38,6 +38,8 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.screenshot.scroll.ScrollCaptureClient.Session;
 
+import org.junit.Assume;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -49,6 +51,12 @@
 @RunWith(AndroidJUnit4.class)
 public class ScrollCaptureControllerTest extends SysuiTestCase {
 
+    @Before
+    public void assumeOnDevice() {
+        // TODO(b/373930957) this class hangs under robolectric
+        Assume.assumeFalse(isRobolectricTest());
+    }
+
     private static final ScrollCaptureResponse EMPTY_RESPONSE =
             new ScrollCaptureResponse.Builder().build();
 
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 d1d3b9b..89ad699 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.bouncer.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.classifier.FalsingManagerFake;
-import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository;
 import com.android.systemui.common.ui.view.LongPressHandlingView;
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor;
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor;
@@ -434,15 +433,6 @@
                 new ShadeInteractorLegacyImpl(
                         mTestScope.getBackgroundScope(),
                         mFakeKeyguardRepository,
-                        new SharedNotificationContainerInteractor(
-                                new FakeConfigurationRepository(),
-                                mContext,
-                                () -> splitShadeStateController,
-                                () -> mShadeInteractor,
-                                mKeyguardInteractor,
-                                deviceEntryUdfpsInteractor,
-                                () -> mLargeScreenHeaderHelper
-                        ),
                         mShadeRepository
                 ),
                 mKosmos.getShadeModeInteractor());
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
index a52f173..2e759a3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
@@ -36,16 +36,12 @@
 import com.android.keyguard.KeyguardStatusView;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository;
-import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository;
-import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor;
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor;
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.fragments.FragmentHostManager;
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.kosmos.KosmosJavaAdapter;
 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager;
@@ -55,7 +51,6 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor;
 import com.android.systemui.qs.QSFragmentLegacy;
 import com.android.systemui.res.R;
-import com.android.systemui.scene.domain.interactor.SceneInteractor;
 import com.android.systemui.screenrecord.RecordingController;
 import com.android.systemui.shade.data.repository.FakeShadeRepository;
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
@@ -72,7 +67,6 @@
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor;
 import com.android.systemui.statusbar.notification.stack.AmbientState;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
-import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.KeyguardBottomAreaView;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
@@ -179,27 +173,11 @@
         mStatusBarStateController = mKosmos.getStatusBarStateController();
 
         mKosmos.getFakeDeviceProvisioningRepository().setDeviceProvisioned(true);
-        FakeConfigurationRepository configurationRepository = new FakeConfigurationRepository();
 
         PowerInteractor powerInteractor = mKosmos.getPowerInteractor();
 
-        SceneInteractor sceneInteractor = mKosmos.getSceneInteractor();
-
         KeyguardTransitionInteractor keyguardTransitionInteractor =
                 mKosmos.getKeyguardTransitionInteractor();
-        KeyguardInteractor keyguardInteractor = new KeyguardInteractor(
-                mKeyguardRepository,
-                powerInteractor,
-                new FakeKeyguardBouncerRepository(),
-                new ConfigurationInteractor(configurationRepository),
-                mShadeRepository,
-                keyguardTransitionInteractor,
-                () -> sceneInteractor,
-                () -> mKosmos.getFromGoneTransitionInteractor(),
-                () -> mKosmos.getFromLockscreenTransitionInteractor(),
-                () -> mKosmos.getFromOccludedTransitionInteractor(),
-                () -> mKosmos.getSharedNotificationContainerInteractor(),
-                mTestScope);
 
         ResourcesSplitShadeStateController splitShadeStateController =
                 new ResourcesSplitShadeStateController();
@@ -222,14 +200,6 @@
                 new ShadeInteractorLegacyImpl(
                         mTestScope.getBackgroundScope(),
                         mKeyguardRepository,
-                        new SharedNotificationContainerInteractor(
-                                configurationRepository,
-                                mContext,
-                                () -> splitShadeStateController,
-                                () -> mShadeInteractor,
-                                keyguardInteractor,
-                                deviceEntryUdfpsInteractor,
-                                () -> mLargeScreenHeaderHelper),
                         mShadeRepository
                 ),
                 mKosmos.getShadeModeInteractor());
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt
index 4592b60..238a1c1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt
@@ -26,9 +26,9 @@
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.res.R
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.shade.shadeTestUtil
 import com.android.systemui.testKosmos
 import com.android.systemui.user.data.repository.fakeUserRepository
 import com.google.common.truth.Truth.assertThat
@@ -46,6 +46,7 @@
 class ShadeInteractorLegacyImplTest : SysuiTestCase() {
     val kosmos = testKosmos()
     val testScope = kosmos.testScope
+    val shadeTestUtil = kosmos.shadeTestUtil
     val configurationRepository = kosmos.fakeConfigurationRepository
     val keyguardRepository = kosmos.fakeKeyguardRepository
     val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
@@ -87,7 +88,7 @@
 
             // WHEN split shade is enabled and QS is expanded
             keyguardRepository.setStatusBarState(StatusBarState.SHADE)
-            overrideResource(R.bool.config_use_split_notification_shade, true)
+            shadeTestUtil.setSplitShade(true)
             configurationRepository.onAnyConfigurationChange()
             shadeRepository.setQsExpansion(.5f)
             shadeRepository.setLegacyShadeExpansion(.7f)
@@ -104,7 +105,7 @@
 
             // WHEN split shade is not enabled and QS is expanded
             keyguardRepository.setStatusBarState(StatusBarState.SHADE)
-            overrideResource(R.bool.config_use_split_notification_shade, false)
+            shadeTestUtil.setSplitShade(false)
             shadeRepository.setQsExpansion(.5f)
             shadeRepository.setLegacyShadeExpansion(1f)
             runCurrent()
@@ -120,7 +121,7 @@
 
             // WHEN split shade is not enabled and QS is expanded
             keyguardRepository.setStatusBarState(StatusBarState.SHADE)
-            overrideResource(R.bool.config_use_split_notification_shade, false)
+            shadeTestUtil.setSplitShade(false)
             shadeRepository.setQsExpansion(1f)
             shadeRepository.setLegacyShadeExpansion(1f)
             runCurrent()
@@ -136,7 +137,7 @@
 
             // WHEN split shade is not enabled and QS partly expanded
             keyguardRepository.setStatusBarState(StatusBarState.SHADE)
-            overrideResource(R.bool.config_use_split_notification_shade, false)
+            shadeTestUtil.setSplitShade(false)
             shadeRepository.setQsExpansion(.4f)
             shadeRepository.setLegacyShadeExpansion(1f)
             runCurrent()
@@ -152,7 +153,7 @@
 
             // WHEN split shade is not enabled and QS collapsed
             keyguardRepository.setStatusBarState(StatusBarState.SHADE)
-            overrideResource(R.bool.config_use_split_notification_shade, false)
+            shadeTestUtil.setSplitShade(false)
             shadeRepository.setQsExpansion(0f)
             shadeRepository.setLegacyShadeExpansion(.6f)
             runCurrent()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt
index 663c341..16da3d2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt
@@ -70,14 +70,14 @@
         }
 
     private fun createController() =
-        PrivacyDotViewController(
+        PrivacyDotViewControllerImpl(
                 executor,
                 testScope.backgroundScope,
                 statusBarStateController,
                 configurationController,
                 contentInsetsProvider,
                 animationScheduler = mock<SystemStatusAnimationScheduler>(),
-                shadeInteractor = null
+                shadeInteractor = null,
             )
             .also { it.setUiExecutor(executor) }
 
@@ -307,7 +307,7 @@
             newTopLeftView,
             newTopRightView,
             newBottomLeftView,
-            newBottomRightView
+            newBottomRightView,
         )
 
         assertThat((newBottomRightView.layoutParams as FrameLayout.LayoutParams).gravity)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt
index 1f1680e..75479ad 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt
@@ -31,6 +31,7 @@
 @RunWith(AndroidJUnit4::class)
 class NotificationScrimNestedScrollConnectionTest : SysuiTestCase() {
     private var isStarted = false
+    private var wasStarted = false
     private var scrimOffset = 0f
     private var contentHeight = 0f
     private var isCurrentGestureOverscroll = false
@@ -46,7 +47,10 @@
             minVisibleScrimHeight = { MIN_VISIBLE_SCRIM_HEIGHT },
             isCurrentGestureOverscroll = { isCurrentGestureOverscroll },
             onStart = { isStarted = true },
-            onStop = { isStarted = false },
+            onStop = {
+                wasStarted = true
+                isStarted = false
+            },
         )
 
     @Test
@@ -165,6 +169,7 @@
             )
 
         assertThat(offsetConsumed).isEqualTo(Offset.Zero)
+        assertThat(wasStarted).isEqualTo(false)
         assertThat(isStarted).isEqualTo(false)
     }
 
@@ -181,7 +186,9 @@
             )
 
         assertThat(offsetConsumed).isEqualTo(Offset.Zero)
-        assertThat(isStarted).isEqualTo(true)
+        // Returning 0 offset will immediately stop the connection
+        assertThat(wasStarted).isEqualTo(true)
+        assertThat(isStarted).isEqualTo(false)
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
index dc9c22f..f1edb41 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
@@ -21,6 +21,7 @@
 import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer;
 import static com.android.systemui.statusbar.notification.collection.GroupEntry.ROOT_ENTRY;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -36,9 +37,11 @@
 
 import static java.util.Objects.requireNonNull;
 
+import android.app.Flags;
 import android.database.ContentObserver;
 import android.os.Handler;
 import android.os.RemoteException;
+import android.platform.test.annotations.EnableFlags;
 import android.testing.TestableLooper;
 
 import androidx.annotation.NonNull;
@@ -61,6 +64,7 @@
 import com.android.systemui.statusbar.notification.collection.inflation.NotifUiAdjustmentProvider;
 import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection;
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener;
+import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
@@ -69,6 +73,8 @@
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
 import com.android.systemui.statusbar.notification.collection.render.NotifViewBarn;
 import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager;
+import com.android.systemui.statusbar.notification.row.icon.AppIconProvider;
+import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider;
 import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController;
 import com.android.systemui.util.settings.SecureSettings;
 
@@ -82,10 +88,12 @@
 import org.mockito.Spy;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Stream;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -93,6 +101,7 @@
 public class PreparationCoordinatorTest extends SysuiTestCase {
     private NotifCollectionListener mCollectionListener;
     private OnBeforeFinalizeFilterListener mBeforeFilterListener;
+    private OnBeforeTransformGroupsListener mBeforeTransformGroupsListener;
     private NotifFilter mUninflatedFilter;
     private NotifFilter mInflationErrorFilter;
     private NotifInflationErrorManager mErrorManager;
@@ -101,6 +110,8 @@
 
     @Captor private ArgumentCaptor<NotifCollectionListener> mCollectionListenerCaptor;
     @Captor private ArgumentCaptor<OnBeforeFinalizeFilterListener> mBeforeFilterListenerCaptor;
+    @Captor private ArgumentCaptor<OnBeforeTransformGroupsListener>
+            mBeforeTransformGroupsListenerCaptor;
     @Captor private ArgumentCaptor<NotifInflater.Params> mParamsCaptor;
 
     @Mock private NotifSectioner mNotifSectioner;
@@ -108,13 +119,14 @@
     @Mock private NotifPipeline mNotifPipeline;
     @Mock private IStatusBarService mService;
     @Mock private BindEventManagerImpl mBindEventManagerImpl;
+    @Mock private AppIconProvider mAppIconProvider;
+    @Mock private NotificationIconStyleProvider mNotificationIconStyleProvider;
     @Mock private NotificationLockscreenUserManager mLockscreenUserManager;
     @Mock private SensitiveNotificationProtectionController mSensitiveNotifProtectionController;
     @Mock private Handler mHandler;
     @Mock private SecureSettings mSecureSettings;
     @Spy private FakeNotifInflater mNotifInflater = new FakeNotifInflater();
-    @Mock
-    HighPriorityProvider mHighPriorityProvider;
+    @Mock HighPriorityProvider mHighPriorityProvider;
     private SectionStyleProvider mSectionStyleProvider;
     @Mock private UserTracker mUserTracker;
     @Mock private GroupMembershipManager mGroupMembershipManager;
@@ -126,6 +138,11 @@
         return new NotificationEntryBuilder().setSection(mNotifSection);
     }
 
+    @NonNull
+    private GroupEntryBuilder getGroupEntryBuilder() {
+        return new GroupEntryBuilder().setSection(mNotifSection);
+    }
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -138,7 +155,7 @@
                 mSectionStyleProvider,
                 mUserTracker,
                 mGroupMembershipManager
-                );
+        );
         mEntry = getNotificationEntryBuilder().setParent(ROOT_ENTRY).build();
         mInflationError = new Exception(TEST_MESSAGE);
         mErrorManager = new NotifInflationErrorManager();
@@ -153,6 +170,8 @@
                 mAdjustmentProvider,
                 mService,
                 mBindEventManagerImpl,
+                mAppIconProvider,
+                mNotificationIconStyleProvider,
                 TEST_CHILD_BIND_CUTOFF,
                 TEST_MAX_GROUP_DELAY);
 
@@ -163,6 +182,15 @@
         mInflationErrorFilter = filters.get(0);
         mUninflatedFilter = filters.get(1);
 
+        if (android.app.Flags.notificationsRedesignAppIcons()) {
+            verify(mNotifPipeline).addOnBeforeTransformGroupsListener(
+                    mBeforeTransformGroupsListenerCaptor.capture());
+            mBeforeTransformGroupsListener = mBeforeTransformGroupsListenerCaptor.getValue();
+        } else {
+            verify(mNotifPipeline, never()).addOnBeforeTransformGroupsListener(
+                    mBeforeTransformGroupsListenerCaptor.capture());
+        }
+
         verify(mNotifPipeline).addCollectionListener(mCollectionListenerCaptor.capture());
         mCollectionListener = mCollectionListenerCaptor.getValue();
 
@@ -199,6 +227,100 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_NOTIFICATIONS_REDESIGN_APP_ICONS)
+    public void testPurgesAppIconProviderCache() {
+        // GIVEN a notification list
+        NotificationEntry entry1 = getNotificationEntryBuilder().setPkg("1").build();
+        NotificationEntry entry2 = getNotificationEntryBuilder().setPkg("2").build();
+        NotificationEntry entry2bis = getNotificationEntryBuilder().setPkg("2").build();
+        NotificationEntry entry3 = getNotificationEntryBuilder().setPkg("3").build();
+
+        String groupKey1 = "group1";
+        NotificationEntry summary =
+                getNotificationEntryBuilder()
+                        .setPkg(groupKey1)
+                        .setGroup(mContext, groupKey1)
+                        .setGroupSummary(mContext, true)
+                        .build();
+        NotificationEntry child1 = getNotificationEntryBuilder().setGroup(mContext, groupKey1)
+                .setPkg(groupKey1).build();
+        NotificationEntry child2 = getNotificationEntryBuilder().setGroup(mContext, groupKey1)
+                .setPkg(groupKey1).build();
+        GroupEntry groupWithSummaryAndChildren = getGroupEntryBuilder().setKey(groupKey1)
+                .setSummary(summary).addChild(child1).addChild(child2).build();
+
+        String groupKey2 = "group2";
+        NotificationEntry summary2 =
+                getNotificationEntryBuilder()
+                        .setPkg(groupKey2)
+                        .setGroup(mContext, groupKey2)
+                        .setGroupSummary(mContext, true)
+                        .build();
+        GroupEntry summaryOnlyGroup = getGroupEntryBuilder().setKey(groupKey2)
+                .setSummary(summary2).build();
+
+        // WHEN onBeforeTransformGroup is called
+        mBeforeTransformGroupsListener.onBeforeTransformGroups(
+                List.of(entry1, entry2, entry2bis, entry3,
+                        groupWithSummaryAndChildren, summaryOnlyGroup));
+
+        // THEN purge should be called
+        ArgumentCaptor<Collection<String>> argumentCaptor = ArgumentCaptor.forClass(List.class);
+        verify(mAppIconProvider).purgeCache(argumentCaptor.capture());
+        List<String> actualList = argumentCaptor.getValue().stream().sorted().toList();
+        List<String> expectedList = Stream.of("1", "2", "3", "group1", "group2")
+                .sorted().toList();
+        assertEquals(expectedList, actualList);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NOTIFICATIONS_REDESIGN_APP_ICONS)
+    public void testPurgesNotificationIconStyleProviderCache() {
+        // GIVEN a notification list
+        NotificationEntry entry1 = getNotificationEntryBuilder().setPkg("1").build();
+        NotificationEntry entry2 = getNotificationEntryBuilder().setPkg("2").build();
+        NotificationEntry entry2bis = getNotificationEntryBuilder().setPkg("2").build();
+        NotificationEntry entry3 = getNotificationEntryBuilder().setPkg("3").build();
+
+        String groupKey1 = "group1";
+        NotificationEntry summary =
+                getNotificationEntryBuilder()
+                        .setPkg(groupKey1)
+                        .setGroup(mContext, groupKey1)
+                        .setGroupSummary(mContext, true)
+                        .build();
+        NotificationEntry child1 = getNotificationEntryBuilder().setGroup(mContext, groupKey1)
+                .setPkg(groupKey1).build();
+        NotificationEntry child2 = getNotificationEntryBuilder().setGroup(mContext, groupKey1)
+                .setPkg(groupKey1).build();
+        GroupEntry groupWithSummaryAndChildren = getGroupEntryBuilder().setKey(groupKey1)
+                .setSummary(summary).addChild(child1).addChild(child2).build();
+
+        String groupKey2 = "group2";
+        NotificationEntry summary2 =
+                getNotificationEntryBuilder()
+                        .setPkg(groupKey2)
+                        .setGroup(mContext, groupKey2)
+                        .setGroupSummary(mContext, true)
+                        .build();
+        GroupEntry summaryOnlyGroup = getGroupEntryBuilder().setKey(groupKey2)
+                .setSummary(summary2).build();
+
+        // WHEN onBeforeTransformGroup is called
+        mBeforeTransformGroupsListener.onBeforeTransformGroups(
+                List.of(entry1, entry2, entry2bis, entry3,
+                        groupWithSummaryAndChildren, summaryOnlyGroup));
+
+        // THEN purge should be called
+        ArgumentCaptor<Collection<String>> argumentCaptor = ArgumentCaptor.forClass(List.class);
+        verify(mNotificationIconStyleProvider).purgeCache(argumentCaptor.capture());
+        List<String> actualList = argumentCaptor.getValue().stream().sorted().toList();
+        List<String> expectedList = Stream.of("1", "2", "3", "group1", "group2")
+                .sorted().toList();
+        assertEquals(expectedList, actualList);
+    }
+
+    @Test
     public void testInflatesNewNotification() {
         // WHEN there is a new notification
         mCollectionListener.onEntryInit(mEntry);
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 add7ac9..25670cb 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
@@ -161,7 +161,7 @@
     fun validateMarginStartInSplitShade() =
         testScope.runTest {
             shadeTestUtil.setSplitShade(true)
-            overrideResource(R.dimen.notification_panel_margin_horizontal, 20)
+            overrideDimensionPixelSize(R.dimen.notification_panel_margin_horizontal, 20)
 
             val dimens by collectLastValue(underTest.configurationBasedDimensions)
 
@@ -174,7 +174,7 @@
     fun validateMarginStart() =
         testScope.runTest {
             shadeTestUtil.setSplitShade(false)
-            overrideResource(R.dimen.notification_panel_margin_horizontal, 20)
+            overrideDimensionPixelSize(R.dimen.notification_panel_margin_horizontal, 20)
 
             val dimens by collectLastValue(underTest.configurationBasedDimensions)
 
@@ -189,8 +189,8 @@
             whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(5)
             shadeTestUtil.setSplitShade(true)
             overrideResource(R.bool.config_use_large_screen_shade_header, true)
-            overrideResource(R.dimen.large_screen_shade_header_height, 10)
-            overrideResource(R.dimen.keyguard_split_shade_top_margin, 50)
+            overrideDimensionPixelSize(R.dimen.large_screen_shade_header_height, 10)
+            overrideDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin, 50)
 
             val paddingTop by collectLastValue(underTest.paddingTopDimen)
             configurationRepository.onAnyConfigurationChange()
@@ -205,8 +205,8 @@
             whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(10)
             shadeTestUtil.setSplitShade(false)
             overrideResource(R.bool.config_use_large_screen_shade_header, true)
-            overrideResource(R.dimen.large_screen_shade_header_height, 10)
-            overrideResource(R.dimen.keyguard_split_shade_top_margin, 50)
+            overrideDimensionPixelSize(R.dimen.large_screen_shade_header_height, 10)
+            overrideDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin, 50)
 
             val paddingTop by collectLastValue(underTest.paddingTopDimen)
 
@@ -221,8 +221,8 @@
             whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(10)
             shadeTestUtil.setSplitShade(false)
             overrideResource(R.bool.config_use_large_screen_shade_header, false)
-            overrideResource(R.dimen.large_screen_shade_header_height, 10)
-            overrideResource(R.dimen.keyguard_split_shade_top_margin, 50)
+            overrideDimensionPixelSize(R.dimen.large_screen_shade_header_height, 10)
+            overrideDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin, 50)
 
             val paddingTop by collectLastValue(underTest.paddingTopDimen)
 
@@ -263,8 +263,11 @@
             whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight())
                 .thenReturn(headerHelperHeight)
             overrideResource(R.bool.config_use_large_screen_shade_header, true)
-            overrideResource(R.dimen.large_screen_shade_header_height, headerResourceHeight)
-            overrideResource(R.dimen.notification_panel_margin_top, 0)
+            overrideDimensionPixelSize(
+                R.dimen.large_screen_shade_header_height,
+                headerResourceHeight,
+            )
+            overrideDimensionPixelSize(R.dimen.notification_panel_margin_top, 0)
 
             val dimens by collectLastValue(underTest.configurationBasedDimensions)
 
@@ -282,8 +285,11 @@
             whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight())
                 .thenReturn(headerHelperHeight)
             overrideResource(R.bool.config_use_large_screen_shade_header, true)
-            overrideResource(R.dimen.large_screen_shade_header_height, headerResourceHeight)
-            overrideResource(R.dimen.notification_panel_margin_top, 0)
+            overrideDimensionPixelSize(
+                R.dimen.large_screen_shade_header_height,
+                headerResourceHeight,
+            )
+            overrideDimensionPixelSize(R.dimen.notification_panel_margin_top, 0)
 
             val dimens by collectLastValue(underTest.configurationBasedDimensions)
 
@@ -480,8 +486,8 @@
     fun validateMarginTop() =
         testScope.runTest {
             overrideResource(R.bool.config_use_large_screen_shade_header, false)
-            overrideResource(R.dimen.large_screen_shade_header_height, 50)
-            overrideResource(R.dimen.notification_panel_margin_top, 0)
+            overrideDimensionPixelSize(R.dimen.large_screen_shade_header_height, 50)
+            overrideDimensionPixelSize(R.dimen.notification_panel_margin_top, 0)
 
             val dimens by collectLastValue(underTest.configurationBasedDimensions)
 
@@ -676,8 +682,8 @@
             whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(5)
             shadeTestUtil.setSplitShade(true)
             overrideResource(R.bool.config_use_large_screen_shade_header, true)
-            overrideResource(R.dimen.large_screen_shade_header_height, 10)
-            overrideResource(R.dimen.keyguard_split_shade_top_margin, 50)
+            overrideDimensionPixelSize(R.dimen.large_screen_shade_header_height, 10)
+            overrideDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin, 50)
 
             configurationRepository.onAnyConfigurationChange()
             runCurrent()
@@ -1310,4 +1316,9 @@
         communalSceneRepository.setTransitionState(transitionState)
         runCurrent()
     }
+
+    private fun overrideDimensionPixelSize(id: Int, pixelSize: Int) {
+        overrideResource(id, pixelSize)
+        configurationRepository.setDimensionPixelSize(id, pixelSize)
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 94753f7..21a317a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -34,6 +34,7 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -56,6 +57,7 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.internal.util.LatencyTracker;
 import com.android.internal.widget.LockPatternUtils;
@@ -675,8 +677,8 @@
         backCallback.onBackProgressed(event);
         verify(mBouncerViewDelegateBackCallback).onBackProgressed(eq(event));
 
-        backCallback.onBackInvoked();
-        verify(mBouncerViewDelegateBackCallback).onBackInvoked();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(backCallback::onBackInvoked);
+        verify(mBouncerViewDelegateBackCallback, timeout(1000)).onBackInvoked();
 
         backCallback.onBackCancelled();
         verify(mBouncerViewDelegateBackCallback).onBackCancelled();
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorTest.kt
new file mode 100644
index 0000000..7d55599
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorTest.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.ringer.domain
+
+import android.media.AudioManager.RINGER_MODE_NORMAL
+import android.media.AudioManager.RINGER_MODE_SILENT
+import android.media.AudioManager.RINGER_MODE_VIBRATE
+import android.media.AudioManager.STREAM_RING
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.volume.shared.model.RingerMode
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.fakeVolumeDialogController
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper
+class VolumeDialogRingerInteractorTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val controller = kosmos.fakeVolumeDialogController
+
+    private lateinit var underTest: VolumeDialogRingerInteractor
+
+    @Before
+    fun setUp() {
+        underTest = kosmos.volumeDialogRingerInteractor
+        controller.setStreamVolume(STREAM_RING, 50)
+    }
+
+    @Test
+    fun setRingerMode_normal() =
+        testScope.runTest {
+            runCurrent()
+            val ringerModel by collectLastValue(underTest.ringerModel)
+
+            underTest.setRingerMode(RingerMode(RINGER_MODE_NORMAL))
+            controller.getState()
+            runCurrent()
+
+            assertThat(ringerModel).isNotNull()
+            assertThat(ringerModel?.currentRingerMode).isEqualTo(RingerMode(RINGER_MODE_NORMAL))
+        }
+
+    @Test
+    fun setRingerMode_silent() =
+        testScope.runTest {
+            runCurrent()
+            val ringerModel by collectLastValue(underTest.ringerModel)
+
+            underTest.setRingerMode(RingerMode(RINGER_MODE_SILENT))
+            controller.getState()
+            runCurrent()
+
+            assertThat(ringerModel).isNotNull()
+            assertThat(ringerModel?.currentRingerMode).isEqualTo(RingerMode(RINGER_MODE_SILENT))
+        }
+
+    @Test
+    fun setRingerMode_vibrate() =
+        testScope.runTest {
+            runCurrent()
+            val ringerModel by collectLastValue(underTest.ringerModel)
+
+            underTest.setRingerMode(RingerMode(RINGER_MODE_VIBRATE))
+            controller.getState()
+            runCurrent()
+
+            assertThat(ringerModel).isNotNull()
+            assertThat(ringerModel?.currentRingerMode).isEqualTo(RingerMode(RINGER_MODE_VIBRATE))
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelTest.kt
new file mode 100644
index 0000000..faf01ed
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelTest.kt
@@ -0,0 +1,144 @@
+/*
+ * 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.ui.viewmodel
+
+import android.media.AudioManager.RINGER_MODE_NORMAL
+import android.media.AudioManager.RINGER_MODE_SILENT
+import android.media.AudioManager.RINGER_MODE_VIBRATE
+import android.media.AudioManager.STREAM_RING
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.volume.shared.model.RingerMode
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.haptics.fakeVibratorHelper
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.fakeVolumeDialogController
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper
+class VolumeDialogRingerDrawerViewModelTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val controller = kosmos.fakeVolumeDialogController
+    private val vibratorHelper = kosmos.fakeVibratorHelper
+
+    private lateinit var underTest: VolumeDialogRingerDrawerViewModel
+
+    @Before
+    fun setUp() {
+        underTest = kosmos.volumeDialogRingerDrawerViewModel
+    }
+
+    @Test
+    fun onSelectedRingerNormalModeButtonClicked_openDrawer() =
+        testScope.runTest {
+            val ringerViewModel by collectLastValue(underTest.ringerViewModel)
+            val normalRingerMode = RingerMode(RINGER_MODE_NORMAL)
+
+            setUpRingerModeAndOpenDrawer(normalRingerMode)
+
+            assertThat(ringerViewModel).isNotNull()
+            assertThat(ringerViewModel?.drawerState)
+                .isEqualTo(RingerDrawerState.Open(normalRingerMode))
+        }
+
+    @Test
+    fun onSelectedRingerButtonClicked_drawerOpened_closeDrawer() =
+        testScope.runTest {
+            val ringerViewModel by collectLastValue(underTest.ringerViewModel)
+            val normalRingerMode = RingerMode(RINGER_MODE_NORMAL)
+
+            setUpRingerModeAndOpenDrawer(normalRingerMode)
+            underTest.onRingerButtonClicked(normalRingerMode)
+            controller.getState()
+
+            assertThat(ringerViewModel).isNotNull()
+            assertThat(ringerViewModel?.drawerState)
+                .isEqualTo(RingerDrawerState.Closed(normalRingerMode))
+        }
+
+    @Test
+    fun onNewRingerButtonClicked_drawerOpened_updateRingerMode_closeDrawer() =
+        testScope.runTest {
+            val ringerViewModel by collectLastValue(underTest.ringerViewModel)
+            val vibrateRingerMode = RingerMode(RINGER_MODE_VIBRATE)
+
+            setUpRingerModeAndOpenDrawer(RingerMode(RINGER_MODE_NORMAL))
+            // Select vibrate ringer mode.
+            underTest.onRingerButtonClicked(vibrateRingerMode)
+            controller.getState()
+            runCurrent()
+
+            assertThat(ringerViewModel).isNotNull()
+            assertThat(
+                    ringerViewModel
+                        ?.availableButtons
+                        ?.get(ringerViewModel!!.currentButtonIndex)
+                        ?.ringerMode
+                )
+                .isEqualTo(vibrateRingerMode)
+            assertThat(ringerViewModel?.drawerState)
+                .isEqualTo(RingerDrawerState.Closed(vibrateRingerMode))
+
+            val silentRingerMode = RingerMode(RINGER_MODE_SILENT)
+            // Open drawer
+            underTest.onRingerButtonClicked(vibrateRingerMode)
+            controller.getState()
+
+            // Select silent ringer mode.
+            underTest.onRingerButtonClicked(silentRingerMode)
+            controller.getState()
+            runCurrent()
+
+            assertThat(ringerViewModel).isNotNull()
+            assertThat(
+                    ringerViewModel
+                        ?.availableButtons
+                        ?.get(ringerViewModel!!.currentButtonIndex)
+                        ?.ringerMode
+                )
+                .isEqualTo(silentRingerMode)
+            assertThat(ringerViewModel?.drawerState)
+                .isEqualTo(RingerDrawerState.Closed(silentRingerMode))
+            assertThat(controller.hasScheduledTouchFeedback).isFalse()
+            assertThat(vibratorHelper.totalVibrations).isEqualTo(2)
+        }
+
+    private fun TestScope.setUpRingerModeAndOpenDrawer(selectedRingerMode: RingerMode) {
+        controller.setStreamVolume(STREAM_RING, 50)
+        controller.setRingerMode(selectedRingerMode.value, false)
+        runCurrent()
+
+        underTest.onRingerButtonClicked(RingerMode(selectedRingerMode.value))
+        controller.getState()
+        runCurrent()
+    }
+}
diff --git a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
index 0029180..2b40df4 100644
--- a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
+++ b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
@@ -148,7 +148,7 @@
                         android:orientation="vertical"
                         android:clickable="false"
                         android:layout_width="wrap_content"
-                        android:layout_height="match_parent"
+                        android:layout_height="wrap_content"
                         android:gravity="start|center_vertical">
                         <TextView
                             android:id="@+id/mobile_title"
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index cdf15ca..c494e85 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3745,6 +3745,10 @@
          use. The helper shows shortcuts in categories, which can be collapsed or expanded.
          [CHAR LIMIT=NONE] -->
     <string name="shortcut_helper_content_description_drag_handle">Drag handle</string>
+    <!-- Label on the keyboard settings button in keyboard shortcut helper, that allows user to
+         open keyboard settings while in shortcut helper. The helper is a  component that shows the
+         user which keyboard shortcuts they can use. [CHAR LIMIT=NONE] -->
+    <string name="shortcut_helper_keyboard_settings_buttons_label">Keyboard Settings</string>
 
 
     <!-- Keyboard touchpad tutorial scheduler-->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index bda3453..1ab9242 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -1354,7 +1354,7 @@
 
     <style name="InternetDialog.Network">
         <item name="android:layout_width">match_parent</item>
-        <item name="android:layout_height">88dp</item>
+        <item name="android:layout_height">wrap_content</item>
         <item name="android:layout_marginStart">@dimen/internet_dialog_network_layout_margin</item>
         <item name="android:layout_marginEnd">@dimen/internet_dialog_network_layout_margin</item>
         <item name="android:layout_gravity">center_vertical|start</item>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 2d27f1c0..6bcacd0 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -32,7 +32,6 @@
 import static androidx.constraintlayout.widget.ConstraintSet.TOP;
 import static androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT;
 
-import static com.android.app.animation.InterpolatorsAndroidX.DECELERATE_QUINT;
 import static com.android.systemui.plugins.FalsingManager.LOW_PENALTY;
 
 import static java.lang.Integer.max;
@@ -271,8 +270,7 @@
         public void onBackProgressed(BackEvent event) {
             float progress = event.getProgress();
             // TODO(b/263819310): Update the interpolator to match spec.
-            float scale = MIN_BACK_SCALE
-                    +  (1 - MIN_BACK_SCALE) * (1 - DECELERATE_QUINT.getInterpolation(progress));
+            float scale = MIN_BACK_SCALE +  (1 - MIN_BACK_SCALE) * (1 - progress);
             setScale(scale);
         }
     };
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 83ab524..b3ea75d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -2368,7 +2368,7 @@
         }
 
         // Take a guess at initial SIM state, battery status and PLMN until we get an update
-        mBatteryStatus = new BatteryStatus(BATTERY_STATUS_UNKNOWN, /* level= */ 100, /* plugged= */
+        mBatteryStatus = new BatteryStatus(BATTERY_STATUS_UNKNOWN, /* level= */ -1, /* plugged= */
                 0, CHARGING_POLICY_DEFAULT, /* maxChargingWattage= */0, /* present= */true);
 
         // Watch for interesting updates
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index 8ae11ab..811b47d 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -43,7 +43,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.process.ProcessWrapper;
 import com.android.systemui.res.R;
-import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.phone.ConfigurationForwarder;
 import com.android.systemui.util.NotificationChannels;
 
 import java.lang.reflect.InvocationTargetException;
@@ -454,13 +454,13 @@
     @Override
     public void onConfigurationChanged(@NonNull Configuration newConfig) {
         if (mServicesStarted) {
-            ConfigurationController configController = mSysUIComponent.getConfigurationController();
+            ConfigurationForwarder configForwarder = mSysUIComponent.getConfigurationForwarder();
             if (Trace.isEnabled()) {
                 Trace.traceBegin(
                         Trace.TRACE_TAG_APP,
-                        configController.getClass().getSimpleName() + ".onConfigurationChanged()");
+                        configForwarder.getClass().getSimpleName() + ".onConfigurationChanged()");
             }
-            configController.onConfigurationChanged(newConfig);
+            configForwarder.onConfigurationChanged(newConfig);
             Trace.endSection();
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
index 3fe6669..17f1961 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -29,6 +29,7 @@
 import com.android.systemui.startable.Dependencies;
 import com.android.systemui.statusbar.NotificationInsetsModule;
 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.back.BackAnimation;
 import com.android.wm.shell.bubbles.Bubbles;
@@ -125,13 +126,20 @@
     BootCompleteCacheImpl provideBootCacheImpl();
 
     /**
-     * Creates a ContextComponentHelper.
+     * Creates a ConfigurationController.
      */
     @SysUISingleton
     @GlobalConfig
     ConfigurationController getConfigurationController();
 
     /**
+     * Creates a ConfigurationForwarder.
+     */
+    @SysUISingleton
+    @GlobalConfig
+    ConfigurationForwarder getConfigurationForwarder();
+
+    /**
      * Creates a ContextComponentHelper.
      */
     @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
index 21922ff..12718e8b 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
@@ -17,6 +17,7 @@
 package com.android.systemui.doze;
 
 import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
+import static android.hardware.biometrics.Flags.screenOffUnlockUdfps;
 
 import static com.android.systemui.doze.DozeLog.REASON_SENSOR_QUICK_PICKUP;
 import static com.android.systemui.doze.DozeLog.REASON_SENSOR_UDFPS_LONG_PRESS;
@@ -248,8 +249,8 @@
                         true /* touchscreen */,
                         false /* ignoresSetting */,
                         dozeParameters.longPressUsesProx(),
-                        false /* immediatelyReRegister */,
-                        true /* requiresAod */
+                        screenOffUnlockUdfps() /* immediatelyReRegister */,
+                        !screenOffUnlockUdfps() /* requiresAod */
                 ),
                 new PluginSensor(
                         new SensorManagerPlugin.Sensor(TYPE_WAKE_DISPLAY),
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt
index 4142be3..058e587 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt
@@ -52,7 +52,7 @@
             Modifier.fillMaxSize()
                 .onKeyEvent { keyEvent: KeyEvent ->
                     if (keyEvent.key == Key.MetaLeft && keyEvent.type == KeyEventType.KeyUp) {
-                        actionState = Finished
+                        actionState = Finished(R.raw.action_key_success)
                     }
                     true
                 }
@@ -80,11 +80,7 @@
                 titleSuccessResId = R.string.tutorial_action_key_success_title,
                 bodySuccessResId = R.string.tutorial_action_key_success_body,
             ),
-        animations =
-            TutorialScreenConfig.Animations(
-                educationResId = R.raw.action_key_edu,
-                successResId = R.raw.action_key_success,
-            ),
+        animations = TutorialScreenConfig.Animations(educationResId = R.raw.action_key_edu),
     )
 
 @Composable
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt
index 8e01e37..3d2baee 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.inputdevice.tutorial.ui.composable
 
+import androidx.annotation.RawRes
 import androidx.annotation.StringRes
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.fadeIn
@@ -48,7 +49,7 @@
         val endMarker: String? = null,
     ) : TutorialActionState
 
-    data object Finished : TutorialActionState
+    data class Finished(@RawRes val successAnimation: Int) : TutorialActionState
 }
 
 @Composable
@@ -68,11 +69,11 @@
         Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
             TutorialDescription(
                 titleTextId =
-                    if (actionState == Finished) config.strings.titleSuccessResId
+                    if (actionState is Finished) config.strings.titleSuccessResId
                     else config.strings.titleResId,
                 titleColor = config.colors.title,
                 bodyTextId =
-                    if (actionState == Finished) config.strings.bodySuccessResId
+                    if (actionState is Finished) config.strings.bodySuccessResId
                     else config.strings.bodyResId,
                 modifier = Modifier.weight(1f),
             )
@@ -83,7 +84,7 @@
                 modifier = Modifier.weight(1f).padding(top = 8.dp),
             )
         }
-        AnimatedVisibility(visible = actionState == Finished, enter = fadeIn()) {
+        AnimatedVisibility(visible = actionState is Finished, enter = fadeIn()) {
             DoneButton(onDoneButtonClicked = onDoneButtonClicked)
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialAnimation.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialAnimation.kt
index ef375a8..720c01f 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialAnimation.kt
@@ -77,7 +77,9 @@
                         config.colors.animationColors,
                     )
                 Finished::class ->
-                    SuccessAnimation(config.animations.successResId, config.colors.animationColors)
+                    // Below cast is safe as Finished state is the last state and afterwards we can
+                    // only leave the screen so this composable would be no longer displayed
+                    SuccessAnimation(actionState as Finished, config.colors.animationColors)
             }
         }
     }
@@ -100,10 +102,11 @@
 
 @Composable
 private fun SuccessAnimation(
-    @RawRes successAnimationId: Int,
+    finishedState: Finished,
     animationProperties: LottieDynamicProperties,
 ) {
-    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(successAnimationId))
+    val composition by
+        rememberLottieComposition(LottieCompositionSpec.RawRes(finishedState.successAnimation))
     val progress by animateLottieCompositionAsState(composition, iterations = 1)
     LottieAnimation(
         composition = composition,
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt
index 55e5f2d..60dfed3 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt
@@ -24,13 +24,13 @@
 data class TutorialScreenConfig(
     val colors: Colors,
     val strings: Strings,
-    val animations: Animations
+    val animations: Animations,
 ) {
 
     data class Colors(
         val background: Color,
         val title: Color,
-        val animationColors: LottieDynamicProperties
+        val animationColors: LottieDynamicProperties,
     )
 
     data class Strings(
@@ -40,8 +40,5 @@
         @StringRes val bodySuccessResId: Int,
     )
 
-    data class Animations(
-        @RawRes val educationResId: Int,
-        @RawRes val successResId: Int,
-    )
+    data class Animations(@RawRes val educationResId: Int)
 }
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 901eafa..5cade68 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
@@ -777,7 +777,7 @@
     ) {
         Row(verticalAlignment = Alignment.CenterVertically) {
             Text(
-                "Keyboard Settings",
+                stringResource(id = R.string.shortcut_helper_keyboard_settings_buttons_label),
                 color = MaterialTheme.colorScheme.onSurfaceVariant,
                 fontSize = 16.sp,
             )
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 0d5ad54..29c6d5a 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
@@ -50,7 +50,6 @@
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.data.repository.ShadeRepository
-import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
 import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
@@ -93,7 +92,6 @@
     private val fromGoneTransitionInteractor: Provider<FromGoneTransitionInteractor>,
     private val fromLockscreenTransitionInteractor: Provider<FromLockscreenTransitionInteractor>,
     private val fromOccludedTransitionInteractor: Provider<FromOccludedTransitionInteractor>,
-    sharedNotificationContainerInteractor: Provider<SharedNotificationContainerInteractor>,
     @Application applicationScope: CoroutineScope,
 ) {
     // TODO(b/296118689): move to a repository
@@ -104,15 +102,16 @@
         SceneContainerFlag.assertInLegacyMode()
         combineTransform(
                 _notificationPlaceholderBounds,
-                sharedNotificationContainerInteractor.get().configurationBasedDimensions,
                 keyguardTransitionInteractor.isInTransition(
                     edge = Edge.create(from = LOCKSCREEN, to = AOD)
                 ),
-            ) { bounds, cfg, isTransitioningToAod ->
+                shadeRepository.isShadeLayoutWide,
+                configurationInteractor.dimensionPixelSize(R.dimen.keyguard_split_shade_top_margin),
+            ) { bounds, isTransitioningToAod, useSplitShade, keyguardSplitShadeTopMargin ->
                 if (isTransitioningToAod) {
                     // Keep bounds stable during this transition, to prevent cases like smartspace
                     // popping in and adjusting the bounds. A prime example would be media playing,
-                    // which then updates smartspace on transition to AOD
+                    // which then updates smartspace on transition to AOD.
                     return@combineTransform
                 }
 
@@ -120,8 +119,8 @@
                 // legacy placement behavior within notifications for splitshade.
                 emit(
                     if (MigrateClocksToBlueprint.isEnabled) {
-                        if (cfg.useSplitShade) {
-                            bounds.copy(bottom = bounds.bottom - cfg.keyguardSplitShadeTopMargin)
+                        if (useSplitShade) {
+                            bounds.copy(bottom = bounds.bottom - keyguardSplitShadeTopMargin)
                         } else {
                             bounds
                         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index 6db91ac..4071b13 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -221,7 +221,7 @@
                 { notificationScrimClippingParams.params.top },
                 // Only allow scrolling when we are fully expanded. That way, we don't intercept
                 // swipes in lockscreen (when somehow QS is receiving touches).
-                { scrollState.canScrollForward && viewModel.isQsFullyExpanded },
+                { (scrollState.canScrollForward && viewModel.isQsFullyExpanded) || isCustomizing },
             )
         frame.addView(
             composeView,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/flags/QsDetailedView.kt b/packages/SystemUI/src/com/android/systemui/qs/flags/QsDetailedView.kt
new file mode 100644
index 0000000..ffeec4e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/flags/QsDetailedView.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.flags
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.shade.shared.flag.DualShade
+
+/** Helper for reading or using the QS Detailed View flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object QsDetailedView {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_QS_TILE_DETAILED_VIEW
+
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
+    /** Is the flag enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() =
+            Flags.qsTileDetailedView() && // mainAconfigFlag
+                DualShade.isEnabled &&
+                SceneContainerFlag.isEnabled
+
+    // NOTE: Changes should also be made in getSecondaryFlags
+
+    /** The main aconfig flag. */
+    inline fun getMainAconfigFlag() = FlagToken(FLAG_NAME, Flags.qsTileDetailedView())
+
+    /** The set of secondary flags which must be enabled for qs detailed view to work properly */
+    inline fun getSecondaryFlags(): Sequence<FlagToken> =
+        sequenceOf(
+            DualShade.token
+            // NOTE: Changes should also be made in isEnabled
+        ) + SceneContainerFlag.getAllRequirements()
+
+    /** The full set of requirements for QsDetailedView */
+    inline fun getAllRequirements(): Sequence<FlagToken> {
+        return sequenceOf(getMainAconfigFlag()) + getSecondaryFlags()
+    }
+
+    /** Return all dependencies of this flag in pairs where [Pair.first] depends on [Pair.second] */
+    inline fun getFlagDependencies(): Sequence<Pair<FlagToken, FlagToken>> {
+        val mainAconfigFlag = getMainAconfigFlag()
+        return getSecondaryFlags().map { mainAconfigFlag to it }
+    }
+
+    /**
+     * 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)
+
+    /** Returns a developer-readable string that describes the current requirement list. */
+    @JvmStatic
+    fun requirementDescription(): String {
+        return buildString {
+            getAllRequirements().forEach { requirement ->
+                append('\n')
+                append(if (requirement.isEnabled) "    [MET]" else "[NOT MET]")
+                append(" ${requirement.name}")
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
index 71fa0ac..7b25939 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
@@ -77,25 +77,24 @@
     colors: TileColors,
     squishiness: () -> Float,
     accessibilityUiState: AccessibilityUiState? = null,
-    toggleClickSupported: Boolean = false,
     iconShape: Shape = RoundedCornerShape(CommonTileDefaults.InactiveCornerRadius),
-    onClick: () -> Unit = {},
-    onLongClick: () -> Unit = {},
+    toggleClick: (() -> Unit)? = null,
+    onLongClick: (() -> Unit)? = null,
 ) {
     Row(
         verticalAlignment = Alignment.CenterVertically,
         horizontalArrangement = tileHorizontalArrangement(),
     ) {
         // Icon
-        val longPressLabel = longPressLabel()
+        val longPressLabel = longPressLabel().takeIf { onLongClick != null }
         Box(
             modifier =
-                Modifier.size(CommonTileDefaults.ToggleTargetSize).thenIf(toggleClickSupported) {
+                Modifier.size(CommonTileDefaults.ToggleTargetSize).thenIf(toggleClick != null) {
                     Modifier.clip(iconShape)
                         .verticalSquish(squishiness)
                         .background(colors.iconBackground, { 1f })
                         .combinedClickable(
-                            onClick = onClick,
+                            onClick = toggleClick!!,
                             onLongClick = onLongClick,
                             onLongClickLabel = longPressLabel,
                         )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
index d2ec958..b581c8b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
@@ -202,14 +202,17 @@
         topBar = { EditModeTopBar(onStopEditing = onStopEditing, onReset = reset) },
     ) { innerPadding ->
         CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
+            val scrollState = rememberScrollState()
+            LaunchedEffect(listState.dragInProgress) {
+                if (listState.dragInProgress) {
+                    scrollState.animateScrollTo(0)
+                }
+            }
+
             Column(
                 verticalArrangement =
                     spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)),
-                modifier =
-                    modifier
-                        .fillMaxSize()
-                        .verticalScroll(rememberScrollState())
-                        .padding(innerPadding),
+                modifier = modifier.fillMaxSize().verticalScroll(scrollState).padding(innerPadding),
             ) {
                 AnimatedContent(
                     targetState = listState.dragInProgress,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
index 52d5261..5f28fe4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
@@ -160,19 +160,18 @@
                 )
             } else {
                 val iconShape = TileDefaults.animateIconShape(uiState.state)
+                val secondaryClick: (() -> Unit)? =
+                    { tile.onSecondaryClick() }.takeIf { uiState.handlesSecondaryClick }
+                val longClick: (() -> Unit)? =
+                    { tile.onLongClick(expandable) }.takeIf { uiState.handlesLongClick }
                 LargeTileContent(
                     label = uiState.label,
                     secondaryLabel = uiState.secondaryLabel,
                     icon = icon,
                     colors = colors,
                     iconShape = iconShape,
-                    toggleClickSupported = state.handlesSecondaryClick,
-                    onClick = {
-                        if (state.handlesSecondaryClick) {
-                            tile.onSecondaryClick()
-                        }
-                    },
-                    onLongClick = { tile.onLongClick(expandable) },
+                    toggleClick = secondaryClick,
+                    onLongClick = longClick,
                     accessibilityUiState = uiState.accessibilityUiState,
                     squishiness = squishiness,
                 )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt
index aa42080..56675e4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt
@@ -33,6 +33,7 @@
     val label: String,
     val secondaryLabel: String,
     val state: Int,
+    val handlesLongClick: Boolean,
     val handlesSecondaryClick: Boolean,
     val icon: Supplier<QSTile.Icon?>,
     val accessibilityUiState: AccessibilityUiState,
@@ -86,6 +87,7 @@
         label = label?.toString() ?: "",
         secondaryLabel = secondaryLabel?.toString() ?: "",
         state = if (disabledByPolicy) Tile.STATE_UNAVAILABLE else state,
+        handlesLongClick = handlesLongClick,
         handlesSecondaryClick = handlesSecondaryClick,
         icon = icon?.let { Supplier { icon } } ?: iconSupplier ?: Supplier { null },
         AccessibilityUiState(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
index fb406d4..1792ebd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
@@ -152,7 +152,14 @@
         recordingController.startCountdown(
             DELAY_MS,
             INTERVAL_MS,
-            pendingServiceIntent(getStartIntent(userContextProvider.userContext)),
+            pendingServiceIntent(
+                getStartIntent(
+                    userContextProvider.userContext,
+                    issueRecordingState.traceConfig,
+                    issueRecordingState.recordScreen,
+                    issueRecordingState.takeBugreport,
+                )
+            ),
             pendingServiceIntent(getStopIntent(userContextProvider.userContext)),
         )
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractor.kt
index 0c8a375..fceee5a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractor.kt
@@ -35,6 +35,7 @@
 import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
 import com.android.systemui.recordissue.IssueRecordingService.Companion.getStartIntent
 import com.android.systemui.recordissue.IssueRecordingService.Companion.getStopIntent
+import com.android.systemui.recordissue.IssueRecordingState
 import com.android.systemui.recordissue.RecordIssueDialogDelegate
 import com.android.systemui.recordissue.RecordIssueModule.Companion.TILE_SPEC
 import com.android.systemui.screenrecord.RecordingController
@@ -52,6 +53,7 @@
 @Inject
 constructor(
     @Main private val mainCoroutineContext: CoroutineContext,
+    private val state: IssueRecordingState,
     private val keyguardDismissUtil: KeyguardDismissUtil,
     private val keyguardStateController: KeyguardStateController,
     private val dialogTransitionAnimator: DialogTransitionAnimator,
@@ -104,8 +106,15 @@
         recordingController.startCountdown(
             DELAY_MS,
             INTERVAL_MS,
-            pendingServiceIntent(getStartIntent(userContextProvider.userContext)),
-            pendingServiceIntent(getStopIntent(userContextProvider.userContext))
+            pendingServiceIntent(
+                getStartIntent(
+                    userContextProvider.userContext,
+                    state.traceConfig,
+                    state.recordScreen,
+                    state.takeBugreport,
+                )
+            ),
+            pendingServiceIntent(getStopIntent(userContextProvider.userContext)),
         )
 
     private fun stopIssueRecordingService() =
@@ -117,6 +126,6 @@
             userContextProvider.userContext,
             RecordingService.REQUEST_CODE,
             action,
-            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
         )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
index 3f875bc..32d9ba8 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
@@ -24,11 +24,9 @@
 import android.net.Uri
 import android.os.Handler
 import android.os.IBinder
-import android.os.Looper
 import android.util.Log
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.animation.DialogTransitionAnimator
-import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.LongRunning
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor
@@ -38,6 +36,9 @@
 import com.android.systemui.screenrecord.RecordingServiceStrings
 import com.android.systemui.settings.UserContextProvider
 import com.android.systemui.statusbar.phone.KeyguardDismissUtil
+import com.android.traceur.MessageConstants.INTENT_EXTRA_TRACE_TYPE
+import com.android.traceur.PresetTraceConfigs
+import com.android.traceur.TraceConfig
 import java.util.concurrent.Executor
 import javax.inject.Inject
 
@@ -45,7 +46,6 @@
 @Inject
 constructor(
     controller: RecordingController,
-    @Background private val bgLooper: Looper,
     @LongRunning private val bgExecutor: Executor,
     @Main handler: Handler,
     uiEventLogger: UiEventLogger,
@@ -57,6 +57,7 @@
     private val issueRecordingState: IssueRecordingState,
     traceurConnectionProvider: TraceurConnection.Provider,
     iActivityManager: IActivityManager,
+    screenRecordingStartTimeStore: ScreenRecordingStartTimeStore,
 ) :
     RecordingService(
         controller,
@@ -66,6 +67,7 @@
         notificationManager,
         userContextProvider,
         keyguardDismissUtil,
+        screenRecordingStartTimeStore,
     ) {
 
     private val traceurConnection: TraceurConnection = traceurConnectionProvider.create()
@@ -80,6 +82,7 @@
             iActivityManager,
             notificationManager,
             userContextProvider,
+            screenRecordingStartTimeStore,
         )
 
     /**
@@ -109,15 +112,23 @@
         Log.d(getTag(), "handling action: ${intent?.action}")
         when (intent?.action) {
             ACTION_START -> {
-                session.start()
-                if (!issueRecordingState.recordScreen) {
+                val screenRecord = intent.getBooleanExtra(EXTRA_SCREEN_RECORD, false)
+                with(session) {
+                    traceConfig =
+                        intent.getParcelableExtra(INTENT_EXTRA_TRACE_TYPE, TraceConfig::class.java)
+                            ?: PresetTraceConfigs.getDefaultConfig()
+                    takeBugReport = intent.getBooleanExtra(EXTRA_BUG_REPORT, false)
+                    this.screenRecord = screenRecord
+                    start()
+                }
+                if (!screenRecord) {
                     // If we don't want to record the screen, the ACTION_SHOW_START_NOTIF action
                     // will circumvent the RecordingService's screen recording start code.
                     return super.onStartCommand(Intent(ACTION_SHOW_START_NOTIF), flags, startId)
                 }
             }
             ACTION_STOP,
-            ACTION_STOP_NOTIF -> session.stop(contentResolver)
+            ACTION_STOP_NOTIF -> session.stop()
             ACTION_SHARE -> {
                 session.share(
                     intent.getIntExtra(EXTRA_NOTIFICATION_ID, mNotificationId),
@@ -136,6 +147,8 @@
     companion object {
         private const val TAG = "IssueRecordingService"
         private const val CHANNEL_ID = "issue_record"
+        const val EXTRA_SCREEN_RECORD = "extra_screenRecord"
+        const val EXTRA_BUG_REPORT = "extra_bugReport"
 
         /**
          * Get an intent to stop the issue recording service.
@@ -153,8 +166,17 @@
          *
          * @param context Context from the requesting activity
          */
-        fun getStartIntent(context: Context): Intent =
-            Intent(context, IssueRecordingService::class.java).setAction(ACTION_START)
+        fun getStartIntent(
+            context: Context,
+            traceConfig: TraceConfig,
+            screenRecord: Boolean,
+            bugReport: Boolean,
+        ): Intent =
+            Intent(context, IssueRecordingService::class.java)
+                .setAction(ACTION_START)
+                .putExtra(INTENT_EXTRA_TRACE_TYPE, traceConfig)
+                .putExtra(EXTRA_SCREEN_RECORD, screenRecord)
+                .putExtra(EXTRA_BUG_REPORT, bugReport)
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceSession.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceSession.kt
index ad9b4fe..06407ac 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceSession.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceSession.kt
@@ -18,13 +18,13 @@
 
 import android.app.IActivityManager
 import android.app.NotificationManager
-import android.content.ContentResolver
 import android.net.Uri
 import android.os.UserHandle
 import android.provider.Settings
 import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor
 import com.android.systemui.settings.UserContextProvider
+import com.android.traceur.PresetTraceConfigs
 import java.util.concurrent.Executor
 
 private const val NOTIFY_SESSION_ENDED_SETTING = "should_notify_trace_session_ended"
@@ -46,17 +46,25 @@
     private val iActivityManager: IActivityManager,
     private val notificationManager: NotificationManager,
     private val userContextProvider: UserContextProvider,
+    private val startTimeStore: ScreenRecordingStartTimeStore,
 ) {
+    var takeBugReport = false
+    var traceConfig = PresetTraceConfigs.getDefaultConfig()
+    var screenRecord = false
 
     fun start() {
-        bgExecutor.execute { traceurConnection.startTracing(issueRecordingState.traceConfig) }
+        bgExecutor.execute { traceurConnection.startTracing(traceConfig) }
         issueRecordingState.isRecording = true
     }
 
-    fun stop(contentResolver: ContentResolver) {
+    fun stop() {
         bgExecutor.execute {
-            if (issueRecordingState.traceConfig.longTrace) {
-                Settings.Global.putInt(contentResolver, NOTIFY_SESSION_ENDED_SETTING, DISABLED)
+            if (traceConfig.longTrace) {
+                Settings.Global.putInt(
+                    userContextProvider.userContext.contentResolver,
+                    NOTIFY_SESSION_ENDED_SETTING,
+                    DISABLED,
+                )
             }
             traceurConnection.stopTracing()
         }
@@ -70,11 +78,17 @@
                 notificationId,
                 UserHandle(userContextProvider.userContext.userId),
             )
-
-            if (issueRecordingState.takeBugreport) {
+            val screenRecordingUris: List<Uri> =
+                mutableListOf<Uri>().apply {
+                    screenRecording?.let { add(it) }
+                    if (traceConfig.winscope && screenRecord) {
+                        startTimeStore.getFileUri(userContextProvider.userContext)?.let { add(it) }
+                    }
+                }
+            if (takeBugReport) {
                 iActivityManager.requestBugReportWithExtraAttachment(screenRecording)
             } else {
-                traceurConnection.shareTraces(screenRecording)
+                traceurConnection.shareTraces(screenRecordingUris)
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/ScreenRecordingStartTimeStore.kt b/packages/SystemUI/src/com/android/systemui/recordissue/ScreenRecordingStartTimeStore.kt
new file mode 100644
index 0000000..5d8bc55
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/ScreenRecordingStartTimeStore.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.recordissue
+
+import android.content.Context
+import android.net.Uri
+import android.os.SystemClock
+import android.util.Log
+import android.util.SparseArray
+import androidx.annotation.VisibleForTesting
+import androidx.core.content.FileProvider
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.settings.UserTracker
+import java.io.File
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+import org.json.JSONObject
+
+private const val TAG = "ScreenRecordingStartTimeStore"
+@VisibleForTesting const val REAL_TO_ELAPSED_TIME_OFFSET_NANOS_KEY = "realToElapsedTimeOffsetNanos"
+@VisibleForTesting const val ELAPSED_REAL_TIME_NANOS_KEY = "elapsedRealTimeNanos"
+private const val RECORDING_METADATA_FILE_SUFFIX = "screen_recording_metadata.json"
+private const val AUTHORITY = "com.android.systemui.fileprovider"
+
+@SysUISingleton
+class ScreenRecordingStartTimeStore @Inject constructor(private val userTracker: UserTracker) {
+    @VisibleForTesting val userIdToScreenRecordingStartTime = SparseArray<JSONObject>()
+
+    fun markStartTime() {
+        val elapsedRealTimeNano = SystemClock.elapsedRealtimeNanos()
+        val realToElapsedTimeOffsetNano =
+            TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()) -
+                SystemClock.elapsedRealtimeNanos()
+        val startTimeMetadata =
+            JSONObject()
+                .put(ELAPSED_REAL_TIME_NANOS_KEY, elapsedRealTimeNano)
+                .put(REAL_TO_ELAPSED_TIME_OFFSET_NANOS_KEY, realToElapsedTimeOffsetNano)
+        userIdToScreenRecordingStartTime.put(userTracker.userId, startTimeMetadata)
+    }
+
+    /**
+     * Outputs start time metadata as Json to a file that can then be shared. Returns the Uri or
+     * null if the file system is not usable and the start time meta data is available. Uses
+     * com.android.systemui.fileprovider's authority.
+     *
+     * Because this file is not uniquely named, it doesn't need to be cleaned up. Every time it is
+     * outputted, it will overwrite the last file's contents. This is a feature, not a bug.
+     */
+    fun getFileUri(context: Context): Uri? {
+        val dir = context.externalCacheDir?.apply { mkdirs() } ?: return null
+        try {
+            val outFile =
+                File(dir, RECORDING_METADATA_FILE_SUFFIX).apply {
+                    userIdToScreenRecordingStartTime.get(userTracker.userId)?.let {
+                        writeText(it.toString())
+                    } ?: return null
+                }
+            return FileProvider.getUriForFile(context, AUTHORITY, outFile)
+        } catch (e: Exception) {
+            Log.e(TAG, "failed to get screen recording start time metadata via file uri", e)
+            return null
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/TraceurConnection.kt b/packages/SystemUI/src/com/android/systemui/recordissue/TraceurConnection.kt
index 81529b3..e6df3cd 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/TraceurConnection.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/TraceurConnection.kt
@@ -76,8 +76,9 @@
     @WorkerThread fun stopTracing() = sendMessage(MessageConstants.STOP_WHAT)
 
     @WorkerThread
-    fun shareTraces(screenRecord: Uri?) {
-        val replyHandler = Messenger(ShareFilesHandler(screenRecord, userContextProvider, bgLooper))
+    fun shareTraces(screenRecordingUris: List<Uri>) {
+        val replyHandler =
+            Messenger(ShareFilesHandler(screenRecordingUris, userContextProvider, bgLooper))
         sendMessage(MessageConstants.SHARE_WHAT, replyTo = replyHandler)
     }
 
@@ -101,7 +102,7 @@
 }
 
 private class ShareFilesHandler(
-    private val screenRecord: Uri?,
+    private val screenRecordingUris: List<Uri>,
     private val userContextProvider: UserContextProvider,
     looper: Looper,
 ) : Handler(looper) {
@@ -122,7 +123,7 @@
             ArrayList<Uri>().apply {
                 perfetto?.let { add(it) }
                 winscope?.let { add(it) }
-                screenRecord?.let { add(it) }
+                screenRecordingUris.forEach { add(it) }
             }
         val fileSharingIntent =
             FileSender.buildSendIntent(userContextProvider.userContext, uris)
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
index c3de067..5028c2e 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
@@ -43,6 +43,7 @@
 import com.android.systemui.dagger.qualifiers.LongRunning;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.mediaprojection.MediaProjectionCaptureTarget;
+import com.android.systemui.recordissue.ScreenRecordingStartTimeStore;
 import com.android.systemui.res.R;
 import com.android.systemui.screenrecord.ScreenMediaRecorder.ScreenMediaRecorderListener;
 import com.android.systemui.settings.UserContextProvider;
@@ -92,6 +93,7 @@
     private boolean mShowTaps;
     private boolean mOriginalShowTaps;
     private ScreenMediaRecorder mRecorder;
+    private final ScreenRecordingStartTimeStore mScreenRecordingStartTimeStore;
     private final Executor mLongExecutor;
     private final UiEventLogger mUiEventLogger;
     protected final NotificationManager mNotificationManager;
@@ -103,7 +105,8 @@
     public RecordingService(RecordingController controller, @LongRunning Executor executor,
             @Main Handler handler, UiEventLogger uiEventLogger,
             NotificationManager notificationManager,
-            UserContextProvider userContextTracker, KeyguardDismissUtil keyguardDismissUtil) {
+            UserContextProvider userContextTracker, KeyguardDismissUtil keyguardDismissUtil,
+            ScreenRecordingStartTimeStore screenRecordingStartTimeStore) {
         mController = controller;
         mLongExecutor = executor;
         mMainHandler = handler;
@@ -111,6 +114,7 @@
         mNotificationManager = notificationManager;
         mUserContextTracker = userContextTracker;
         mKeyguardDismissUtil = keyguardDismissUtil;
+        mScreenRecordingStartTimeStore = screenRecordingStartTimeStore;
     }
 
     /**
@@ -178,7 +182,8 @@
                         currentUid,
                         mAudioSource,
                         captureTarget,
-                        this
+                        this,
+                        mScreenRecordingStartTimeStore
                 );
 
                 if (startRecording()) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
index e024710..54da1b0 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
@@ -55,6 +55,7 @@
 
 import com.android.internal.R;
 import com.android.systemui.mediaprojection.MediaProjectionCaptureTarget;
+import com.android.systemui.recordissue.ScreenRecordingStartTimeStore;
 
 import java.io.Closeable;
 import java.io.File;
@@ -91,6 +92,7 @@
     private ScreenInternalAudioRecorder mAudio;
     private ScreenRecordingAudioSource mAudioSource;
     private final MediaProjectionCaptureTarget mCaptureRegion;
+    private final ScreenRecordingStartTimeStore mScreenRecordingStartTimeStore;
     private final Handler mHandler;
 
     private Context mContext;
@@ -99,13 +101,15 @@
     public ScreenMediaRecorder(Context context, Handler handler,
             int uid, ScreenRecordingAudioSource audioSource,
             MediaProjectionCaptureTarget captureRegion,
-            ScreenMediaRecorderListener listener) {
+            ScreenMediaRecorderListener listener,
+            ScreenRecordingStartTimeStore screenRecordingStartTimeStore) {
         mContext = context;
         mHandler = handler;
         mUid = uid;
         mCaptureRegion = captureRegion;
         mListener = listener;
         mAudioSource = audioSource;
+        mScreenRecordingStartTimeStore = screenRecordingStartTimeStore;
     }
 
     private void prepare() throws IOException, RemoteException, RuntimeException {
@@ -278,6 +282,7 @@
         Log.d(TAG, "start recording");
         prepare();
         mMediaRecorder.start();
+        mScreenRecordingStartTimeStore.markStartTime();
         recordInternalAudio();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAware.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAware.kt
new file mode 100644
index 0000000..111d335
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAware.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.shade
+
+import javax.inject.Qualifier
+
+/**
+ * Qualifies classes that provide display-specific info for shade window components.
+ *
+ * The Shade window can be moved between displays with different characteristics (e.g., density,
+ * size). This annotation ensures that components within the shade window use the correct context
+ * and resources for the display they are currently on.
+ *
+ * Classes annotated with `@ShadeDisplayAware` (e.g., 'Context`, `Resources`, `LayoutInflater`,
+ * `ConfigurationController`) will be dynamically updated to reflect the current display's
+ * configuration. This ensures consistent rendering even when the shade window is moved to an
+ * external display.
+ */
+@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class ShadeDisplayAware
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
index 0902c39..a1c3692 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
@@ -24,7 +24,6 @@
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.shade.data.repository.ShadeRepository
-import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.currentCoroutineContext
@@ -45,7 +44,6 @@
 constructor(
     @Application val scope: CoroutineScope,
     keyguardRepository: KeyguardRepository,
-    sharedNotificationContainerInteractor: SharedNotificationContainerInteractor,
     repository: ShadeRepository,
 ) : BaseShadeInteractor {
     init {
@@ -62,17 +60,17 @@
                 keyguardRepository.statusBarState,
                 repository.legacyShadeExpansion,
                 repository.qsExpansion,
-                sharedNotificationContainerInteractor.isSplitShadeEnabled,
+                repository.isShadeLayoutWide,
             ) {
                 lockscreenShadeExpansion,
                 statusBarState,
                 legacyShadeExpansion,
                 qsExpansion,
-                splitShadeEnabled ->
+                isShadeLayoutWide ->
                 when (statusBarState) {
                     // legacyShadeExpansion is 1 instead of 0 when QS is expanded
                     StatusBarState.SHADE ->
-                        if (!splitShadeEnabled && qsExpansion > 0f) 1f - qsExpansion
+                        if (!isShadeLayoutWide && qsExpansion > 0f) 1f - qsExpansion
                         else legacyShadeExpansion
                     StatusBarState.KEYGUARD -> lockscreenShadeExpansion
                     // dragDownAmount, which drives lockscreenShadeExpansion resets to 0f when
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index f99d8f1..520cbf9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -214,7 +214,7 @@
     private boolean mEnableBatteryDefender;
     private boolean mIncompatibleCharger;
     private int mChargingWattage;
-    private int mBatteryLevel;
+    private int mBatteryLevel = -1;
     private boolean mBatteryPresent = true;
     protected long mChargingTimeRemaining;
     private Pair<String, BiometricSourceType> mBiometricErrorMessageToShowOnScreenOn;
@@ -1032,12 +1032,16 @@
             } else if (!TextUtils.isEmpty(mTransientIndication)) {
                 newIndication = mTransientIndication;
             } else if (!mBatteryPresent) {
-                // If there is no battery detected, hide the indication and bail
+                // If there is no battery detected, hide the indication area and bail
                 mIndicationArea.setVisibility(GONE);
                 return;
             } else if (!TextUtils.isEmpty(mAlignmentIndication)) {
                 useMisalignmentColor = true;
                 newIndication = mAlignmentIndication;
+            } else if (mBatteryLevel == -1) {
+                // If the battery level is not initialized, hide the indication area
+                mIndicationArea.setVisibility(GONE);
+                return;
             } else if (mPowerPluggedIn || mEnableBatteryDefender) {
                 newIndication = computePowerIndication();
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
index 2930de2..0eef8d6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
@@ -44,8 +44,12 @@
 import com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE
 import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN
 import com.android.systemui.util.leak.RotationUtils.Rotation
+import dagger.Module
+import dagger.Provides
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
 import java.util.concurrent.Executor
-import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 
@@ -63,26 +67,57 @@
  * NOTE: any operation that modifies views directly must run on the provided executor, because these
  * views are owned by ScreenDecorations and it runs in its own thread
  */
-@SysUISingleton
-open class PrivacyDotViewController
-@Inject
+interface PrivacyDotViewController {
+
+    // Only can be modified on @UiThread
+    var currentViewState: ViewState
+
+    var showingListener: ShowingListener?
+
+    fun setUiExecutor(e: DelayableExecutor)
+
+    fun getUiExecutor(): DelayableExecutor?
+
+    @UiThread fun setNewRotation(rot: Int)
+
+    @UiThread fun hideDotView(dot: View, animate: Boolean)
+
+    @UiThread fun showDotView(dot: View, animate: Boolean)
+
+    // Update the gravity and margins of the privacy views
+    @UiThread fun updateRotations(rotation: Int, paddingTop: Int)
+
+    @UiThread fun setCornerSizes(state: ViewState)
+
+    fun initialize(topLeft: View, topRight: View, bottomLeft: View, bottomRight: View)
+
+    @UiThread fun updateDotView(state: ViewState)
+
+    interface ShowingListener {
+        fun onPrivacyDotShown(v: View?)
+
+        fun onPrivacyDotHidden(v: View?)
+    }
+}
+
+open class PrivacyDotViewControllerImpl
+@AssistedInject
 constructor(
     @Main private val mainExecutor: Executor,
-    @Application scope: CoroutineScope,
+    @Assisted scope: CoroutineScope,
     private val stateController: StatusBarStateController,
-    private val configurationController: ConfigurationController,
-    private val contentInsetsProvider: StatusBarContentInsetsProvider,
+    @Assisted private val configurationController: ConfigurationController,
+    @Assisted private val contentInsetsProvider: StatusBarContentInsetsProvider,
     private val animationScheduler: SystemStatusAnimationScheduler,
-    shadeInteractor: ShadeInteractor?
-) {
+    shadeInteractor: ShadeInteractor?,
+) : PrivacyDotViewController {
     private lateinit var tl: View
     private lateinit var tr: View
     private lateinit var bl: View
     private lateinit var br: View
 
     // Only can be modified on @UiThread
-    var currentViewState: ViewState = ViewState()
-        get() = field
+    override var currentViewState: ViewState = ViewState()
 
     @GuardedBy("lock")
     private var nextViewState: ViewState = currentViewState.copy()
@@ -100,11 +135,7 @@
     private val views: Sequence<View>
         get() = if (!this::tl.isInitialized) sequenceOf() else sequenceOf(tl, tr, br, bl)
 
-    var showingListener: ShowingListener? = null
-        set(value) {
-            field = value
-        }
-        get() = field
+    override var showingListener: PrivacyDotViewController.ShowingListener? = null
 
     init {
         contentInsetsProvider.addCallback(
@@ -153,16 +184,16 @@
         }
     }
 
-    fun setUiExecutor(e: DelayableExecutor) {
+    override fun setUiExecutor(e: DelayableExecutor) {
         uiExecutor = e
     }
 
-    fun getUiExecutor(): DelayableExecutor? {
+    override fun getUiExecutor(): DelayableExecutor? {
         return uiExecutor
     }
 
     @UiThread
-    fun setNewRotation(rot: Int) {
+    override fun setNewRotation(rot: Int) {
         dlog("updateRotation: $rot")
 
         val isRtl: Boolean
@@ -187,13 +218,13 @@
                     rotation = rot,
                     paddingTop = paddingTop,
                     designatedCorner = newCorner,
-                    cornerIndex = index
+                    cornerIndex = index,
                 )
         }
     }
 
     @UiThread
-    fun hideDotView(dot: View, animate: Boolean) {
+    override fun hideDotView(dot: View, animate: Boolean) {
         dot.clearAnimation()
         if (animate) {
             dot.animate()
@@ -212,7 +243,7 @@
     }
 
     @UiThread
-    fun showDotView(dot: View, animate: Boolean) {
+    override fun showDotView(dot: View, animate: Boolean) {
         dot.clearAnimation()
         if (animate) {
             dot.visibility = View.VISIBLE
@@ -229,9 +260,8 @@
         showingListener?.onPrivacyDotShown(dot)
     }
 
-    // Update the gravity and margins of the privacy views
     @UiThread
-    open fun updateRotations(rotation: Int, paddingTop: Int) {
+    override fun updateRotations(rotation: Int, paddingTop: Int) {
         // To keep a view in the corner, its gravity is always the description of its current corner
         // Therefore, just figure out which view is in which corner. This turns out to be something
         // like (myCorner - rot) mod 4, where topLeft = 0, topRight = 1, etc. and portrait = 0, and
@@ -262,7 +292,7 @@
     }
 
     @UiThread
-    open fun setCornerSizes(state: ViewState) {
+    override fun setCornerSizes(state: ViewState) {
         // StatusBarContentInsetsProvider can tell us the location of the privacy indicator dot
         // in every rotation. The only thing we need to check is rtl
         val rtl = state.layoutRtl
@@ -415,7 +445,7 @@
         }
     }
 
-    fun initialize(topLeft: View, topRight: View, bottomLeft: View, bottomRight: View) {
+    override fun initialize(topLeft: View, topRight: View, bottomLeft: View, bottomRight: View) {
         if (
             this::tl.isInitialized &&
                 this::tr.isInitialized &&
@@ -457,7 +487,7 @@
                     landscapeRect = right,
                     upsideDownRect = bottom,
                     paddingTop = paddingTop,
-                    layoutRtl = rtl
+                    layoutRtl = rtl,
                 )
         }
     }
@@ -533,7 +563,7 @@
     }
 
     @UiThread
-    open fun updateDotView(state: ViewState) {
+    override fun updateDotView(state: ViewState) {
         val shouldShow = state.shouldShowDot()
         if (shouldShow != currentViewState.shouldShowDot()) {
             if (shouldShow && state.designatedCorner != null) {
@@ -553,7 +583,7 @@
                     nextViewState =
                         nextViewState.copy(
                             systemPrivacyEventIsActive = true,
-                            contentDescription = contentDescr
+                            contentDescription = contentDescr,
                         )
                 }
 
@@ -595,15 +625,18 @@
                     seascapeRect = rects[0],
                     portraitRect = rects[1],
                     landscapeRect = rects[2],
-                    upsideDownRect = rects[3]
+                    upsideDownRect = rects[3],
                 )
         }
     }
 
-    interface ShowingListener {
-        fun onPrivacyDotShown(v: View?)
-
-        fun onPrivacyDotHidden(v: View?)
+    @AssistedFactory
+    interface Factory {
+        fun create(
+            scope: CoroutineScope,
+            configurationController: ConfigurationController,
+            contentInsetsProvider: StatusBarContentInsetsProvider,
+        ): PrivacyDotViewControllerImpl
     }
 }
 
@@ -662,7 +695,7 @@
     val paddingTop: Int = 0,
     val cornerIndex: Int = -1,
     val designatedCorner: View? = null,
-    val contentDescription: String? = null
+    val contentDescription: String? = null,
 ) {
     fun shouldShowDot(): Boolean {
         return systemPrivacyEventIsActive && !shadeExpanded && !qsExpanded
@@ -687,3 +720,18 @@
         }
     }
 }
+
+@Module
+object PrivacyDotViewControllerModule {
+
+    @Provides
+    @SysUISingleton
+    fun controller(
+        factory: PrivacyDotViewControllerImpl.Factory,
+        @Application scope: CoroutineScope,
+        configurationController: ConfigurationController,
+        contentInsetsProvider: StatusBarContentInsetsProvider,
+    ): PrivacyDotViewController {
+        return factory.create(scope, configurationController, contentInsetsProvider)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
index c6f3d7d..564d52a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
@@ -16,17 +16,19 @@
 
 package com.android.systemui.statusbar.events
 
-import android.annotation.IntDef
 import androidx.core.animation.Animator
 import androidx.core.animation.AnimatorSet
 import androidx.core.animation.PathInterpolator
 import com.android.systemui.Dumpable
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState
 import com.android.systemui.statusbar.policy.CallbackController
+import kotlinx.coroutines.flow.StateFlow
 
 interface SystemStatusAnimationScheduler :
     CallbackController<SystemStatusAnimationCallback>, Dumpable {
 
-    @SystemAnimationState fun getAnimationState(): Int
+    /** StateFlow holding the current [SystemEventAnimationState] at any time. */
+    val animationState: StateFlow<SystemEventAnimationState>
 
     fun onStatusEvent(event: StatusEvent)
 
@@ -63,34 +65,6 @@
     }
 }
 
-/** Animation state IntDef */
-@Retention(AnnotationRetention.SOURCE)
-@IntDef(
-    value =
-        [
-            IDLE,
-            ANIMATION_QUEUED,
-            ANIMATING_IN,
-            RUNNING_CHIP_ANIM,
-            ANIMATING_OUT,
-            SHOWING_PERSISTENT_DOT,
-        ]
-)
-annotation class SystemAnimationState
-
-/** No animation is in progress */
-@SystemAnimationState const val IDLE = 0
-/** An animation is queued, and awaiting the debounce period */
-const val ANIMATION_QUEUED = 1
-/** System is animating out, and chip is animating in */
-const val ANIMATING_IN = 2
-/** Chip has animated in and is awaiting exit animation, and optionally playing its own animation */
-const val RUNNING_CHIP_ANIM = 3
-/** Chip is animating away and system is animating back */
-const val ANIMATING_OUT = 4
-/** Chip has animated away, and the persistent dot is showing */
-const val SHOWING_PERSISTENT_DOT = 5
-
 /** Commonly-needed interpolators can go here */
 @JvmField val STATUS_BAR_X_MOVE_OUT = PathInterpolator(0.33f, 0f, 0f, 1f)
 @JvmField val STATUS_BAR_X_MOVE_IN = PathInterpolator(0f, 0f, 0f, 1f)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt
index 5f9e426..5ff4423 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt
@@ -23,6 +23,12 @@
 import androidx.core.animation.AnimatorSet
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingIn
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingOut
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimationQueued
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.RunningChipAnim
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.ShowingPersistentDot
 import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
 import com.android.systemui.util.Assert
 import com.android.systemui.util.time.SystemClock
@@ -33,6 +39,7 @@
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.debounce
 import kotlinx.coroutines.flow.first
@@ -85,8 +92,8 @@
      */
     private var currentlyDisplayedEvent: StatusEvent? = null
 
-    /** StateFlow holding the current [SystemAnimationState] at any time. */
-    private var animationState = MutableStateFlow(IDLE)
+    private val _animationState = MutableStateFlow(Idle)
+    override val animationState = _animationState.asStateFlow()
 
     /** True if the persistent privacy dot should be active */
     var hasPersistentDot = false
@@ -109,24 +116,22 @@
             // Wait for animationState to become ANIMATION_QUEUED and scheduledEvent to be non null.
             // Once this combination is stable for at least DEBOUNCE_DELAY, then start a chip enter
             // animation
-            animationState
+            _animationState
                 .combine(scheduledEvent) { animationState, scheduledEvent ->
                     Pair(animationState, scheduledEvent)
                 }
                 .debounce(DEBOUNCE_DELAY)
                 .collect { (animationState, event) ->
-                    if (animationState == ANIMATION_QUEUED && event != null) {
+                    if (animationState == AnimationQueued && event != null) {
                         startAnimationLifecycle(event)
                         scheduledEvent.value = null
                     }
                 }
         }
 
-        coroutineScope.launch { animationState.collect { logger?.logAnimationStateUpdate(it) } }
+        coroutineScope.launch { _animationState.collect { logger?.logAnimationStateUpdate(it) } }
     }
 
-    @SystemAnimationState override fun getAnimationState(): Int = animationState.value
-
     override fun onStatusEvent(event: StatusEvent) {
         Assert.isMainThread()
 
@@ -146,11 +151,11 @@
             logger?.logScheduleEvent(event)
             scheduleEvent(event)
         } else if (currentlyDisplayedEvent?.shouldUpdateFromEvent(event) == true) {
-            logger?.logUpdateEvent(event, animationState.value)
+            logger?.logUpdateEvent(event, _animationState.value)
             currentlyDisplayedEvent?.updateFromEvent(event)
             if (event.forceVisible) hasPersistentDot = true
         } else if (scheduledEvent.value?.shouldUpdateFromEvent(event) == true) {
-            logger?.logUpdateEvent(event, animationState.value)
+            logger?.logUpdateEvent(event, _animationState.value)
             scheduledEvent.value?.updateFromEvent(event)
         } else {
             logger?.logIgnoreEvent(event)
@@ -170,15 +175,15 @@
         // the disappear animation will not animate into a dot but remove the chip entirely
         hasPersistentDot = false
 
-        if (animationState.value == SHOWING_PERSISTENT_DOT) {
+        if (_animationState.value == ShowingPersistentDot) {
             // if we are currently showing a persistent dot, hide it and update the animationState
             notifyHidePersistentDot()
             if (scheduledEvent.value != null) {
-                animationState.value = ANIMATION_QUEUED
+                _animationState.value = AnimationQueued
             } else {
-                animationState.value = IDLE
+                _animationState.value = Idle
             }
-        } else if (animationState.value == ANIMATING_OUT) {
+        } else if (_animationState.value == AnimatingOut) {
             // if we are currently animating out, hide the dot. The animationState will be updated
             // once the animation has ended in the onAnimationEnd callback
             notifyHidePersistentDot()
@@ -206,9 +211,9 @@
             cancelCurrentlyDisplayedEvent()
             return
         }
-        if (animationState.value == IDLE) {
+        if (_animationState.value == Idle) {
             // If we are in IDLE state, set it to ANIMATION_QUEUED now
-            animationState.value = ANIMATION_QUEUED
+            _animationState.value = AnimationQueued
         }
     }
 
@@ -223,7 +228,7 @@
                 withTimeout(APPEAR_ANIMATION_DURATION) {
                     // wait for animationState to become RUNNING_CHIP_ANIM, then cancel the running
                     // animation job and run the disappear animation immediately
-                    animationState.first { it == RUNNING_CHIP_ANIM }
+                    _animationState.first { it == RunningChipAnim }
                     currentlyRunningAnimationJob?.cancel()
                     runChipDisappearAnimation()
                 }
@@ -241,7 +246,7 @@
 
         if (!event.showAnimation && event.forceVisible) {
             // If animations are turned off, we'll transition directly to the dot
-            animationState.value = SHOWING_PERSISTENT_DOT
+            _animationState.value = ShowingPersistentDot
             notifyTransitionToPersistentDot(event)
             return
         }
@@ -277,7 +282,7 @@
         if (hasPersistentDot) {
             statusBarWindowControllerStore.defaultDisplay.setForceStatusBarVisible(true)
         }
-        animationState.value = ANIMATING_IN
+        _animationState.value = AnimatingIn
 
         val animSet = collectStartAnimations()
         if (animSet.totalDuration > 500) {
@@ -289,7 +294,7 @@
         animSet.addListener(
             object : AnimatorListenerAdapter() {
                 override fun onAnimationEnd(animation: Animator) {
-                    animationState.value = RUNNING_CHIP_ANIM
+                    _animationState.value = RunningChipAnim
                 }
             }
         )
@@ -299,15 +304,15 @@
     private fun runChipDisappearAnimation() {
         Assert.isMainThread()
         val animSet2 = collectFinishAnimations()
-        animationState.value = ANIMATING_OUT
+        _animationState.value = AnimatingOut
         animSet2.addListener(
             object : AnimatorListenerAdapter() {
                 override fun onAnimationEnd(animation: Animator) {
-                    animationState.value =
+                    _animationState.value =
                         when {
-                            hasPersistentDot -> SHOWING_PERSISTENT_DOT
-                            scheduledEvent.value != null -> ANIMATION_QUEUED
-                            else -> IDLE
+                            hasPersistentDot -> ShowingPersistentDot
+                            scheduledEvent.value != null -> AnimationQueued
+                            else -> Idle
                         }
                     statusBarWindowControllerStore.defaultDisplay.setForceStatusBarVisible(false)
                 }
@@ -401,7 +406,7 @@
         pw.println("Scheduled event: ${scheduledEvent.value}")
         pw.println("Currently displayed event: $currentlyDisplayedEvent")
         pw.println("Has persistent privacy dot: $hasPersistentDot")
-        pw.println("Animation state: ${animationState.value}")
+        pw.println("Animation state: ${_animationState.value}")
         pw.println("Listeners:")
         if (listeners.isEmpty()) {
             pw.println("(none)")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerLogger.kt
index 22b0b69..a1f7a9b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerLogger.kt
@@ -3,15 +3,14 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState
 import javax.inject.Inject
 
 /** Logs for the SystemStatusAnimationScheduler. */
 @SysUISingleton
 class SystemStatusAnimationSchedulerLogger
 @Inject
-constructor(
-    @SystemStatusAnimationSchedulerLog private val logBuffer: LogBuffer,
-) {
+constructor(@SystemStatusAnimationSchedulerLog private val logBuffer: LogBuffer) {
 
     fun logScheduleEvent(event: StatusEvent) {
         logBuffer.log(
@@ -23,11 +22,11 @@
                 bool1 = event.forceVisible
                 bool2 = event.showAnimation
             },
-            { "Scheduling event: $str1(forceVisible=$bool1, priority=$int1, showAnimation=$bool2)" }
+            { "Scheduling event: $str1(forceVisible=$bool1, priority=$int1, showAnimation=$bool2)" },
         )
     }
 
-    fun logUpdateEvent(event: StatusEvent, @SystemAnimationState animationState: Int) {
+    fun logUpdateEvent(event: StatusEvent, animationState: SystemEventAnimationState) {
         logBuffer.log(
             TAG,
             LogLevel.DEBUG,
@@ -36,12 +35,12 @@
                 int1 = event.priority
                 bool1 = event.forceVisible
                 bool2 = event.showAnimation
-                int2 = animationState
+                str2 = animationState.name
             },
             {
                 "Updating current event from: $str1(forceVisible=$bool1, priority=$int1, " +
-                    "showAnimation=$bool2), animationState=${animationState.name()}"
-            }
+                    "showAnimation=$bool2), animationState=$str2"
+            },
         )
     }
 
@@ -55,7 +54,7 @@
                 bool1 = event.forceVisible
                 bool2 = event.showAnimation
             },
-            { "Ignore event: $str1(forceVisible=$bool1, priority=$int1, showAnimation=$bool2)" }
+            { "Ignore event: $str1(forceVisible=$bool1, priority=$int1, showAnimation=$bool2)" },
         )
     }
 
@@ -67,26 +66,14 @@
         logBuffer.log(TAG, LogLevel.DEBUG, "Transition to persistent dot callback invoked")
     }
 
-    fun logAnimationStateUpdate(@SystemAnimationState animationState: Int) {
+    fun logAnimationStateUpdate(animationState: SystemEventAnimationState) {
         logBuffer.log(
             TAG,
             LogLevel.DEBUG,
-            { int1 = animationState },
-            { "AnimationState update: ${int1.name()}" }
+            { str1 = animationState.name },
+            { "AnimationState update: $str1" },
         )
-        animationState.name()
     }
-
-    private fun @receiver:SystemAnimationState Int.name() =
-        when (this) {
-            IDLE -> "IDLE"
-            ANIMATION_QUEUED -> "ANIMATION_QUEUED"
-            ANIMATING_IN -> "ANIMATING_IN"
-            RUNNING_CHIP_ANIM -> "RUNNING_CHIP_ANIM"
-            ANIMATING_OUT -> "ANIMATING_OUT"
-            SHOWING_PERSISTENT_DOT -> "SHOWING_PERSISTENT_DOT"
-            else -> "UNKNOWN_ANIMATION_STATE"
-        }
 }
 
 private const val TAG = "SystemStatusAnimationSchedulerLog"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/shared/model/SystemEventAnimationState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/shared/model/SystemEventAnimationState.kt
new file mode 100644
index 0000000..2446b81
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/shared/model/SystemEventAnimationState.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.events.shared.model
+
+/** Direct representation of the system event animation scheduler's current state */
+enum class SystemEventAnimationState {
+    /** No animation is in progress */
+    Idle,
+    /** An animation is queued, and awaiting the debounce period */
+    AnimationQueued,
+    /** System is animating out, and chip is animating in */
+    AnimatingIn,
+    /**
+     * Chip has animated in and is awaiting exit animation, and optionally playing its own animation
+     */
+    RunningChipAnim,
+    /** Chip is animating away and system is animating back */
+    AnimatingOut,
+    /** Chip has animated away, and the persistent dot is showing */
+    ShowingPersistentDot,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt
index 231a0b0..9fe4a54 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt
@@ -32,13 +32,13 @@
 interface NotificationActivityStarter {
 
     /** Called when the user clicks on the notification bubble icon. */
-    fun onNotificationBubbleIconClicked(entry: NotificationEntry?)
+    fun onNotificationBubbleIconClicked(entry: NotificationEntry)
 
     /** Called when the user clicks on the surface of a notification. */
-    fun onNotificationClicked(entry: NotificationEntry?, row: ExpandableNotificationRow?)
+    fun onNotificationClicked(entry: NotificationEntry, row: ExpandableNotificationRow)
 
     /** Called when the user clicks on a button in the notification guts which fires an intent. */
-    fun startNotificationGutsIntent(intent: Intent?, appUid: Int, row: ExpandableNotificationRow?)
+    fun startNotificationGutsIntent(intent: Intent, appUid: Int, row: ExpandableNotificationRow)
 
     /**
      * Called when the user clicks "Manage" or "History" in the Shade. Prefer using
@@ -56,7 +56,7 @@
     fun startSettingsIntent(view: View, intentInfo: SettingsIntent)
 
     /** Called when the user succeed to drop notification to proper target view. */
-    fun onDragSuccess(entry: NotificationEntry?)
+    fun onDragSuccess(entry: NotificationEntry)
 
     val isCollapsingToShowActivityOverLockscreen: Boolean
         get() = false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt
index 2ee1dffd..9580016 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt
@@ -35,6 +35,9 @@
  * This cache is safe for multithreaded usage, and is recommended for objects that take a while to
  * resolve (such as drawables, or things that require binder calls). As such, [getOrFetch] is
  * recommended to be run on a background thread, while [purge] can be done from any thread.
+ *
+ * Important: This cache does NOT have a maximum size, cleaning it up (via [purge]) is the
+ * responsibility of the caller, to avoid keeping things in memory unnecessarily.
  */
 @SuppressLint("DumpableNotRegistered") // this will be dumped by container classes
 class NotifCollectionCache<V>(
@@ -151,7 +154,7 @@
      * purge((c));    // deletes a from the cache and marks b for deletion, etc.
      * ```
      */
-    fun purge(wantedKeys: List<String>) {
+    fun purge(wantedKeys: Collection<String>) {
         for ((key, entry) in cache) {
             if (key in wantedKeys) {
                 entry.resetLives()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
index 9b075a6..f75163d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
@@ -27,6 +27,7 @@
 import android.service.notification.StatusBarNotification;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -49,13 +50,18 @@
 import com.android.systemui.statusbar.notification.collection.render.NotifViewController;
 import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager;
 import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager.NotifInflationErrorListener;
+import com.android.systemui.statusbar.notification.row.icon.AppIconProvider;
+import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider;
 import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation;
 import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Collection;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import javax.inject.Inject;
 
@@ -104,6 +110,8 @@
     /** How long we can delay a group while waiting for all children to inflate */
     private final long mMaxGroupInflationDelay;
     private final BindEventManagerImpl mBindEventManager;
+    private final AppIconProvider mAppIconProvider;
+    private final NotificationIconStyleProvider mNotificationIconStyleProvider;
 
     @Inject
     public PreparationCoordinator(
@@ -113,7 +121,9 @@
             NotifViewBarn viewBarn,
             NotifUiAdjustmentProvider adjustmentProvider,
             IStatusBarService service,
-            BindEventManagerImpl bindEventManager) {
+            BindEventManagerImpl bindEventManager,
+            AppIconProvider appIconProvider,
+            NotificationIconStyleProvider notificationIconStyleProvider) {
         this(
                 logger,
                 notifInflater,
@@ -122,6 +132,8 @@
                 adjustmentProvider,
                 service,
                 bindEventManager,
+                appIconProvider,
+                notificationIconStyleProvider,
                 CHILD_BIND_CUTOFF,
                 MAX_GROUP_INFLATION_DELAY);
     }
@@ -135,6 +147,8 @@
             NotifUiAdjustmentProvider adjustmentProvider,
             IStatusBarService service,
             BindEventManagerImpl bindEventManager,
+            AppIconProvider appIconProvider,
+            NotificationIconStyleProvider notificationIconStyleProvider,
             int childBindCutoff,
             long maxGroupInflationDelay) {
         mLogger = logger;
@@ -146,6 +160,8 @@
         mChildBindCutoff = childBindCutoff;
         mMaxGroupInflationDelay = maxGroupInflationDelay;
         mBindEventManager = bindEventManager;
+        mAppIconProvider = appIconProvider;
+        mNotificationIconStyleProvider = notificationIconStyleProvider;
     }
 
     @Override
@@ -155,6 +171,9 @@
                 () -> mNotifInflatingFilter.invalidateList("adjustmentProviderChanged"));
 
         pipeline.addCollectionListener(mNotifCollectionListener);
+        if (android.app.Flags.notificationsRedesignAppIcons()) {
+            pipeline.addOnBeforeTransformGroupsListener(this::purgeCaches);
+        }
         // Inflate after grouping/sorting since that affects what views to inflate.
         pipeline.addOnBeforeFinalizeFilterListener(this::inflateAllRequiredViews);
         pipeline.addFinalizeFilter(mNotifInflationErrorFilter);
@@ -260,6 +279,29 @@
         }
     };
 
+    private void purgeCaches(Collection<ListEntry> entries) {
+        Set<String> wantedPackages = getPackages(entries);
+        mAppIconProvider.purgeCache(wantedPackages);
+        mNotificationIconStyleProvider.purgeCache(wantedPackages);
+    }
+
+    /**
+     * Get all app packages present in {@param entries}.
+     */
+    private static @NonNull Set<String> getPackages(Collection<ListEntry> entries) {
+        Set<String> packages = new HashSet<>();
+        for (ListEntry entry : entries) {
+            NotificationEntry notificationEntry = entry.getRepresentativeEntry();
+            if (notificationEntry == null) {
+                Log.wtf(TAG, "notification entry " + entry.getKey()
+                        + " has no representative entry");
+                continue;
+            }
+            packages.add(notificationEntry.getSbn().getPackageName());
+        }
+        return packages;
+    }
+
     private void inflateAllRequiredViews(List<ListEntry> entries) {
         for (int i = 0, size = entries.size(); i < size; i++) {
             ListEntry entry = entries.get(i);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/AppIconProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/AppIconProvider.kt
index 24b5cf1a..0ddf9f72 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/AppIconProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/AppIconProvider.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification.row.icon
 
+import android.annotation.WorkerThread
 import android.app.ActivityManager
 import android.app.Flags
 import android.content.Context
@@ -27,20 +28,45 @@
 import android.util.Log
 import com.android.internal.R
 import com.android.launcher3.icons.BaseIconFactory
+import com.android.systemui.Dumpable
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.notification.collection.NotifCollectionCache
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.withIncreasedIndent
 import dagger.Module
 import dagger.Provides
+import java.io.PrintWriter
 import javax.inject.Inject
 import javax.inject.Provider
 
 /** A provider used to cache and fetch app icons used by notifications. */
 interface AppIconProvider {
+    /**
+     * Loads the icon corresponding to [packageName] into cache, or fetches it from there if already
+     * present. This should only be called from the background.
+     */
     @Throws(NameNotFoundException::class)
+    @WorkerThread
     fun getOrFetchAppIcon(packageName: String, context: Context): Drawable
+
+    /**
+     * Mark all the entries in the cache that are NOT in [wantedPackages] to be cleared. If they're
+     * still not needed on the next call of this method (made after a timeout of 1s, in case they
+     * happen more frequently than that), they will be purged. This can be done from any thread.
+     */
+    fun purgeCache(wantedPackages: Collection<String>)
 }
 
 @SysUISingleton
-class AppIconProviderImpl @Inject constructor(private val sysuiContext: Context) : AppIconProvider {
+class AppIconProviderImpl
+@Inject
+constructor(private val sysuiContext: Context, dumpManager: DumpManager) :
+    AppIconProvider, Dumpable {
+    init {
+        dumpManager.registerNormalDumpable(TAG, this)
+    }
+
     private val iconFactory: BaseIconFactory
         get() {
             val isLowRam = ActivityManager.isLowRamDeviceStatic()
@@ -53,13 +79,42 @@
             return BaseIconFactory(sysuiContext, res.configuration.densityDpi, iconSize)
         }
 
+    private val cache = NotifCollectionCache<Drawable>()
+
     override fun getOrFetchAppIcon(packageName: String, context: Context): Drawable {
+        return cache.getOrFetch(packageName) { fetchAppIcon(packageName, context) }
+    }
+
+    @WorkerThread
+    private fun fetchAppIcon(packageName: String, context: Context): BitmapDrawable {
         val icon = context.packageManager.getApplicationIcon(packageName)
         return BitmapDrawable(
             context.resources,
             iconFactory.createScaledBitmap(icon, BaseIconFactory.MODE_HARDWARE),
         )
     }
+
+    override fun purgeCache(wantedPackages: Collection<String>) {
+        cache.purge(wantedPackages)
+    }
+
+    override fun dump(pwOrig: PrintWriter, args: Array<out String>) {
+        val pw = pwOrig.asIndenting()
+
+        pw.println("cache information:")
+        pw.withIncreasedIndent { cache.dump(pw, args) }
+
+        val iconFactory = iconFactory
+        pw.println("icon factory information:")
+        pw.withIncreasedIndent {
+            pw.println("fullResIconDpi = ${iconFactory.fullResIconDpi}")
+            pw.println("iconSize = ${iconFactory.iconBitmapSize}")
+        }
+    }
+
+    companion object {
+        const val TAG = "AppIconProviderImpl"
+    }
 }
 
 class NoOpIconProvider : AppIconProvider {
@@ -71,6 +126,10 @@
         Log.wtf(TAG, "NoOpIconProvider should not be used anywhere.")
         return ColorDrawable(Color.WHITE)
     }
+
+    override fun purgeCache(wantedPackages: Collection<String>) {
+        Log.wtf(TAG, "NoOpIconProvider should not be used anywhere.")
+    }
 }
 
 @Module
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProvider.kt
index 165c1a7..35e38c2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProvider.kt
@@ -22,9 +22,15 @@
 import android.content.pm.ApplicationInfo
 import android.service.notification.StatusBarNotification
 import android.util.Log
+import com.android.systemui.Dumpable
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.notification.collection.NotifCollectionCache
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.withIncreasedIndent
 import dagger.Module
 import dagger.Provides
+import java.io.PrintWriter
 import javax.inject.Inject
 import javax.inject.Provider
 
@@ -33,15 +39,35 @@
  * notifications.
  */
 interface NotificationIconStyleProvider {
+    /**
+     * Determines whether the [notification] should display the app icon instead of the small icon.
+     * This can result in a binder call, and therefore should only be called from the background.
+     */
     @WorkerThread
     fun shouldShowAppIcon(notification: StatusBarNotification, context: Context): Boolean
+
+    /**
+     * Mark all the entries in the cache that are NOT in [wantedPackages] to be cleared. If they're
+     * still not needed on the next call of this method (made after a timeout of 1s, in case they
+     * happen more frequently than that), they will be purged. This can be done from any thread.
+     */
+    fun purgeCache(wantedPackages: Collection<String>)
 }
 
 @SysUISingleton
-class NotificationIconStyleProviderImpl @Inject constructor() : NotificationIconStyleProvider {
+class NotificationIconStyleProviderImpl @Inject constructor(dumpManager: DumpManager) :
+    NotificationIconStyleProvider, Dumpable {
+    init {
+        dumpManager.registerNormalDumpable(TAG, this)
+    }
+
+    private val cache = NotifCollectionCache<Boolean>()
+
     override fun shouldShowAppIcon(notification: StatusBarNotification, context: Context): Boolean {
         val packageContext = notification.getPackageContext(context)
-        return !belongsToHeadlessSystemApp(packageContext)
+        return cache.getOrFetch(notification.packageName) {
+            !belongsToHeadlessSystemApp(packageContext)
+        }
     }
 
     @WorkerThread
@@ -62,6 +88,20 @@
             return false
         }
     }
+
+    override fun purgeCache(wantedPackages: Collection<String>) {
+        cache.purge(wantedPackages)
+    }
+
+    override fun dump(pwOrig: PrintWriter, args: Array<out String>) {
+        val pw = pwOrig.asIndenting()
+        pw.println("cache information:")
+        pw.withIncreasedIndent { cache.dump(pw, args) }
+    }
+
+    companion object {
+        const val TAG = "NotificationIconStyleProviderImpl"
+    }
 }
 
 class NoOpIconStyleProvider : NotificationIconStyleProvider {
@@ -73,6 +113,10 @@
         Log.wtf(TAG, "NoOpIconStyleProvider should not be used anywhere.")
         return true
     }
+
+    override fun purgeCache(wantedPackages: Collection<String>) {
+        Log.wtf(TAG, "NoOpIconStyleProvider should not be used anywhere.")
+    }
 }
 
 @Module
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
index 5d37476..6042964 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
@@ -18,7 +18,7 @@
 package com.android.systemui.statusbar.notification.stack.domain.interactor
 
 import android.content.Context
-import com.android.systemui.common.ui.data.repository.ConfigurationRepository
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -29,6 +29,8 @@
 import com.android.systemui.statusbar.policy.SplitShadeStateController
 import dagger.Lazy
 import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.FlowPreview
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
@@ -36,17 +38,17 @@
 import kotlinx.coroutines.flow.debounce
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onStart
 
 /** Encapsulates business-logic specifically related to the shared notification stack container. */
+@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
 @SysUISingleton
 class SharedNotificationContainerInteractor
 @Inject
 constructor(
-    configurationRepository: ConfigurationRepository,
     private val context: Context,
     private val splitShadeStateController: Lazy<SplitShadeStateController>,
     private val shadeInteractor: Lazy<ShadeInteractor>,
+    configurationInteractor: ConfigurationInteractor,
     keyguardInteractor: KeyguardInteractor,
     deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
     largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>,
@@ -59,9 +61,6 @@
     /** An internal modification was made to notifications */
     val notificationStackChanged = _notificationStackChanged.debounce(20L)
 
-    private val configurationChangeEvents =
-        configurationRepository.onAnyConfigurationChange.onStart { emit(Unit) }
-
     /* Warning: Even though the value it emits only contains the split shade status, this flow must
      * emit a value whenever the configuration *or* the split shade status changes. Adding a
      * distinctUntilChanged() to this would cause configurationBasedDimensions to miss configuration
@@ -69,13 +68,14 @@
      */
     private val dimensionsUpdateEventsWithShouldUseSplitShade: Flow<Boolean> =
         if (SceneContainerFlag.isEnabled) {
-            combine(configurationChangeEvents, shadeInteractor.get().isShadeLayoutWide) {
-                _,
-                isShadeLayoutWide ->
+            combine(
+                configurationInteractor.onAnyConfigurationChange,
+                shadeInteractor.get().isShadeLayoutWide,
+            ) { _, isShadeLayoutWide ->
                 isShadeLayoutWide
             }
         } else {
-            configurationChangeEvents.map {
+            configurationInteractor.onAnyConfigurationChange.map {
                 splitShadeStateController.get().shouldUseSplitNotificationShade(context.resources)
             }
         }
@@ -115,11 +115,6 @@
             isUdfpsSupported || !ambientIndicationVisible
         }
 
-    val isSplitShadeEnabled: Flow<Boolean> =
-        configurationBasedDimensions
-            .map { dimens: ConfigurationBasedDimensions -> dimens.useSplitShade }
-            .distinctUntilChanged()
-
     /** Top position (without translation) of the shared container. */
     fun setTopPosition(top: Float) {
         _topPosition.value = top
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
index 574ca3f..56b3356 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
@@ -104,7 +104,7 @@
             1f
         } else if (
             change.isTransitioningBetween(Scenes.Gone, Scenes.Shade) ||
-                change.isTransitioning(from = Scenes.Gone, to = Scenes.Lockscreen)
+                change.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen)
         ) {
             shadeExpansion
         } else if (change.isTransitioningBetween(Scenes.Gone, Scenes.QuickSettings)) {
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 878ae91..9515029 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
@@ -484,19 +484,28 @@
     }
 
     fun keyguardAlpha(viewState: ViewStateAccessor, scope: CoroutineScope): Flow<Float> {
+        val isKeyguardOccluded =
+            keyguardTransitionInteractor.transitionValue(OCCLUDED).map { it == 1f }
+
+        val isKeyguardNotVisibleInState =
+            if (SceneContainerFlag.isEnabled) {
+                isKeyguardOccluded
+            } else {
+                anyOf(
+                    isKeyguardOccluded,
+                    keyguardTransitionInteractor
+                        .transitionValue(scene = Scenes.Gone, stateWithoutSceneContainer = GONE)
+                        .map { it == 1f },
+                )
+            }
+
         // Transitions are not (yet) authoritative for NSSL; they still rely on StatusBarState to
         // help determine when the device has fully moved to GONE or OCCLUDED state. Once SHADE
         // state has been set, let shade alpha take over
         val isKeyguardNotVisible =
-            combine(
-                anyOf(
-                    keyguardTransitionInteractor.transitionValue(OCCLUDED).map { it == 1f },
-                    keyguardTransitionInteractor
-                        .transitionValue(scene = Scenes.Gone, stateWithoutSceneContainer = GONE)
-                        .map { it == 1f },
-                ),
-                keyguardInteractor.statusBarState,
-            ) { isKeyguardNotVisibleInState, statusBarState ->
+            combine(isKeyguardNotVisibleInState, keyguardInteractor.statusBarState) {
+                isKeyguardNotVisibleInState,
+                statusBarState ->
                 isKeyguardNotVisibleInState && statusBarState == SHADE
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt
index a8c823c..858cac1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt
@@ -29,9 +29,8 @@
 
 class ConfigurationControllerImpl
 @AssistedInject
-constructor(
-    @Assisted private val context: Context,
-) : ConfigurationController, StatusBarConfigurationController {
+constructor(@Assisted private val context: Context) :
+    ConfigurationController, StatusBarConfigurationController {
 
     private val listeners: MutableList<ConfigurationListener> = ArrayList()
     private val lastConfig = Configuration()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationForwarder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationForwarder.kt
new file mode 100644
index 0000000..3fd46fc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationForwarder.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone
+
+import android.content.res.Configuration
+
+/**
+ * Used to forward a configuration change to other components.
+ *
+ * This is commonly used to propagate configs to [ConfigurationController]. Note that there could be
+ * different configuration forwarder, for example each display, window or group of classes (e.g.
+ * shade window classes).
+ */
+interface ConfigurationForwarder {
+    /** Should be called when a new configuration is received. */
+    fun onConfigurationChanged(newConfiguration: Configuration)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index f7fea7b..92b609e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -56,6 +56,7 @@
 import com.android.keyguard.ViewMediatorCallback;
 import com.android.systemui.DejankUtils;
 import com.android.systemui.Flags;
+import com.android.systemui.animation.back.FlingOnBackAnimationCallback;
 import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor;
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor;
@@ -236,9 +237,10 @@
             }
     };
 
-    private final OnBackAnimationCallback mOnBackInvokedCallback = new OnBackAnimationCallback() {
+    private final OnBackAnimationCallback mOnBackInvokedCallback =
+            new FlingOnBackAnimationCallback() {
         @Override
-        public void onBackInvoked() {
+        public void onBackInvokedCompat() {
             if (DEBUG) {
                 Log.d(TAG, "onBackInvokedCallback() called, invoking onBackPressed()");
             }
@@ -249,21 +251,21 @@
         }
 
         @Override
-        public void onBackProgressed(BackEvent event) {
+        public void onBackProgressedCompat(@NonNull BackEvent event) {
             if (shouldPlayBackAnimation() && mPrimaryBouncerView.getDelegate() != null) {
                 mPrimaryBouncerView.getDelegate().getBackCallback().onBackProgressed(event);
             }
         }
 
         @Override
-        public void onBackCancelled() {
+        public void onBackCancelledCompat() {
             if (shouldPlayBackAnimation() && mPrimaryBouncerView.getDelegate() != null) {
                 mPrimaryBouncerView.getDelegate().getBackCallback().onBackCancelled();
             }
         }
 
         @Override
-        public void onBackStarted(BackEvent event) {
+        public void onBackStartedCompat(@NonNull BackEvent event) {
             if (shouldPlayBackAnimation() && mPrimaryBouncerView.getDelegate() != null) {
                 mPrimaryBouncerView.getDelegate().getBackCallback().onBackStarted(event);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index 93db2db..af98311 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -20,7 +20,6 @@
 import static android.service.notification.NotificationListenerService.REASON_CLICK;
 
 import static com.android.systemui.statusbar.phone.CentralSurfaces.getActivityOptions;
-import static com.android.systemui.util.kotlin.NullabilityKt.expectNotNull;
 
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
@@ -231,8 +230,7 @@
      * @param entry notification that bubble icon was clicked
      */
     @Override
-    public void onNotificationBubbleIconClicked(NotificationEntry entry) {
-        expectNotNull(TAG, "entry", entry);
+    public void onNotificationBubbleIconClicked(@NonNull NotificationEntry entry) {
         Runnable action = () -> {
             mBubblesManagerOptional.ifPresent(bubblesManager ->
                     bubblesManager.onUserChangedBubble(entry, !entry.isBubble()));
@@ -258,9 +256,8 @@
      * @param row   row for that notification
      */
     @Override
-    public void onNotificationClicked(NotificationEntry entry, ExpandableNotificationRow row) {
-        expectNotNull(TAG, "entry", entry);
-        expectNotNull(TAG, "row", row);
+    public void onNotificationClicked(@NonNull NotificationEntry entry,
+            @NonNull ExpandableNotificationRow row) {
         mLogger.logStartingActivityFromClick(entry, row.isHeadsUpState(),
                 mKeyguardStateController.isVisible(),
                 mNotificationShadeWindowController.getPanelExpanded());
@@ -442,8 +439,7 @@
      * @param entry notification entry that is dropped.
      */
     @Override
-    public void onDragSuccess(NotificationEntry entry) {
-        expectNotNull(TAG, "entry", entry);
+    public void onDragSuccess(@NonNull NotificationEntry entry) {
         // this method is not responsible for intent sending.
         // will focus follow operation only after drag-and-drop that notification.
         final NotificationVisibility nv = mVisibilityProvider.obtain(entry, true);
@@ -534,10 +530,8 @@
     }
 
     @Override
-    public void startNotificationGutsIntent(final Intent intent, final int appUid,
-            ExpandableNotificationRow row) {
-        expectNotNull(TAG, "intent", intent);
-        expectNotNull(TAG, "row", row);
+    public void startNotificationGutsIntent(@NonNull final Intent intent, final int appUid,
+            @NonNull ExpandableNotificationRow row) {
         boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */);
         ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() {
             @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt
index 09e191d..92d0ebe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.statusbar.core.StatusBarOrchestrator
 import com.android.systemui.statusbar.core.StatusBarSimpleFragment
 import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore
+import com.android.systemui.statusbar.events.PrivacyDotViewControllerModule
 import com.android.systemui.statusbar.phone.CentralSurfacesCommandQueueCallbacks
 import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
 import com.android.systemui.statusbar.window.data.repository.StatusBarWindowStateRepositoryStore
@@ -45,7 +46,7 @@
 import kotlinx.coroutines.CoroutineScope
 
 /** Similar in purpose to [StatusBarModule], but scoped only to phones */
-@Module
+@Module(includes = [PrivacyDotViewControllerModule::class])
 interface StatusBarPhoneModule {
 
     @Binds
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java
index cec77c1..1bb4e8c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java
@@ -16,16 +16,15 @@
 
 import android.content.res.Configuration;
 
+import com.android.systemui.statusbar.phone.ConfigurationForwarder;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
 
 /**
  * Common listener for configuration or subsets of configuration changes (like density or
  * font scaling), providing easy static dependence on these events.
  */
-public interface ConfigurationController extends CallbackController<ConfigurationListener> {
-
-    /** Alert controller of a change in the configuration. */
-    void onConfigurationChanged(Configuration newConfiguration);
+public interface ConfigurationController extends CallbackController<ConfigurationListener>,
+        ConfigurationForwarder {
 
     /** Alert controller of a change in between light and dark themes. */
     void notifyThemeChanged();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
index b81af86..c7bd5a1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
@@ -37,6 +37,7 @@
 import com.android.systemui.statusbar.connectivity.NetworkControllerImpl;
 import com.android.systemui.statusbar.connectivity.WifiPickerTrackerFactory;
 import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
+import com.android.systemui.statusbar.phone.ConfigurationForwarder;
 import com.android.systemui.statusbar.policy.BatteryControllerLogger;
 import com.android.systemui.statusbar.policy.BluetoothController;
 import com.android.systemui.statusbar.policy.BluetoothControllerImpl;
@@ -186,6 +187,13 @@
             DevicePostureControllerImpl devicePostureControllerImpl);
 
     /** */
+    @Binds
+    @SysUISingleton
+    @GlobalConfig
+    ConfigurationForwarder provideGlobalConfigurationForwarder(
+            @GlobalConfig ConfigurationController configurationController);
+
+    /** */
     @Provides
     @SysUISingleton
     @GlobalConfig
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
index e89a31f..2337ec1 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
@@ -27,7 +27,10 @@
 import com.android.systemui.inputdevice.tutorial.ui.composable.rememberColorFilterProperty
 import com.android.systemui.res.R
 import com.android.systemui.touchpad.tutorial.ui.gesture.BackGestureRecognizer
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureFlowAdapter
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureRecognizer
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
 
 @Composable
 fun BackGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
@@ -41,14 +44,20 @@
                     titleSuccessResId = R.string.touchpad_back_gesture_success_title,
                     bodySuccessResId = R.string.touchpad_back_gesture_success_body,
                 ),
-            animations =
-                TutorialScreenConfig.Animations(
-                    educationResId = R.raw.trackpad_back_edu,
-                    successResId = R.raw.trackpad_back_success,
-                ),
+            animations = TutorialScreenConfig.Animations(educationResId = R.raw.trackpad_back_edu),
         )
     val recognizer = rememberBackGestureRecognizer(LocalContext.current.resources)
-    GestureTutorialScreen(screenConfig, recognizer, onDoneButtonClicked, onBack)
+    val gestureUiState: Flow<GestureUiState> =
+        remember(recognizer) {
+            GestureFlowAdapter(recognizer).gestureStateAsFlow.map {
+                it.toGestureUiState(
+                    progressStartMark = "",
+                    progressEndMark = "",
+                    successAnimation = R.raw.trackpad_back_success,
+                )
+            }
+        }
+    GestureTutorialScreen(screenConfig, recognizer, gestureUiState, onDoneButtonClicked, onBack)
 }
 
 @Composable
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
index 7899f5b..e058527 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.touchpad.tutorial.ui.composable
 
 import androidx.activity.compose.BackHandler
+import androidx.annotation.RawRes
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.layout.Box
@@ -31,23 +32,49 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.pointer.pointerInteropFilter
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.systemui.inputdevice.tutorial.ui.composable.ActionTutorialContent
 import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState
 import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig
+import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.Finished
+import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.NotStarted
 import com.android.systemui.touchpad.tutorial.ui.gesture.EasterEggGestureMonitor
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureRecognizer
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState
-import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.Finished
-import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress
-import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted
 import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureHandler
+import kotlinx.coroutines.flow.Flow
 
-fun GestureState.toTutorialActionState(): TutorialActionState {
+sealed interface GestureUiState {
+    data object NotStarted : GestureUiState
+
+    data class Finished(@RawRes val successAnimation: Int) : GestureUiState
+
+    data class InProgress(
+        val progress: Float = 0f,
+        val progressStartMark: String = "",
+        val progressEndMark: String = "",
+    ) : GestureUiState
+}
+
+fun GestureState.toGestureUiState(
+    progressStartMark: String,
+    progressEndMark: String,
+    successAnimation: Int,
+): GestureUiState {
+    return when (this) {
+        GestureState.NotStarted -> NotStarted
+        is GestureState.InProgress ->
+            GestureUiState.InProgress(this.progress, progressStartMark, progressEndMark)
+        is GestureState.Finished -> GestureUiState.Finished(successAnimation)
+    }
+}
+
+fun GestureUiState.toTutorialActionState(): TutorialActionState {
     return when (this) {
         NotStarted -> TutorialActionState.NotStarted
         // progress is disabled for now as views are not ready to handle varying progress
-        is InProgress -> TutorialActionState.InProgress(0f)
-        Finished -> TutorialActionState.Finished
+        is GestureUiState.InProgress -> TutorialActionState.InProgress(progress = 0f)
+        is Finished -> TutorialActionState.Finished(successAnimation)
     }
 }
 
@@ -55,15 +82,13 @@
 fun GestureTutorialScreen(
     screenConfig: TutorialScreenConfig,
     gestureRecognizer: GestureRecognizer,
+    gestureUiStateFlow: Flow<GestureUiState>,
     onDoneButtonClicked: () -> Unit,
     onBack: () -> Unit,
 ) {
     BackHandler(onBack = onBack)
-    var gestureState: GestureState by remember { mutableStateOf(NotStarted) }
     var easterEggTriggered by remember { mutableStateOf(false) }
-    LaunchedEffect(gestureRecognizer) {
-        gestureRecognizer.addGestureStateCallback { gestureState = it }
-    }
+    val gestureState by gestureUiStateFlow.collectAsStateWithLifecycle(NotStarted)
     val easterEggMonitor = EasterEggGestureMonitor { easterEggTriggered = true }
     val gestureHandler =
         remember(gestureRecognizer) { TouchpadGestureHandler(gestureRecognizer, easterEggMonitor) }
@@ -84,7 +109,7 @@
 @Composable
 private fun TouchpadGesturesHandlingBox(
     gestureHandler: TouchpadGestureHandler,
-    gestureState: GestureState,
+    gestureState: GestureUiState,
     easterEggTriggered: Boolean,
     resetEasterEggFlag: () -> Unit,
     modifier: Modifier = Modifier,
@@ -110,7 +135,7 @@
                 .pointerInteropFilter(
                     onTouchEvent = { event ->
                         // FINISHED is the final state so we don't need to process touches anymore
-                        if (gestureState == Finished) {
+                        if (gestureState is Finished) {
                             false
                         } else {
                             gestureHandler.onMotionEvent(event)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
index 3ddf760..55749b2 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
@@ -25,8 +25,11 @@
 import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig
 import com.android.systemui.inputdevice.tutorial.ui.composable.rememberColorFilterProperty
 import com.android.systemui.res.R
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureFlowAdapter
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureRecognizer
 import com.android.systemui.touchpad.tutorial.ui.gesture.HomeGestureRecognizer
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
 
 @Composable
 fun HomeGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
@@ -40,14 +43,20 @@
                     titleSuccessResId = R.string.touchpad_home_gesture_success_title,
                     bodySuccessResId = R.string.touchpad_home_gesture_success_body,
                 ),
-            animations =
-                TutorialScreenConfig.Animations(
-                    educationResId = R.raw.trackpad_home_edu,
-                    successResId = R.raw.trackpad_home_success,
-                ),
+            animations = TutorialScreenConfig.Animations(educationResId = R.raw.trackpad_home_edu),
         )
     val recognizer = rememberHomeGestureRecognizer(LocalContext.current.resources)
-    GestureTutorialScreen(screenConfig, recognizer, onDoneButtonClicked, onBack)
+    val gestureUiState: Flow<GestureUiState> =
+        remember(recognizer) {
+            GestureFlowAdapter(recognizer).gestureStateAsFlow.map {
+                it.toGestureUiState(
+                    progressStartMark = "",
+                    progressEndMark = "",
+                    successAnimation = R.raw.trackpad_home_success,
+                )
+            }
+        }
+    GestureTutorialScreen(screenConfig, recognizer, gestureUiState, onDoneButtonClicked, onBack)
 }
 
 @Composable
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
index 30a21bf..d928535 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
@@ -25,8 +25,11 @@
 import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig
 import com.android.systemui.inputdevice.tutorial.ui.composable.rememberColorFilterProperty
 import com.android.systemui.res.R
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureFlowAdapter
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureRecognizer
 import com.android.systemui.touchpad.tutorial.ui.gesture.RecentAppsGestureRecognizer
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
 
 @Composable
 fun RecentAppsGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
@@ -41,13 +44,20 @@
                     bodySuccessResId = R.string.touchpad_recent_apps_gesture_success_body,
                 ),
             animations =
-                TutorialScreenConfig.Animations(
-                    educationResId = R.raw.trackpad_recent_apps_edu,
-                    successResId = R.raw.trackpad_recent_apps_success,
-                ),
+                TutorialScreenConfig.Animations(educationResId = R.raw.trackpad_recent_apps_edu),
         )
     val recognizer = rememberRecentAppsGestureRecognizer(LocalContext.current.resources)
-    GestureTutorialScreen(screenConfig, recognizer, onDoneButtonClicked, onBack)
+    val gestureUiState: Flow<GestureUiState> =
+        remember(recognizer) {
+            GestureFlowAdapter(recognizer).gestureStateAsFlow.map {
+                it.toGestureUiState(
+                    progressStartMark = "",
+                    progressEndMark = "",
+                    successAnimation = R.raw.trackpad_recent_apps_success,
+                )
+            }
+        }
+    GestureTutorialScreen(screenConfig, recognizer, gestureUiState, onDoneButtonClicked, onBack)
 }
 
 @Composable
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt
index 80f8003..024048c 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt
@@ -33,6 +33,10 @@
         gestureStateChangedCallback = callback
     }
 
+    override fun clearGestureStateCallback() {
+        gestureStateChangedCallback = {}
+    }
+
     override fun accept(event: MotionEvent) {
         if (!isThreeFingerTouchpadSwipe(event)) return
         val gestureState = distanceTracker.processEvent(event)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureFlowAdapter.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureFlowAdapter.kt
new file mode 100644
index 0000000..23e31b0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureFlowAdapter.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.touchpad.tutorial.ui.gesture
+
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+class GestureFlowAdapter(gestureRecognizer: GestureRecognizer) {
+
+    val gestureStateAsFlow: Flow<GestureState> = conflatedCallbackFlow {
+        val callback: (GestureState) -> Unit = { trySend(it) }
+        gestureRecognizer.addGestureStateCallback(callback)
+        awaitClose { gestureRecognizer.clearGestureStateCallback() }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureRecognizer.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureRecognizer.kt
index d146268..68a2ef9 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureRecognizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureRecognizer.kt
@@ -22,6 +22,8 @@
 /** Based on passed [MotionEvent]s recognizes different states of gesture and notifies callback. */
 interface GestureRecognizer : Consumer<MotionEvent> {
     fun addGestureStateCallback(callback: (GestureState) -> Unit)
+
+    fun clearGestureStateCallback()
 }
 
 fun isThreeFingerTouchpadSwipe(event: MotionEvent) = isNFingerTouchpadSwipe(event, fingerCount = 3)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt
index 2b84a4c..b804b9a 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt
@@ -29,6 +29,10 @@
         gestureStateChangedCallback = callback
     }
 
+    override fun clearGestureStateCallback() {
+        gestureStateChangedCallback = {}
+    }
+
     override fun accept(event: MotionEvent) {
         if (!isThreeFingerTouchpadSwipe(event)) return
         val gestureState = distanceTracker.processEvent(event)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt
index 69b7c5e..7d484ee 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt
@@ -38,6 +38,10 @@
         gestureStateChangedCallback = callback
     }
 
+    override fun clearGestureStateCallback() {
+        gestureStateChangedCallback = {}
+    }
+
     override fun accept(event: MotionEvent) {
         if (!isThreeFingerTouchpadSwipe(event)) return
         val gestureState = distanceTracker.processEvent(event)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 3d2ebf2..07509e6 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -64,6 +64,8 @@
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.LayerDrawable;
 import android.graphics.drawable.RotateDrawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.RoundRectShape;
 import android.media.AudioManager;
 import android.media.AudioSystem;
 import android.os.Debug;
@@ -115,6 +117,7 @@
 import com.android.internal.view.RotationPolicy;
 import com.android.settingslib.Utils;
 import com.android.systemui.Dumpable;
+import com.android.systemui.Flags;
 import com.android.systemui.Prefs;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.haptics.slider.HapticSliderViewBinder;
@@ -656,6 +659,11 @@
             mRingerIcon = mRinger.findViewById(R.id.ringer_icon);
         }
 
+        if (Flags.hideRingerButtonInSingleVolumeMode() && AudioSystem.isSingleVolume(mContext)) {
+            mRingerAndDrawerContainer.setVisibility(INVISIBLE);
+            mRinger.setVisibility(INVISIBLE);
+        }
+
         mSelectedRingerIcon = mDialog.findViewById(R.id.volume_new_ringer_active_icon);
         mSelectedRingerContainer = mDialog.findViewById(
                 R.id.volume_new_ringer_active_icon_container);
@@ -2341,10 +2349,31 @@
             return;
         }
 
-        final ColorDrawable solidDrawable = new ColorDrawable(
+        LayerDrawable background;
+        // mRingerAndDrawerContainer has rounded corner.
+        // But when it's not visible, mTopContainer needs to have rounded corner.
+        if (Flags.hideRingerButtonInSingleVolumeMode()
+                && mRingerAndDrawerContainer.getVisibility() != VISIBLE
+        ) {
+            float[] radius = new float[] {
+                mDialogCornerRadius, mDialogCornerRadius,  // Top-left corner
+                mDialogCornerRadius, mDialogCornerRadius,  // Top-right corner
+                0, 0,  // Bottom-right corner
+                0, 0   // Bottom-left corner
+            };
+
+            ShapeDrawable roundedDrawable = new ShapeDrawable(
+                    new RoundRectShape(radius, null, null));
+            roundedDrawable.getPaint().setColor(Utils.getColorAttrDefaultColor(
+                    mContext, com.android.internal.R.attr.colorSurface));
+
+            background = new LayerDrawable(new Drawable[] { roundedDrawable });
+        } else {
+            final ColorDrawable solidDrawable = new ColorDrawable(
                 Utils.getColorAttrDefaultColor(mContext, com.android.internal.R.attr.colorSurface));
 
-        final LayerDrawable background = new LayerDrawable(new Drawable[] { solidDrawable });
+            background = new LayerDrawable(new Drawable[] { solidDrawable });
+        }
 
         // Size the solid color to match the primary volume row. In landscape, extend it upwards
         // slightly so that it fills in the bottom corners of the ringer icon, whose background is
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/data/VolumeDialogRingerRepository.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/data/VolumeDialogRingerRepository.kt
deleted file mode 100644
index 73b97f6..0000000
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/data/VolumeDialogRingerRepository.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.volume.dialog.ringer.data
-
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.volume.dialog.ringer.shared.model.VolumeDialogRingerModel
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.update
-
-/** Stores the state of volume dialog ringer model */
-@SysUISingleton
-class VolumeDialogRingerRepository @Inject constructor() {
-
-    private val mutableRingerModel = MutableStateFlow<VolumeDialogRingerModel?>(null)
-    val ringerModel: Flow<VolumeDialogRingerModel> = mutableRingerModel.filterNotNull()
-
-    fun updateRingerModel(update: (current: VolumeDialogRingerModel?) -> VolumeDialogRingerModel) {
-        mutableRingerModel.update(update)
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt
new file mode 100644
index 0000000..7265b821
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.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.ringer.domain
+
+import android.media.AudioManager
+import android.media.AudioManager.RINGER_MODE_NORMAL
+import android.media.AudioManager.RINGER_MODE_SILENT
+import android.media.AudioManager.RINGER_MODE_VIBRATE
+import android.provider.Settings
+import com.android.settingslib.volume.shared.model.RingerMode
+import com.android.systemui.plugins.VolumeDialogController
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogStateInteractor
+import com.android.systemui.volume.dialog.ringer.shared.model.VolumeDialogRingerModel
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogStateModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.stateIn
+
+/** Exposes [VolumeDialogRingerModel]. */
+@VolumeDialog
+class VolumeDialogRingerInteractor
+@Inject
+constructor(
+    @VolumeDialog private val coroutineScope: CoroutineScope,
+    volumeDialogStateInteractor: VolumeDialogStateInteractor,
+    private val controller: VolumeDialogController,
+) {
+
+    val ringerModel: Flow<VolumeDialogRingerModel> =
+        volumeDialogStateInteractor.volumeDialogState
+            .mapNotNull { toRingerModel(it) }
+            .stateIn(coroutineScope, SharingStarted.Eagerly, null)
+            .filterNotNull()
+
+    private fun toRingerModel(state: VolumeDialogStateModel): VolumeDialogRingerModel? {
+        return state.streamModels[AudioManager.STREAM_RING]?.let {
+            VolumeDialogRingerModel(
+                availableModes =
+                    mutableListOf(RingerMode(RINGER_MODE_NORMAL), RingerMode(RINGER_MODE_SILENT))
+                        .also { list ->
+                            if (controller.hasVibrator()) {
+                                list.add(RingerMode(RINGER_MODE_VIBRATE))
+                            }
+                        },
+                currentRingerMode = RingerMode(state.ringerModeInternal),
+                isEnabled =
+                    !(state.zenMode == Settings.Global.ZEN_MODE_ALARMS ||
+                        state.zenMode == Settings.Global.ZEN_MODE_NO_INTERRUPTIONS ||
+                        (state.zenMode == Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS &&
+                            state.disallowRinger)),
+                isMuted = it.level == 0 || it.muted,
+                level = it.level,
+                levelMax = it.levelMax,
+            )
+        }
+    }
+
+    fun setRingerMode(ringerMode: RingerMode) {
+        controller.setRingerMode(ringerMode.value, false)
+    }
+
+    fun scheduleTouchFeedback() {
+        controller.scheduleTouchFeedback()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerButtonViewModel.kt
new file mode 100644
index 0000000..78d2d16
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerButtonViewModel.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.ringer.ui.viewmodel
+
+import android.annotation.DrawableRes
+import android.annotation.StringRes
+import com.android.settingslib.volume.shared.model.RingerMode
+
+/** Models ringer button that corresponds to each ringer mode. */
+data class RingerButtonViewModel(
+    /** Image resource id for the image button. */
+    @DrawableRes val imageResId: Int,
+    /** Content description for a11y. */
+    @StringRes val contentDescriptionResId: Int,
+    /** Hint label for accessibility use. */
+    @StringRes val hintLabelResId: Int,
+    /** Used to notify view model when button is clicked. */
+    val ringerMode: RingerMode,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerDrawerState.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerDrawerState.kt
new file mode 100644
index 0000000..f321837
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerDrawerState.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.ringer.ui.viewmodel
+
+import com.android.settingslib.volume.shared.model.RingerMode
+
+/** Models volume dialog ringer drawer state */
+sealed interface RingerDrawerState {
+
+    /** When clicked to open drawer */
+    data class Open(val mode: RingerMode) : RingerDrawerState
+
+    /** When clicked to close drawer */
+    data class Closed(val mode: RingerMode) : RingerDrawerState
+
+    /** Initial state when volume dialog is shown with a closed drawer. */
+    interface Initial : RingerDrawerState {
+        companion object : Initial
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerViewModel.kt
new file mode 100644
index 0000000..a09bfeb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerViewModel.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.ui.viewmodel
+
+/** Models volume dialog ringer */
+data class RingerViewModel(
+    /** List of the available buttons according to the available modes */
+    val availableButtons: List<RingerButtonViewModel?>,
+    /** The index of the currently selected button */
+    val currentButtonIndex: Int,
+    /** For open and close animations */
+    val drawerState: RingerDrawerState,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
new file mode 100644
index 0000000..ac82ae3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
@@ -0,0 +1,172 @@
+/*
+ * 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.ui.viewmodel
+
+import android.media.AudioAttributes
+import android.media.AudioManager.RINGER_MODE_NORMAL
+import android.media.AudioManager.RINGER_MODE_SILENT
+import android.media.AudioManager.RINGER_MODE_VIBRATE
+import android.os.VibrationEffect
+import com.android.settingslib.volume.shared.model.RingerMode
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.volume.Events
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.ringer.domain.VolumeDialogRingerInteractor
+import com.android.systemui.volume.dialog.ringer.shared.model.VolumeDialogRingerModel
+import com.android.systemui.volume.dialog.shared.VolumeDialogLogger
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.stateIn
+
+private const val TAG = "VolumeDialogRingerDrawerViewModel"
+
+class VolumeDialogRingerDrawerViewModel
+@AssistedInject
+constructor(
+    @VolumeDialog private val coroutineScope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val interactor: VolumeDialogRingerInteractor,
+    private val vibrator: VibratorHelper,
+    private val volumeDialogLogger: VolumeDialogLogger,
+) {
+
+    private val drawerState = MutableStateFlow<RingerDrawerState>(RingerDrawerState.Initial)
+
+    val ringerViewModel: Flow<RingerViewModel> =
+        combine(interactor.ringerModel, drawerState) { ringerModel, state ->
+                ringerModel.toViewModel(state)
+            }
+            .flowOn(backgroundDispatcher)
+            .stateIn(coroutineScope, SharingStarted.Eagerly, null)
+            .filterNotNull()
+
+    // Vibration attributes.
+    private val sonificiationVibrationAttributes =
+        AudioAttributes.Builder()
+            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+            .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
+            .build()
+
+    fun onRingerButtonClicked(ringerMode: RingerMode) {
+        if (drawerState.value is RingerDrawerState.Open) {
+            Events.writeEvent(Events.EVENT_RINGER_TOGGLE, ringerMode.value)
+            provideTouchFeedback(ringerMode)
+            interactor.setRingerMode(ringerMode)
+        }
+        drawerState.value =
+            when (drawerState.value) {
+                is RingerDrawerState.Initial -> {
+                    RingerDrawerState.Open(ringerMode)
+                }
+                is RingerDrawerState.Open -> {
+                    RingerDrawerState.Closed(ringerMode)
+                }
+                is RingerDrawerState.Closed -> {
+                    RingerDrawerState.Open(ringerMode)
+                }
+            }
+    }
+
+    private fun provideTouchFeedback(ringerMode: RingerMode) {
+        when (ringerMode.value) {
+            RINGER_MODE_NORMAL -> {
+                interactor.scheduleTouchFeedback()
+                null
+            }
+            RINGER_MODE_SILENT -> VibrationEffect.get(VibrationEffect.EFFECT_CLICK)
+            RINGER_MODE_VIBRATE -> VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK)
+            else -> VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK)
+        }?.let { vibrator.vibrate(it, sonificiationVibrationAttributes) }
+    }
+
+    private fun VolumeDialogRingerModel.toViewModel(
+        drawerState: RingerDrawerState
+    ): RingerViewModel {
+        val currentIndex = availableModes.indexOf(currentRingerMode)
+        if (currentIndex == -1) {
+            volumeDialogLogger.onCurrentRingerModeIsUnsupported(currentRingerMode)
+        }
+        return RingerViewModel(
+            availableButtons = availableModes.map { mode -> toButtonViewModel(mode) },
+            currentButtonIndex = currentIndex,
+            drawerState = drawerState,
+        )
+    }
+
+    private fun VolumeDialogRingerModel.toButtonViewModel(
+        ringerMode: RingerMode
+    ): RingerButtonViewModel? {
+        return when (ringerMode.value) {
+            RINGER_MODE_SILENT ->
+                RingerButtonViewModel(
+                    imageResId = R.drawable.ic_speaker_mute,
+                    contentDescriptionResId = R.string.volume_ringer_status_silent,
+                    hintLabelResId = R.string.volume_ringer_hint_unmute,
+                    ringerMode = ringerMode,
+                )
+            RINGER_MODE_VIBRATE ->
+                RingerButtonViewModel(
+                    imageResId = R.drawable.ic_volume_ringer_vibrate,
+                    contentDescriptionResId = R.string.volume_ringer_status_vibrate,
+                    hintLabelResId = R.string.volume_ringer_hint_vibrate,
+                    ringerMode = ringerMode,
+                )
+            RINGER_MODE_NORMAL ->
+                when {
+                    isMuted && isEnabled ->
+                        RingerButtonViewModel(
+                            imageResId = R.drawable.ic_speaker_mute,
+                            contentDescriptionResId = R.string.volume_ringer_status_normal,
+                            hintLabelResId = R.string.volume_ringer_hint_unmute,
+                            ringerMode = ringerMode,
+                        )
+
+                    availableModes.contains(RingerMode(RINGER_MODE_VIBRATE)) ->
+                        RingerButtonViewModel(
+                            imageResId = R.drawable.ic_speaker_on,
+                            contentDescriptionResId = R.string.volume_ringer_status_normal,
+                            hintLabelResId = R.string.volume_ringer_hint_vibrate,
+                            ringerMode = ringerMode,
+                        )
+
+                    else ->
+                        RingerButtonViewModel(
+                            imageResId = R.drawable.ic_speaker_on,
+                            contentDescriptionResId = R.string.volume_ringer_status_normal,
+                            hintLabelResId = R.string.volume_ringer_hint_mute,
+                            ringerMode = ringerMode,
+                        )
+                }
+            else -> null
+        }
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(): VolumeDialogRingerDrawerViewModel
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/VolumeDialogLogger.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/VolumeDialogLogger.kt
index 59c38c0..9a3aa7e 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/VolumeDialogLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/VolumeDialogLogger.kt
@@ -15,6 +15,7 @@
  */
 package com.android.systemui.volume.dialog.shared
 
+import com.android.settingslib.volume.shared.model.RingerMode
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel
 import com.android.systemui.log.dagger.VolumeLog
@@ -43,4 +44,13 @@
             { "Dismiss: ${Events.DISMISS_REASONS[int1]}" },
         )
     }
+
+    fun onCurrentRingerModeIsUnsupported(ringerMode: RingerMode) {
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            { int1 = ringerMode.value },
+            { "Current ringer mode: $int1, ringer mode is unsupported in ringer drawer options" },
+        )
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
index 7889b3c..61eeab3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -65,7 +65,6 @@
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.VibratorHelper
-import com.android.systemui.statusbar.events.ANIMATING_OUT
 import com.android.systemui.testKosmos
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.util.concurrency.FakeExecutor
@@ -176,7 +175,7 @@
             BiometricStatusInteractorImpl(
                 activityTaskManager,
                 biometricStatusRepository,
-                fingerprintRepository
+                fingerprintRepository,
             )
         iconProvider = IconProvider(context)
         // Set up default logo icon
@@ -245,7 +244,7 @@
     @Test
     fun testIgnoresAnimatedInWhenDialogAnimatingOut() {
         val container = initializeFingerprintContainer(addToView = false)
-        container.mContainerState = ANIMATING_OUT
+        container.mContainerState = 4 // STATE_ANIMATING_OUT
         container.addToView()
         waitForIdleSync()
 
@@ -278,7 +277,7 @@
             .onDismissed(
                 eq(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED),
                 eq<ByteArray?>(null), /* credentialAttestation */
-                eq(authContainer?.requestId ?: 0L)
+                eq(authContainer?.requestId ?: 0L),
             )
         assertThat(container.parent).isNull()
     }
@@ -292,13 +291,13 @@
         verify(callback)
             .onSystemEvent(
                 eq(BiometricConstants.BIOMETRIC_SYSTEM_EVENT_EARLY_USER_CANCEL),
-                eq(authContainer?.requestId ?: 0L)
+                eq(authContainer?.requestId ?: 0L),
             )
         verify(callback)
             .onDismissed(
                 eq(AuthDialogCallback.DISMISSED_USER_CANCELED),
                 eq<ByteArray?>(null), /* credentialAttestation */
-                eq(authContainer?.requestId ?: 0L)
+                eq(authContainer?.requestId ?: 0L),
             )
         assertThat(container.parent).isNull()
     }
@@ -313,7 +312,7 @@
             .onDismissed(
                 eq(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE),
                 eq<ByteArray?>(null), /* credentialAttestation */
-                eq(authContainer?.requestId ?: 0L)
+                eq(authContainer?.requestId ?: 0L),
             )
         assertThat(container.parent).isNull()
     }
@@ -340,7 +339,7 @@
             .onDismissed(
                 eq(AuthDialogCallback.DISMISSED_ERROR),
                 eq<ByteArray?>(null), /* credentialAttestation */
-                eq(authContainer?.requestId ?: 0L)
+                eq(authContainer?.requestId ?: 0L),
             )
         assertThat(authContainer!!.parent).isNull()
     }
@@ -454,7 +453,7 @@
         val container =
             initializeFingerprintContainer(
                 authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL,
-                verticalListContentView = PromptVerticalListContentView.Builder().build()
+                verticalListContentView = PromptVerticalListContentView.Builder().build(),
             )
         // Two-step credential view should show -
         // 1. biometric prompt without sensor 2. credential view ui
@@ -479,7 +478,7 @@
         val container =
             initializeFingerprintContainer(
                 authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL,
-                contentViewWithMoreOptionsButton = contentView
+                contentViewWithMoreOptionsButton = contentView,
             )
         waitForIdleSync()
 
@@ -565,7 +564,7 @@
     }
 
     private fun initializeCredentialPasswordContainer(
-        addToView: Boolean = true,
+        addToView: Boolean = true
     ): TestAuthContainerView {
         whenever(userManager.getCredentialOwnerProfile(anyInt())).thenReturn(20)
         whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(eq(20)))
@@ -597,25 +596,25 @@
                 fingerprintProps = fingerprintSensorPropertiesInternal(),
                 verticalListContentView = verticalListContentView,
             ),
-            addToView
+            addToView,
         )
 
     private fun initializeCoexContainer(
         authenticators: Int = BiometricManager.Authenticators.BIOMETRIC_WEAK,
-        addToView: Boolean = true
+        addToView: Boolean = true,
     ) =
         initializeContainer(
             TestAuthContainerView(
                 authenticators = authenticators,
                 fingerprintProps = fingerprintSensorPropertiesInternal(),
-                faceProps = faceSensorPropertiesInternal()
+                faceProps = faceSensorPropertiesInternal(),
             ),
-            addToView
+            addToView,
         )
 
     private fun initializeContainer(
         view: TestAuthContainerView,
-        addToView: Boolean
+        addToView: Boolean,
     ): TestAuthContainerView {
         authContainer = view
 
@@ -668,7 +667,7 @@
                 biometricStatusInteractor,
                 udfpsUtils,
                 iconProvider,
-                activityTaskManager
+                activityTaskManager,
             ),
             { credentialViewModel },
             fakeExecutor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
index 3d1a0d0..96f4a60 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
@@ -481,6 +481,25 @@
         verify(mAuthController).onAodInterrupt(anyInt(), anyInt(), anyFloat(), anyFloat());
     }
 
+    @Test
+    @EnableFlags(android.hardware.biometrics.Flags.FLAG_SCREEN_OFF_UNLOCK_UDFPS)
+    public void udfpsLongPress_triggeredWhenDoze() {
+        // GIVEN device is DOZE
+        when(mMachine.getState()).thenReturn(DozeMachine.State.DOZE);
+
+        // WHEN udfps long-press is triggered
+        mTriggers.onSensor(DozeLog.REASON_SENSOR_UDFPS_LONG_PRESS, 100, 100,
+                new float[]{0, 1, 2, 3, 4});
+
+        // THEN the pulse is NOT dropped
+        verify(mDozeLog, never()).tracePulseDropped(anyString(), any());
+
+        // WHEN the screen state is OFF
+        mTriggers.onScreenState(Display.STATE_OFF);
+
+        // THEN aod interrupt never be sent
+        verify(mAuthController, never()).onAodInterrupt(anyInt(), anyInt(), anyFloat(), anyFloat());
+    }
 
     @Test
     public void udfpsLongPress_dozeState_notRegistered() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
index a75d7b2..da0029f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
@@ -21,6 +21,8 @@
 import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_TOO_DARK;
 import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT;
 import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_TIMEOUT;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
 
 import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FACE_NOT_AVAILABLE;
 import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FACE_NOT_RECOGNIZED;
@@ -777,6 +779,24 @@
     }
 
     @Test
+    public void indicationAreaHidden_untilBatteryInfoArrives() {
+        createController();
+        // level of -1 indicates missing info
+        BatteryStatus status = new BatteryStatus(BatteryManager.BATTERY_STATUS_UNKNOWN,
+                -1 /* level */, BatteryManager.BATTERY_PLUGGED_WIRELESS, 100 /* health */,
+                0 /* maxChargingWattage */, true /* present */);
+
+        mController.setVisible(true);
+        mStatusBarStateListener.onDozingChanged(true);
+        reset(mIndicationArea);
+
+        mController.getKeyguardCallback().onRefreshBatteryInfo(status);
+        // VISIBLE is always called first
+        verify(mIndicationArea).setVisibility(VISIBLE);
+        verify(mIndicationArea).setVisibility(GONE);
+    }
+
+    @Test
     public void onRefreshBatteryInfo_computesChargingTime() throws RemoteException {
         createController();
         BatteryStatus status = new BatteryStatus(BatteryManager.BATTERY_STATUS_CHARGING,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
index 4e7de81..4cad5f7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
@@ -29,21 +29,24 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.privacy.OngoingPrivacyChip
 import com.android.systemui.statusbar.BatteryStatusChip
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingIn
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingOut
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimationQueued
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.RunningChipAnim
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.ShowingPersistentDot
 import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
 import com.android.systemui.statusbar.window.StatusBarWindowController
 import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.time.FakeSystemClock
-import junit.framework.Assert.assertEquals
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -54,6 +57,10 @@
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
 
 @RunWith(AndroidJUnit4::class)
 @RunWithLooper(setAsMainLooper = true)
@@ -120,12 +127,12 @@
         val batteryChip = createAndScheduleFakeBatteryEvent()
 
         // assert that animation is queued
-        assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(AnimationQueued, systemStatusAnimationScheduler.animationState.value)
 
         // skip debounce delay
         advanceTimeBy(DEBOUNCE_DELAY + 1)
         // status chip starts animating in after debounce delay
-        assertEquals(ANIMATING_IN, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(AnimatingIn, systemStatusAnimationScheduler.animationState.value)
         assertEquals(0f, batteryChip.contentView.alpha)
         assertEquals(0f, batteryChip.view.alpha)
         verify(listener, times(1)).onSystemEventAnimationBegin()
@@ -134,14 +141,14 @@
         animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
         advanceTimeBy(APPEAR_ANIMATION_DURATION)
         // assert that status chip is visible
-        assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(RunningChipAnim, systemStatusAnimationScheduler.animationState.value)
         assertEquals(1f, batteryChip.contentView.alpha)
         assertEquals(1f, batteryChip.view.alpha)
 
         // skip status chip display time
         advanceTimeBy(DISPLAY_LENGTH + 1)
-        // assert that it is still visible but switched to the ANIMATING_OUT state
-        assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+        // assert that it is still visible but switched to the AnimatingOut state
+        assertEquals(AnimatingOut, systemStatusAnimationScheduler.animationState.value)
         assertEquals(1f, batteryChip.contentView.alpha)
         assertEquals(1f, batteryChip.view.alpha)
         verify(listener, times(1)).onSystemEventAnimationFinish(false)
@@ -149,7 +156,7 @@
         // skip disappear animation
         animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
         // assert that it is not visible anymore
-        assertEquals(IDLE, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(Idle, systemStatusAnimationScheduler.animationState.value)
         assertEquals(0f, batteryChip.contentView.alpha)
         assertEquals(0f, batteryChip.view.alpha)
     }
@@ -166,7 +173,7 @@
         createAndScheduleFakePrivacyEvent()
 
         // THEN the privacy event still happens
-        assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(AnimationQueued, systemStatusAnimationScheduler.animationState.value)
     }
 
     @Test
@@ -177,12 +184,12 @@
         val privacyChip = createAndScheduleFakePrivacyEvent()
 
         // assert that animation is queued
-        assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(AnimationQueued, systemStatusAnimationScheduler.animationState.value)
 
         // skip debounce delay
         advanceTimeBy(DEBOUNCE_DELAY + 1)
         // status chip starts animating in after debounce delay
-        assertEquals(ANIMATING_IN, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(AnimatingIn, systemStatusAnimationScheduler.animationState.value)
         assertEquals(0f, privacyChip.view.alpha)
         verify(listener, times(1)).onSystemEventAnimationBegin()
 
@@ -190,13 +197,13 @@
         animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
         advanceTimeBy(APPEAR_ANIMATION_DURATION + 1)
         // assert that status chip is visible
-        assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(RunningChipAnim, systemStatusAnimationScheduler.animationState.value)
         assertEquals(1f, privacyChip.view.alpha)
 
         // skip status chip display time
         advanceTimeBy(DISPLAY_LENGTH + 1)
-        // assert that it is still visible but switched to the ANIMATING_OUT state
-        assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+        // assert that it is still visible but switched to the AnimatingOut state
+        assertEquals(AnimatingOut, systemStatusAnimationScheduler.animationState.value)
         assertEquals(1f, privacyChip.view.alpha)
         verify(listener, times(1)).onSystemEventAnimationFinish(true)
         verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any())
@@ -205,13 +212,13 @@
         advanceTimeBy(DISAPPEAR_ANIMATION_DURATION + 1)
         animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
         // assert that it the dot is now visible
-        assertEquals(SHOWING_PERSISTENT_DOT, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(ShowingPersistentDot, systemStatusAnimationScheduler.animationState.value)
         assertEquals(1f, privacyChip.view.alpha)
 
         // notify SystemStatusAnimationScheduler to remove persistent dot
         systemStatusAnimationScheduler.removePersistentDot()
-        // assert that IDLE state is entered
-        assertEquals(IDLE, systemStatusAnimationScheduler.getAnimationState())
+        // assert that Idle state is entered
+        assertEquals(Idle, systemStatusAnimationScheduler.animationState.value)
         verify(listener, times(1)).onHidePersistentDot()
     }
 
@@ -225,19 +232,19 @@
         batteryChip.view.alpha = 0f
 
         // assert that animation is queued
-        assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(AnimationQueued, systemStatusAnimationScheduler.animationState.value)
 
         // create and schedule high priority event
         val privacyChip = createAndScheduleFakePrivacyEvent()
 
         // assert that animation is queued
-        assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(AnimationQueued, systemStatusAnimationScheduler.animationState.value)
 
         // skip debounce delay and appear animation duration
-        fastForwardAnimationToState(RUNNING_CHIP_ANIM)
+        fastForwardAnimationToState(RunningChipAnim)
 
         // high priority status chip is visible while low priority status chip is not visible
-        assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(RunningChipAnim, systemStatusAnimationScheduler.animationState.value)
         assertEquals(1f, privacyChip.view.alpha)
         assertEquals(0f, batteryChip.view.alpha)
     }
@@ -250,11 +257,11 @@
         // create and schedule low priority event
         val batteryChip = createAndScheduleFakeBatteryEvent()
 
-        // fast forward to RUNNING_CHIP_ANIM state
-        fastForwardAnimationToState(RUNNING_CHIP_ANIM)
+        // fast forward to RunningChipAnim state
+        fastForwardAnimationToState(RunningChipAnim)
 
         // assert that chip is displayed
-        assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(RunningChipAnim, systemStatusAnimationScheduler.animationState.value)
         assertEquals(1f, batteryChip.view.alpha)
 
         // create and schedule high priority event
@@ -264,20 +271,20 @@
         testScheduler.runCurrent()
 
         // assert that currently displayed chip is immediately animated out
-        assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(AnimatingOut, systemStatusAnimationScheduler.animationState.value)
 
         // skip disappear animation
         animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
 
         // assert that high priority privacy chip animation is queued
-        assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(AnimationQueued, systemStatusAnimationScheduler.animationState.value)
 
         // skip debounce delay and appear animation
         advanceTimeBy(DEBOUNCE_DELAY + APPEAR_ANIMATION_DURATION + 1)
         animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
 
         // high priority status chip is visible while low priority status chip is not visible
-        assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(RunningChipAnim, systemStatusAnimationScheduler.animationState.value)
         assertEquals(1f, privacyChip.view.alpha)
         assertEquals(0f, batteryChip.view.alpha)
     }
@@ -294,7 +301,7 @@
         advanceTimeBy(DEBOUNCE_DELAY + 1)
 
         // assert that chip is animated in
-        assertEquals(ANIMATING_IN, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(AnimatingIn, systemStatusAnimationScheduler.animationState.value)
 
         // create and schedule high priority event
         val privacyChip = createAndScheduleFakePrivacyEvent()
@@ -303,7 +310,7 @@
         testScheduler.runCurrent()
 
         // assert that currently animated chip keeps animating
-        assertEquals(ANIMATING_IN, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(AnimatingIn, systemStatusAnimationScheduler.animationState.value)
 
         // skip appear animation
         animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
@@ -311,20 +318,20 @@
 
         // assert that low priority chip is animated out immediately after finishing the appear
         // animation
-        assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(AnimatingOut, systemStatusAnimationScheduler.animationState.value)
 
         // skip disappear animation
         animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
 
         // assert that high priority privacy chip animation is queued
-        assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(AnimationQueued, systemStatusAnimationScheduler.animationState.value)
 
         // skip debounce delay and appear animation
         advanceTimeBy(DEBOUNCE_DELAY + APPEAR_ANIMATION_DURATION + 1)
         animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
 
         // high priority status chip is visible while low priority status chip is not visible
-        assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(RunningChipAnim, systemStatusAnimationScheduler.animationState.value)
         assertEquals(1f, privacyChip.view.alpha)
         assertEquals(0f, batteryChip.view.alpha)
     }
@@ -346,7 +353,7 @@
         animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
 
         // high priority status chip is visible while low priority status chip is not visible
-        assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(RunningChipAnim, systemStatusAnimationScheduler.animationState.value)
         assertEquals(1f, privacyChip.view.alpha)
         assertEquals(0f, batteryChip.view.alpha)
     }
@@ -359,14 +366,14 @@
         // create and schedule high priority event
         createAndScheduleFakePrivacyEvent()
 
-        // skip chip animation lifecycle and fast forward to SHOWING_PERSISTENT_DOT state
-        fastForwardAnimationToState(SHOWING_PERSISTENT_DOT)
-        assertEquals(SHOWING_PERSISTENT_DOT, systemStatusAnimationScheduler.getAnimationState())
+        // skip chip animation lifecycle and fast forward to ShowingPersistentDot state
+        fastForwardAnimationToState(ShowingPersistentDot)
+        assertEquals(ShowingPersistentDot, systemStatusAnimationScheduler.animationState.value)
         verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any())
 
-        // remove persistent dot and verify that animationState changes to IDLE
+        // remove persistent dot and verify that animationState changes to Idle
         systemStatusAnimationScheduler.removePersistentDot()
-        assertEquals(IDLE, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(Idle, systemStatusAnimationScheduler.animationState.value)
         verify(listener, times(1)).onHidePersistentDot()
     }
 
@@ -377,14 +384,14 @@
         val accessibilityDesc = "Some desc"
         val mockView = mock<View>()
         val mockAnimatableView =
-            mock<BackgroundAnimatableView> { whenever(view).thenReturn(mockView) }
+            mock<BackgroundAnimatableView> { whenever(it.view).thenReturn(mockView) }
 
         scheduleFakeEventWithView(
             accessibilityDesc,
             mockAnimatableView,
             shouldAnnounceAccessibilityEvent = true,
         )
-        fastForwardAnimationToState(ANIMATING_OUT)
+        fastForwardAnimationToState(AnimatingOut)
 
         verify(mockView).announceForAccessibility(eq(accessibilityDesc))
     }
@@ -396,14 +403,14 @@
         val accessibilityDesc = null
         val mockView = mock<View>()
         val mockAnimatableView =
-            mock<BackgroundAnimatableView> { whenever(view).thenReturn(mockView) }
+            mock<BackgroundAnimatableView> { whenever(it.view).thenReturn(mockView) }
 
         scheduleFakeEventWithView(
             accessibilityDesc,
             mockAnimatableView,
             shouldAnnounceAccessibilityEvent = true,
         )
-        fastForwardAnimationToState(ANIMATING_OUT)
+        fastForwardAnimationToState(AnimatingOut)
 
         verify(mockView, never()).announceForAccessibility(any())
     }
@@ -415,14 +422,14 @@
         val accessibilityDesc = "something"
         val mockView = mock<View>()
         val mockAnimatableView =
-            mock<BackgroundAnimatableView> { whenever(view).thenReturn(mockView) }
+            mock<BackgroundAnimatableView> { whenever(it.view).thenReturn(mockView) }
 
         scheduleFakeEventWithView(
             accessibilityDesc,
             mockAnimatableView,
             shouldAnnounceAccessibilityEvent = false,
         )
-        fastForwardAnimationToState(ANIMATING_OUT)
+        fastForwardAnimationToState(AnimatingOut)
 
         verify(mockView, never()).announceForAccessibility(any())
     }
@@ -435,21 +442,21 @@
         // create and schedule high priority event
         createAndScheduleFakePrivacyEvent()
 
-        // skip chip animation lifecycle and fast forward to RUNNING_CHIP_ANIM state
-        fastForwardAnimationToState(RUNNING_CHIP_ANIM)
-        assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+        // skip chip animation lifecycle and fast forward to RunningChipAnim state
+        fastForwardAnimationToState(RunningChipAnim)
+        assertEquals(RunningChipAnim, systemStatusAnimationScheduler.animationState.value)
 
         // request removal of persistent dot
         systemStatusAnimationScheduler.removePersistentDot()
 
         // skip display time and verify that disappear animation is run
         advanceTimeBy(DISPLAY_LENGTH + 1)
-        assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(AnimatingOut, systemStatusAnimationScheduler.animationState.value)
 
-        // skip disappear animation and verify that animationState changes to IDLE instead of
-        // SHOWING_PERSISTENT_DOT
+        // skip disappear animation and verify that animationState changes to Idle instead of
+        // ShowingPersistentDot
         animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
-        assertEquals(IDLE, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(Idle, systemStatusAnimationScheduler.animationState.value)
         // verify that the persistent dot callbacks are not invoked
         verify(listener, never()).onSystemStatusAnimationTransitionToPersistentDot(any())
         verify(listener, never()).onHidePersistentDot()
@@ -463,9 +470,9 @@
         // create and schedule high priority event
         createAndScheduleFakePrivacyEvent()
 
-        // fast forward to ANIMATING_OUT state
-        fastForwardAnimationToState(ANIMATING_OUT)
-        assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+        // fast forward to AnimatingOut state
+        fastForwardAnimationToState(AnimatingOut)
+        assertEquals(AnimatingOut, systemStatusAnimationScheduler.animationState.value)
         verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any())
 
         // remove persistent dot
@@ -478,8 +485,8 @@
         animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
         testScheduler.runCurrent()
 
-        // verify that animationState changes to IDLE
-        assertEquals(IDLE, systemStatusAnimationScheduler.getAnimationState())
+        // verify that animationState changes to Idle
+        assertEquals(Idle, systemStatusAnimationScheduler.animationState.value)
     }
 
     @Test
@@ -494,11 +501,11 @@
         // create and schedule a privacy event again (resets forceVisible to true)
         createAndScheduleFakePrivacyEvent()
 
-        // skip chip animation lifecycle and fast forward to SHOWING_PERSISTENT_DOT state
-        fastForwardAnimationToState(SHOWING_PERSISTENT_DOT)
+        // skip chip animation lifecycle and fast forward to ShowingPersistentDot state
+        fastForwardAnimationToState(ShowingPersistentDot)
 
-        // verify that we reach SHOWING_PERSISTENT_DOT and that listener callback is invoked
-        assertEquals(SHOWING_PERSISTENT_DOT, systemStatusAnimationScheduler.getAnimationState())
+        // verify that we reach ShowingPersistentDot and that listener callback is invoked
+        assertEquals(ShowingPersistentDot, systemStatusAnimationScheduler.animationState.value)
         verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any())
     }
 
@@ -511,21 +518,21 @@
         createAndScheduleFakePrivacyEvent()
         // request removal of persistent dot (sets forceVisible to false)
         systemStatusAnimationScheduler.removePersistentDot()
-        fastForwardAnimationToState(RUNNING_CHIP_ANIM)
+        fastForwardAnimationToState(RunningChipAnim)
 
         // create and schedule a privacy event again (resets forceVisible to true)
         createAndScheduleFakePrivacyEvent()
 
         // skip status chip display time
         advanceTimeBy(DISPLAY_LENGTH + 1)
-        assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(AnimatingOut, systemStatusAnimationScheduler.animationState.value)
         verify(listener, times(1)).onSystemEventAnimationFinish(anyBoolean())
 
         // skip disappear animation
         animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
 
-        // verify that we reach SHOWING_PERSISTENT_DOT and that listener callback is invoked
-        assertEquals(SHOWING_PERSISTENT_DOT, systemStatusAnimationScheduler.getAnimationState())
+        // verify that we reach ShowingPersistentDot and that listener callback is invoked
+        assertEquals(ShowingPersistentDot, systemStatusAnimationScheduler.animationState.value)
         verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any())
     }
 
@@ -537,9 +544,9 @@
         // create and schedule high priority event
         createAndScheduleFakePrivacyEvent()
 
-        // skip chip animation lifecycle and fast forward to ANIMATING_OUT state
-        fastForwardAnimationToState(ANIMATING_OUT)
-        assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+        // skip chip animation lifecycle and fast forward to AnimatingOut state
+        fastForwardAnimationToState(AnimatingOut)
+        assertEquals(AnimatingOut, systemStatusAnimationScheduler.animationState.value)
         verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any())
 
         // request removal of persistent dot
@@ -548,13 +555,13 @@
         // schedule another high priority event while the event is animating out
         createAndScheduleFakePrivacyEvent()
 
-        // verify that the state is still ANIMATING_OUT
-        assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+        // verify that the state is still AnimatingOut
+        assertEquals(AnimatingOut, systemStatusAnimationScheduler.animationState.value)
 
-        // skip disappear animation duration and verify that new state is ANIMATION_QUEUED
+        // skip disappear animation duration and verify that new state is AnimationQueued
         animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
         testScheduler.runCurrent()
-        assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(AnimationQueued, systemStatusAnimationScheduler.animationState.value)
         // also verify that onHidePersistentDot callback is called
         verify(listener, times(1)).onHidePersistentDot()
     }
@@ -567,16 +574,16 @@
         // create and schedule high priority event
         createAndScheduleFakePrivacyEvent()
 
-        // skip chip animation lifecycle and fast forward to ANIMATING_OUT state
-        fastForwardAnimationToState(ANIMATING_OUT)
-        assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+        // skip chip animation lifecycle and fast forward to AnimatingOut state
+        fastForwardAnimationToState(AnimatingOut)
+        assertEquals(AnimatingOut, systemStatusAnimationScheduler.animationState.value)
         verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any())
 
         // request removal of persistent dot
         systemStatusAnimationScheduler.removePersistentDot()
 
-        // verify that the state is still ANIMATING_OUT
-        assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+        // verify that the state is still AnimatingOut
+        assertEquals(AnimatingOut, systemStatusAnimationScheduler.animationState.value)
 
         // skip disappear animation duration
         testScheduler.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION + 1)
@@ -591,33 +598,33 @@
         // verify that onHidePersistentDot is invoked despite the animator callback being delayed
         // (it's invoked more than DISAPPEAR_ANIMATION_DURATION after the dot removal was requested)
         verify(listener, times(1)).onHidePersistentDot()
-        // verify that animationState is IDLE
-        assertEquals(IDLE, systemStatusAnimationScheduler.getAnimationState())
+        // verify that animationState is Idle
+        assertEquals(Idle, systemStatusAnimationScheduler.animationState.value)
     }
 
-    private fun TestScope.fastForwardAnimationToState(@SystemAnimationState animationState: Int) {
+    private fun TestScope.fastForwardAnimationToState(animationState: SystemEventAnimationState) {
         // this function should only be called directly after posting a status event
-        assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
-        if (animationState == IDLE || animationState == ANIMATION_QUEUED) return
+        assertEquals(AnimationQueued, systemStatusAnimationScheduler.animationState.value)
+        if (animationState == Idle || animationState == AnimationQueued) return
         // skip debounce delay
         advanceTimeBy(DEBOUNCE_DELAY + 1)
 
         // status chip starts animating in after debounce delay
-        assertEquals(ANIMATING_IN, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(AnimatingIn, systemStatusAnimationScheduler.animationState.value)
         verify(listener, times(1)).onSystemEventAnimationBegin()
-        if (animationState == ANIMATING_IN) return
+        if (animationState == AnimatingIn) return
 
         // skip appear animation
         animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
         advanceTimeBy(APPEAR_ANIMATION_DURATION)
-        assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
-        if (animationState == RUNNING_CHIP_ANIM) return
+        assertEquals(RunningChipAnim, systemStatusAnimationScheduler.animationState.value)
+        if (animationState == RunningChipAnim) return
 
         // skip status chip display time
         advanceTimeBy(DISPLAY_LENGTH + 1)
-        assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+        assertEquals(AnimatingOut, systemStatusAnimationScheduler.animationState.value)
         verify(listener, times(1)).onSystemEventAnimationFinish(anyBoolean())
-        if (animationState == ANIMATING_OUT) return
+        if (animationState == AnimatingOut) return
 
         // skip disappear animation
         animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt
index 0b5f8d5..723c0d7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt
@@ -74,22 +74,31 @@
 import com.android.systemui.util.kotlin.JavaAdapter
 import com.android.systemui.wmshell.BubblesManager
 import java.util.Optional
-import junit.framework.Assert
 import kotlin.test.assertEquals
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.test.runCurrent
+import org.junit.Assert
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers
 import org.mockito.Mock
-import org.mockito.Mockito
-import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 import org.mockito.invocation.InvocationOnMock
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
 
 /** Tests for [NotificationGutsManager] with the scene container enabled. */
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @RunWithLooper
@@ -99,7 +108,7 @@
         NotificationChannel(
             TEST_CHANNEL_ID,
             TEST_CHANNEL_ID,
-            NotificationManager.IMPORTANCE_DEFAULT
+            NotificationManager.IMPORTANCE_DEFAULT,
         )
 
     private val kosmos = testKosmos()
@@ -146,7 +155,7 @@
         MockitoAnnotations.initMocks(this)
         allowTestableLooperAsMainThread()
         helper = NotificationTestHelper(mContext, mDependency)
-        Mockito.`when`(accessibilityManager.isTouchExplorationEnabled).thenReturn(false)
+        whenever(accessibilityManager.isTouchExplorationEnabled).thenReturn(false)
         windowRootViewVisibilityInteractor =
             WindowRootViewVisibilityInteractor(
                 testScope.backgroundScope,
@@ -185,12 +194,12 @@
                 deviceProvisionedController,
                 metricsLogger,
                 headsUpManager,
-                activityStarter
+                activityStarter,
             )
         gutsManager.setUpWithPresenter(
             presenter,
             notificationListContainer,
-            onSettingsClickListener
+            onSettingsClickListener,
         )
         gutsManager.setNotificationActivityStarter(notificationActivityStarter)
         gutsManager.start()
@@ -198,49 +207,31 @@
 
     @Test
     fun testOpenAndCloseGuts() {
-        val guts = Mockito.spy(NotificationGuts(mContext))
-        Mockito.`when`(guts.post(ArgumentMatchers.any())).thenAnswer { invocation: InvocationOnMock
-            ->
+        val guts = spy(NotificationGuts(mContext))
+        whenever(guts.post(any())).thenAnswer { invocation: InvocationOnMock ->
             handler.post((invocation.arguments[0] as Runnable))
             null
         }
 
         // Test doesn't support animation since the guts view is not attached.
-        Mockito.doNothing()
-            .`when`(guts)
-            .openControls(
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyBoolean(),
-                ArgumentMatchers.any(Runnable::class.java)
-            )
+        doNothing()
+            .whenever(guts)
+            .openControls(any<Int>(), any<Int>(), any<Boolean>(), any<Runnable>())
         val realRow = createTestNotificationRow()
         val menuItem = createTestMenuItem(realRow)
-        val row = Mockito.spy(realRow)
-        Mockito.`when`(row!!.windowToken).thenReturn(Binder())
-        Mockito.`when`(row.guts).thenReturn(guts)
+        val row = spy(realRow)
+        whenever(row!!.windowToken).thenReturn(Binder())
+        whenever(row.guts).thenReturn(guts)
         Assert.assertTrue(gutsManager.openGutsInternal(row, 0, 0, menuItem))
         assertEquals(View.INVISIBLE.toLong(), guts.visibility.toLong())
         executor.runAllReady()
-        verify(guts)
-            .openControls(
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyBoolean(),
-                ArgumentMatchers.any(Runnable::class.java)
-            )
+        verify(guts).openControls(any<Int>(), any<Int>(), any<Boolean>(), any<Runnable>())
         verify(headsUpManager).setGutsShown(realRow!!.entry, true)
         assertEquals(View.VISIBLE.toLong(), guts.visibility.toLong())
         gutsManager.closeAndSaveGuts(false, false, true, 0, 0, false)
         verify(guts)
-            .closeControls(
-                ArgumentMatchers.anyBoolean(),
-                ArgumentMatchers.anyBoolean(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyBoolean()
-            )
-        verify(row, Mockito.times(1)).setGutsView(ArgumentMatchers.any())
+            .closeControls(any<Boolean>(), any<Boolean>(), any<Int>(), any<Int>(), any<Boolean>())
+        verify(row, times(1)).setGutsView(any())
         executor.runAllReady()
         verify(headsUpManager).setGutsShown(realRow.entry, false)
     }
@@ -250,7 +241,7 @@
         // First, start out lockscreen or shade as not visible
         setIsLockscreenOrShadeVisible(false)
         testScope.testScheduler.runCurrent()
-        val guts = Mockito.mock(NotificationGuts::class.java)
+        val guts = mock<NotificationGuts>()
         gutsManager.exposedGuts = guts
 
         // WHEN the lockscreen or shade becomes visible
@@ -258,15 +249,9 @@
         testScope.testScheduler.runCurrent()
 
         // THEN the guts are not closed
-        verify(guts, Mockito.never()).removeCallbacks(ArgumentMatchers.any())
-        verify(guts, Mockito.never())
-            .closeControls(
-                ArgumentMatchers.anyBoolean(),
-                ArgumentMatchers.anyBoolean(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyBoolean()
-            )
+        verify(guts, never()).removeCallbacks(any())
+        verify(guts, never())
+            .closeControls(any<Boolean>(), any<Boolean>(), any<Int>(), any<Int>(), any<Boolean>())
     }
 
     @Test
@@ -274,7 +259,7 @@
         // First, start out lockscreen or shade as visible
         setIsLockscreenOrShadeVisible(true)
         testScope.testScheduler.runCurrent()
-        val guts = Mockito.mock(NotificationGuts::class.java)
+        val guts = mock<NotificationGuts>()
         gutsManager.exposedGuts = guts
 
         // WHEN the lockscreen or shade is no longer visible
@@ -282,14 +267,14 @@
         testScope.testScheduler.runCurrent()
 
         // THEN the guts are closed
-        verify(guts).removeCallbacks(ArgumentMatchers.any())
+        verify(guts).removeCallbacks(anyOrNull())
         verify(guts)
             .closeControls(
-                /* leavebehinds= */ ArgumentMatchers.eq(true),
-                /* controls= */ ArgumentMatchers.eq(true),
-                /* x= */ ArgumentMatchers.anyInt(),
-                /* y= */ ArgumentMatchers.anyInt(),
-                /* force= */ ArgumentMatchers.eq(true)
+                /* leavebehinds= */ eq(true),
+                /* controls= */ eq(true),
+                /* x= */ any<Int>(),
+                /* y= */ any<Int>(),
+                /* force= */ eq(true),
             )
     }
 
@@ -304,95 +289,68 @@
         testScope.testScheduler.runCurrent()
 
         // THEN the list container is reset
-        verify(notificationListContainer)
-            .resetExposedMenuView(ArgumentMatchers.anyBoolean(), ArgumentMatchers.anyBoolean())
+        verify(notificationListContainer).resetExposedMenuView(any<Boolean>(), any<Boolean>())
     }
 
     @Test
     fun testChangeDensityOrFontScale() {
-        val guts = Mockito.spy(NotificationGuts(mContext))
-        Mockito.`when`(guts.post(ArgumentMatchers.any())).thenAnswer { invocation: InvocationOnMock
-            ->
+        val guts = spy(NotificationGuts(mContext))
+        whenever(guts.post(any())).thenAnswer { invocation: InvocationOnMock ->
             handler.post((invocation.arguments[0] as Runnable))
             null
         }
 
         // Test doesn't support animation since the guts view is not attached.
-        Mockito.doNothing()
-            .`when`(guts)
-            .openControls(
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyBoolean(),
-                ArgumentMatchers.any(Runnable::class.java)
-            )
+        doNothing()
+            .whenever(guts)
+            .openControls(any<Int>(), any<Int>(), any<Boolean>(), any<Runnable>())
         val realRow = createTestNotificationRow()
         val menuItem = createTestMenuItem(realRow)
-        val row = Mockito.spy(realRow)
-        Mockito.`when`(row!!.windowToken).thenReturn(Binder())
-        Mockito.`when`(row.guts).thenReturn(guts)
-        Mockito.doNothing().`when`(row).ensureGutsInflated()
+        val row = spy(realRow)
+        whenever(row!!.windowToken).thenReturn(Binder())
+        whenever(row.guts).thenReturn(guts)
+        doNothing().whenever(row).ensureGutsInflated()
         val realEntry = realRow!!.entry
-        val entry = Mockito.spy(realEntry)
-        Mockito.`when`(entry.row).thenReturn(row)
-        Mockito.`when`(entry.getGuts()).thenReturn(guts)
+        val entry = spy(realEntry)
+        whenever(entry.row).thenReturn(row)
+        whenever(entry.getGuts()).thenReturn(guts)
         Assert.assertTrue(gutsManager.openGutsInternal(row, 0, 0, menuItem))
         executor.runAllReady()
-        verify(guts)
-            .openControls(
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyBoolean(),
-                ArgumentMatchers.any(Runnable::class.java)
-            )
+        verify(guts).openControls(any<Int>(), any<Int>(), any<Boolean>(), any<Runnable>())
 
         // called once by mGutsManager.bindGuts() in mGutsManager.openGuts()
-        verify(row).setGutsView(ArgumentMatchers.any())
+        verify(row).setGutsView(any())
         row.onDensityOrFontScaleChanged()
         gutsManager.onDensityOrFontScaleChanged(entry)
         executor.runAllReady()
         gutsManager.closeAndSaveGuts(false, false, false, 0, 0, false)
         verify(guts)
-            .closeControls(
-                ArgumentMatchers.anyBoolean(),
-                ArgumentMatchers.anyBoolean(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.anyBoolean()
-            )
+            .closeControls(any<Boolean>(), any<Boolean>(), any<Int>(), any<Int>(), any<Boolean>())
 
         // called again by mGutsManager.bindGuts(), in mGutsManager.onDensityOrFontScaleChanged()
-        verify(row, Mockito.times(2)).setGutsView(ArgumentMatchers.any())
+        verify(row, times(2)).setGutsView(any())
     }
 
     @Test
     fun testAppOpsSettingsIntent_camera() {
         val ops = ArraySet<Int>()
         ops.add(AppOpsManager.OP_CAMERA)
-        gutsManager.startAppOpsSettingsActivity("", 0, ops, null)
-        val captor = ArgumentCaptor.forClass(Intent::class.java)
-        verify(notificationActivityStarter, Mockito.times(1))
-            .startNotificationGutsIntent(
-                captor.capture(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.any()
-            )
-        assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.value.action)
+        gutsManager.startAppOpsSettingsActivity("", 0, ops, mock<ExpandableNotificationRow>())
+        val captor = argumentCaptor<Intent>()
+        verify(notificationActivityStarter, times(1))
+            .startNotificationGutsIntent(captor.capture(), any<Int>(), any())
+        assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.lastValue.action)
     }
 
     @Test
     fun testAppOpsSettingsIntent_mic() {
         val ops = ArraySet<Int>()
         ops.add(AppOpsManager.OP_RECORD_AUDIO)
-        gutsManager.startAppOpsSettingsActivity("", 0, ops, null)
-        val captor = ArgumentCaptor.forClass(Intent::class.java)
-        verify(notificationActivityStarter, Mockito.times(1))
-            .startNotificationGutsIntent(
-                captor.capture(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.any()
-            )
-        assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.value.action)
+        gutsManager.startAppOpsSettingsActivity("", 0, ops, mock<ExpandableNotificationRow>())
+        val captor = argumentCaptor<Intent>()
+        verify(notificationActivityStarter, times(1))
+            .startNotificationGutsIntent(captor.capture(), any<Int>(), any())
+        assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.lastValue.action)
     }
 
     @Test
@@ -400,30 +358,22 @@
         val ops = ArraySet<Int>()
         ops.add(AppOpsManager.OP_CAMERA)
         ops.add(AppOpsManager.OP_RECORD_AUDIO)
-        gutsManager.startAppOpsSettingsActivity("", 0, ops, null)
-        val captor = ArgumentCaptor.forClass(Intent::class.java)
-        verify(notificationActivityStarter, Mockito.times(1))
-            .startNotificationGutsIntent(
-                captor.capture(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.any()
-            )
-        assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.value.action)
+        gutsManager.startAppOpsSettingsActivity("", 0, ops, mock<ExpandableNotificationRow>())
+        val captor = argumentCaptor<Intent>()
+        verify(notificationActivityStarter, times(1))
+            .startNotificationGutsIntent(captor.capture(), any<Int>(), any())
+        assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.lastValue.action)
     }
 
     @Test
     fun testAppOpsSettingsIntent_overlay() {
         val ops = ArraySet<Int>()
         ops.add(AppOpsManager.OP_SYSTEM_ALERT_WINDOW)
-        gutsManager.startAppOpsSettingsActivity("", 0, ops, null)
-        val captor = ArgumentCaptor.forClass(Intent::class.java)
-        verify(notificationActivityStarter, Mockito.times(1))
-            .startNotificationGutsIntent(
-                captor.capture(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.any()
-            )
-        assertEquals(Settings.ACTION_MANAGE_APP_OVERLAY_PERMISSION, captor.value.action)
+        gutsManager.startAppOpsSettingsActivity("", 0, ops, mock<ExpandableNotificationRow>())
+        val captor = argumentCaptor<Intent>()
+        verify(notificationActivityStarter, times(1))
+            .startNotificationGutsIntent(captor.capture(), any<Int>(), any())
+        assertEquals(Settings.ACTION_MANAGE_APP_OVERLAY_PERMISSION, captor.lastValue.action)
     }
 
     @Test
@@ -432,15 +382,11 @@
         ops.add(AppOpsManager.OP_CAMERA)
         ops.add(AppOpsManager.OP_RECORD_AUDIO)
         ops.add(AppOpsManager.OP_SYSTEM_ALERT_WINDOW)
-        gutsManager.startAppOpsSettingsActivity("", 0, ops, null)
-        val captor = ArgumentCaptor.forClass(Intent::class.java)
-        verify(notificationActivityStarter, Mockito.times(1))
-            .startNotificationGutsIntent(
-                captor.capture(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.any()
-            )
-        assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.value.action)
+        gutsManager.startAppOpsSettingsActivity("", 0, ops, mock<ExpandableNotificationRow>())
+        val captor = argumentCaptor<Intent>()
+        verify(notificationActivityStarter, times(1))
+            .startNotificationGutsIntent(captor.capture(), any<Int>(), any())
+        assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.lastValue.action)
     }
 
     @Test
@@ -448,15 +394,11 @@
         val ops = ArraySet<Int>()
         ops.add(AppOpsManager.OP_CAMERA)
         ops.add(AppOpsManager.OP_SYSTEM_ALERT_WINDOW)
-        gutsManager.startAppOpsSettingsActivity("", 0, ops, null)
-        val captor = ArgumentCaptor.forClass(Intent::class.java)
-        verify(notificationActivityStarter, Mockito.times(1))
-            .startNotificationGutsIntent(
-                captor.capture(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.any()
-            )
-        assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.value.action)
+        gutsManager.startAppOpsSettingsActivity("", 0, ops, mock<ExpandableNotificationRow>())
+        val captor = argumentCaptor<Intent>()
+        verify(notificationActivityStarter, times(1))
+            .startNotificationGutsIntent(captor.capture(), any<Int>(), any())
+        assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.lastValue.action)
     }
 
     @Test
@@ -464,112 +406,108 @@
         val ops = ArraySet<Int>()
         ops.add(AppOpsManager.OP_RECORD_AUDIO)
         ops.add(AppOpsManager.OP_SYSTEM_ALERT_WINDOW)
-        gutsManager.startAppOpsSettingsActivity("", 0, ops, null)
-        val captor = ArgumentCaptor.forClass(Intent::class.java)
-        verify(notificationActivityStarter, Mockito.times(1))
-            .startNotificationGutsIntent(
-                captor.capture(),
-                ArgumentMatchers.anyInt(),
-                ArgumentMatchers.any()
-            )
-        assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.value.action)
+        gutsManager.startAppOpsSettingsActivity("", 0, ops, mock<ExpandableNotificationRow>())
+        val captor = argumentCaptor<Intent>()
+        verify(notificationActivityStarter, times(1))
+            .startNotificationGutsIntent(captor.capture(), any<Int>(), any())
+        assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.lastValue.action)
     }
 
     @Test
     @Throws(Exception::class)
     fun testInitializeNotificationInfoView_highPriority() {
-        val notificationInfoView = Mockito.mock(NotificationInfo::class.java)
-        val row = Mockito.spy(helper.createRow())
+        val notificationInfoView = mock<NotificationInfo>()
+        val row = spy(helper.createRow())
         val entry = row.entry
         NotificationEntryHelper.modifyRanking(entry)
             .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE)
             .setImportance(NotificationManager.IMPORTANCE_HIGH)
             .build()
-        Mockito.`when`(row.getIsNonblockable()).thenReturn(false)
-        Mockito.`when`(highPriorityProvider.isHighPriority(entry)).thenReturn(true)
+        whenever(row.getIsNonblockable()).thenReturn(false)
+        whenever(highPriorityProvider.isHighPriority(entry)).thenReturn(true)
         val statusBarNotification = entry.sbn
         gutsManager.initializeNotificationInfo(row, notificationInfoView)
         verify(notificationInfoView)
             .bindNotification(
-                ArgumentMatchers.any(PackageManager::class.java),
-                ArgumentMatchers.any(INotificationManager::class.java),
-                ArgumentMatchers.eq(onUserInteractionCallback),
-                ArgumentMatchers.eq(channelEditorDialogController),
-                ArgumentMatchers.eq(statusBarNotification.packageName),
-                ArgumentMatchers.any(NotificationChannel::class.java),
-                ArgumentMatchers.eq(entry),
-                ArgumentMatchers.any(NotificationInfo.OnSettingsClickListener::class.java),
-                ArgumentMatchers.any(NotificationInfo.OnAppSettingsClickListener::class.java),
-                ArgumentMatchers.any(UiEventLogger::class.java),
-                ArgumentMatchers.eq(true),
-                ArgumentMatchers.eq(false),
-                ArgumentMatchers.eq(true), /* wasShownHighPriority */
-                ArgumentMatchers.eq(assistantFeedbackController),
-                ArgumentMatchers.any(MetricsLogger::class.java)
+                any<PackageManager>(),
+                any<INotificationManager>(),
+                eq(onUserInteractionCallback),
+                eq(channelEditorDialogController),
+                eq(statusBarNotification.packageName),
+                any<NotificationChannel>(),
+                eq(entry),
+                any<NotificationInfo.OnSettingsClickListener>(),
+                any<NotificationInfo.OnAppSettingsClickListener>(),
+                any<UiEventLogger>(),
+                eq(true),
+                eq(false),
+                eq(true), /* wasShownHighPriority */
+                eq(assistantFeedbackController),
+                any<MetricsLogger>(),
             )
     }
 
     @Test
     @Throws(Exception::class)
     fun testInitializeNotificationInfoView_PassesAlongProvisionedState() {
-        val notificationInfoView = Mockito.mock(NotificationInfo::class.java)
-        val row = Mockito.spy(helper.createRow())
+        val notificationInfoView = mock<NotificationInfo>()
+        val row = spy(helper.createRow())
         NotificationEntryHelper.modifyRanking(row.entry)
             .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE)
             .build()
-        Mockito.`when`(row.getIsNonblockable()).thenReturn(false)
+        whenever(row.getIsNonblockable()).thenReturn(false)
         val statusBarNotification = row.entry.sbn
         val entry = row.entry
         gutsManager.initializeNotificationInfo(row, notificationInfoView)
         verify(notificationInfoView)
             .bindNotification(
-                ArgumentMatchers.any(PackageManager::class.java),
-                ArgumentMatchers.any(INotificationManager::class.java),
-                ArgumentMatchers.eq(onUserInteractionCallback),
-                ArgumentMatchers.eq(channelEditorDialogController),
-                ArgumentMatchers.eq(statusBarNotification.packageName),
-                ArgumentMatchers.any(NotificationChannel::class.java),
-                ArgumentMatchers.eq(entry),
-                ArgumentMatchers.any(NotificationInfo.OnSettingsClickListener::class.java),
-                ArgumentMatchers.any(NotificationInfo.OnAppSettingsClickListener::class.java),
-                ArgumentMatchers.any(UiEventLogger::class.java),
-                ArgumentMatchers.eq(true),
-                ArgumentMatchers.eq(false),
-                ArgumentMatchers.eq(false), /* wasShownHighPriority */
-                ArgumentMatchers.eq(assistantFeedbackController),
-                ArgumentMatchers.any(MetricsLogger::class.java)
+                any<PackageManager>(),
+                any<INotificationManager>(),
+                eq(onUserInteractionCallback),
+                eq(channelEditorDialogController),
+                eq(statusBarNotification.packageName),
+                any<NotificationChannel>(),
+                eq(entry),
+                any<NotificationInfo.OnSettingsClickListener>(),
+                any<NotificationInfo.OnAppSettingsClickListener>(),
+                any<UiEventLogger>(),
+                eq(true),
+                eq(false),
+                eq(false), /* wasShownHighPriority */
+                eq(assistantFeedbackController),
+                any<MetricsLogger>(),
             )
     }
 
     @Test
     @Throws(Exception::class)
     fun testInitializeNotificationInfoView_withInitialAction() {
-        val notificationInfoView = Mockito.mock(NotificationInfo::class.java)
-        val row = Mockito.spy(helper.createRow())
+        val notificationInfoView = mock<NotificationInfo>()
+        val row = spy(helper.createRow())
         NotificationEntryHelper.modifyRanking(row.entry)
             .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE)
             .build()
-        Mockito.`when`(row.getIsNonblockable()).thenReturn(false)
+        whenever(row.getIsNonblockable()).thenReturn(false)
         val statusBarNotification = row.entry.sbn
         val entry = row.entry
         gutsManager.initializeNotificationInfo(row, notificationInfoView)
         verify(notificationInfoView)
             .bindNotification(
-                ArgumentMatchers.any(PackageManager::class.java),
-                ArgumentMatchers.any(INotificationManager::class.java),
-                ArgumentMatchers.eq(onUserInteractionCallback),
-                ArgumentMatchers.eq(channelEditorDialogController),
-                ArgumentMatchers.eq(statusBarNotification.packageName),
-                ArgumentMatchers.any(NotificationChannel::class.java),
-                ArgumentMatchers.eq(entry),
-                ArgumentMatchers.any(NotificationInfo.OnSettingsClickListener::class.java),
-                ArgumentMatchers.any(NotificationInfo.OnAppSettingsClickListener::class.java),
-                ArgumentMatchers.any(UiEventLogger::class.java),
-                ArgumentMatchers.eq(true),
-                ArgumentMatchers.eq(false),
-                ArgumentMatchers.eq(false), /* wasShownHighPriority */
-                ArgumentMatchers.eq(assistantFeedbackController),
-                ArgumentMatchers.any(MetricsLogger::class.java)
+                any<PackageManager>(),
+                any<INotificationManager>(),
+                eq(onUserInteractionCallback),
+                eq(channelEditorDialogController),
+                eq(statusBarNotification.packageName),
+                any<NotificationChannel>(),
+                eq(entry),
+                any<NotificationInfo.OnSettingsClickListener>(),
+                any<NotificationInfo.OnAppSettingsClickListener>(),
+                any<UiEventLogger>(),
+                eq(true),
+                eq(false),
+                eq(false), /* wasShownHighPriority */
+                eq(assistantFeedbackController),
+                any<MetricsLogger>(),
             )
     }
 
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 9593dfb..8c4ec4c 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
@@ -28,8 +28,6 @@
 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.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
-import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor.ConfigurationBasedDimensions
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import kotlinx.coroutines.CoroutineScope
@@ -56,7 +54,6 @@
         fromGoneTransitionInteractor: FromGoneTransitionInteractor = mock(),
         fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor = mock(),
         fromOccludedTransitionInteractor: FromOccludedTransitionInteractor = mock(),
-        sharedNotificationContainerInteractor: SharedNotificationContainerInteractor? = null,
         powerInteractor: PowerInteractor = PowerInteractorFactory.create().powerInteractor,
         testScope: CoroutineScope = TestScope(),
     ): WithDependencies {
@@ -69,23 +66,6 @@
                 whenever(it.transitionState).thenReturn(transitionStateFlow)
                 whenever(it.isFinishedIn(any(), any())).thenReturn(MutableStateFlow(false))
             }
-        val configurationDimensionFlow = MutableSharedFlow<ConfigurationBasedDimensions>()
-        configurationDimensionFlow.tryEmit(
-            ConfigurationBasedDimensions(
-                useSplitShade = false,
-                useLargeScreenHeader = false,
-                marginHorizontal = 0,
-                marginBottom = 0,
-                marginTop = 0,
-                marginTopLargeScreen = 0,
-                keyguardSplitShadeTopMargin = 0,
-            )
-        )
-        val sncInteractor =
-            sharedNotificationContainerInteractor
-                ?: mock<SharedNotificationContainerInteractor>().also {
-                    whenever(it.configurationBasedDimensions).thenReturn(configurationDimensionFlow)
-                }
         return WithDependencies(
             repository = repository,
             featureFlags = featureFlags,
@@ -104,7 +84,6 @@
                 fromGoneTransitionInteractor = { fromGoneTransitionInteractor },
                 fromLockscreenTransitionInteractor = { fromLockscreenTransitionInteractor },
                 fromOccludedTransitionInteractor = { fromOccludedTransitionInteractor },
-                sharedNotificationContainerInteractor = { sncInteractor },
                 applicationScope = testScope,
             ),
         )
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 e85114d..da261bf 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
@@ -24,7 +24,6 @@
 import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.shade.data.repository.shadeRepository
-import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
 
 val Kosmos.keyguardInteractor: KeyguardInteractor by
     Kosmos.Fixture {
@@ -39,7 +38,6 @@
             fromGoneTransitionInteractor = { fromGoneTransitionInteractor },
             fromLockscreenTransitionInteractor = { fromLockscreenTransitionInteractor },
             fromOccludedTransitionInteractor = { fromOccludedTransitionInteractor },
-            sharedNotificationContainerInteractor = { sharedNotificationContainerInteractor },
             applicationScope = testScope.backgroundScope,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
index 60141c6..6944e6c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
@@ -187,6 +187,7 @@
         context
             .getOrCreateTestableResources()
             .addOverride(R.bool.config_use_split_notification_shade, splitShade)
+        shadeRepository.setShadeLayoutWide(splitShade)
         testScope.runCurrent()
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
index 92075ea..39f58ae 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
@@ -26,7 +26,6 @@
 import com.android.systemui.shade.ShadeModule
 import com.android.systemui.shade.data.repository.shadeRepository
 import com.android.systemui.statusbar.disableflags.data.repository.disableFlagsRepository
-import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
 import com.android.systemui.statusbar.phone.dozeParameters
 import com.android.systemui.statusbar.policy.data.repository.userSetupRepository
 import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor
@@ -52,7 +51,6 @@
         ShadeInteractorLegacyImpl(
             scope = applicationCoroutineScope,
             keyguardRepository = keyguardRepository,
-            sharedNotificationContainerInteractor = sharedNotificationContainerInteractor,
             repository = shadeRepository,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
index 7f4c670..c3996e40 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
@@ -85,7 +85,6 @@
 import com.android.systemui.util.Assert.runWithCurrentThreadAsMainThread
 import com.android.systemui.util.DeviceConfigProxyFake
 import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.time.FakeSystemClock
 import com.android.systemui.wmshell.BubblesManager
@@ -126,6 +125,7 @@
     private val mMainCoroutineContext = mTestScope.coroutineContext
     private val mFakeSystemClock = FakeSystemClock()
     private val mMainExecutor = FakeExecutor(mFakeSystemClock)
+    private val mDumpManager = DumpManager()
 
     init {
         featureFlags.setDefault(Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE)
@@ -142,8 +142,7 @@
         mGroupMembershipManager = GroupMembershipManagerImpl()
         mSmartReplyController = Mockito.mock(SmartReplyController::class.java, STUB_ONLY)
 
-        val dumpManager = DumpManager()
-        mGroupExpansionManager = GroupExpansionManagerImpl(dumpManager, mGroupMembershipManager)
+        mGroupExpansionManager = GroupExpansionManagerImpl(mDumpManager, mGroupMembershipManager)
         mHeadsUpManager = Mockito.mock(HeadsUpManager::class.java, STUB_ONLY)
         mIconManager =
             IconManager(
@@ -289,8 +288,8 @@
             NotificationOptimizedLinearLayoutFactory(),
             { Mockito.mock(NotificationViewFlipperFactory::class.java) },
             NotificationRowIconViewInflaterFactory(
-                AppIconProviderImpl(context),
-                NotificationIconStyleProviderImpl(),
+                AppIconProviderImpl(context, mDumpManager),
+                NotificationIconStyleProviderImpl(mDumpManager),
             ),
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/AppIconProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/AppIconProviderKosmos.kt
index 08c6bba..0fd0f14 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/AppIconProviderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/AppIconProviderKosmos.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.statusbar.notification.row.icon
 
 import android.content.applicationContext
+import com.android.systemui.dump.dumpManager
 import com.android.systemui.kosmos.Kosmos
 
-val Kosmos.appIconProvider by Kosmos.Fixture { AppIconProviderImpl(applicationContext) }
+val Kosmos.appIconProvider by
+    Kosmos.Fixture { AppIconProviderImpl(applicationContext, dumpManager) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProviderKosmos.kt
index 611c90a..0fe84fb 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProviderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProviderKosmos.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.notification.row.icon
 
+import com.android.systemui.dump.dumpManager
 import com.android.systemui.kosmos.Kosmos
 
-val Kosmos.notificationIconStyleProvider by Kosmos.Fixture { NotificationIconStyleProviderImpl() }
+val Kosmos.notificationIconStyleProvider by
+    Kosmos.Fixture { NotificationIconStyleProviderImpl(dumpManager) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorKosmos.kt
index 3234e66..83fc3e9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorKosmos.kt
@@ -14,26 +14,29 @@
  * limitations under the License.
  */
 
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
 package com.android.systemui.statusbar.notification.stack.domain.interactor
 
 import android.content.applicationContext
-import com.android.systemui.common.ui.data.repository.configurationRepository
+import com.android.systemui.common.ui.domain.interactor.configurationInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.shade.largeScreenHeaderHelper
 import com.android.systemui.statusbar.policy.splitShadeStateController
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 
 val Kosmos.sharedNotificationContainerInteractor by
     Kosmos.Fixture {
         SharedNotificationContainerInteractor(
-            configurationRepository = configurationRepository,
             context = applicationContext,
             splitShadeStateController = { splitShadeStateController },
             shadeInteractor = { shadeInteractor },
+            configurationInteractor = configurationInteractor,
             keyguardInteractor = keyguardInteractor,
             deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
-            largeScreenHeaderHelperLazy = { largeScreenHeaderHelper }
+            largeScreenHeaderHelperLazy = { largeScreenHeaderHelper },
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeConfigurationController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeConfigurationController.kt
index 6be13be..3219127 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeConfigurationController.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeConfigurationController.kt
@@ -23,7 +23,7 @@
         listeners -= listener
     }
 
-    override fun onConfigurationChanged(newConfiguration: Configuration?) {
+    override fun onConfigurationChanged(newConfiguration: Configuration) {
         listeners.forEach { it.onConfigChanged(newConfiguration) }
     }
 
@@ -36,7 +36,7 @@
     }
 
     fun notifyConfigurationChanged() {
-        onConfigurationChanged(newConfiguration = null)
+        onConfigurationChanged(newConfiguration = Configuration())
     }
 
     fun notifyLayoutDirectionChanged(isRtl: Boolean) {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt
index bca13c6..65247a5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt
@@ -24,6 +24,6 @@
     override fun create(
         context: Context,
         viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager,
-        statusBarConfigurationController: StatusBarConfigurationController
+        statusBarConfigurationController: StatusBarConfigurationController,
     ) = FakeStatusBarWindowController()
 }
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
new file mode 100644
index 0000000..c2a1544
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.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.ringer.domain
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.plugins.volumeDialogController
+import com.android.systemui.volume.dialog.domain.interactor.volumeDialogStateInteractor
+
+val Kosmos.volumeDialogRingerInteractor by
+    Kosmos.Fixture {
+        VolumeDialogRingerInteractor(
+            coroutineScope = applicationCoroutineScope,
+            volumeDialogStateInteractor = volumeDialogStateInteractor,
+            controller = volumeDialogController,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelKosmos.kt
new file mode 100644
index 0000000..db1c01a
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelKosmos.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.ringer.ui.viewmodel
+
+import com.android.systemui.haptics.vibratorHelper
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.volume.dialog.ringer.domain.volumeDialogRingerInteractor
+import com.android.systemui.volume.dialog.shared.volumeDialogLogger
+
+val Kosmos.volumeDialogRingerDrawerViewModel by
+    Kosmos.Fixture {
+        VolumeDialogRingerDrawerViewModel(
+            backgroundDispatcher = testDispatcher,
+            coroutineScope = applicationCoroutineScope,
+            interactor = volumeDialogRingerInteractor,
+            vibrator = vibratorHelper,
+            volumeDialogLogger = volumeDialogLogger,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/VolumeDialogRingerRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/shared/VolumeDialogLoggerKosmos.kt
similarity index 77%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/VolumeDialogRingerRepositoryKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/shared/VolumeDialogLoggerKosmos.kt
index 2c51886..f9d4a99 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/VolumeDialogRingerRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/shared/VolumeDialogLoggerKosmos.kt
@@ -14,8 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.systemui.volume.dialog.ringer.data
+package com.android.systemui.volume.dialog.shared
 
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.logcatLogBuffer
 
-val Kosmos.volumeDialogRingerRepository by Kosmos.Fixture { VolumeDialogRingerRepository() }
+val Kosmos.volumeDialogLogger by Kosmos.Fixture { VolumeDialogLogger(logcatLogBuffer()) }
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index 39ac515..363807d 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -68,6 +68,7 @@
 import android.telephony.DisconnectCause;
 import android.telephony.LinkCapacityEstimate;
 import android.telephony.LocationAccessPolicy;
+import android.telephony.NetworkRegistrationInfo;
 import android.telephony.PhoneCapability;
 import android.telephony.PhoneStateListener;
 import android.telephony.PhysicalChannelConfig;
@@ -90,6 +91,7 @@
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.IntArray;
 import android.util.LocalLog;
 import android.util.Pair;
 import android.util.SparseArray;
@@ -429,6 +431,8 @@
     private boolean[] mCarrierRoamingNtnMode = null;
     private boolean[] mCarrierRoamingNtnEligible = null;
 
+    private List<IntArray> mCarrierRoamingNtnAvailableServices;
+
     /**
      * Per-phone map of precise data connection state. The key of the map is the pair of transport
      * type and APN setting. This is the cache to prevent redundant callbacks to the listeners.
@@ -741,6 +745,7 @@
                 cutListToSize(mCarrierServiceStates, mNumPhones);
                 cutListToSize(mCallStateLists, mNumPhones);
                 cutListToSize(mMediaQualityStatus, mNumPhones);
+                cutListToSize(mCarrierRoamingNtnAvailableServices, mNumPhones);
                 return;
             }
 
@@ -789,6 +794,7 @@
                 mSCBMDuration[i] = 0;
                 mCarrierRoamingNtnMode[i] = false;
                 mCarrierRoamingNtnEligible[i] = false;
+                mCarrierRoamingNtnAvailableServices.add(i, new IntArray());
             }
         }
     }
@@ -864,6 +870,7 @@
         mSCBMDuration = new long[numPhones];
         mCarrierRoamingNtnMode = new boolean[numPhones];
         mCarrierRoamingNtnEligible = new boolean[numPhones];
+        mCarrierRoamingNtnAvailableServices = new ArrayList<>();
 
         for (int i = 0; i < numPhones; i++) {
             mCallState[i] =  TelephonyManager.CALL_STATE_IDLE;
@@ -909,6 +916,7 @@
             mSCBMDuration[i] = 0;
             mCarrierRoamingNtnMode[i] = false;
             mCarrierRoamingNtnEligible[i] = false;
+            mCarrierRoamingNtnAvailableServices.add(i, new IntArray());
         }
 
         mAppOps = mContext.getSystemService(AppOpsManager.class);
@@ -1533,6 +1541,15 @@
                         remove(r.binder);
                     }
                 }
+                if (events.contains(
+                        TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_AVAILABLE_SERVICES_CHANGED)) {
+                    try {
+                        r.callback.onCarrierRoamingNtnAvailableServicesChanged(
+                                mCarrierRoamingNtnAvailableServices.get(r.phoneId).toArray());
+                    } catch (RemoteException ex) {
+                        remove(r.binder);
+                    }
+                }
             }
         }
     }
@@ -3642,6 +3659,47 @@
         }
     }
 
+    /**
+     * Notify external listeners that carrier roaming non-terrestrial available services changed.
+     * @param availableServices The list of the supported services.
+     */
+    public void notifyCarrierRoamingNtnAvailableServicesChanged(
+            int subId, @NetworkRegistrationInfo.ServiceType int[] availableServices) {
+        if (!checkNotifyPermission("notifyCarrierRoamingNtnEligibleStateChanged")) {
+            log("notifyCarrierRoamingNtnAvailableServicesChanged: caller does not have required "
+                    + "permissions.");
+            return;
+        }
+
+        if (VDBG) {
+            log("notifyCarrierRoamingNtnAvailableServicesChanged: "
+                    + "availableServices=" + Arrays.toString(availableServices));
+        }
+
+        synchronized (mRecords) {
+            int phoneId = getPhoneIdFromSubId(subId);
+            if (!validatePhoneId(phoneId)) {
+                loge("Invalid phone ID " + phoneId + " for " + subId);
+                return;
+            }
+            IntArray availableServicesIntArray = new IntArray(availableServices.length);
+            availableServicesIntArray.addAll(availableServices);
+            mCarrierRoamingNtnAvailableServices.set(phoneId, availableServicesIntArray);
+            for (Record r : mRecords) {
+                if (r.matchTelephonyCallbackEvent(
+                        TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_AVAILABLE_SERVICES_CHANGED)
+                        && idMatch(r, subId, phoneId)) {
+                    try {
+                        r.callback.onCarrierRoamingNtnAvailableServicesChanged(availableServices);
+                    } catch (RemoteException ex) {
+                        mRemoveList.add(r.binder);
+                    }
+                }
+            }
+            handleRemoveListLocked();
+        }
+    }
+
     @NeverCompile // Avoid size overhead of debugging code.
     @Override
     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
@@ -3706,6 +3764,8 @@
                 Pair<String, Integer> carrierServiceState = mCarrierServiceStates.get(i);
                 pw.println("mCarrierServiceState=<package=" + pii(carrierServiceState.first)
                         + ", uid=" + carrierServiceState.second + ">");
+                pw.println("mCarrierRoamingNtnAvailableServices="
+                        + mCarrierRoamingNtnAvailableServices.get(i));
                 pw.decreaseIndent();
             }
 
diff --git a/services/core/java/com/android/server/TradeInModeService.java b/services/core/java/com/android/server/TradeInModeService.java
new file mode 100644
index 0000000..9ad550b
--- /dev/null
+++ b/services/core/java/com/android/server/TradeInModeService.java
@@ -0,0 +1,342 @@
+/*
+ * 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;
+
+import static com.android.tradeinmode.flags.Flags.enableTradeInMode;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.OnAccountsUpdateListener;
+import android.annotation.RequiresPermission;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.ITradeInMode;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.service.persistentdata.PersistentDataBlockManager;
+import android.util.Slog;
+
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+
+public final class TradeInModeService extends SystemService {
+    private static final String TAG = "TradeInModeService";
+
+    private static final String TIM_PROP = "persist.adb.tradeinmode";
+
+    private static final int TIM_STATE_UNSET = 0;
+
+    // adbd_tradeinmode was stopped.
+    private static final int TIM_STATE_DISABLED = -1;
+
+    // adbd_tradeinmode has started.
+    private static final int TIM_STATE_FOYER = 1;
+
+    // Full non-root adb granted; factory reset is guaranteed.
+    private static final int TIM_STATE_EVALUATION_MODE = 2;
+
+    // This file contains a single integer counter of how many boot attempts
+    // have been made since entering evaluation mode.
+    private static final String WIPE_INDICATOR_FILE = "/metadata/tradeinmode/wipe";
+
+    private final Context mContext;
+    private TradeInMode mTradeInMode;
+
+    private ConnectivityManager mConnectivityManager;
+    private ConnectivityManager.NetworkCallback mNetworkCallback = null;
+
+    private AccountManager mAccountManager;
+    private OnAccountsUpdateListener mAccountsListener = null;
+
+    public TradeInModeService(Context context) {
+        super(context);
+
+        mContext = context;
+    }
+
+    @Override
+    public void onStart() {
+        if (!enableTradeInMode()) {
+            return;
+        }
+
+        mTradeInMode = new TradeInMode();
+        publishBinderService("tradeinmode", mTradeInMode);
+    }
+
+    @Override
+    public void onBootPhase(@BootPhase int phase) {
+        if (phase == PHASE_SYSTEM_SERVICES_READY) {
+            final int state = getTradeInModeState();
+
+            if (isAdbEnabled() && !isDebuggable() && !isDeviceSetup()
+                    && state == TIM_STATE_DISABLED) {
+                // If we fail to start trade-in mode, the persist property may linger
+                // past reboot. If we detect this, disable ADB and clear TIM state.
+                Slog.i(TAG, "Resetting trade-in mode state.");
+                SystemProperties.set(TIM_PROP, "");
+
+                final ContentResolver cr = mContext.getContentResolver();
+                Settings.Global.putInt(cr, Settings.Global.ADB_ENABLED, 0);
+            } else if (state == TIM_STATE_FOYER) {
+                // If zygote crashed or we rebooted, and TIM is still enabled, make
+                // sure it's allowed to be enabled. If it is, we need to re-add our
+                // setup completion observer.
+                if (isDeviceSetup()) {
+                    stopTradeInMode();
+                } else {
+                    watchForSetupCompletion();
+                }
+            }
+        }
+    }
+
+    private final class TradeInMode extends ITradeInMode.Stub {
+        @Override
+        @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE)
+        public boolean start() {
+            mContext.enforceCallingOrSelfPermission("android.permission.ENTER_TRADE_IN_MODE",
+                                                    "Cannot enter trade-in mode foyer");
+            final int state = getTradeInModeState();
+            if (state == TIM_STATE_FOYER) {
+                return true;
+            }
+
+            if (state != TIM_STATE_UNSET) {
+                Slog.e(TAG, "Cannot enter trade-in mode in state: " + state);
+                return false;
+            }
+
+            if (isDeviceSetup()) {
+                Slog.i(TAG, "Not starting trade-in mode, device is setup.");
+                return false;
+            }
+            if (SystemProperties.getInt("ro.debuggable", 0) == 1) {
+                // We don't want to force adbd into TIM on debug builds.
+                Slog.e(TAG, "Not starting trade-in mode, device is debuggable.");
+                return false;
+            }
+            if (isAdbEnabled()) {
+                Slog.e(TAG, "Not starting trade-in mode, adb is already enabled.");
+                return false;
+            }
+
+            final long callingId = Binder.clearCallingIdentity();
+            try {
+                startTradeInMode();
+            } finally {
+                Binder.restoreCallingIdentity(callingId);
+            }
+            return true;
+        }
+
+        @Override
+        @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE)
+        public boolean enterEvaluationMode() {
+            mContext.enforceCallingOrSelfPermission("android.permission.ENTER_TRADE_IN_MODE",
+                                                    "Cannot enter trade-in evaluation mode");
+            final int state = getTradeInModeState();
+            if (state != TIM_STATE_FOYER) {
+                Slog.e(TAG, "Cannot enter evaluation mode in state: " + state);
+                return false;
+            }
+            if (isFrpActive()) {
+                Slog.e(TAG, "Cannot enter evaluation mode, FRP lock is present.");
+                return false;
+            }
+
+            try (FileWriter fw = new FileWriter(WIPE_INDICATOR_FILE,
+                                                StandardCharsets.US_ASCII)) {
+                fw.write("0");
+            } catch (IOException e) {
+                Slog.e(TAG, "Failed to write " + WIPE_INDICATOR_FILE, e);
+                return false;
+            }
+
+            final long callingId = Binder.clearCallingIdentity();
+            try {
+                removeNetworkWatch();
+                removeAccountsWatch();
+            } finally {
+                Binder.restoreCallingIdentity(callingId);
+            }
+
+            SystemProperties.set(TIM_PROP, Integer.toString(TIM_STATE_EVALUATION_MODE));
+            SystemProperties.set("ctl.restart", "adbd");
+            return true;
+        }
+
+        @Override
+        @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE)
+        public boolean isEvaluationModeAllowed() {
+            mContext.enforceCallingOrSelfPermission("android.permission.ENTER_TRADE_IN_MODE",
+                                        "Cannot test for trade-in evaluation mode allowed");
+            return !isFrpActive();
+        }
+    }
+
+    private void startTradeInMode() {
+        Slog.i(TAG, "Enabling trade-in mode.");
+
+        SystemProperties.set(TIM_PROP, Integer.toString(TIM_STATE_FOYER));
+
+        final ContentResolver cr = mContext.getContentResolver();
+        Settings.Global.putInt(cr, Settings.Global.ADB_ENABLED, 1);
+
+        watchForSetupCompletion();
+        watchForNetworkChange();
+        watchForAccountsCreated();
+    }
+
+    private void stopTradeInMode() {
+        Slog.i(TAG, "Stopping trade-in mode.");
+
+        SystemProperties.set(TIM_PROP, Integer.toString(TIM_STATE_DISABLED));
+
+        removeNetworkWatch();
+        removeAccountsWatch();
+
+        final ContentResolver cr = mContext.getContentResolver();
+        Settings.Global.putInt(cr, Settings.Global.ADB_ENABLED, 0);
+    }
+
+    private int getTradeInModeState() {
+        return SystemProperties.getInt(TIM_PROP, TIM_STATE_UNSET);
+    }
+
+    private boolean isDebuggable() {
+        return SystemProperties.getInt("ro.debuggable", 0) == 1;
+    }
+
+    private boolean isAdbEnabled() {
+        final ContentResolver cr = mContext.getContentResolver();
+        return Settings.Global.getInt(cr, Settings.Global.ADB_ENABLED, 0) == 1;
+    }
+
+    private boolean isFrpActive() {
+        try {
+            PersistentDataBlockManager pdb =
+                    mContext.getSystemService(PersistentDataBlockManager.class);
+            if (pdb == null) {
+                return false;
+            }
+            return pdb.isFactoryResetProtectionActive();
+        } catch (Exception e) {
+            Slog.e(TAG, "Could not read PDB", e);
+            return false;
+        }
+    }
+
+    // This returns true if the device has progressed far enough into Setup Wizard that it no
+    // longer makes sense to enable trade-in mode. As a last stop, we check the SUW completion
+    // bits.
+    private boolean isDeviceSetup() {
+        final ContentResolver cr = mContext.getContentResolver();
+        try {
+            if (Settings.Secure.getIntForUser(cr, Settings.Secure.USER_SETUP_COMPLETE, 0) != 0) {
+                return true;
+            }
+        } catch (SettingNotFoundException e) {
+            Slog.e(TAG, "Could not find USER_SETUP_COMPLETE setting", e);
+        }
+
+        if (Settings.Global.getInt(cr, Settings.Global.DEVICE_PROVISIONED, 0) != 0) {
+            return true;
+        }
+        return false;
+    }
+
+    private final class SettingsObserver extends ContentObserver {
+        SettingsObserver() {
+            super(null);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            if (getTradeInModeState() == TIM_STATE_FOYER && isDeviceSetup()) {
+                stopTradeInMode();
+            }
+        }
+    }
+
+    private void watchForSetupCompletion() {
+        final Uri userSetupComplete = Settings.Secure.getUriFor(
+                Settings.Secure.USER_SETUP_COMPLETE);
+        final Uri deviceProvisioned = Settings.Global.getUriFor(
+                Settings.Global.DEVICE_PROVISIONED);
+        final ContentResolver cr = mContext.getContentResolver();
+        final SettingsObserver observer = new SettingsObserver();
+
+        cr.registerContentObserver(userSetupComplete, false, observer);
+        cr.registerContentObserver(deviceProvisioned, false, observer);
+    }
+
+
+    private void watchForNetworkChange() {
+        mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
+        NetworkRequest networkRequest = new NetworkRequest.Builder()
+                    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                    .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                    .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+                    .build();
+
+        mNetworkCallback = new ConnectivityManager.NetworkCallback() {
+            @Override
+            public void onAvailable(Network network) {
+                super.onAvailable(network);
+                stopTradeInMode();
+            }
+        };
+
+        mConnectivityManager.registerNetworkCallback(networkRequest, mNetworkCallback);
+    }
+
+    private void removeNetworkWatch() {
+        if (mNetworkCallback != null) {
+            mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
+            mNetworkCallback = null;
+        }
+    }
+
+    private void watchForAccountsCreated() {
+        mAccountManager = mContext.getSystemService(AccountManager.class);
+        mAccountsListener = new OnAccountsUpdateListener() {
+            @Override
+            public void onAccountsUpdated(Account[] accounts) {
+                stopTradeInMode();
+            }
+        };
+        mAccountManager.addOnAccountsUpdatedListener(mAccountsListener, null, false);
+    }
+
+    private void removeAccountsWatch() {
+        if (mAccountsListener != null) {
+            mAccountManager.removeOnAccountsUpdatedListener(mAccountsListener);
+            mAccountsListener = null;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 2f4e8bb..7afcb13 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -176,6 +176,7 @@
         "core_libraries",
         "crumpet",
         "dck_framework",
+        "desktop_stats",
         "devoptions_settings",
         "game",
         "gpu",
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 78a1fa7..bfef685 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -68,10 +68,11 @@
 import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume;
 import static com.android.media.audio.Flags.equalScoLeaVcIndexRange;
 import static com.android.media.audio.Flags.replaceStreamBtSco;
-import static com.android.media.audio.Flags.ringerModeAffectsAlarm;
 import static com.android.media.audio.Flags.ringMyCar;
+import static com.android.media.audio.Flags.ringerModeAffectsAlarm;
 import static com.android.media.audio.Flags.setStreamVolumeOrder;
 import static com.android.media.audio.Flags.vgsVssSyncMuteOrder;
+import static com.android.media.flags.Flags.enableAudioInputDeviceRoutingAndVolumeControl;
 import static com.android.server.audio.SoundDoseHelper.ACTION_CHECK_MUSIC_ACTIVE;
 import static com.android.server.utils.EventLogger.Event.ALOGE;
 import static com.android.server.utils.EventLogger.Event.ALOGI;
@@ -491,6 +492,10 @@
     private static final int MSG_INIT_SPATIALIZER = 102;
     private static final int MSG_INIT_ADI_DEVICE_STATES = 103;
 
+    private static final int MSG_INIT_INPUT_GAINS = 104;
+    private static final int MSG_SET_INPUT_GAIN_INDEX = 105;
+    private static final int MSG_PERSIST_INPUT_GAIN_INDEX = 106;
+
     // end of messages handled under wakelock
 
     // retry delay in case of failure to indicate system ready to AudioFlinger
@@ -512,6 +517,11 @@
      **/
     private SparseArray<VolumeStreamState> mStreamStates;
 
+    /**
+     * @see InputDeviceVolumeHelper
+     */
+    private InputDeviceVolumeHelper mInputDeviceVolumeHelper;
+
     /*package*/ int getVssVolumeForDevice(int stream, int device) {
         final VolumeStreamState streamState = mStreamStates.get(stream);
         return streamState != null ? streamState.getIndex(device) : -1;
@@ -1501,6 +1511,15 @@
                 0 /* arg1 */, 0 /* arg2 */, null /* obj */, 0 /* delay */);
         queueMsgUnderWakeLock(mAudioHandler, MSG_INIT_SPATIALIZER,
                 0 /* arg1 */, 0 /* arg2 */, null /* obj */, 0 /* delay */);
+        if (enableAudioInputDeviceRoutingAndVolumeControl()) {
+            queueMsgUnderWakeLock(
+                    mAudioHandler,
+                    MSG_INIT_INPUT_GAINS,
+                    0 /* arg1 */,
+                    0 /* arg2 */,
+                    null /* obj */,
+                    0 /* delay */);
+        }
 
         mDisplayManager = context.getSystemService(DisplayManager.class);
 
@@ -1594,6 +1613,16 @@
         }
     }
 
+    /** Called by handling of MSG_INIT_INPUT_GAINS */
+    private void onInitInputGains() {
+        mInputDeviceVolumeHelper =
+                new InputDeviceVolumeHelper(
+                        mSettings,
+                        mContentResolver,
+                        mSettingsLock,
+                        System.INPUT_GAIN_INDEX_SETTINGS);
+    }
+
     private SubscriptionManager.OnSubscriptionsChangedListener mSubscriptionChangedListener =
             new SubscriptionManager.OnSubscriptionsChangedListener() {
                 @Override
@@ -5742,6 +5771,90 @@
                 : aliasStreamType == sStreamVolumeAlias.get(AudioSystem.STREAM_SYSTEM);
     }
 
+    /**
+     * @see AudioDeviceVolumeManager#setInputGainIndex(AudioDeviceAttributes, int)
+     */
+    @Override
+    @android.annotation.EnforcePermission(MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    public void setInputGainIndex(@NonNull AudioDeviceAttributes ada, int index) {
+        super.setInputGainIndex_enforcePermission();
+
+        if (mInputDeviceVolumeHelper.setInputGainIndex(ada, index)) {
+            // Post message to set system volume (it in turn will post a message
+            // to persist).
+            sendMsg(
+                    mAudioHandler,
+                    MSG_SET_INPUT_GAIN_INDEX,
+                    SENDMSG_QUEUE,
+                    /*arg1*/ index,
+                    /*arg2*/ 0,
+                    /*obj*/ ada,
+                    /*delay*/ 0);
+        }
+    }
+
+    private void setInputGainIndexInt(@NonNull AudioDeviceAttributes ada, int index) {
+        // TODO(b/364923030): call AudioSystem to apply input gain in native layer.
+
+        // Post a persist input gain msg.
+        sendMsg(
+                mAudioHandler,
+                MSG_PERSIST_INPUT_GAIN_INDEX,
+                SENDMSG_QUEUE,
+                /*arg1*/ index,
+                /*arg2*/ 0,
+                /*obj*/ ada,
+                PERSIST_DELAY);
+    }
+
+    private void persistInputGainIndex(@NonNull AudioDeviceAttributes ada, int index) {
+        mInputDeviceVolumeHelper.persistInputGainIndex(ada, index);
+    }
+
+    /**
+     * @see AudioDeviceVolumeManager#getInputGainIndex(AudioDeviceAttributes)
+     */
+    @Override
+    @android.annotation.EnforcePermission(MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    public int getInputGainIndex(@NonNull AudioDeviceAttributes ada) {
+        super.getInputGainIndex_enforcePermission();
+
+        return mInputDeviceVolumeHelper.getInputGainIndex(ada);
+    }
+
+    /**
+     * @see AudioDeviceVolumeManager#getMaxInputGainIndex()
+     */
+    @Override
+    @android.annotation.EnforcePermission(MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    public int getMaxInputGainIndex() {
+        super.getMaxInputGainIndex_enforcePermission();
+
+        return mInputDeviceVolumeHelper.getMaxInputGainIndex();
+    }
+
+    /**
+     * @see AudioDeviceVolumeManager#getMinInputGainIndex()
+     */
+    @Override
+    @android.annotation.EnforcePermission(MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    public int getMinInputGainIndex() {
+        super.getMinInputGainIndex_enforcePermission();
+
+        return mInputDeviceVolumeHelper.getMinInputGainIndex();
+    }
+
+    /**
+     * @see AudioDeviceVolumeManager#isInputGainFixed(AudioDeviceAttributes)
+     */
+    @Override
+    @android.annotation.EnforcePermission(MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+    public boolean isInputGainFixed(@NonNull AudioDeviceAttributes ada) {
+        super.isInputGainFixed_enforcePermission();
+
+        return mInputDeviceVolumeHelper.isInputGainFixed(ada);
+    }
+
     /** @see AudioManager#setMicrophoneMute(boolean) */
     @Override
     public void setMicrophoneMute(boolean on, String callingPackage, int userId,
@@ -10077,6 +10190,14 @@
                     vgs.persistVolumeGroup(msg.arg1);
                     break;
 
+                case MSG_SET_INPUT_GAIN_INDEX:
+                    setInputGainIndexInt((AudioDeviceAttributes) msg.obj, msg.arg1);
+                    break;
+
+                case MSG_PERSIST_INPUT_GAIN_INDEX:
+                    persistInputGainIndex((AudioDeviceAttributes) msg.obj, msg.arg1);
+                    break;
+
                 case MSG_PERSIST_RINGER_MODE:
                     // note that the value persisted is the current ringer mode, not the
                     // value of ringer mode as of the time the request was made to persist
@@ -10147,6 +10268,11 @@
                     mAudioEventWakeLock.release();
                     break;
 
+                case MSG_INIT_INPUT_GAINS:
+                    onInitInputGains();
+                    mAudioEventWakeLock.release();
+                    break;
+
                 case MSG_INIT_ADI_DEVICE_STATES:
                     onInitAdiDeviceStates();
                     mAudioEventWakeLock.release();
diff --git a/services/core/java/com/android/server/audio/InputDeviceVolumeHelper.java b/services/core/java/com/android/server/audio/InputDeviceVolumeHelper.java
new file mode 100644
index 0000000..d83dca6
--- /dev/null
+++ b/services/core/java/com/android/server/audio/InputDeviceVolumeHelper.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.audio;
+
+import static android.media.AudioManager.GET_DEVICES_INPUTS;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ContentResolver;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.AudioSystem;
+import android.os.UserHandle;
+import android.util.IntArray;
+import android.util.SparseIntArray;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/** Maintains the current state of input gains. */
+/*package*/ class InputDeviceVolumeHelper {
+    private static final String TAG = "InputDeviceVolumeHelper";
+
+    // TODO(b/364923030): retrieve these constants from AudioPolicyManager.
+    private final int INDEX_MIN = 0;
+    private final int INDEX_MAX = 100;
+    private final int INDEX_DEFAULT = 50;
+
+    private final SettingsAdapter mSettings;
+    private final ContentResolver mContentResolver;
+    private final Object mSettingsLock;
+    private final String mInputGainIndexSettingsName;
+
+    // A map between device internal type (e.g. AudioSystem.DEVICE_IN_BUILTIN_MIC) to its input gain
+    // index.
+    private final SparseIntArray mInputGainIndexMap;
+    private final Set<Integer> mSupportedDeviceTypes;
+
+    InputDeviceVolumeHelper(
+            SettingsAdapter settings,
+            ContentResolver contentResolver,
+            Object settingsLock,
+            String settingsName) {
+        mSettings = settings;
+        mContentResolver = contentResolver;
+        mSettingsLock = settingsLock;
+        mInputGainIndexSettingsName = settingsName;
+
+        IntArray internalDeviceTypes = new IntArray();
+        int status = AudioSystem.getSupportedDeviceTypes(GET_DEVICES_INPUTS, internalDeviceTypes);
+        mInputGainIndexMap =
+                new SparseIntArray(
+                        status == AudioManager.SUCCESS
+                                ? internalDeviceTypes.size()
+                                : AudioSystem.DEVICE_IN_ALL_SET.size());
+
+        if (status == AudioManager.SUCCESS) {
+            Set<Integer> supportedDeviceTypes = new HashSet<>();
+            for (int i = 0; i < internalDeviceTypes.size(); i++) {
+                supportedDeviceTypes.add(internalDeviceTypes.get(i));
+            }
+            mSupportedDeviceTypes = supportedDeviceTypes;
+        } else {
+            mSupportedDeviceTypes = AudioSystem.DEVICE_IN_ALL_SET;
+        }
+
+        readSettings();
+    }
+
+    public void readSettings() {
+        synchronized (InputDeviceVolumeHelper.class) {
+            for (int inputDeviceType : mSupportedDeviceTypes) {
+                // Retrieve current input gain for device. If no input gain stored for current
+                // device, use default input gain.
+                int index;
+                if (!hasValidSettingsName()) {
+                    index = INDEX_DEFAULT;
+                } else {
+                    String name = getSettingNameForDevice(inputDeviceType);
+                    index =
+                            mSettings.getSystemIntForUser(
+                                    mContentResolver, name, INDEX_DEFAULT, UserHandle.USER_CURRENT);
+                }
+
+                mInputGainIndexMap.put(inputDeviceType, getValidIndex(index));
+            }
+        }
+    }
+
+    public boolean hasValidSettingsName() {
+        return mInputGainIndexSettingsName != null && !mInputGainIndexSettingsName.isEmpty();
+    }
+
+    public @Nullable String getSettingNameForDevice(int inputDeviceType) {
+        if (!hasValidSettingsName()) {
+            return null;
+        }
+        final String suffix = AudioSystem.getInputDeviceName(inputDeviceType);
+        if (suffix.isEmpty()) {
+            return mInputGainIndexSettingsName;
+        }
+        return mInputGainIndexSettingsName + "_" + suffix;
+    }
+
+    private int getValidIndex(int index) {
+        if (index < INDEX_MIN) {
+            return INDEX_MIN;
+        }
+        if (index > INDEX_MAX) {
+            return INDEX_MAX;
+        }
+        return index;
+    }
+
+    public int getInputGainIndex(@NonNull AudioDeviceAttributes ada) {
+        int inputDeviceType = AudioDeviceInfo.convertDeviceTypeToInternalInputDevice(ada.getType());
+        ensureValidInputDeviceType(inputDeviceType);
+
+        synchronized (InputDeviceVolumeHelper.class) {
+            return mInputGainIndexMap.get(inputDeviceType, INDEX_DEFAULT);
+        }
+    }
+
+    public int getMaxInputGainIndex() {
+        return INDEX_MAX;
+    }
+
+    public int getMinInputGainIndex() {
+        return INDEX_MIN;
+    }
+
+    public boolean isInputGainFixed(@NonNull AudioDeviceAttributes ada) {
+        int inputDeviceType = AudioDeviceInfo.convertDeviceTypeToInternalInputDevice(ada.getType());
+        ensureValidInputDeviceType(inputDeviceType);
+
+        // For simplicity, all devices have non fixed input gain. This might change
+        // when more input devices are supported and some do not support input gain control.
+        return false;
+    }
+
+    public boolean setInputGainIndex(@NonNull AudioDeviceAttributes ada, int index) {
+        int inputDeviceType = AudioDeviceInfo.convertDeviceTypeToInternalInputDevice(ada.getType());
+        ensureValidInputDeviceType(inputDeviceType);
+
+        int oldIndex;
+        synchronized (mSettingsLock) {
+            synchronized (InputDeviceVolumeHelper.class) {
+                oldIndex = getInputGainIndex(ada);
+                index = getValidIndex(index);
+
+                if (oldIndex == index) {
+                    return false;
+                }
+
+                mInputGainIndexMap.put(inputDeviceType, index);
+                return true;
+            }
+        }
+    }
+
+    public void persistInputGainIndex(@NonNull AudioDeviceAttributes ada, int index) {
+        int inputDeviceType = AudioDeviceInfo.convertDeviceTypeToInternalInputDevice(ada.getType());
+        ensureValidInputDeviceType(inputDeviceType);
+
+        if (hasValidSettingsName()) {
+            mSettings.putSystemIntForUser(
+                    mContentResolver,
+                    getSettingNameForDevice(inputDeviceType),
+                    index,
+                    UserHandle.USER_CURRENT);
+        }
+    }
+
+    public boolean isValidInputDeviceType(int inputDeviceType) {
+        return mSupportedDeviceTypes.contains(inputDeviceType);
+    }
+
+    private void ensureValidInputDeviceType(int inputDeviceType) {
+        if (!isValidInputDeviceType(inputDeviceType)) {
+            throw new IllegalArgumentException("Bad input device type " + inputDeviceType);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
index 7740411..448c42b7 100644
--- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java
+++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
@@ -82,7 +82,8 @@
     @IntDef(prefix = { "AUTO_BRIGHTNESS_MODE_" }, value = {
             AUTO_BRIGHTNESS_MODE_DEFAULT,
             AUTO_BRIGHTNESS_MODE_IDLE,
-            AUTO_BRIGHTNESS_MODE_DOZE
+            AUTO_BRIGHTNESS_MODE_DOZE,
+            AUTO_BRIGHTNESS_MODE_BEDTIME_WEAR
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface AutomaticBrightnessMode{}
@@ -90,6 +91,7 @@
     public static final int AUTO_BRIGHTNESS_MODE_DEFAULT = 0;
     public static final int AUTO_BRIGHTNESS_MODE_IDLE = 1;
     public static final int AUTO_BRIGHTNESS_MODE_DOZE = 2;
+    public static final int AUTO_BRIGHTNESS_MODE_BEDTIME_WEAR = 3;
     public static final int AUTO_BRIGHTNESS_MODE_MAX = AUTO_BRIGHTNESS_MODE_DOZE;
 
     // How long the current sensor reading is assumed to be valid beyond the current time.
diff --git a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
index 6a019f3..570d5d0 100644
--- a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
+++ b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
@@ -18,6 +18,7 @@
 
 import static android.text.TextUtils.formatSimple;
 
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_BEDTIME_WEAR;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE;
@@ -114,7 +115,7 @@
                 luxLevels = getLuxLevels(context.getResources().getIntArray(
                         com.android.internal.R.array.config_autoBrightnessLevelsIdle));
             }
-            case AUTO_BRIGHTNESS_MODE_DOZE -> {
+            case AUTO_BRIGHTNESS_MODE_DOZE, AUTO_BRIGHTNESS_MODE_BEDTIME_WEAR -> {
                 luxLevels = displayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(mode, preset);
                 brightnessLevels =
                         displayDeviceConfig.getAutoBrightnessBrighteningLevels(mode, preset);
diff --git a/services/core/java/com/android/server/display/BrightnessRangeController.java b/services/core/java/com/android/server/display/BrightnessRangeController.java
index 1d68ee54..83b0801 100644
--- a/services/core/java/com/android/server/display/BrightnessRangeController.java
+++ b/services/core/java/com/android/server/display/BrightnessRangeController.java
@@ -67,6 +67,10 @@
             mNormalBrightnessModeController.resetNbmData(
                     displayDeviceConfig.getLuxThrottlingData());
         }
+        if (flags.useNewHdrBrightnessModifier()) {
+            // HDR boost is handled by HdrBrightnessModifier and should be disabled in HbmController
+            mHbmController.disableHdrBoost();
+        }
         updateHdrClamper(info, displayToken, displayDeviceConfig);
     }
 
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 42a62f0..5c8430b 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -19,6 +19,7 @@
 import static android.hardware.display.DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
 import static android.hardware.display.DisplayManagerInternal.DisplayPowerRequest.POLICY_OFF;
 
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_BEDTIME_WEAR;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE;
@@ -1086,6 +1087,16 @@
             brightnessMappers.put(AUTO_BRIGHTNESS_MODE_DOZE, dozeModeBrightnessMapper);
         }
 
+        if (mFlags.areAutoBrightnessModesEnabled()
+                && mFlags.isAutoBrightnessModeBedtimeWearEnabled()) {
+            BrightnessMappingStrategy bedtimeBrightnessMapper =
+                    BrightnessMappingStrategy.create(context, mDisplayDeviceConfig,
+                            AUTO_BRIGHTNESS_MODE_BEDTIME_WEAR, mDisplayWhiteBalanceController);
+            if (bedtimeBrightnessMapper != null) {
+                brightnessMappers.put(AUTO_BRIGHTNESS_MODE_BEDTIME_WEAR, bedtimeBrightnessMapper);
+            }
+        }
+
         float userLux = BrightnessMappingStrategy.INVALID_LUX;
         float userNits = BrightnessMappingStrategy.INVALID_NITS;
         if (mAutomaticBrightnessController != null) {
@@ -1503,7 +1514,6 @@
                 // use the current brightness setting scaled by the doze scale factor
                 rawBrightnessState = getDozeBrightnessForOffload();
                 brightnessState = clampScreenBrightness(rawBrightnessState);
-                updateScreenBrightnessSetting = false;
                 mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE_MANUAL);
                 mTempBrightnessEvent.setFlags(
                         mTempBrightnessEvent.getFlags() | BrightnessEvent.FLAG_DOZE_SCALE);
@@ -1513,6 +1523,7 @@
                 brightnessState = clampScreenBrightness(rawBrightnessState);
                 mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE_DEFAULT);
             }
+            updateScreenBrightnessSetting = false;
         }
 
         if (!mFlags.isRefactorDisplayPowerControllerEnabled()) {
diff --git a/services/core/java/com/android/server/display/HighBrightnessModeController.java b/services/core/java/com/android/server/display/HighBrightnessModeController.java
index 135cab6..6be0c12 100644
--- a/services/core/java/com/android/server/display/HighBrightnessModeController.java
+++ b/services/core/java/com/android/server/display/HighBrightnessModeController.java
@@ -38,6 +38,7 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.display.DisplayManagerService.Clock;
 import com.android.server.display.config.HighBrightnessModeData;
+import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.utils.DebugUtils;
 
 import java.io.PrintWriter;
@@ -119,6 +120,14 @@
     @Nullable
     private HighBrightnessModeMetadata mHighBrightnessModeMetadata;
 
+    /**
+     * If {@link DisplayManagerFlags#useNewHdrBrightnessModifier()} is ON, hdr boost is handled by
+     * {@link com.android.server.display.brightness.clamper.HdrBrightnessModifier} and should be
+     * disabled in this class. After flag is cleaned up, this field together with HDR handling
+     * should be cleaned up from this class.
+     */
+    private boolean mHdrBoostDisabled = false;
+
     HighBrightnessModeController(Handler handler, int width, int height, IBinder displayToken,
             String displayUniqueId, float brightnessMin, float brightnessMax,
             HighBrightnessModeData hbmData, HdrBrightnessDeviceConfig hdrBrightnessCfg,
@@ -323,6 +332,7 @@
         pw.println("  mIsTimeAvailable= " + mIsTimeAvailable);
         pw.println("  mIsBlockedByLowPowerMode=" + mIsBlockedByLowPowerMode);
         pw.println("  width*height=" + mWidth + "*" + mHeight);
+        pw.println("  mHdrBoostDisabled=" + mHdrBoostDisabled);
 
         if (mHighBrightnessModeMetadata != null) {
             pw.println("  mRunningStartTimeMillis="
@@ -373,6 +383,11 @@
         return mHbmData != null && mHighBrightnessModeMetadata != null;
     }
 
+    void disableHdrBoost() {
+        mHdrBoostDisabled = true;
+        unregisterHdrListener();
+    }
+
     private long calculateRemainingTime(long currentTime) {
         if (!deviceSupportsHbm()) {
             return 0;
@@ -583,6 +598,9 @@
     }
 
     private void registerHdrListener(IBinder displayToken) {
+        if (mHdrBoostDisabled) {
+            return;
+        }
         if (mRegisteredDisplayToken == displayToken) {
             return;
         }
diff --git a/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java b/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java
index e0bdda5..458438c 100644
--- a/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java
+++ b/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java
@@ -16,6 +16,7 @@
 
 package com.android.server.display.config;
 
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_BEDTIME_WEAR;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE;
@@ -247,6 +248,9 @@
             case AUTO_BRIGHTNESS_MODE_DOZE -> {
                 return AutoBrightnessModeName.doze.getRawName();
             }
+            case AUTO_BRIGHTNESS_MODE_BEDTIME_WEAR -> {
+                return AutoBrightnessModeName.bedtime_wear.getRawName();
+            }
             default -> throw new IllegalArgumentException("Unknown auto-brightness mode: " + mode);
         }
     }
diff --git a/services/core/java/com/android/server/incident/PendingReports.java b/services/core/java/com/android/server/incident/PendingReports.java
index adcda0a..35b3673 100644
--- a/services/core/java/com/android/server/incident/PendingReports.java
+++ b/services/core/java/com/android/server/incident/PendingReports.java
@@ -304,16 +304,16 @@
             denyReportBeforeAddingRec(listener, callingPackage);
             return;
         }
+        AttributionSource attributionSource =
+                    new AttributionSource.Builder(callingUid)
+                            .setPackageName(callingPackage)
+                            .build();
 
         // Only with userdebug/eng build: it could check capture consentless bugreport permission
         // and approve the report when it's granted.
         boolean captureConsentlessBugreportOnUserdebugBuildGranted = false;
         if ((Build.IS_USERDEBUG || Build.IS_ENG)
                 && (flags & IncidentManager.FLAG_ALLOW_CONSENTLESS_BUGREPORT) != 0) {
-            AttributionSource attributionSource =
-                    new AttributionSource.Builder(callingUid)
-                            .setPackageName(callingPackage)
-                            .build();
             captureConsentlessBugreportOnUserdebugBuildGranted =
                     mPermissionManager.checkPermissionForDataDelivery(
                             Manifest.permission.CAPTURE_CONSENTLESS_BUGREPORT_ON_USERDEBUG_BUILD,
@@ -321,12 +321,32 @@
                             /* message= */ null)
                             == PERMISSION_GRANTED;
         }
-        if (captureConsentlessBugreportOnUserdebugBuildGranted) {
+
+        // Allow system apps to skip the consent dialog and use their in-built consent mechanism
+        // instead.
+        boolean captureConsentlessBugreportDelegatedConsentGranted = false;
+        if ((flags & IncidentManager.FLAG_ALLOW_CONSENTLESS_BUGREPORT) != 0) {
+            captureConsentlessBugreportDelegatedConsentGranted =
+                    mPermissionManager.checkPermissionForDataDelivery(
+                                    Manifest.permission
+                                            .CAPTURE_CONSENTLESS_BUGREPORT_DELEGATED_CONSENT,
+                                    attributionSource,
+                                    /* message= */ null)
+                            == PERMISSION_GRANTED;
+        }
+
+        if (captureConsentlessBugreportOnUserdebugBuildGranted
+                || captureConsentlessBugreportDelegatedConsentGranted) {
             try {
                 PendingReportRec rec =
                         new PendingReportRec(
                                 callingPackage, receiverClass, reportId, flags, listener);
-                Log.d(TAG, "approving consentless report: " + rec.getUri());
+                if (captureConsentlessBugreportOnUserdebugBuildGranted) {
+                    Log.d(TAG, "approving consentless report: " + rec.getUri());
+                }
+                if (captureConsentlessBugreportDelegatedConsentGranted) {
+                    Log.d(TAG, "delegating consent for report: " + rec.getUri());
+                }
                 listener.onReportApproved();
                 return;
             } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/incident/TEST_MAPPING b/services/core/java/com/android/server/incident/TEST_MAPPING
new file mode 100644
index 0000000..4f789db
--- /dev/null
+++ b/services/core/java/com/android/server/incident/TEST_MAPPING
@@ -0,0 +1,10 @@
+{
+  "postsubmit": [
+    {
+      "name": "CtsRootBugreportTestCases"
+    },
+    {
+      "name": "BugreportManagerTestCases"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java
index c888eef..e40d855 100644
--- a/services/core/java/com/android/server/input/InputManagerInternal.java
+++ b/services/core/java/com/android/server/input/InputManagerInternal.java
@@ -270,4 +270,18 @@
      * @param scaleFactor the new scale factor to be applied for pointer icons.
      */
     public abstract void setAccessibilityPointerIconScaleFactor(int displayId, float scaleFactor);
+
+    /**
+     * Set whether the given input device can wake up the kernel from sleep
+     * when it generates input events. By default, usually only internal (built-in)
+     * input devices can wake the kernel from sleep. For an external input device
+     * that supports remote wakeup to be able to wake the kernel, this must be called
+     * after each time the device is connected/added.
+     *
+     * @param deviceId the device ID of the input device.
+     * @param enabled When true, device will be configured to wake up kernel.
+     *
+     * @return true if setting power wakeup was successful.
+     */
+    public abstract boolean setKernelWakeEnabled(int deviceId, boolean enabled);
 }
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 98e5319..bea520f 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -3511,6 +3511,11 @@
         public void setAccessibilityPointerIconScaleFactor(int displayId, float scaleFactor) {
             InputManagerService.this.setAccessibilityPointerIconScaleFactor(displayId, scaleFactor);
         }
+
+        @Override
+        public boolean setKernelWakeEnabled(int deviceId, boolean enabled) {
+            return mNative.setKernelWakeEnabled(deviceId, enabled);
+        }
     }
 
     @Override
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index 21e8bcc..283fdea 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -287,6 +287,17 @@
      */
     int getLastUsedInputDeviceId();
 
+    /**
+     * Set whether the given input device can wake up the kernel from sleep
+     * when it generates input events. By default, usually only internal (built-in)
+     * input devices can wake the kernel from sleep. For an external input device
+     * that supports remote wakeup to be able to wake the kernel, this must be called
+     * after each time the device is connected/added.
+     *
+     * Returns true if setting power wakeup was successful.
+     */
+    boolean setKernelWakeEnabled(int deviceId, boolean enabled);
+
     /** The native implementation of InputManagerService methods. */
     class NativeImpl implements NativeInputManagerService {
         /** Pointer to native input manager service object, used by native code. */
@@ -573,5 +584,8 @@
 
         @Override
         public native int getLastUsedInputDeviceId();
+
+        @Override
+        public native boolean setKernelWakeEnabled(int deviceId, boolean enabled);
     }
 }
diff --git a/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java b/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java
index d1576c5..bb4ae96 100644
--- a/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java
+++ b/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java
@@ -74,7 +74,6 @@
     private final Context mContext;
     private final Handler mHandler;
     private final PackageManagerInternal mPackageManagerInternal;
-    private final IntegrityFileManager mIntegrityFileManager;
 
     /** Create an instance of {@link AppIntegrityManagerServiceImpl}. */
     public static AppIntegrityManagerServiceImpl create(Context context) {
@@ -84,7 +83,6 @@
         return new AppIntegrityManagerServiceImpl(
                 context,
                 LocalServices.getService(PackageManagerInternal.class),
-                IntegrityFileManager.getInstance(),
                 handlerThread.getThreadHandler());
     }
 
@@ -92,11 +90,9 @@
     AppIntegrityManagerServiceImpl(
             Context context,
             PackageManagerInternal packageManagerInternal,
-            IntegrityFileManager integrityFileManager,
             Handler handler) {
         mContext = context;
         mPackageManagerInternal = packageManagerInternal;
-        mIntegrityFileManager = integrityFileManager;
         mHandler = handler;
 
         IntentFilter integrityVerificationFilter = new IntentFilter();
@@ -127,80 +123,40 @@
     @BinderThread
     public void updateRuleSet(
             String version, ParceledListSlice<Rule> rules, IntentSender statusReceiver) {
-        String ruleProvider = getCallerPackageNameOrThrow(Binder.getCallingUid());
-        if (DEBUG_INTEGRITY_COMPONENT) {
-            Slog.i(TAG, String.format("Calling rule provider name is: %s.", ruleProvider));
+        Intent intent = new Intent();
+        intent.putExtra(EXTRA_STATUS, STATUS_SUCCESS);
+        try {
+            statusReceiver.sendIntent(
+                mContext,
+                /* code= */ 0,
+                intent,
+                /* onFinished= */ null,
+                /* handler= */ null);
+        } catch (Exception e) {
+            Slog.e(TAG, "Error sending status feedback.", e);
         }
-
-        mHandler.post(
-                () -> {
-                    boolean success = true;
-                    try {
-                        mIntegrityFileManager.writeRules(version, ruleProvider, rules.getList());
-                    } catch (Exception e) {
-                        Slog.e(TAG, "Error writing rules.", e);
-                        success = false;
-                    }
-
-                    if (DEBUG_INTEGRITY_COMPONENT) {
-                        Slog.i(
-                                TAG,
-                                String.format(
-                                        "Successfully pushed rule set to version '%s' from '%s'",
-                                        version, ruleProvider));
-                    }
-
-                    Intent intent = new Intent();
-                    intent.putExtra(EXTRA_STATUS, success ? STATUS_SUCCESS : STATUS_FAILURE);
-                    try {
-                        statusReceiver.sendIntent(
-                                mContext,
-                                /* code= */ 0,
-                                intent,
-                                /* onFinished= */ null,
-                                /* handler= */ null);
-                    } catch (Exception e) {
-                        Slog.e(TAG, "Error sending status feedback.", e);
-                    }
-                });
     }
 
     @Override
     @BinderThread
     public String getCurrentRuleSetVersion() {
-        getCallerPackageNameOrThrow(Binder.getCallingUid());
-
-        RuleMetadata ruleMetadata = mIntegrityFileManager.readMetadata();
-        return (ruleMetadata != null && ruleMetadata.getVersion() != null)
-                ? ruleMetadata.getVersion()
-                : "";
+        return "";
     }
 
     @Override
     @BinderThread
     public String getCurrentRuleSetProvider() {
-        getCallerPackageNameOrThrow(Binder.getCallingUid());
-
-        RuleMetadata ruleMetadata = mIntegrityFileManager.readMetadata();
-        return (ruleMetadata != null && ruleMetadata.getRuleProvider() != null)
-                ? ruleMetadata.getRuleProvider()
-                : "";
+        return "";
     }
 
     @Override
     public ParceledListSlice<Rule> getCurrentRules() {
-        List<Rule> rules = Collections.emptyList();
-        try {
-            rules = mIntegrityFileManager.readRules(/* appInstallMetadata= */ null);
-        } catch (Exception e) {
-            Slog.e(TAG, "Error getting current rules", e);
-        }
-        return new ParceledListSlice<>(rules);
+        return new ParceledListSlice<>(Collections.emptyList());
     }
 
     @Override
     public List<String> getWhitelistedRuleProviders() {
-        return getAllowedRuleProviderSystemApps();
+        return Collections.emptyList();
     }
 
     private void handleIntegrityVerification(Intent intent) {
@@ -208,90 +164,4 @@
         mPackageManagerInternal.setIntegrityVerificationResult(
                 verificationId, PackageManagerInternal.INTEGRITY_VERIFICATION_ALLOW);
     }
-
-    /** We will use the SHA256 digest of a package name if it is more than 32 bytes long. */
-    private String getPackageNameNormalized(String packageName) {
-        if (packageName.length() <= 32) {
-            return packageName;
-        }
-
-        try {
-            MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
-            byte[] hashBytes = messageDigest.digest(packageName.getBytes(StandardCharsets.UTF_8));
-            return getHexDigest(hashBytes);
-        } catch (NoSuchAlgorithmException e) {
-            throw new RuntimeException("SHA-256 algorithm not found", e);
-        }
-    }
-
-    private String getCallerPackageNameOrThrow(int callingUid) {
-        String callerPackageName = getCallingRulePusherPackageName(callingUid);
-        if (callerPackageName == null) {
-            throw new SecurityException(
-                    "Only system packages specified in config_integrityRuleProviderPackages are "
-                            + "allowed to call this method.");
-        }
-        return callerPackageName;
-    }
-
-    private String getCallingRulePusherPackageName(int callingUid) {
-        // Obtain the system apps that are allowlisted in config_integrityRuleProviderPackages.
-        List<String> allowedRuleProviders = getAllowedRuleProviderSystemApps();
-        if (DEBUG_INTEGRITY_COMPONENT) {
-            Slog.i(
-                    TAG,
-                    String.format(
-                            "Rule provider system app list contains: %s", allowedRuleProviders));
-        }
-
-        // Identify the package names in the caller list.
-        List<String> callingPackageNames = getPackageListForUid(callingUid);
-
-        // Find the intersection between the allowed and calling packages. Ideally, we will have
-        // at most one package name here. But if we have more, it is fine.
-        List<String> allowedCallingPackages = new ArrayList<>();
-        for (String packageName : callingPackageNames) {
-            if (allowedRuleProviders.contains(packageName)) {
-                allowedCallingPackages.add(packageName);
-            }
-        }
-
-        return allowedCallingPackages.isEmpty() ? null : allowedCallingPackages.get(0);
-    }
-
-    private List<String> getAllowedRuleProviderSystemApps() {
-        List<String> integrityRuleProviders =
-                Arrays.asList(
-                        mContext.getResources()
-                                .getStringArray(R.array.config_integrityRuleProviderPackages));
-
-        // Filter out the rule provider packages that are not system apps.
-        List<String> systemAppRuleProviders = new ArrayList<>();
-        for (String ruleProvider : integrityRuleProviders) {
-            if (isSystemApp(ruleProvider)) {
-                systemAppRuleProviders.add(ruleProvider);
-            }
-        }
-        return systemAppRuleProviders;
-    }
-
-    private boolean isSystemApp(String packageName) {
-        try {
-            PackageInfo existingPackageInfo =
-                    mContext.getPackageManager().getPackageInfo(packageName, /* flags= */ 0);
-            return existingPackageInfo.applicationInfo != null
-                    && existingPackageInfo.applicationInfo.isSystemApp();
-        } catch (PackageManager.NameNotFoundException e) {
-            return false;
-        }
-    }
-
-    private List<String> getPackageListForUid(int uid) {
-        try {
-            return Arrays.asList(mContext.getPackageManager().getPackagesForUid(uid));
-        } catch (NullPointerException e) {
-            Slog.w(TAG, String.format("No packages were found for uid: %d", uid));
-            return List.of();
-        }
-    }
 }
diff --git a/services/core/java/com/android/server/power/ThermalManagerService.java b/services/core/java/com/android/server/power/ThermalManagerService.java
index dc6b164..78bc06c 100644
--- a/services/core/java/com/android/server/power/ThermalManagerService.java
+++ b/services/core/java/com/android/server/power/ThermalManagerService.java
@@ -31,6 +31,7 @@
 import android.hardware.thermal.IThermal;
 import android.hardware.thermal.IThermalChangedCallback;
 import android.hardware.thermal.TemperatureThreshold;
+import android.hardware.thermal.TemperatureType;
 import android.hardware.thermal.ThrottlingSeverity;
 import android.hardware.thermal.V1_0.ThermalStatus;
 import android.hardware.thermal.V1_0.ThermalStatusCode;
@@ -134,6 +135,31 @@
     @VisibleForTesting
     final TemperatureWatcher mTemperatureWatcher = new TemperatureWatcher();
 
+    private final ThermalHalWrapper.WrapperThermalChangedCallback mWrapperCallback =
+            new ThermalHalWrapper.WrapperThermalChangedCallback() {
+                @Override
+                public void onTemperatureChanged(Temperature temperature) {
+                    final long token = Binder.clearCallingIdentity();
+                    try {
+                        ThermalManagerService.this.onTemperatureChanged(temperature, true);
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
+                }
+
+                @Override
+                public void onThresholdChanged(TemperatureThreshold threshold) {
+                    final long token = Binder.clearCallingIdentity();
+                    try {
+                        synchronized (mTemperatureWatcher.mSamples) {
+                            mTemperatureWatcher.updateTemperatureThresholdLocked(threshold, true);
+                        }
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
+                }
+            };
+
     private final Context mContext;
 
     public ThermalManagerService(Context context) {
@@ -146,7 +172,7 @@
         mContext = context;
         mHalWrapper = halWrapper;
         if (halWrapper != null) {
-            halWrapper.setCallback(this::onTemperatureChangedCallback);
+            halWrapper.setCallback(mWrapperCallback);
         }
         mStatus = Temperature.THROTTLING_NONE;
     }
@@ -171,19 +197,19 @@
             // Connect to HAL and post to listeners.
             boolean halConnected = (mHalWrapper != null);
             if (!halConnected) {
-                mHalWrapper = new ThermalHalAidlWrapper(this::onTemperatureChangedCallback);
+                mHalWrapper = new ThermalHalAidlWrapper(mWrapperCallback);
                 halConnected = mHalWrapper.connectToHal();
             }
             if (!halConnected) {
-                mHalWrapper = new ThermalHal20Wrapper(this::onTemperatureChangedCallback);
+                mHalWrapper = new ThermalHal20Wrapper(mWrapperCallback);
                 halConnected = mHalWrapper.connectToHal();
             }
             if (!halConnected) {
-                mHalWrapper = new ThermalHal11Wrapper(this::onTemperatureChangedCallback);
+                mHalWrapper = new ThermalHal11Wrapper(mWrapperCallback);
                 halConnected = mHalWrapper.connectToHal();
             }
             if (!halConnected) {
-                mHalWrapper = new ThermalHal10Wrapper(this::onTemperatureChangedCallback);
+                mHalWrapper = new ThermalHal10Wrapper(mWrapperCallback);
                 halConnected = mHalWrapper.connectToHal();
             }
             if (!halConnected) {
@@ -200,7 +226,7 @@
                 onTemperatureChanged(temperatures.get(i), false);
             }
             onTemperatureMapChangedLocked();
-            mTemperatureWatcher.updateThresholds();
+            mTemperatureWatcher.getAndUpdateThresholds();
             mHalReady.set(true);
         }
     }
@@ -335,16 +361,6 @@
         }
     }
 
-    /* HwBinder callback **/
-    private void onTemperatureChangedCallback(Temperature temperature) {
-        final long token = Binder.clearCallingIdentity();
-        try {
-            onTemperatureChanged(temperature, true);
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
     private void registerStatsCallbacks() {
         final StatsManager statsManager = mContext.getSystemService(StatsManager.class);
         if (statsManager != null) {
@@ -924,19 +940,19 @@
         /** Lock to protect HAL handle. */
         protected final Object mHalLock = new Object();
 
-        @FunctionalInterface
-        interface TemperatureChangedCallback {
-            void onValues(Temperature temperature);
+        interface WrapperThermalChangedCallback {
+            void onTemperatureChanged(Temperature temperature);
+            void onThresholdChanged(TemperatureThreshold threshold);
         }
 
         /** Temperature callback. */
-        protected TemperatureChangedCallback mCallback;
+        protected WrapperThermalChangedCallback mCallback;
 
         /** Cookie for matching the right end point. */
         protected static final int THERMAL_HAL_DEATH_COOKIE = 5612;
 
         @VisibleForTesting
-        protected void setCallback(TemperatureChangedCallback cb) {
+        protected void setCallback(WrapperThermalChangedCallback cb) {
             mCallback = cb;
         }
 
@@ -959,7 +975,7 @@
                 List<Temperature> temperatures = getCurrentTemperatures(false, 0);
                 final int count = temperatures.size();
                 for (int i = 0; i < count; i++) {
-                    mCallback.onValues(temperatures.get(i));
+                    mCallback.onTemperatureChanged(temperatures.get(i));
                 }
             }
         }
@@ -985,31 +1001,42 @@
         private IThermal mInstance = null;
 
         /** Callback for Thermal HAL AIDL. */
-        private final IThermalChangedCallback mThermalChangedCallback =
+        private final IThermalChangedCallback mThermalCallbackAidl =
                 new IThermalChangedCallback.Stub() {
-                    @Override public void notifyThrottling(
-                            android.hardware.thermal.Temperature temperature)
-                            throws RemoteException {
+                    @Override
+                    public void notifyThrottling(
+                            android.hardware.thermal.Temperature temperature) {
                         Temperature svcTemperature = new Temperature(temperature.value,
                                 temperature.type, temperature.name, temperature.throttlingStatus);
                         final long token = Binder.clearCallingIdentity();
                         try {
-                            mCallback.onValues(svcTemperature);
+                            mCallback.onTemperatureChanged(svcTemperature);
                         } finally {
                             Binder.restoreCallingIdentity(token);
                         }
                     }
 
-            @Override public int getInterfaceVersion() throws RemoteException {
-                return this.VERSION;
-            }
+                    @Override
+                    public void notifyThresholdChanged(TemperatureThreshold threshold) {
+                        if (Flags.allowThermalThresholdsCallback()) {
+                            if (threshold.type == TemperatureType.SKIN) {
+                                mCallback.onThresholdChanged(threshold);
+                            }
+                        }
+                    }
 
-            @Override public String getInterfaceHash() throws RemoteException {
-                return this.HASH;
-            }
-        };
+                    @Override
+                    public int getInterfaceVersion() throws RemoteException {
+                        return this.VERSION;
+                    }
 
-        ThermalHalAidlWrapper(TemperatureChangedCallback callback) {
+                    @Override
+                    public String getInterfaceHash() throws RemoteException {
+                        return this.HASH;
+                    }
+                };
+
+        ThermalHalAidlWrapper(WrapperThermalChangedCallback callback) {
             mCallback = callback;
         }
 
@@ -1153,7 +1180,7 @@
         @VisibleForTesting
         void registerThermalChangedCallback() {
             try {
-                mInstance.registerThermalChangedCallback(mThermalChangedCallback);
+                mInstance.registerThermalChangedCallback(mThermalCallbackAidl);
             } catch (IllegalArgumentException | IllegalStateException e) {
                 Slog.e(TAG, "Couldn't registerThermalChangedCallback due to invalid status",
                         e);
@@ -1185,7 +1212,7 @@
         @GuardedBy("mHalLock")
         private android.hardware.thermal.V1_0.IThermal mThermalHal10 = null;
 
-        ThermalHal10Wrapper(TemperatureChangedCallback callback) {
+        ThermalHal10Wrapper(WrapperThermalChangedCallback callback) {
             mCallback = callback;
         }
 
@@ -1317,14 +1344,14 @@
                                         : Temperature.THROTTLING_NONE);
                         final long token = Binder.clearCallingIdentity();
                         try {
-                            mCallback.onValues(thermalSvcTemp);
+                            mCallback.onTemperatureChanged(thermalSvcTemp);
                         } finally {
                             Binder.restoreCallingIdentity(token);
                         }
                     }
                 };
 
-        ThermalHal11Wrapper(TemperatureChangedCallback callback) {
+        ThermalHal11Wrapper(WrapperThermalChangedCallback callback) {
             mCallback = callback;
         }
 
@@ -1455,14 +1482,14 @@
                                 temperature.throttlingStatus);
                         final long token = Binder.clearCallingIdentity();
                         try {
-                            mCallback.onValues(thermalSvcTemp);
+                            mCallback.onTemperatureChanged(thermalSvcTemp);
                         } finally {
                             Binder.restoreCallingIdentity(token);
                         }
                     }
                 };
 
-        ThermalHal20Wrapper(TemperatureChangedCallback callback) {
+        ThermalHal20Wrapper(WrapperThermalChangedCallback callback) {
             mCallback = callback;
         }
 
@@ -1627,52 +1654,57 @@
         @VisibleForTesting
         long mInactivityThresholdMillis = INACTIVITY_THRESHOLD_MILLIS;
 
-        void updateThresholds() {
+        void getAndUpdateThresholds() {
             List<TemperatureThreshold> thresholds =
                         mHalWrapper.getTemperatureThresholds(true, Temperature.TYPE_SKIN);
             synchronized (mSamples) {
                 if (Flags.allowThermalHeadroomThresholds()) {
                     Arrays.fill(mHeadroomThresholds, Float.NaN);
                 }
-                for (int t = 0; t < thresholds.size(); ++t) {
-                    TemperatureThreshold threshold = thresholds.get(t);
-                    if (threshold.hotThrottlingThresholds.length <= ThrottlingSeverity.SEVERE) {
-                        continue;
-                    }
-                    float severeThreshold =
-                            threshold.hotThrottlingThresholds[ThrottlingSeverity.SEVERE];
-                    if (!Float.isNaN(severeThreshold)) {
-                        mSevereThresholds.put(threshold.name, severeThreshold);
-                        if (Flags.allowThermalHeadroomThresholds()) {
-                            for (int severity = ThrottlingSeverity.LIGHT;
-                                    severity <= ThrottlingSeverity.SHUTDOWN; severity++) {
-                                if (threshold.hotThrottlingThresholds.length > severity) {
-                                    updateHeadroomThreshold(severity,
-                                            threshold.hotThrottlingThresholds[severity],
-                                            severeThreshold);
-                                }
-                            }
-                        }
-                    }
+                for (final TemperatureThreshold threshold : thresholds) {
+                    updateTemperatureThresholdLocked(threshold, false);
                 }
             }
         }
 
         // For an older device with multiple SKIN sensors, we will set a severity's headroom
-        // threshold based on the minimum value of all as a workaround.
-        void updateHeadroomThreshold(int severity, float threshold, float severeThreshold) {
-            if (!Float.isNaN(threshold)) {
-                synchronized (mSamples) {
-                    if (severity == ThrottlingSeverity.SEVERE) {
-                        mHeadroomThresholds[severity] = 1.0f;
-                        return;
+        // threshold based on the minimum value of all as a workaround, unless override.
+        @GuardedBy("mSamples")
+        void updateTemperatureThresholdLocked(TemperatureThreshold threshold, boolean override) {
+            if (threshold.hotThrottlingThresholds.length <= ThrottlingSeverity.SEVERE) {
+                return;
+            }
+            float severeThreshold =
+                    threshold.hotThrottlingThresholds[ThrottlingSeverity.SEVERE];
+            if (Float.isNaN(severeThreshold)) {
+                return;
+            }
+            mSevereThresholds.put(threshold.name, severeThreshold);
+            if (!Flags.allowThermalHeadroomThresholds()) {
+                return;
+            }
+            if (override) {
+                Arrays.fill(mHeadroomThresholds, Float.NaN);
+            }
+            for (int severity = ThrottlingSeverity.LIGHT;
+                    severity <= ThrottlingSeverity.SHUTDOWN; severity++) {
+                if (threshold.hotThrottlingThresholds.length > severity) {
+                    float t = threshold.hotThrottlingThresholds[severity];
+                    if (Float.isNaN(t)) {
+                        continue;
                     }
-                    float headroom = normalizeTemperature(threshold, severeThreshold);
-                    if (Float.isNaN(mHeadroomThresholds[severity])) {
-                        mHeadroomThresholds[severity] = headroom;
-                    } else {
-                        float lastHeadroom = mHeadroomThresholds[severity];
-                        mHeadroomThresholds[severity] = Math.min(lastHeadroom, headroom);
+                    synchronized (mSamples) {
+                        if (severity == ThrottlingSeverity.SEVERE) {
+                            mHeadroomThresholds[severity] = 1.0f;
+                            continue;
+                        }
+                        float headroom = normalizeTemperature(t, severeThreshold);
+                        if (Float.isNaN(mHeadroomThresholds[severity])) {
+                            mHeadroomThresholds[severity] = headroom;
+                        } else {
+                            float lastHeadroom = mHeadroomThresholds[severity];
+                            mHeadroomThresholds[severity] = Math.min(lastHeadroom, headroom);
+                        }
                     }
                 }
             }
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 2c4179f..0d98707 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -1757,6 +1757,13 @@
             startedActivityRootTask.setAlwaysOnTop(true);
         }
 
+        if (isIndependentLaunch && !mDoResume && avoidMoveToFront() && !mTransientLaunch
+                && !started.shouldBeVisible(true /* ignoringKeyguard */)) {
+            Slog.i(TAG, "Abort " + transition + " of invisible launch " + started);
+            transition.abort();
+            return startedActivityRootTask;
+        }
+
         // If there is no state change (e.g. a resumed activity is reparented to top of
         // another display) to trigger a visibility/configuration checking, we have to
         // update the configuration for changing to different display.
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index a5085fc..ea0b02c 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -191,7 +191,7 @@
         "android.hardware.power.stats@1.0",
         "android.hardware.power.stats-V1-ndk",
         "android.hardware.thermal@1.0",
-        "android.hardware.thermal-V2-ndk",
+        "android.hardware.thermal-V3-ndk",
         "android.hardware.tv.input@1.0",
         "android.hardware.tv.input-V2-ndk",
         "android.hardware.vibrator-V3-ndk",
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 248ed1a..416e60f 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -3056,6 +3056,12 @@
     im->setMouseSwapPrimaryButtonEnabled(enabled);
 }
 
+static jboolean nativeSetKernelWakeEnabled(JNIEnv* env, jobject nativeImplObj, jint deviceId,
+                                      jboolean enabled) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+    return im->getInputManager()->getReader().setKernelWakeEnabled(deviceId, enabled);
+}
+
 // ----------------------------------------------------------------------------
 
 static const JNINativeMethod gInputManagerMethods[] = {
@@ -3172,6 +3178,7 @@
          (void*)nativeSetAccessibilityStickyKeysEnabled},
         {"setInputMethodConnectionIsActive", "(Z)V", (void*)nativeSetInputMethodConnectionIsActive},
         {"getLastUsedInputDeviceId", "()I", (void*)nativeGetLastUsedInputDeviceId},
+        {"setKernelWakeEnabled", "(IZ)Z", (void*)nativeSetKernelWakeEnabled},
 };
 
 #define FIND_CLASS(var, className) \
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index 20c69ac..6eac826 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -955,6 +955,7 @@
             <xs:enumeration value="default"/>
             <xs:enumeration value="idle"/>
             <xs:enumeration value="doze"/>
+            <xs:enumeration value="bedtime_wear"/>
         </xs:restriction>
     </xs:simpleType>
 
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index a8f18b3..a29e42c 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -21,6 +21,7 @@
   public enum AutoBrightnessModeName {
     method public String getRawName();
     enum_constant public static final com.android.server.display.config.AutoBrightnessModeName _default;
+    enum_constant public static final com.android.server.display.config.AutoBrightnessModeName bedtime_wear;
     enum_constant public static final com.android.server.display.config.AutoBrightnessModeName doze;
     enum_constant public static final com.android.server.display.config.AutoBrightnessModeName idle;
   }
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index e052f94..b87867a 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1399,6 +1399,10 @@
         mSystemServiceManager.startService(BatteryService.class);
         t.traceEnd();
 
+        t.traceBegin("StartTradeInModeService");
+        mSystemServiceManager.startService(TradeInModeService.class);
+        t.traceEnd();
+
         // Tracks application usage stats.
         t.traceBegin("StartUsageService");
         mSystemServiceManager.startService(UsageStatsService.class);
@@ -1504,6 +1508,8 @@
         boolean isTv = context.getPackageManager().hasSystemFeature(
                 PackageManager.FEATURE_LEANBACK);
 
+        boolean isAutomotive = RoSystemFeatures.hasFeatureAutomotive(context);
+
         boolean enableVrService = context.getPackageManager().hasSystemFeature(
                 PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE);
 
@@ -1760,7 +1766,8 @@
                 t.traceEnd();
             }
 
-            if (android.security.Flags.aapmApi()) {
+            if (!isWatch && !isTv && !isAutomotive
+                    && android.security.Flags.aapmApi()) {
                 t.traceBegin("StartAdvancedProtectionService");
                 mSystemServiceManager.startService(AdvancedProtectionService.Lifecycle.class);
                 t.traceEnd();
@@ -3137,7 +3144,7 @@
                 }, WEBVIEW_PREPARATION);
             }
 
-            if (RoSystemFeatures.hasFeatureAutomotive(context)) {
+            if (isAutomotive) {
                 t.traceBegin("StartCarServiceHelperService");
                 final SystemService cshs = mSystemServiceManager
                         .startService(CAR_SERVICE_HELPER_SERVICE_CLASS);
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
index 2c78504..2bc8af1 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
@@ -159,15 +159,16 @@
 
         // Press home key to hide soft keyboard.
         Log.i(TAG, "Press home");
-        verifyInputViewStatus(
-                () -> assertThat(mUiDevice.pressHome()).isTrue(),
-                true /* expected */,
-                false /* inputViewStarted */);
         if (Flags.refactorInsetsController()) {
+            assertThat(mUiDevice.pressHome()).isTrue();
             // The IME visibility is only sent at the end of the animation. Therefore, we have to
             // wait until the visibility was sent to the server and the IME window hidden.
             eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isFalse());
         } else {
+            verifyInputViewStatus(
+                    () -> assertThat(mUiDevice.pressHome()).isTrue(),
+                    true /* expected */,
+                    false /* inputViewStarted */);
             assertThat(mInputMethodService.isInputViewShown()).isFalse();
         }
     }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
index 2220f43..2fd135e 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -18,6 +18,7 @@
 
 
 import static com.android.internal.display.BrightnessSynchronizer.brightnessIntToFloat;
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_BEDTIME_WEAR;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
 import static com.android.server.display.config.SensorData.TEMPERATURE_TYPE_SKIN;
@@ -885,6 +886,34 @@
                 mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(
                         AUTO_BRIGHTNESS_MODE_DOZE,
                         Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_DIM), SMALL_DELTA);
+
+        // Wear Bedtime mode curve
+        assertArrayEquals(new float[]{0.0f, 10.0f},
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(
+                        AUTO_BRIGHTNESS_MODE_BEDTIME_WEAR,
+                        Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_DIM), ZERO_DELTA);
+        assertArrayEquals(new float[]{0.20f, 0.30f},
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(
+                        AUTO_BRIGHTNESS_MODE_BEDTIME_WEAR,
+                        Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_DIM), SMALL_DELTA);
+
+        assertArrayEquals(new float[]{0.0f, 20.0f},
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(
+                        AUTO_BRIGHTNESS_MODE_BEDTIME_WEAR,
+                        Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL), ZERO_DELTA);
+        assertArrayEquals(new float[]{0.30f, 0.65f},
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(
+                        AUTO_BRIGHTNESS_MODE_BEDTIME_WEAR,
+                        Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL), SMALL_DELTA);
+
+        assertArrayEquals(new float[]{0.0f, 30.0f},
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(
+                        AUTO_BRIGHTNESS_MODE_BEDTIME_WEAR,
+                        Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_BRIGHT), ZERO_DELTA);
+        assertArrayEquals(new float[]{0.65f, 0.95f},
+                mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(
+                        AUTO_BRIGHTNESS_MODE_BEDTIME_WEAR,
+                        Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_BRIGHT), SMALL_DELTA);
     }
 
     @Test
@@ -1296,6 +1325,51 @@
                 +   "</screenBrightnessRampSlowIncreaseIdle>\n";
     }
 
+    private String getBedTimeModeWearCurveConfig() {
+        return  "<luxToBrightnessMapping>\n"
+                +           "<mode>bedtime_wear</mode>\n"
+                +           "<setting>dim</setting>\n"
+                +           "<map>\n"
+                +               "<point>\n"
+                +                   "<first>0</first>\n"
+                +                   "<second>0.2</second>\n"
+                +               "</point>\n"
+                +               "<point>\n"
+                +                   "<first>10</first>\n"
+                +                   "<second>0.3</second>\n"
+                +               "</point>\n"
+                +           "</map>\n"
+                +       "</luxToBrightnessMapping>\n"
+                +       "<luxToBrightnessMapping>\n"
+                +           "<mode>bedtime_wear</mode>\n"
+                +           "<setting>normal</setting>\n"
+                +           "<map>\n"
+                +               "<point>\n"
+                +                   "<first>0</first>\n"
+                +                   "<second>0.3</second>\n"
+                +               "</point>\n"
+                +               "<point>\n"
+                +                   "<first>20</first>\n"
+                +                   "<second>0.65</second>\n"
+                +               "</point>\n"
+                +           "</map>\n"
+                +       "</luxToBrightnessMapping>\n"
+                +       "<luxToBrightnessMapping>\n"
+                +           "<mode>bedtime_wear</mode>\n"
+                +           "<setting>bright</setting>\n"
+                +           "<map>\n"
+                +               "<point>\n"
+                +                   "<first>0</first>\n"
+                +                   "<second>0.65</second>\n"
+                +               "</point>\n"
+                +               "<point>\n"
+                +                   "<first>30</first>\n"
+                +                   "<second>0.95</second>\n"
+                +               "</point>\n"
+                +           "</map>\n"
+                +       "</luxToBrightnessMapping>\n";
+    }
+
     private String getPowerThrottlingConfig() {
         return  "<powerThrottlingConfig >\n"
                 +       "<brightnessLowestCapAllowed>0.1</brightnessLowestCapAllowed>\n"
@@ -1481,6 +1555,7 @@
                 +               "</point>\n"
                 +           "</map>\n"
                 +       "</luxToBrightnessMapping>\n"
+                +       getBedTimeModeWearCurveConfig()
                 +       "<idleStylusTimeoutMillis>1000</idleStylusTimeoutMillis>\n"
                 +   "</autoBrightness>\n"
                 +  getPowerThrottlingConfig()
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index fdf6b80..9189c2f 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -2224,6 +2224,8 @@
         verify(mHolder.animator).animateTo(eq(DEFAULT_DOZE_BRIGHTNESS),
                 /* linearSecondTarget= */ anyFloat(), eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE),
                 eq(false));
+        // This brightness shouldn't be stored in the setting
+        verify(mHolder.brightnessSetting, never()).setBrightness(DEFAULT_DOZE_BRIGHTNESS);
 
         // The display device changes and the default doze brightness changes
         setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class),
diff --git a/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java b/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java
index aa9d8c6..f1072da 100644
--- a/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java
@@ -18,6 +18,7 @@
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
@@ -29,10 +30,16 @@
 import android.hardware.thermal.ThrottlingSeverity;
 import android.os.Binder;
 import android.os.CoolingDevice;
+import android.os.Flags;
 import android.os.RemoteException;
 import android.os.Temperature;
+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.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
@@ -40,16 +47,36 @@
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
 
 
 public class ThermalManagerServiceMockingTest {
-    @Mock private IThermal mAidlHalMock;
+    @ClassRule
+    public static final SetFlagsRule.ClassRule mSetFlagsClassRule = new SetFlagsRule.ClassRule();
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = mSetFlagsClassRule.createSetFlagsRule();
+
+    @Mock
+    private IThermal mAidlHalMock;
     private Binder mAidlBinder = new Binder();
     private CompletableFuture<Temperature> mTemperatureFuture;
-    private ThermalManagerService.ThermalHalWrapper.TemperatureChangedCallback mTemperatureCallback;
+    private CompletableFuture<TemperatureThreshold> mThresholdFuture;
+    private ThermalManagerService.ThermalHalWrapper.WrapperThermalChangedCallback
+            mTemperatureCallback =
+            new ThermalManagerService.ThermalHalWrapper.WrapperThermalChangedCallback() {
+                @Override
+                public void onTemperatureChanged(Temperature temperature) {
+                    mTemperatureFuture.complete(temperature);
+                }
+
+                @Override
+                public void onThresholdChanged(TemperatureThreshold threshold) {
+                    mThresholdFuture.complete(threshold);
+                }
+            };
     private ThermalManagerService.ThermalHalAidlWrapper mAidlWrapper;
     @Captor
     ArgumentCaptor<IThermalChangedCallback> mAidlCallbackCaptor;
@@ -60,27 +87,63 @@
         Mockito.when(mAidlHalMock.asBinder()).thenReturn(mAidlBinder);
         mAidlBinder.attachInterface(mAidlHalMock, IThermal.class.getName());
         mTemperatureFuture = new CompletableFuture<>();
-        mTemperatureCallback = temperature -> mTemperatureFuture.complete(temperature);
+        mThresholdFuture = new CompletableFuture<>();
         mAidlWrapper = new ThermalManagerService.ThermalHalAidlWrapper(mTemperatureCallback);
         mAidlWrapper.initProxyAndRegisterCallback(mAidlBinder);
     }
 
     @Test
+    @EnableFlags({Flags.FLAG_ALLOW_THERMAL_THRESHOLDS_CALLBACK})
     public void setCallback_aidl() throws Exception {
         Mockito.verify(mAidlHalMock, Mockito.times(1)).registerThermalChangedCallback(
                 mAidlCallbackCaptor.capture());
-        android.hardware.thermal.Temperature halT =
+        android.hardware.thermal.Temperature halTemperature =
                 new android.hardware.thermal.Temperature();
-        halT.type = TemperatureType.SOC;
-        halT.name = "test";
-        halT.throttlingStatus = ThrottlingSeverity.SHUTDOWN;
-        halT.value = 99.0f;
-        mAidlCallbackCaptor.getValue().notifyThrottling(halT);
+        halTemperature.type = TemperatureType.SOC;
+        halTemperature.name = "test";
+        halTemperature.throttlingStatus = ThrottlingSeverity.SHUTDOWN;
+        halTemperature.value = 99.0f;
+
+        android.hardware.thermal.TemperatureThreshold halThreshold =
+                new android.hardware.thermal.TemperatureThreshold();
+        halThreshold.type = TemperatureType.SKIN;
+        halThreshold.name = "test";
+        halThreshold.hotThrottlingThresholds = new float[ThrottlingSeverity.SHUTDOWN + 1];
+        Arrays.fill(halThreshold.hotThrottlingThresholds, Float.NaN);
+        halThreshold.hotThrottlingThresholds[ThrottlingSeverity.SEVERE] = 44.0f;
+
+        mAidlCallbackCaptor.getValue().notifyThrottling(halTemperature);
+        mAidlCallbackCaptor.getValue().notifyThresholdChanged(halThreshold);
+
         Temperature temperature = mTemperatureFuture.get(100, TimeUnit.MILLISECONDS);
-        assertEquals(halT.name, temperature.getName());
-        assertEquals(halT.type, temperature.getType());
-        assertEquals(halT.value, temperature.getValue(), 0.1f);
-        assertEquals(halT.throttlingStatus, temperature.getStatus());
+        assertEquals(halTemperature.name, temperature.getName());
+        assertEquals(halTemperature.type, temperature.getType());
+        assertEquals(halTemperature.value, temperature.getValue(), 0.1f);
+        assertEquals(halTemperature.throttlingStatus, temperature.getStatus());
+
+        TemperatureThreshold threshold = mThresholdFuture.get(100, TimeUnit.MILLISECONDS);
+        assertEquals(halThreshold.name, threshold.name);
+        assertEquals(halThreshold.type, threshold.type);
+        assertArrayEquals(halThreshold.hotThrottlingThresholds, threshold.hotThrottlingThresholds,
+                0.01f);
+    }
+
+    @Test
+    @DisableFlags({Flags.FLAG_ALLOW_THERMAL_THRESHOLDS_CALLBACK})
+    public void setCallback_aidl_allow_thermal_thresholds_callback_false() throws Exception {
+        Mockito.verify(mAidlHalMock, Mockito.times(1)).registerThermalChangedCallback(
+                mAidlCallbackCaptor.capture());
+        android.hardware.thermal.TemperatureThreshold halThreshold =
+                new android.hardware.thermal.TemperatureThreshold();
+        halThreshold.type = TemperatureType.SOC;
+        halThreshold.name = "test";
+        halThreshold.hotThrottlingThresholds = new float[ThrottlingSeverity.SHUTDOWN + 1];
+        Arrays.fill(halThreshold.hotThrottlingThresholds, Float.NaN);
+        halThreshold.hotThrottlingThresholds[ThrottlingSeverity.SEVERE] = 44.0f;
+
+        mAidlCallbackCaptor.getValue().notifyThresholdChanged(halThreshold);
+        Thread.sleep(1000);
+        assertFalse(mThresholdFuture.isDone());
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
index b7100ea..c9e9f00 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
@@ -15,6 +15,8 @@
  */
 package com.android.server.audio;
 
+import static android.media.AudioDeviceInfo.TYPE_BUILTIN_MIC;
+
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
@@ -27,16 +29,20 @@
 
 import android.app.AppOpsManager;
 import android.content.Context;
+import android.media.AudioDeviceAttributes;
 import android.media.AudioSystem;
 import android.os.Looper;
 import android.os.PermissionEnforcer;
 import android.os.UserHandle;
+import android.platform.test.annotations.EnableFlags;
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.MediumTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.media.flags.Flags;
+
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Rule;
@@ -53,6 +59,7 @@
     private static final String TAG = "AudioServiceTest";
 
     private static final int MAX_MESSAGE_HANDLING_DELAY_MS = 100;
+    private static final int DEFAULT_INPUT_GAIN_INDEX = 50;
 
     @Rule
     public final MockitoRule mockito = MockitoJUnit.rule();
@@ -202,4 +209,29 @@
             reset(mSpySystemServer);
         }
     }
+
+    /** Test input gain index setter and getter */
+    @EnableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+    @Test
+    public void testInputGainIndex() throws Exception {
+        Log.i(TAG, "running testInputGainIndex");
+        Assert.assertNotNull(mAudioService);
+        Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS); // wait for full AudioService initialization
+
+        AudioDeviceAttributes ada =
+                new AudioDeviceAttributes(
+                        AudioDeviceAttributes.ROLE_INPUT, TYPE_BUILTIN_MIC, /* address= */ "");
+
+        Assert.assertEquals(
+                "default input gain index reporting wrong value",
+                DEFAULT_INPUT_GAIN_INDEX,
+                mAudioService.getInputGainIndex(ada));
+
+        int inputGainIndex = 20;
+        mAudioService.setInputGainIndex(ada, inputGainIndex);
+        Assert.assertEquals(
+                "input gain index reporting wrong value",
+                inputGainIndex,
+                mAudioService.getInputGainIndex(ada));
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java b/services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java
index 9c6412b..93aa10b 100644
--- a/services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java
@@ -135,7 +135,6 @@
     @Mock PlatformCompat mPlatformCompat;
     @Mock Context mMockContext;
     @Mock Resources mMockResources;
-    @Mock IntegrityFileManager mIntegrityFileManager;
     @Mock Handler mHandler;
 
     private final Context mRealContext = InstrumentationRegistry.getTargetContext();
@@ -169,7 +168,6 @@
                 new AppIntegrityManagerServiceImpl(
                         mMockContext,
                         mPackageManagerInternal,
-                        mIntegrityFileManager,
                         mHandler);
 
         mSpyPackageManager = spy(mRealContext.getPackageManager());
@@ -177,7 +175,6 @@
         when(mMockContext.getPackageManager()).thenReturn(mSpyPackageManager);
         when(mMockContext.getResources()).thenReturn(mMockResources);
         when(mMockResources.getStringArray(anyInt())).thenReturn(new String[] {});
-        when(mIntegrityFileManager.initialized()).thenReturn(true);
         // These are needed to override the Settings.Global.get result.
         when(mMockContext.getContentResolver()).thenReturn(mRealContext.getContentResolver());
         setIntegrityCheckIncludesRuleProvider(true);
@@ -191,98 +188,6 @@
     }
 
     @Test
-    public void updateRuleSet_notAuthorized() throws Exception {
-        makeUsSystemApp();
-        Rule rule =
-                new Rule(
-                        new AtomicFormula.BooleanAtomicFormula(AtomicFormula.PRE_INSTALLED, true),
-                        Rule.DENY);
-        TestUtils.assertExpectException(
-                SecurityException.class,
-                "Only system packages specified in config_integrityRuleProviderPackages are"
-                        + " allowed to call this method.",
-                () ->
-                        mService.updateRuleSet(
-                                VERSION,
-                                new ParceledListSlice<>(Arrays.asList(rule)),
-                                /* statusReceiver= */ null));
-    }
-
-    @Test
-    public void updateRuleSet_notSystemApp() throws Exception {
-        allowlistUsAsRuleProvider();
-        makeUsSystemApp(false);
-        Rule rule =
-                new Rule(
-                        new AtomicFormula.BooleanAtomicFormula(AtomicFormula.PRE_INSTALLED, true),
-                        Rule.DENY);
-        TestUtils.assertExpectException(
-                SecurityException.class,
-                "Only system packages specified in config_integrityRuleProviderPackages are"
-                        + " allowed to call this method.",
-                () ->
-                        mService.updateRuleSet(
-                                VERSION,
-                                new ParceledListSlice<>(Arrays.asList(rule)),
-                                /* statusReceiver= */ null));
-    }
-
-    @Test
-    public void updateRuleSet_authorized() throws Exception {
-        allowlistUsAsRuleProvider();
-        makeUsSystemApp();
-        Rule rule =
-                new Rule(
-                        new AtomicFormula.BooleanAtomicFormula(AtomicFormula.PRE_INSTALLED, true),
-                        Rule.DENY);
-
-        // no SecurityException
-        mService.updateRuleSet(
-                VERSION, new ParceledListSlice<>(Arrays.asList(rule)), mock(IntentSender.class));
-    }
-
-    @Test
-    public void updateRuleSet_correctMethodCall() throws Exception {
-        allowlistUsAsRuleProvider();
-        makeUsSystemApp();
-        IntentSender mockReceiver = mock(IntentSender.class);
-        List<Rule> rules =
-                Arrays.asList(
-                        new Rule(
-                                IntegrityFormula.Application.packageNameEquals(PACKAGE_NAME),
-                                Rule.DENY));
-
-        mService.updateRuleSet(VERSION, new ParceledListSlice<>(rules), mockReceiver);
-        runJobInHandler();
-
-        verify(mIntegrityFileManager).writeRules(VERSION, TEST_FRAMEWORK_PACKAGE, rules);
-        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
-        verify(mockReceiver).sendIntent(any(), anyInt(), intentCaptor.capture(), any(), any());
-        assertEquals(STATUS_SUCCESS, intentCaptor.getValue().getIntExtra(EXTRA_STATUS, -1));
-    }
-
-    @Test
-    public void updateRuleSet_fail() throws Exception {
-        allowlistUsAsRuleProvider();
-        makeUsSystemApp();
-        doThrow(new IOException()).when(mIntegrityFileManager).writeRules(any(), any(), any());
-        IntentSender mockReceiver = mock(IntentSender.class);
-        List<Rule> rules =
-                Arrays.asList(
-                        new Rule(
-                                IntegrityFormula.Application.packageNameEquals(PACKAGE_NAME),
-                                Rule.DENY));
-
-        mService.updateRuleSet(VERSION, new ParceledListSlice<>(rules), mockReceiver);
-        runJobInHandler();
-
-        verify(mIntegrityFileManager).writeRules(VERSION, TEST_FRAMEWORK_PACKAGE, rules);
-        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
-        verify(mockReceiver).sendIntent(any(), anyInt(), intentCaptor.capture(), any(), any());
-        assertEquals(STATUS_FAILURE, intentCaptor.getValue().getIntExtra(EXTRA_STATUS, -1));
-    }
-
-    @Test
     public void broadcastReceiverRegistration() throws Exception {
         allowlistUsAsRuleProvider();
         makeUsSystemApp();
@@ -316,71 +221,6 @@
                         1, PackageManagerInternal.INTEGRITY_VERIFICATION_ALLOW);
     }
 
-    @Test
-    public void handleBroadcast_notInitialized() throws Exception {
-        allowlistUsAsRuleProvider();
-        makeUsSystemApp();
-        when(mIntegrityFileManager.initialized()).thenReturn(false);
-        ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor =
-                ArgumentCaptor.forClass(BroadcastReceiver.class);
-        verify(mMockContext)
-                .registerReceiver(broadcastReceiverCaptor.capture(), any(), any(), any());
-        Intent intent = makeVerificationIntent();
-
-        broadcastReceiverCaptor.getValue().onReceive(mMockContext, intent);
-        runJobInHandler();
-
-        // The evaluation will still run since we still evaluate manifest based rules.
-        verify(mPackageManagerInternal)
-                .setIntegrityVerificationResult(
-                        1, PackageManagerInternal.INTEGRITY_VERIFICATION_ALLOW);
-    }
-
-    @Test
-    public void verifierAsInstaller_skipIntegrityVerification() throws Exception {
-        allowlistUsAsRuleProvider();
-        makeUsSystemApp();
-        setIntegrityCheckIncludesRuleProvider(false);
-        ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor =
-                ArgumentCaptor.forClass(BroadcastReceiver.class);
-        verify(mMockContext, atLeastOnce())
-                .registerReceiver(broadcastReceiverCaptor.capture(), any(), any(), any());
-        Intent intent = makeVerificationIntent(TEST_FRAMEWORK_PACKAGE);
-
-        broadcastReceiverCaptor.getValue().onReceive(mMockContext, intent);
-        runJobInHandler();
-
-        verify(mPackageManagerInternal)
-                .setIntegrityVerificationResult(
-                        1, PackageManagerInternal.INTEGRITY_VERIFICATION_ALLOW);
-    }
-
-    @Test
-    public void getCurrentRules() throws Exception {
-        allowlistUsAsRuleProvider();
-        makeUsSystemApp();
-        Rule rule = new Rule(IntegrityFormula.Application.packageNameEquals("package"), Rule.DENY);
-        when(mIntegrityFileManager.readRules(any())).thenReturn(Arrays.asList(rule));
-
-        assertThat(mService.getCurrentRules().getList()).containsExactly(rule);
-    }
-
-    @Test
-    public void getWhitelistedRuleProviders_returnsEmptyForNonSystemApps() throws Exception {
-        allowlistUsAsRuleProvider();
-        makeUsSystemApp(false);
-
-        assertThat(mService.getWhitelistedRuleProviders()).isEmpty();
-    }
-
-    @Test
-    public void getWhitelistedRuleProviders() throws Exception {
-        allowlistUsAsRuleProvider();
-        makeUsSystemApp();
-
-        assertThat(mService.getWhitelistedRuleProviders()).containsExactly(TEST_FRAMEWORK_PACKAGE);
-    }
-
     private void allowlistUsAsRuleProvider() {
         Resources mockResources = mock(Resources.class);
         when(mockResources.getStringArray(R.array.config_integrityRuleProviderPackages))
diff --git a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java
index 6d79ae4..cfe3d84 100644
--- a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java
@@ -296,11 +296,11 @@
     }
 
     @Test
-    public void testNotify() throws RemoteException {
+    public void testNotifyThrottling() throws RemoteException {
         int status = Temperature.THROTTLING_SEVERE;
         // Should only notify event not status
         Temperature newBattery = new Temperature(50, Temperature.TYPE_BATTERY, "batt", status);
-        mFakeHal.mCallback.onValues(newBattery);
+        mFakeHal.mCallback.onTemperatureChanged(newBattery);
         verify(mEventListener1, timeout(CALLBACK_TIMEOUT_MILLI_SEC)
                 .times(1)).notifyThrottling(newBattery);
         verify(mStatusListener1, timeout(CALLBACK_TIMEOUT_MILLI_SEC)
@@ -312,7 +312,7 @@
         resetListenerMock();
         // Notify both event and status
         Temperature newSkin = new Temperature(50, Temperature.TYPE_SKIN, "skin1", status);
-        mFakeHal.mCallback.onValues(newSkin);
+        mFakeHal.mCallback.onTemperatureChanged(newSkin);
         verify(mEventListener1, timeout(CALLBACK_TIMEOUT_MILLI_SEC)
                 .times(1)).notifyThrottling(newSkin);
         verify(mStatusListener1, timeout(CALLBACK_TIMEOUT_MILLI_SEC)
@@ -325,7 +325,7 @@
         // Back to None, should only notify event not status
         status = Temperature.THROTTLING_NONE;
         newBattery = new Temperature(50, Temperature.TYPE_BATTERY, "batt", status);
-        mFakeHal.mCallback.onValues(newBattery);
+        mFakeHal.mCallback.onTemperatureChanged(newBattery);
         verify(mEventListener1, timeout(CALLBACK_TIMEOUT_MILLI_SEC)
                 .times(1)).notifyThrottling(newBattery);
         verify(mStatusListener1, timeout(CALLBACK_TIMEOUT_MILLI_SEC)
@@ -337,7 +337,7 @@
         resetListenerMock();
         // Should also notify status
         newSkin = new Temperature(50, Temperature.TYPE_SKIN, "skin1", status);
-        mFakeHal.mCallback.onValues(newSkin);
+        mFakeHal.mCallback.onTemperatureChanged(newSkin);
         verify(mEventListener1, timeout(CALLBACK_TIMEOUT_MILLI_SEC)
                 .times(1)).notifyThrottling(newSkin);
         verify(mStatusListener1, timeout(CALLBACK_TIMEOUT_MILLI_SEC)
@@ -362,7 +362,7 @@
     public void testGetCurrentStatus() throws RemoteException {
         int status = Temperature.THROTTLING_SEVERE;
         Temperature newSkin = new Temperature(100, Temperature.TYPE_SKIN, "skin1", status);
-        mFakeHal.mCallback.onValues(newSkin);
+        mFakeHal.mCallback.onTemperatureChanged(newSkin);
         assertEquals(status, mService.mService.getCurrentThermalStatus());
         int battStatus = Temperature.THROTTLING_EMERGENCY;
         Temperature newBattery = new Temperature(60, Temperature.TYPE_BATTERY, "batt", battStatus);
@@ -373,11 +373,11 @@
     public void testThermalShutdown() throws RemoteException {
         int status = Temperature.THROTTLING_SHUTDOWN;
         Temperature newSkin = new Temperature(100, Temperature.TYPE_SKIN, "skin1", status);
-        mFakeHal.mCallback.onValues(newSkin);
+        mFakeHal.mCallback.onTemperatureChanged(newSkin);
         verify(mIPowerManagerMock, timeout(CALLBACK_TIMEOUT_MILLI_SEC)
                 .times(1)).shutdown(false, PowerManager.SHUTDOWN_THERMAL_STATE, false);
         Temperature newBattery = new Temperature(60, Temperature.TYPE_BATTERY, "batt", status);
-        mFakeHal.mCallback.onValues(newBattery);
+        mFakeHal.mCallback.onTemperatureChanged(newBattery);
         verify(mIPowerManagerMock, timeout(CALLBACK_TIMEOUT_MILLI_SEC)
                 .times(1)).shutdown(false, PowerManager.SHUTDOWN_BATTERY_THERMAL_STATE, false);
     }
@@ -419,15 +419,35 @@
     }
 
     @Test
-    public void testTemperatureWatcherUpdateSevereThresholds() throws RemoteException {
+    public void testTemperatureWatcherUpdateSevereThresholds() {
         TemperatureWatcher watcher = mService.mTemperatureWatcher;
-        watcher.mSevereThresholds.erase();
-        watcher.updateThresholds();
-        assertEquals(1, watcher.mSevereThresholds.size());
-        assertEquals("skin1", watcher.mSevereThresholds.keyAt(0));
-        Float threshold = watcher.mSevereThresholds.get("skin1");
-        assertNotNull(threshold);
-        assertEquals(40.0f, threshold, 0.0f);
+        synchronized (watcher.mSamples) {
+            watcher.mSevereThresholds.erase();
+            watcher.getAndUpdateThresholds();
+            assertEquals(1, watcher.mSevereThresholds.size());
+            assertEquals("skin1", watcher.mSevereThresholds.keyAt(0));
+            Float threshold = watcher.mSevereThresholds.get("skin1");
+            assertNotNull(threshold);
+            assertEquals(40.0f, threshold, 0.0f);
+            assertArrayEquals("Got" + Arrays.toString(watcher.mHeadroomThresholds),
+                    new float[]{Float.NaN, 0.6667f, 0.8333f, 1.0f, 1.166f, 1.3333f,
+                            1.5f},
+                    watcher.mHeadroomThresholds, 0.01f);
+
+            TemperatureThreshold newThreshold = new TemperatureThreshold();
+            newThreshold.name = "skin1";
+            newThreshold.hotThrottlingThresholds = new float[] {
+                    Float.NaN, 44.0f, 47.0f, 50.0f, Float.NaN, Float.NaN, Float.NaN
+            };
+            mFakeHal.mCallback.onThresholdChanged(newThreshold);
+            threshold = watcher.mSevereThresholds.get("skin1");
+            assertNotNull(threshold);
+            assertEquals(50.0f, threshold, 0.0f);
+            assertArrayEquals("Got" + Arrays.toString(watcher.mHeadroomThresholds),
+                    new float[]{Float.NaN, 0.8f, 0.9f, 1.0f, Float.NaN, Float.NaN,
+                            Float.NaN},
+                    watcher.mHeadroomThresholds, 0.01f);
+        }
     }
 
     @Test
@@ -436,25 +456,19 @@
         synchronized (watcher.mSamples) {
             Arrays.fill(watcher.mHeadroomThresholds, Float.NaN);
         }
-        watcher.updateHeadroomThreshold(ThrottlingSeverity.LIGHT, 40, 49);
-        watcher.updateHeadroomThreshold(ThrottlingSeverity.MODERATE, 46, 49);
-        watcher.updateHeadroomThreshold(ThrottlingSeverity.SEVERE, 49, 49);
-        watcher.updateHeadroomThreshold(ThrottlingSeverity.CRITICAL, 64, 49);
-        watcher.updateHeadroomThreshold(ThrottlingSeverity.EMERGENCY, 70, 49);
-        watcher.updateHeadroomThreshold(ThrottlingSeverity.SHUTDOWN, 79, 49);
+        TemperatureThreshold threshold = new TemperatureThreshold();
+        threshold.hotThrottlingThresholds = new float[]{Float.NaN, 40, 46, 49, 64, 70, 79};
         synchronized (watcher.mSamples) {
+            watcher.updateTemperatureThresholdLocked(threshold, false /*override*/);
             assertArrayEquals(new float[]{Float.NaN, 0.7f, 0.9f, 1.0f, 1.5f, 1.7f, 2.0f},
                     watcher.mHeadroomThresholds, 0.01f);
         }
 
         // when another sensor reports different threshold, we expect to see smaller one to be used
-        watcher.updateHeadroomThreshold(ThrottlingSeverity.LIGHT, 37, 52);
-        watcher.updateHeadroomThreshold(ThrottlingSeverity.MODERATE, 46, 52);
-        watcher.updateHeadroomThreshold(ThrottlingSeverity.SEVERE, 52, 52);
-        watcher.updateHeadroomThreshold(ThrottlingSeverity.CRITICAL, 64, 52);
-        watcher.updateHeadroomThreshold(ThrottlingSeverity.EMERGENCY, 100, 52);
-        watcher.updateHeadroomThreshold(ThrottlingSeverity.SHUTDOWN, 200, 52);
+        threshold = new TemperatureThreshold();
+        threshold.hotThrottlingThresholds = new float[]{Float.NaN, 37, 46, 52, 64, 100, 200};
         synchronized (watcher.mSamples) {
+            watcher.updateTemperatureThresholdLocked(threshold, false /*override*/);
             assertArrayEquals(new float[]{Float.NaN, 0.5f, 0.8f, 1.0f, 1.4f, 1.7f, 2.0f},
                     watcher.mHeadroomThresholds, 0.01f);
         }
@@ -486,7 +500,7 @@
         TemperatureWatcher watcher = mService.mTemperatureWatcher;
         ArrayList<TemperatureThreshold> thresholds = new ArrayList<>();
         mFakeHal.mTemperatureThresholdList = thresholds;
-        watcher.updateThresholds();
+        watcher.getAndUpdateThresholds();
         synchronized (watcher.mSamples) {
             assertArrayEquals(
                     new float[]{Float.NaN, Float.NaN, Float.NaN, Float.NaN, Float.NaN, Float.NaN,
@@ -501,7 +515,7 @@
         Arrays.fill(nanThresholds.hotThrottlingThresholds, Float.NaN);
         Arrays.fill(nanThresholds.coldThrottlingThresholds, Float.NaN);
         thresholds.add(nanThresholds);
-        watcher.updateThresholds();
+        watcher.getAndUpdateThresholds();
         synchronized (watcher.mSamples) {
             assertArrayEquals(
                     new float[]{Float.NaN, Float.NaN, Float.NaN, Float.NaN, Float.NaN, Float.NaN,
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 44de65a..94be3d4 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -127,7 +127,9 @@
 
     /**
      * Exception from the satellite service containing the {@link SatelliteResult} error code.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static class SatelliteException extends Exception {
         @SatelliteResult private final int mErrorCode;
@@ -257,140 +259,210 @@
 
     /**
      * The request was successfully processed.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_SUCCESS = 0;
+
     /**
      * A generic error which should be used only when other specific errors cannot be used.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_ERROR = 1;
+
     /**
      * Error received from the satellite server.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_SERVER_ERROR = 2;
+
     /**
      * Error received from the vendor service. This generic error code should be used
      * only when the error cannot be mapped to other specific service error codes.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_SERVICE_ERROR = 3;
+
     /**
      * Error received from satellite modem. This generic error code should be used only when
      * the error cannot be mapped to other specific modem error codes.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_MODEM_ERROR = 4;
+
     /**
      * Error received from the satellite network. This generic error code should be used only when
      * the error cannot be mapped to other specific network error codes.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_NETWORK_ERROR = 5;
+
     /**
      * Telephony is not in a valid state to receive requests from clients.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_INVALID_TELEPHONY_STATE = 6;
+
     /**
      * Satellite modem is not in a valid state to receive requests from clients.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_INVALID_MODEM_STATE = 7;
+
     /**
      * Either vendor service, or modem, or Telephony framework has received a request with
      * invalid arguments from its clients.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_INVALID_ARGUMENTS = 8;
+
     /**
      * Telephony framework failed to send a request or receive a response from the vendor service
      * or satellite modem due to internal error.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_REQUEST_FAILED = 9;
+
     /**
      * Radio did not start or is resetting.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_RADIO_NOT_AVAILABLE = 10;
+
     /**
      * The request is not supported by either the satellite modem or the network.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_REQUEST_NOT_SUPPORTED = 11;
+
     /**
      * Satellite modem or network has no resources available to handle requests from clients.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_NO_RESOURCES = 12;
+
     /**
      * Satellite service is not provisioned yet.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_SERVICE_NOT_PROVISIONED = 13;
+
     /**
      * Satellite service provision is already in progress.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_SERVICE_PROVISION_IN_PROGRESS = 14;
+
     /**
      * The ongoing request was aborted by either the satellite modem or the network.
      * This error is also returned when framework decides to abort current send request as one
      * of the previous send request failed.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_REQUEST_ABORTED = 15;
+
     /**
      * The device/subscriber is barred from accessing the satellite service.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_ACCESS_BARRED = 16;
+
     /**
      * Satellite modem timeout to receive ACK or response from the satellite network after
      * sending a request to the network.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_NETWORK_TIMEOUT = 17;
+
     /**
      * Satellite network is not reachable from the modem.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_NOT_REACHABLE = 18;
+
     /**
      * The device/subscriber is not authorized to register with the satellite service provider.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_NOT_AUTHORIZED = 19;
+
     /**
      * The device does not support satellite.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_NOT_SUPPORTED = 20;
 
     /**
      * The current request is already in-progress.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_REQUEST_IN_PROGRESS = 21;
 
     /**
      * Satellite modem is currently busy due to which current request cannot be processed.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_MODEM_BUSY = 22;
 
     /**
      * Telephony process is not currently available or satellite is not supported.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_ILLEGAL_STATE = 23;
 
     /**
      * Telephony framework timeout to receive ACK or response from the satellite modem after
      * sending a request to the modem.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_MODEM_TIMEOUT = 24;
 
@@ -475,27 +547,41 @@
     /**
      * Unknown Non-Terrestrial radio technology. This generic radio technology should be used
      * only when the radio technology cannot be mapped to other specific radio technologies.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int NT_RADIO_TECHNOLOGY_UNKNOWN = 0;
+
     /**
      * 3GPP NB-IoT (Narrowband Internet of Things) over Non-Terrestrial-Networks technology.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int NT_RADIO_TECHNOLOGY_NB_IOT_NTN = 1;
+
     /**
      * 3GPP 5G NR over Non-Terrestrial-Networks technology.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int NT_RADIO_TECHNOLOGY_NR_NTN = 2;
+
     /**
      * 3GPP eMTC (enhanced Machine-Type Communication) over Non-Terrestrial-Networks technology.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int NT_RADIO_TECHNOLOGY_EMTC_NTN = 3;
+
     /**
      * Proprietary technology.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int NT_RADIO_TECHNOLOGY_PROPRIETARY = 4;
 
@@ -510,16 +596,35 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface NTRadioTechnology {}
 
-    /** Suggested device hold position is unknown. */
+    /**
+     * Suggested device hold position is unknown.
+     * @hide
+     */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int DEVICE_HOLD_POSITION_UNKNOWN = 0;
-    /** User is suggested to hold the device in portrait mode. */
+
+    /**
+     * User is suggested to hold the device in portrait mode.
+     * @hide
+     */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int DEVICE_HOLD_POSITION_PORTRAIT = 1;
-    /** User is suggested to hold the device in landscape mode with left hand. */
+
+    /**
+     * User is suggested to hold the device in landscape mode with left hand.
+     * @hide
+     */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int DEVICE_HOLD_POSITION_LANDSCAPE_LEFT = 2;
-    /** User is suggested to hold the device in landscape mode with right hand. */
+
+    /**
+     * User is suggested to hold the device in landscape mode with right hand.
+     * @hide
+     */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int DEVICE_HOLD_POSITION_LANDSCAPE_RIGHT = 3;
 
@@ -533,18 +638,37 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface DeviceHoldPosition {}
 
-    /** Display mode is unknown. */
+    /**
+     *  Display mode is unknown.
+     * @hide
+     */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int DISPLAY_MODE_UNKNOWN = 0;
-    /** Display mode of the device used for satellite communication for non-foldable phones. */
+
+    /**
+     * Display mode of the device used for satellite communication for non-foldable phones.
+     * @hide
+     */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int DISPLAY_MODE_FIXED = 1;
-    /** Display mode of the device used for satellite communication for foldabale phones when the
-     * device is opened. */
+
+    /**
+     * Display mode of the device used for satellite communication for foldabale phones when the
+     * device is opened.
+     * @hide
+     */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int DISPLAY_MODE_OPENED = 2;
-    /** Display mode of the device used for satellite communication for foldabable phones when the
-     * device is closed. */
+
+    /**
+     * Display mode of the device used for satellite communication for foldabable phones when the
+     * device is closed.
+     * @hide
+     */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int DISPLAY_MODE_CLOSED = 3;
 
@@ -561,13 +685,18 @@
     /**
      * The emergency call is handed over to oem-enabled satellite SOS messaging. SOS messages are
      * sent to SOS providers, which will then forward the messages to emergency providers.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_SOS = 1;
+
     /**
      * The emergency call is handed over to carrier-enabled satellite T911 messaging. T911 messages
      * are sent directly to local emergency providers.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
     public static final int EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_T911 = 2;
 
@@ -582,6 +711,22 @@
             "android.telephony.action.ACTION_SATELLITE_SUBSCRIBER_ID_LIST_CHANGED";
 
     /**
+     * Meta-data represents whether the application supports P2P SMS over carrier roaming satellite
+     * which needs manual trigger to connect to satellite. The messaging applications that supports
+     * P2P SMS over carrier roaming satellites should add the following in their AndroidManifest.
+     * {@code
+     * <application
+     *   <meta-data
+     *     android:name="android.telephony.METADATA_SATELLITE_MANUAL_CONNECT_P2P_SUPPORT"
+     *     android:value="true"/>
+     * </application>
+     * }
+     * @hide
+     */
+    public static final String METADATA_SATELLITE_MANUAL_CONNECT_P2P_SUPPORT =
+            "android.telephony.METADATA_SATELLITE_MANUAL_CONNECT_P2P_SUPPORT";
+
+    /**
      * Request to enable or disable the satellite modem and demo mode.
      * If satellite modem and cellular modem cannot work concurrently,
      * then this will disable the cellular modem if satellite modem is enabled,
@@ -598,7 +743,10 @@
      * @param resultListener Listener for the {@link SatelliteResult} result of the operation.
      *
      * @throws SecurityException if the caller doesn't have required permission.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public void requestEnabled(@NonNull EnableRequestAttributes attributes,
@@ -644,7 +792,10 @@
      *                 will return a {@link SatelliteException} with the {@link SatelliteResult}.
      *
      * @throws SecurityException if the caller doesn't have required permission.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public void requestIsEnabled(@NonNull @CallbackExecutor Executor executor,
@@ -701,7 +852,10 @@
      *                 will return a {@link SatelliteException} with the {@link SatelliteResult}.
      *
      * @throws SecurityException if the caller doesn't have required permission.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public void requestIsDemoModeEnabled(@NonNull @CallbackExecutor Executor executor,
@@ -758,7 +912,10 @@
      *                 will return a {@link SatelliteException} with the {@link SatelliteResult}.
      *
      * @throws SecurityException if the caller doesn't have required permission.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public void requestIsEmergencyModeEnabled(@NonNull @CallbackExecutor Executor executor,
@@ -816,7 +973,10 @@
      *                 service is supported on the device and {@code false} otherwise.
      *                 If the request is not successful, {@link OutcomeReceiver#onError(Throwable)}
      *                 will return a {@link SatelliteException} with the {@link SatelliteResult}.
+     *
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public void requestIsSupported(@NonNull @CallbackExecutor Executor executor,
             @NonNull OutcomeReceiver<Boolean, SatelliteException> callback) {
@@ -871,7 +1031,10 @@
      *                 will return a {@link SatelliteException} with the {@link SatelliteResult}.
      *
      * @throws SecurityException if the caller doesn't have required permission.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public void requestCapabilities(@NonNull @CallbackExecutor Executor executor,
@@ -920,56 +1083,80 @@
     /**
      * The default state indicating that datagram transfer is idle.
      * This should be sent if there are no message transfer activity happening.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_IDLE = 0;
+
     /**
      * A transition state indicating that a datagram is being sent.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_SENDING = 1;
+
     /**
      * An end state indicating that datagram sending completed successfully.
      * After datagram transfer completes, {@link #SATELLITE_DATAGRAM_TRANSFER_STATE_IDLE}
      * will be sent if no more messages are pending.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_SEND_SUCCESS = 2;
+
     /**
      * An end state indicating that datagram sending completed with a failure.
      * After datagram transfer completes, {@link #SATELLITE_DATAGRAM_TRANSFER_STATE_IDLE}
      * must be sent before reporting any additional datagram transfer state changes. All pending
      * messages will be reported as failed, to the corresponding applications.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_SEND_FAILED = 3;
+
     /**
      * A transition state indicating that a datagram is being received.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_RECEIVING = 4;
+
     /**
      * An end state indicating that datagram receiving completed successfully.
      * After datagram transfer completes, {@link #SATELLITE_DATAGRAM_TRANSFER_STATE_IDLE}
      * will be sent if no more messages are pending.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_RECEIVE_SUCCESS = 5;
+
     /**
      * An end state indicating that datagram receive operation found that there are no
      * messages to be retrieved from the satellite.
      * After datagram transfer completes, {@link #SATELLITE_DATAGRAM_TRANSFER_STATE_IDLE}
      * will be sent if no more messages are pending.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_RECEIVE_NONE = 6;
+
     /**
      * An end state indicating that datagram receive completed with a failure.
      * After datagram transfer completes, {@link #SATELLITE_DATAGRAM_TRANSFER_STATE_IDLE}
      * will be sent if no more messages are pending.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_RECEIVE_FAILED = 7;
+
     /**
      * A transition state indicating that Telephony is waiting for satellite modem to connect to a
      * satellite network before sending a datagram or polling for datagrams. If the satellite modem
@@ -978,14 +1165,19 @@
      * {@link #SATELLITE_DATAGRAM_TRANSFER_STATE_RECEIVING} will be sent. Otherwise,
      * either {@link #SATELLITE_DATAGRAM_TRANSFER_STATE_SEND_FAILED} or
      * {@link #SATELLITE_DATAGRAM_TRANSFER_STATE_RECEIVE_FAILED} will be sent.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_WAITING_TO_CONNECT = 8;
+
     /**
      * The datagram transfer state is unknown. This generic datagram transfer state should be used
      * only when the datagram transfer state cannot be mapped to other specific datagram transfer
      * states.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_UNKNOWN = -1;
 
@@ -1008,58 +1200,86 @@
 
     /**
      * Satellite modem is in idle state.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_MODEM_STATE_IDLE = 0;
+
     /**
      * Satellite modem is listening for incoming datagrams.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_MODEM_STATE_LISTENING = 1;
+
     /**
      * Satellite modem is sending and/or receiving datagrams.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING = 2;
+
     /**
      * Satellite modem is retrying to send and/or receive datagrams.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_MODEM_STATE_DATAGRAM_RETRYING = 3;
+
     /**
      * Satellite modem is powered off.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_MODEM_STATE_OFF = 4;
+
     /**
      * Satellite modem is unavailable.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_MODEM_STATE_UNAVAILABLE = 5;
+
     /**
      * The satellite modem is powered on but the device is not registered to a satellite cell.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_MODEM_STATE_NOT_CONNECTED = 6;
+
     /**
      * The satellite modem is powered on and the device is registered to a satellite cell.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_MODEM_STATE_CONNECTED = 7;
+
     /**
      * The satellite modem is being powered on.
      * @hide
      */
     public static final int SATELLITE_MODEM_STATE_ENABLING_SATELLITE = 8;
+
     /**
      * The satellite modem is being powered off.
      * @hide
      */
     public static final int SATELLITE_MODEM_STATE_DISABLING_SATELLITE = 9;
+
     /**
      * Satellite modem state is unknown. This generic modem state should be used only when the
      * modem state cannot be mapped to other specific modem states.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_MODEM_STATE_UNKNOWN = -1;
 
@@ -1083,43 +1303,56 @@
     /**
      * Datagram type is unknown. This generic datagram type should be used only when the
      * datagram type cannot be mapped to other specific datagram types.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int DATAGRAM_TYPE_UNKNOWN = 0;
+
     /**
      * Datagram type indicating that the datagram to be sent or received is of type SOS message.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int DATAGRAM_TYPE_SOS_MESSAGE = 1;
+
     /**
      * Datagram type indicating that the datagram to be sent or received is of type
      * location sharing.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int DATAGRAM_TYPE_LOCATION_SHARING = 2;
+
     /**
      * This type of datagram is used to keep the device in satellite connected state or check if
      * there is any incoming message.
      * @hide
      */
     public static final int DATAGRAM_TYPE_KEEP_ALIVE = 3;
+
     /**
      * Datagram type indicating that the datagram to be sent or received is of type SOS message and
      * is the last message to emergency service provider indicating still needs help.
      * @hide
      */
     public static final int DATAGRAM_TYPE_LAST_SOS_MESSAGE_STILL_NEED_HELP = 4;
+
     /**
      * Datagram type indicating that the datagram to be sent or received is of type SOS message and
      * is the last message to emergency service provider indicating no more help is needed.
      * @hide
      */
     public static final int DATAGRAM_TYPE_LAST_SOS_MESSAGE_NO_HELP_NEEDED = 5;
+
     /**
      * Datagram type indicating that the message to be sent or received is of type SMS.
      * @hide
      */
     public static final int DATAGRAM_TYPE_SMS = 6;
+
     /**
      * Datagram type indicating that the message to be sent is an SMS checking
      * for pending incoming SMS.
@@ -1150,7 +1383,9 @@
     /**
      * Satellite communication restricted by geolocation. This can be
      * triggered based upon geofence input provided by carrier to enable or disable satellite.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_COMMUNICATION_RESTRICTION_REASON_GEOLOCATION = 1;
 
@@ -1158,7 +1393,9 @@
      * Satellite communication restricted by entitlement server. This can be triggered based on
      * the EntitlementStatus value received from the entitlement server to enable or disable
      * satellite.
+     * @hide
      */
+    @SystemApi
     @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_COMMUNICATION_RESTRICTION_REASON_ENTITLEMENT = 2;
 
@@ -1185,7 +1422,10 @@
      * @param callback The callback to notify of satellite transmission updates.
      *
      * @throws SecurityException if the caller doesn't have required permission.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     @SuppressWarnings("SamShouldBeLast")
@@ -1268,7 +1508,10 @@
      * @param resultListener Listener for the {@link SatelliteResult} result of the operation.
      *
      * @throws SecurityException if the caller doesn't have required permission.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public void stopTransmissionUpdates(@NonNull SatelliteTransmissionUpdateCallback callback,
@@ -1326,7 +1569,10 @@
      * @param resultListener Listener for the {@link SatelliteResult} result of the operation.
      *
      * @throws SecurityException if the caller doesn't have required permission.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public void provisionService(@NonNull String token, @NonNull byte[] provisionData,
@@ -1381,7 +1627,10 @@
      * @param resultListener Listener for the {@link SatelliteResult} result of the operation.
      *
      * @throws SecurityException if the caller doesn't have required permission.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public void deprovisionService(@NonNull String token,
@@ -1424,7 +1673,10 @@
      *
      * @throws SecurityException if the caller doesn't have required permission.
      * @throws IllegalStateException if the Telephony process is not currently available.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     @SatelliteResult public int registerForProvisionStateChanged(
@@ -1476,7 +1728,10 @@
      *
      * @throws SecurityException if the caller doesn't have required permission.
      * @throws IllegalStateException if the Telephony process is not currently available.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public void unregisterForProvisionStateChanged(
@@ -1514,7 +1769,10 @@
      *                 will return a {@link SatelliteException} with the {@link SatelliteResult}.
      *
      * @throws SecurityException if the caller doesn't have required permission.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public void requestIsProvisioned(@NonNull @CallbackExecutor Executor executor,
@@ -1569,7 +1827,10 @@
      *
      * @throws SecurityException if the caller doesn't have required permission.
      * @throws IllegalStateException if the Telephony process is not currently available.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     @SatelliteResult public int registerForModemStateChanged(
@@ -1623,7 +1884,10 @@
      *
      * @throws SecurityException if the caller doesn't have required permission.
      * @throws IllegalStateException if the Telephony process is not currently available.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public void unregisterForModemStateChanged(
@@ -1663,7 +1927,10 @@
      *
      * @throws SecurityException if the caller doesn't have required permission.
      * @throws IllegalStateException if the Telephony process is not currently available.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     @SatelliteResult public int registerForIncomingDatagram(
@@ -1719,7 +1986,10 @@
      *
      * @throws SecurityException if the caller doesn't have required permission.
      * @throws IllegalStateException if the Telephony process is not currently available.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public void unregisterForIncomingDatagram(@NonNull SatelliteDatagramCallback callback) {
@@ -1757,7 +2027,10 @@
      * @param resultListener Listener for the {@link SatelliteResult} result of the operation.
      *
      * @throws SecurityException if the caller doesn't have required permission.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public void pollPendingDatagrams(@NonNull @CallbackExecutor Executor executor,
@@ -1812,7 +2085,10 @@
      * @param resultListener Listener for the {@link SatelliteResult} result of the operation.
      *
      * @throws SecurityException if the caller doesn't have required permission.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public void sendDatagram(@DatagramType int datagramType,
@@ -1860,7 +2136,10 @@
      *                 will return a {@link SatelliteException} with the {@link SatelliteResult}.
      *
      * @throws SecurityException if the caller doesn't have required permission.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public void requestIsCommunicationAllowedForCurrentLocation(
@@ -1918,7 +2197,10 @@
      *                 will return a {@link SatelliteException} with the {@link SatelliteResult}.
      *
      * @throws SecurityException if the caller doesn't have required permission.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public void requestTimeForNextSatelliteVisibility(@NonNull @CallbackExecutor Executor executor,
@@ -1976,7 +2258,10 @@
      *
      * @throws SecurityException if the caller doesn't have required permission.
      * @throws IllegalStateException if the Telephony process is not currently available.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public void setDeviceAlignedWithSatellite(boolean isAligned) {
@@ -2016,7 +2301,10 @@
      *
      * @throws SecurityException if the caller doesn't have required permission.
      * @throws IllegalArgumentException if the subscription is invalid.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
     public void requestAttachEnabledForCarrier(int subId, boolean enableSatellite,
@@ -2050,7 +2338,10 @@
      * @throws SecurityException if the caller doesn't have required permission.
      * @throws IllegalStateException if the Telephony process is not currently available.
      * @throws IllegalArgumentException if the subscription is invalid.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
     public void requestIsAttachEnabledForCarrier(int subId,
@@ -2075,7 +2366,10 @@
      *
      * @throws SecurityException if the caller doesn't have required permission.
      * @throws IllegalArgumentException if the subscription is invalid.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
     public void addAttachRestrictionForCarrier(int subId,
@@ -2120,7 +2414,10 @@
      *
      * @throws SecurityException if the caller doesn't have required permission.
      * @throws IllegalArgumentException if the subscription is invalid.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
     public void removeAttachRestrictionForCarrier(int subId,
@@ -2164,7 +2461,10 @@
      * @throws SecurityException if the caller doesn't have required permission.
      * @throws IllegalStateException if the Telephony process is not currently available.
      * @throws IllegalArgumentException if the subscription is invalid.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @SatelliteCommunicationRestrictionReason
     @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
@@ -2214,7 +2514,10 @@
      * {@link SatelliteException} with the {@link SatelliteResult}.
      *
      * @throws SecurityException if the caller doesn't have required permission.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public void requestNtnSignalStrength(@NonNull @CallbackExecutor Executor executor,
@@ -2277,7 +2580,10 @@
      *
      * @throws SecurityException if the caller doesn't have required permission.
      * @throws IllegalStateException if the Telephony process is not currently available.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public void registerForNtnSignalStrengthChanged(@NonNull @CallbackExecutor Executor executor,
@@ -2326,7 +2632,10 @@
      * @throws IllegalArgumentException if the callback is not valid or has already been
      * unregistered.
      * @throws IllegalStateException if the Telephony process is not currently available.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public void unregisterForNtnSignalStrengthChanged(@NonNull NtnSignalStrengthCallback callback) {
@@ -2360,7 +2669,10 @@
      *
      * @throws SecurityException if the caller doesn't have required permission.
      * @throws IllegalStateException if the Telephony process is not currently available.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     @SatelliteResult public int registerForCapabilitiesChanged(
@@ -2403,7 +2715,10 @@
      *
      * @throws SecurityException if the caller doesn't have required permission.
      * @throws IllegalStateException if the Telephony process is not currently available.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public void unregisterForCapabilitiesChanged(
@@ -2436,7 +2751,10 @@
      *
      * @return List of plmn for carrier satellite service. If no plmn is available, empty list will
      * be returned.
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
     @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
     @NonNull public List<String> getSatellitePlmnsForCarrier(int subId) {
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
index c6855b4..4ac567c 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
@@ -285,7 +285,11 @@
 
         val displayRect = getDisplayRect(wmHelper)
 
-        val endX = if (isLeft) displayRect.left else displayRect.right
+        val endX = if (isLeft) {
+            displayRect.left + SNAP_RESIZE_DRAG_INSET
+        } else {
+            displayRect.right - SNAP_RESIZE_DRAG_INSET
+        }
         val endY = displayRect.centerY() / 2
 
         // drag the window to snap resize
@@ -391,6 +395,7 @@
 
     private companion object {
         val TIMEOUT: Duration = Duration.ofSeconds(3)
+        const val SNAP_RESIZE_DRAG_INSET: Int = 5 // inset to avoid dragging to display edge
         const val CAPTION: String = "desktop_mode_caption"
         const val MAXIMIZE_BUTTON_VIEW: String = "maximize_button_view"
         const val MAXIMIZE_MENU: String = "maximize_menu"
diff --git a/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java
similarity index 96%
rename from tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
rename to tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java
index 6f3deab..2692e12 100644
--- a/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
+++ b/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java
@@ -40,7 +40,6 @@
 import android.tools.traces.monitors.PerfettoTraceMonitor;
 import android.tools.traces.protolog.ProtoLogTrace;
 import android.tracing.perfetto.DataSource;
-import android.util.proto.ProtoInputStream;
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
@@ -74,7 +73,7 @@
 @SuppressWarnings("ConstantConditions")
 @Presubmit
 @RunWith(JUnit4.class)
-public class PerfettoProtoLogImplTest {
+public class ProcessedPerfettoProtoLogImplTest {
     private static final String TEST_PROTOLOG_DATASOURCE_NAME = "test.android.protolog";
     private static final String MOCK_VIEWER_CONFIG_FILE = "my/mock/viewer/config/file.pb";
     private final File mTracingDirectory = InstrumentationRegistry.getInstrumentation()
@@ -100,7 +99,7 @@
 
     private static ProtoLogViewerConfigReader sReader;
 
-    public PerfettoProtoLogImplTest() throws IOException {
+    public ProcessedPerfettoProtoLogImplTest() throws IOException {
     }
 
     @BeforeClass
@@ -151,7 +150,8 @@
         ViewerConfigInputStreamProvider viewerConfigInputStreamProvider = Mockito.mock(
                 ViewerConfigInputStreamProvider.class);
         Mockito.when(viewerConfigInputStreamProvider.getInputStream())
-                .thenAnswer(it -> new ProtoInputStream(sViewerConfigBuilder.build().toByteArray()));
+                .thenAnswer(it -> new AutoClosableProtoInputStream(
+                        sViewerConfigBuilder.build().toByteArray()));
 
         sCacheUpdater = () -> {};
         sReader = Mockito.spy(new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider));
@@ -165,21 +165,16 @@
                     throw new RuntimeException(
                             "Unexpected viewer config file path provided");
                 }
-                return new ProtoInputStream(sViewerConfigBuilder.build().toByteArray());
+                return new AutoClosableProtoInputStream(sViewerConfigBuilder.build().toByteArray());
             });
         };
         sProtoLogConfigurationService =
                 new ProtoLogConfigurationServiceImpl(dataSourceBuilder, tracer);
 
-        if (android.tracing.Flags.clientSideProtoLogging()) {
-            sProtoLog = new PerfettoProtoLogImpl(
-                    MOCK_VIEWER_CONFIG_FILE, sReader, () -> sCacheUpdater.run(),
-                    TestProtoLogGroup.values(), dataSourceBuilder, sProtoLogConfigurationService);
-        } else {
-            sProtoLog = new PerfettoProtoLogImpl(
-                    viewerConfigInputStreamProvider, sReader, () -> sCacheUpdater.run(),
-                    TestProtoLogGroup.values(), dataSourceBuilder, sProtoLogConfigurationService);
-        }
+        sProtoLog = new ProcessedPerfettoProtoLogImpl(
+                MOCK_VIEWER_CONFIG_FILE, viewerConfigInputStreamProvider, sReader,
+                () -> sCacheUpdater.run(), TestProtoLogGroup.values(), dataSourceBuilder,
+                sProtoLogConfigurationService);
 
         busyWaitForDataSourceRegistration(TEST_PROTOLOG_DATASOURCE_NAME);
     }
@@ -398,18 +393,17 @@
     }
 
     @Test
-    public void log_logcatEnabledNoMessage() {
+    public void log_logcatEnabledNoMessageThrows() {
         when(sReader.getViewerString(anyLong())).thenReturn(null);
         PerfettoProtoLogImpl implSpy = Mockito.spy(sProtoLog);
         TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
         TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
 
-        implSpy.log(LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321,
-                new Object[]{5});
-
-        verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
-                LogLevel.INFO), eq("UNKNOWN MESSAGE args = (5)"));
-        verify(sReader).getViewerString(eq(1234L));
+        var assertion = assertThrows(RuntimeException.class, () ->
+                implSpy.log(LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321,
+                    new Object[]{5}));
+        Truth.assertThat(assertion).hasMessageThat()
+                .contains("Failed to decode message for logcat");
     }
 
     @Test
@@ -539,16 +533,12 @@
         PerfettoTraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
                 .enableProtoLog(TEST_PROTOLOG_DATASOURCE_NAME)
                 .build();
-        long before;
-        long after;
         try {
             traceMonitor.start();
-            before = SystemClock.elapsedRealtimeNanos();
             sProtoLog.log(
                     LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, messageHash,
                     0b01100100,
                     new Object[]{"test", 1, 0.1, true});
-            after = SystemClock.elapsedRealtimeNanos();
         } finally {
             traceMonitor.stop(mWriter);
         }
@@ -606,7 +596,8 @@
         Truth.assertThat(stacktrace).doesNotContain(DataSource.class.getSimpleName() + ".java");
         Truth.assertThat(stacktrace)
                 .doesNotContain(ProtoLogImpl.class.getSimpleName() + ".java");
-        Truth.assertThat(stacktrace).contains(PerfettoProtoLogImplTest.class.getSimpleName());
+        Truth.assertThat(stacktrace)
+                .contains(ProcessedPerfettoProtoLogImplTest.class.getSimpleName());
         Truth.assertThat(stacktrace).contains("stackTraceTrimmed");
     }
 
diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java
index 8ecddaa..3d1e208 100644
--- a/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java
+++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java
@@ -47,12 +47,12 @@
     }
 
     @Test
-    public void throwOnRegisteringDuplicateGroup() {
-        final var assertion = assertThrows(RuntimeException.class,
-                () -> ProtoLog.init(TEST_GROUP_1, TEST_GROUP_1, TEST_GROUP_2));
+    public void deduplicatesRegisteringDuplicateGroup() {
+        ProtoLog.init(TEST_GROUP_1, TEST_GROUP_1, TEST_GROUP_2);
 
-        Truth.assertThat(assertion).hasMessageThat().contains("" + TEST_GROUP_1.getId());
-        Truth.assertThat(assertion).hasMessageThat().contains("duplicate");
+        final var instance = ProtoLog.getSingleInstance();
+        Truth.assertThat(instance.getRegisteredGroups())
+                .containsExactly(TEST_GROUP_1, TEST_GROUP_2);
     }
 
     @Test
diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java
index d78ced1..9e029a8 100644
--- a/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java
+++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java
@@ -19,9 +19,12 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 
+import android.os.Build;
 import android.platform.test.annotations.Presubmit;
-import android.util.proto.ProtoInputStream;
 
+import com.google.common.truth.Truth;
+
+import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -29,6 +32,8 @@
 
 import perfetto.protos.ProtologCommon;
 
+import java.io.File;
+
 @Presubmit
 @RunWith(JUnit4.class)
 public class ProtoLogViewerConfigReaderTest {
@@ -83,7 +88,7 @@
                 ).build().toByteArray();
 
     private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider =
-            () -> new ProtoInputStream(TEST_VIEWER_CONFIG);
+            () -> new AutoClosableProtoInputStream(TEST_VIEWER_CONFIG);
 
     private ProtoLogViewerConfigReader mConfig;
 
@@ -123,6 +128,31 @@
     }
 
     @Test
+    public void viewerConfigIsOnDevice() {
+        Assume.assumeFalse(Build.FINGERPRINT.contains("robolectric"));
+
+        final String[] viewerConfigPaths;
+        if (android.tracing.Flags.perfettoProtologTracing()) {
+            viewerConfigPaths = new String[] {
+                    "/system_ext/etc/wmshell.protolog.pb",
+                    "/system/etc/core.protolog.pb",
+            };
+        } else {
+            viewerConfigPaths = new String[] {
+                    "/system_ext/etc/wmshell.protolog.json.gz",
+                    "/system/etc/protolog.conf.json.gz",
+            };
+        }
+
+        for (final var viewerConfigPath : viewerConfigPaths) {
+            File f = new File(viewerConfigPath);
+
+            Truth.assertWithMessage(f.getAbsolutePath() + " exists").that(f.exists()).isTrue();
+        }
+
+    }
+
+    @Test
     public void loadUnloadAndReloadViewerConfig() {
         loadViewerConfig();
         unloadViewerConfig();