Merge "Remove failed icon bindings on subsequent success" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 52200bf..b5f398b 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -58,11 +58,11 @@
     ":android.service.autofill.flags-aconfig-java{.generated_srcjars}",
     ":com.android.net.flags-aconfig-java{.generated_srcjars}",
     ":device_policy_aconfig_flags_lib{.generated_srcjars}",
-    ":service-jobscheduler-deviceidle.flags-aconfig-java{.generated_srcjars}",
     ":surfaceflinger_flags_java_lib{.generated_srcjars}",
     ":android.view.contentcapture.flags-aconfig-java{.generated_srcjars}",
     ":android.hardware.usb.flags-aconfig-java{.generated_srcjars}",
     ":android.tracing.flags-aconfig-java{.generated_srcjars}",
+    ":android.appwidget.flags-aconfig-java{.generated_srcjars}",
 ]
 
 filegroup {
@@ -108,6 +108,11 @@
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
 
+cc_aconfig_library {
+    name: "telephony_flags_c_lib",
+    aconfig_declarations: "telephony_flags",
+}
+
 // Window
 aconfig_declarations {
     name: "com.android.window.flags.window-aconfig",
@@ -220,6 +225,7 @@
     name: "android.security.flags-aconfig-java-host",
     aconfig_declarations: "android.security.flags-aconfig",
     host_supported: true,
+    mode: "test",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
 
@@ -249,6 +255,13 @@
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
 
+java_aconfig_library {
+    name: "android.os.flags-aconfig-java-host",
+    aconfig_declarations: "android.os.flags-aconfig",
+    host_supported: true,
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
 // VirtualDeviceManager
 java_aconfig_library {
     name: "android.companion.virtual.flags-aconfig-java",
@@ -722,3 +735,16 @@
     aconfig_declarations: "android.tracing.flags-aconfig",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
+
+// App Widgets
+aconfig_declarations {
+    name: "android.appwidget.flags-aconfig",
+    package: "android.appwidget.flags",
+    srcs: ["core/java/android/appwidget/flags.aconfig"],
+}
+
+java_aconfig_library {
+    name: "android.appwidget.flags-aconfig-java",
+    aconfig_declarations: "android.appwidget.flags-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/INPUT_OWNERS b/INPUT_OWNERS
index e02ba77..44b2f38 100644
--- a/INPUT_OWNERS
+++ b/INPUT_OWNERS
@@ -5,3 +5,5 @@
 prabirmsp@google.com
 svv@google.com
 vdevmurari@google.com
+
+per-file Virtual*=file:/services/companion/java/com/android/server/companion/virtual/OWNERS
diff --git a/Ravenwood.bp b/Ravenwood.bp
index 2f67090..b497871 100644
--- a/Ravenwood.bp
+++ b/Ravenwood.bp
@@ -82,6 +82,7 @@
         "junit",
         "truth",
         "ravenwood-junit",
+        "android.test.mock",
     ],
 }
 
diff --git a/apex/jobscheduler/service/Android.bp b/apex/jobscheduler/service/Android.bp
index 6c83add..8b55e07 100644
--- a/apex/jobscheduler/service/Android.bp
+++ b/apex/jobscheduler/service/Android.bp
@@ -13,10 +13,6 @@
     name: "service-jobscheduler",
     installable: true,
 
-    defaults: [
-        "service-jobscheduler-aconfig-libraries",
-    ],
-
     srcs: [
         "java/**/*.java",
         ":framework-jobscheduler-shared-srcs",
@@ -32,6 +28,7 @@
 
     static_libs: [
         "modules-utils-fastxmlserializer",
+        "service-jobscheduler-job.flags-aconfig-java",
     ],
 
     // Rename classes shared with the framework
diff --git a/apex/jobscheduler/service/aconfig/Android.bp b/apex/jobscheduler/service/aconfig/Android.bp
index 7d8a363..3ba7aa2 100644
--- a/apex/jobscheduler/service/aconfig/Android.bp
+++ b/apex/jobscheduler/service/aconfig/Android.bp
@@ -10,7 +10,6 @@
 java_aconfig_library {
     name: "service-jobscheduler-deviceidle.flags-aconfig-java",
     aconfig_declarations: "service-deviceidle.flags-aconfig",
-    defaults: ["framework-minus-apex-aconfig-java-defaults"],
     visibility: ["//frameworks/base:__subpackages__"],
 }
 
@@ -26,21 +25,5 @@
 java_aconfig_library {
     name: "service-jobscheduler-job.flags-aconfig-java",
     aconfig_declarations: "service-job.flags-aconfig",
-    defaults: ["framework-minus-apex-aconfig-java-defaults"],
-    visibility: ["//frameworks/base:__subpackages__"],
-}
-
-service_jobscheduler_aconfig_srcjars = [
-    ":service-jobscheduler-deviceidle.flags-aconfig-java{.generated_srcjars}",
-    ":service-jobscheduler-job.flags-aconfig-java{.generated_srcjars}",
-]
-
-// Aconfig declarations and libraries for the core framework
-java_defaults {
-    name: "service-jobscheduler-aconfig-libraries",
-    // Add java_aconfig_libraries to here to add them to the core framework
-    srcs: service_jobscheduler_aconfig_srcjars,
-    // Add aconfig-annotations-lib as a dependency for the optimization
-    libs: ["aconfig-annotations-lib"],
     visibility: ["//frameworks/base:__subpackages__"],
 }
diff --git a/core/api/current.txt b/core/api/current.txt
index 91e3a3a..30216f7 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -12848,6 +12848,7 @@
     field public static final String FEATURE_CAMERA_LEVEL_FULL = "android.hardware.camera.level.full";
     field public static final String FEATURE_CANT_SAVE_STATE = "android.software.cant_save_state";
     field public static final String FEATURE_COMPANION_DEVICE_SETUP = "android.software.companion_device_setup";
+    field @FlaggedApi("android.view.inputmethod.concurrent_input_methods") public static final String FEATURE_CONCURRENT_INPUT_METHODS = "android.software.concurrent_input_methods";
     field @Deprecated public static final String FEATURE_CONNECTION_SERVICE = "android.software.connectionservice";
     field public static final String FEATURE_CONSUMER_IR = "android.hardware.consumerir";
     field public static final String FEATURE_CONTROLS = "android.software.controls";
@@ -33179,6 +33180,7 @@
     method public int getCurrentThermalStatus();
     method public int getLocationPowerSaveMode();
     method public float getThermalHeadroom(@IntRange(from=0, to=60) int);
+    method @FlaggedApi("android.os.allow_thermal_headroom_thresholds") @NonNull public java.util.Map<java.lang.Integer,java.lang.Float> getThermalHeadroomThresholds();
     method public boolean isAllowedInLowPowerStandby(int);
     method public boolean isAllowedInLowPowerStandby(@NonNull String);
     method public boolean isBatteryDischargePredictionPersonalized();
@@ -44755,10 +44757,16 @@
     method public int getLastCauseCode();
     method @Nullable public android.net.LinkProperties getLinkProperties();
     method public int getNetworkType();
+    method @FlaggedApi("com.android.internal.telephony.flags.network_validation") public int getNetworkValidationStatus();
     method public int getState();
     method public int getTransportType();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.telephony.PreciseDataConnectionState> CREATOR;
+    field @FlaggedApi("com.android.internal.telephony.flags.network_validation") public static final int NETWORK_VALIDATION_FAILURE = 4; // 0x4
+    field @FlaggedApi("com.android.internal.telephony.flags.network_validation") public static final int NETWORK_VALIDATION_IN_PROGRESS = 2; // 0x2
+    field @FlaggedApi("com.android.internal.telephony.flags.network_validation") public static final int NETWORK_VALIDATION_NOT_REQUESTED = 1; // 0x1
+    field @FlaggedApi("com.android.internal.telephony.flags.network_validation") public static final int NETWORK_VALIDATION_SUCCESS = 3; // 0x3
+    field @FlaggedApi("com.android.internal.telephony.flags.network_validation") public static final int NETWORK_VALIDATION_UNSUPPORTED = 0; // 0x0
   }
 
   public final class RadioAccessSpecifier implements android.os.Parcelable {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index fbd2142..afea9b5 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -4598,6 +4598,14 @@
     field public static final int VIRTUAL_DISPLAY_FLAG_TRUSTED = 1024; // 0x400
   }
 
+  public final class VirtualDisplayConfig implements android.os.Parcelable {
+    method @FlaggedApi("android.companion.virtual.flags.vdm_custom_home") public boolean isHomeSupported();
+  }
+
+  public static final class VirtualDisplayConfig.Builder {
+    method @FlaggedApi("android.companion.virtual.flags.vdm_custom_home") @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setHomeSupported(boolean);
+  }
+
 }
 
 package android.hardware.hdmi {
@@ -14837,6 +14845,7 @@
     method @Deprecated public int getMtu();
     method public int getMtuV4();
     method public int getMtuV6();
+    method @FlaggedApi("com.android.internal.telephony.flags.network_validation") public int getNetworkValidationStatus();
     method @NonNull public java.util.List<java.net.InetAddress> getPcscfAddresses();
     method public int getPduSessionId();
     method public int getProtocolType();
@@ -14873,6 +14882,7 @@
     method @Deprecated @NonNull public android.telephony.data.DataCallResponse.Builder setMtu(int);
     method @NonNull public android.telephony.data.DataCallResponse.Builder setMtuV4(int);
     method @NonNull public android.telephony.data.DataCallResponse.Builder setMtuV6(int);
+    method @FlaggedApi("com.android.internal.telephony.flags.network_validation") @NonNull public android.telephony.data.DataCallResponse.Builder setNetworkValidationStatus(int);
     method @NonNull public android.telephony.data.DataCallResponse.Builder setPcscfAddresses(@NonNull java.util.List<java.net.InetAddress>);
     method @NonNull public android.telephony.data.DataCallResponse.Builder setPduSessionId(@IntRange(from=android.telephony.data.DataCallResponse.PDU_SESSION_ID_NOT_SET, to=15) int);
     method @NonNull public android.telephony.data.DataCallResponse.Builder setProtocolType(int);
@@ -14952,6 +14962,7 @@
     method public final void notifyDataCallListChanged(java.util.List<android.telephony.data.DataCallResponse>);
     method public final void notifyDataProfileUnthrottled(@NonNull android.telephony.data.DataProfile);
     method public void requestDataCallList(@NonNull android.telephony.data.DataServiceCallback);
+    method @FlaggedApi("com.android.internal.telephony.flags.network_validation") public void requestValidation(int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
     method public void setDataProfile(@NonNull java.util.List<android.telephony.data.DataProfile>, boolean, @NonNull android.telephony.data.DataServiceCallback);
     method public void setInitialAttachApn(@NonNull android.telephony.data.DataProfile, boolean, @NonNull android.telephony.data.DataServiceCallback);
     method public void setupDataCall(int, @NonNull android.telephony.data.DataProfile, boolean, boolean, int, @Nullable android.net.LinkProperties, @NonNull android.telephony.data.DataServiceCallback);
@@ -15013,6 +15024,7 @@
     method public final int getSlotIndex();
     method public void reportEmergencyDataNetworkPreferredTransportChanged(int);
     method public void reportThrottleStatusChanged(@NonNull java.util.List<android.telephony.data.ThrottleStatus>);
+    method @FlaggedApi("com.android.internal.telephony.flags.network_validation") public void requestNetworkValidation(int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
     method public final void updateQualifiedNetworkTypes(int, @NonNull java.util.List<java.lang.Integer>);
   }
 
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index 26f1c4b..9c279c3 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -2563,11 +2563,14 @@
         public static final int TYPE_LOCKSCREEN = 3;
         /** Launched from recents gesture handler. */
         public static final int TYPE_RECENTS_ANIMATION = 4;
+        /** Launched from desktop's transition handler. */
+        public static final int TYPE_DESKTOP_ANIMATION = 5;
 
         @IntDef(prefix = { "TYPE_" }, value = {
                 TYPE_LAUNCHER,
                 TYPE_NOTIFICATION,
                 TYPE_LOCKSCREEN,
+                TYPE_DESKTOP_ANIMATION
         })
         @Retention(RetentionPolicy.SOURCE)
         public @interface SourceType {}
diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java
index 0b8d1df..6b558d0 100644
--- a/core/java/android/app/backup/BackupAgent.java
+++ b/core/java/android/app/backup/BackupAgent.java
@@ -184,6 +184,14 @@
     public static final int FLAG_DEVICE_TO_DEVICE_TRANSFER = 2;
 
     /**
+     * Flag for {@link RestoreSet#backupTransportFlags} to indicate if restore should be skipped
+     * for apps that have already been launched.
+     *
+     * @hide
+     */
+    public static final int FLAG_SKIP_RESTORE_FOR_LAUNCHED_APPS = 1 << 2;
+
+    /**
      * Flag for {@link BackupDataOutput#getTransportFlags()} and
      * {@link FullBackupDataOutput#getTransportFlags()} only.
      *
diff --git a/core/java/android/appwidget/flags.aconfig b/core/java/android/appwidget/flags.aconfig
new file mode 100644
index 0000000..6a735a4
--- /dev/null
+++ b/core/java/android/appwidget/flags.aconfig
@@ -0,0 +1,8 @@
+package: "android.appwidget.flags"
+
+flag {
+  name: "generated_previews"
+  namespace: "app_widgets"
+  description: "Enable support for generated previews in AppWidgetManager"
+  bug: "306546610"
+}
diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java
index f6a7d2a..da8277c 100644
--- a/core/java/android/companion/virtual/VirtualDeviceInternal.java
+++ b/core/java/android/companion/virtual/VirtualDeviceInternal.java
@@ -271,7 +271,7 @@
             final IBinder token = new Binder(
                     "android.hardware.input.VirtualDpad:" + config.getInputDeviceName());
             mVirtualDevice.createVirtualDpad(config, token);
-            return new VirtualDpad(mVirtualDevice, token);
+            return new VirtualDpad(config, mVirtualDevice, token);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -283,7 +283,7 @@
             final IBinder token = new Binder(
                     "android.hardware.input.VirtualKeyboard:" + config.getInputDeviceName());
             mVirtualDevice.createVirtualKeyboard(config, token);
-            return new VirtualKeyboard(mVirtualDevice, token);
+            return new VirtualKeyboard(config, mVirtualDevice, token);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -295,7 +295,7 @@
             final IBinder token = new Binder(
                     "android.hardware.input.VirtualMouse:" + config.getInputDeviceName());
             mVirtualDevice.createVirtualMouse(config, token);
-            return new VirtualMouse(mVirtualDevice, token);
+            return new VirtualMouse(config, mVirtualDevice, token);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -308,7 +308,7 @@
             final IBinder token = new Binder(
                     "android.hardware.input.VirtualTouchscreen:" + config.getInputDeviceName());
             mVirtualDevice.createVirtualTouchscreen(config, token);
-            return new VirtualTouchscreen(mVirtualDevice, token);
+            return new VirtualTouchscreen(config, mVirtualDevice, token);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -322,7 +322,7 @@
                     "android.hardware.input.VirtualNavigationTouchpad:"
                             + config.getInputDeviceName());
             mVirtualDevice.createVirtualNavigationTouchpad(config, token);
-            return new VirtualNavigationTouchpad(mVirtualDevice, token);
+            return new VirtualNavigationTouchpad(config, mVirtualDevice, token);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java
index 97a7aa4..0d73e44 100644
--- a/core/java/android/companion/virtual/VirtualDeviceParams.java
+++ b/core/java/android/companion/virtual/VirtualDeviceParams.java
@@ -37,6 +37,7 @@
 import android.companion.virtual.sensor.VirtualSensorDirectChannelCallback;
 import android.content.ComponentName;
 import android.content.Context;
+import android.hardware.display.VirtualDisplayConfig;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.SharedMemory;
@@ -326,8 +327,8 @@
      * support home activities.
      *
      * @see Builder#setHomeComponent
+     * @see VirtualDisplayConfig#isHomeSupported()
      */
-    // TODO(b/297168328): Link to the relevant API for creating displays with home support.
     @FlaggedApi(Flags.FLAG_VDM_CUSTOM_HOME)
     @Nullable
     public ComponentName getHomeComponent() {
@@ -737,8 +738,9 @@
          *
          * @param homeComponent The component name to be used as home. If unset, then the system-
          *   default secondary home activity will be used.
+         *
+         * @see VirtualDisplayConfig#isHomeSupported()
          */
-        // TODO(b/297168328): Link to the relevant API for creating displays with home support.
         @FlaggedApi(Flags.FLAG_VDM_CUSTOM_HOME)
         @NonNull
         public Builder setHomeComponent(@Nullable ComponentName homeComponent) {
diff --git a/core/java/android/content/ClipData.java b/core/java/android/content/ClipData.java
index 0bc459a..67759f4 100644
--- a/core/java/android/content/ClipData.java
+++ b/core/java/android/content/ClipData.java
@@ -163,6 +163,7 @@
  * into an editor), then {@link Item#coerceToText(Context)} will ask the content
  * provider for the clip URI as text and successfully paste the entire note.
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public class ClipData implements Parcelable {
     static final String[] MIMETYPES_TEXT_PLAIN = new String[] {
         ClipDescription.MIMETYPE_TEXT_PLAIN };
@@ -387,6 +388,7 @@
          * @return Returns the item's textual representation.
          */
 //BEGIN_INCLUDE(coerceToText)
+        @android.ravenwood.annotation.RavenwoodThrow
         public CharSequence coerceToText(Context context) {
             // If this Item has an explicit textual value, simply return that.
             CharSequence text = getText();
@@ -470,6 +472,7 @@
          * and other things can be retrieved.
          * @return Returns the item's textual representation.
          */
+        @android.ravenwood.annotation.RavenwoodThrow
         public CharSequence coerceToStyledText(Context context) {
             CharSequence text = getText();
             if (text instanceof Spanned) {
@@ -520,6 +523,7 @@
          * and other things can be retrieved.
          * @return Returns the item's representation as HTML text.
          */
+        @android.ravenwood.annotation.RavenwoodThrow
         public String coerceToHtmlText(Context context) {
             // If the item has an explicit HTML value, simply return that.
             String htmlText = getHtmlText();
@@ -540,6 +544,7 @@
             return text != null ? text.toString() : null;
         }
 
+        @android.ravenwood.annotation.RavenwoodThrow
         private CharSequence coerceToHtmlOrStyledText(Context context, boolean styled) {
             // If this Item has a URI value, try using that.
             if (mUri != null) {
@@ -1030,6 +1035,7 @@
      *
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodThrow
     public void prepareToLeaveProcess(boolean leavingPackage) {
         // Assume that callers are going to be granting permissions
         prepareToLeaveProcess(leavingPackage, Intent.FLAG_GRANT_READ_URI_PERMISSION);
@@ -1040,6 +1046,7 @@
      *
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodThrow
     public void prepareToLeaveProcess(boolean leavingPackage, int intentFlags) {
         final int size = mItems.size();
         for (int i = 0; i < size; i++) {
@@ -1060,6 +1067,7 @@
     }
 
     /** {@hide} */
+    @android.ravenwood.annotation.RavenwoodThrow
     public void prepareToEnterProcess(AttributionSource source) {
         final int size = mItems.size();
         for (int i = 0; i < size; i++) {
@@ -1073,6 +1081,7 @@
     }
 
     /** @hide */
+    @android.ravenwood.annotation.RavenwoodThrow
     public void fixUris(int contentUserHint) {
         final int size = mItems.size();
         for (int i = 0; i < size; i++) {
@@ -1090,6 +1099,7 @@
      * Only fixing the data field of the intents
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodThrow
     public void fixUrisLight(int contentUserHint) {
         final int size = mItems.size();
         for (int i = 0; i < size; i++) {
diff --git a/core/java/android/content/ClipDescription.java b/core/java/android/content/ClipDescription.java
index de2ba44..5953890 100644
--- a/core/java/android/content/ClipDescription.java
+++ b/core/java/android/content/ClipDescription.java
@@ -48,6 +48,7 @@
  * developer guide.</p>
  * </div>
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public class ClipDescription implements Parcelable {
     /**
      * The MIME type for a clip holding plain text.
diff --git a/core/java/android/content/ComponentName.java b/core/java/android/content/ComponentName.java
index f12e971..a6a6bcc 100644
--- a/core/java/android/content/ComponentName.java
+++ b/core/java/android/content/ComponentName.java
@@ -37,6 +37,7 @@
  * name inside of that package.
  *
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public final class ComponentName implements Parcelable, Cloneable, Comparable<ComponentName> {
     private final String mPackage;
     private final String mClass;
diff --git a/core/java/android/content/ContentUris.java b/core/java/android/content/ContentUris.java
index 767d3f6..093faff 100644
--- a/core/java/android/content/ContentUris.java
+++ b/core/java/android/content/ContentUris.java
@@ -70,6 +70,7 @@
 *</dl>
 *
 */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public class ContentUris {
 
     /**
diff --git a/core/java/android/content/ContentValues.java b/core/java/android/content/ContentValues.java
index 02a5ba1..bde2f3e 100644
--- a/core/java/android/content/ContentValues.java
+++ b/core/java/android/content/ContentValues.java
@@ -35,6 +35,7 @@
  * This class is used to store a set of values that the {@link ContentResolver}
  * can process.
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public final class ContentValues implements Parcelable {
     public static final String TAG = "ContentValues";
 
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index ea54c91..665ba11 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -660,6 +660,7 @@
  * {@link #setFlags} and {@link #addFlags}.  See {@link #setFlags} for a list
  * of all possible flags.
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public class Intent implements Parcelable, Cloneable {
     private static final String TAG = "Intent";
 
@@ -2800,6 +2801,12 @@
     /**
      * Broadcast Action: An application package that was previously in the stopped state has been
      * started and is no longer considered stopped.
+     * <p>When a package is force-stopped, the {@link #ACTION_PACKAGE_RESTARTED} broadcast is sent
+     * and the package in the stopped state cannot self-start for any reason unless there's an
+     * explicit request to start a component in the package. The {@link #ACTION_PACKAGE_UNSTOPPED}
+     * broadcast is sent when such an explicit process start occurs and the package is taken
+     * out of the stopped state.
+     * </p>
      * <ul>
      * <li> {@link #EXTRA_UID} containing the integer uid assigned to the package.
      * <li> {@link #EXTRA_TIME} containing the {@link SystemClock#elapsedRealtime()
@@ -2807,6 +2814,9 @@
      * </ul>
      *
      * <p class="note">This is a protected intent that can only be sent by the system.
+     *
+     * @see ApplicationInfo#FLAG_STOPPED
+     * @see #ACTION_PACKAGE_RESTARTED
      */
     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
     @FlaggedApi(android.content.pm.Flags.FLAG_STAY_STOPPED)
@@ -12171,6 +12181,7 @@
      *
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodThrow
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public void prepareToLeaveProcess(Context context) {
         final boolean leavingPackage;
@@ -12192,6 +12203,7 @@
      *
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodThrow
     public void prepareToLeaveProcess(boolean leavingPackage) {
         setAllowFds(false);
 
@@ -12287,6 +12299,7 @@
     /**
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodThrow
     public void prepareToEnterProcess(boolean fromProtectedComponent, AttributionSource source) {
         if (fromProtectedComponent) {
             prepareToEnterProcess(LOCAL_FLAG_FROM_PROTECTED_COMPONENT, source);
@@ -12298,6 +12311,7 @@
     /**
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodThrow
     public void prepareToEnterProcess(int localFlags, AttributionSource source) {
         // We just entered destination process, so we should be able to read all
         // parcelables inside.
@@ -12369,6 +12383,7 @@
     /**
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodThrow
      public void fixUris(int contentUserHint) {
         Uri data = getData();
         if (data != null) {
@@ -12408,6 +12423,7 @@
      * @return Whether any contents were migrated.
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodThrow
     public boolean migrateExtraStreamToClipData() {
         return migrateExtraStreamToClipData(AppGlobals.getInitialApplication());
     }
@@ -12421,6 +12437,7 @@
      * @return Whether any contents were migrated.
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodThrow
     public boolean migrateExtraStreamToClipData(Context context) {
         // Refuse to touch if extras already parcelled
         if (mExtras != null && mExtras.isParcelled()) return false;
@@ -12536,6 +12553,7 @@
         return false;
     }
 
+    @android.ravenwood.annotation.RavenwoodThrow
     private Uri maybeConvertFileToContentUri(Context context, Uri uri) {
         if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())
                 && context.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.R) {
@@ -12589,6 +12607,7 @@
 
     // TODO(b/299109198): Refactor into the {@link SdkSandboxManagerLocal}
     /** @hide */
+    @android.ravenwood.annotation.RavenwoodThrow
     public boolean isSandboxActivity(@NonNull Context context) {
         if (mAction != null && mAction.equals(ACTION_START_SANDBOXED_ACTIVITY)) {
             return true;
diff --git a/core/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java
index f946754..ad3acd7 100644
--- a/core/java/android/content/IntentFilter.java
+++ b/core/java/android/content/IntentFilter.java
@@ -152,6 +152,7 @@
  * that unlike the action, an IntentFilter with no categories
  * will only match an Intent that does not have any categories.
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public class IntentFilter implements Parcelable {
     private static final String TAG = "IntentFilter";
 
diff --git a/core/java/android/content/TEST_MAPPING b/core/java/android/content/TEST_MAPPING
index addede4..a2cfbf5 100644
--- a/core/java/android/content/TEST_MAPPING
+++ b/core/java/android/content/TEST_MAPPING
@@ -57,5 +57,11 @@
       ],
       "file_patterns": ["(/|^)Context.java", "(/|^)ContextWrapper.java"]
     }
+  ],
+  "ravenwood-presubmit": [
+    {
+      "name": "CtsContentTestCasesRavenwood",
+      "host": true
+    }
   ]
-}
\ No newline at end of file
+}
diff --git a/core/java/android/content/UriMatcher.java b/core/java/android/content/UriMatcher.java
index 7fa48f0..4422ade 100644
--- a/core/java/android/content/UriMatcher.java
+++ b/core/java/android/content/UriMatcher.java
@@ -119,6 +119,7 @@
     }
 </pre>
 */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public class UriMatcher
 {
     public static final int NO_MATCH = -1;
diff --git a/core/java/android/content/pm/Checksum.java b/core/java/android/content/pm/Checksum.java
index 2096727..072ffd7 100644
--- a/core/java/android/content/pm/Checksum.java
+++ b/core/java/android/content/pm/Checksum.java
@@ -139,6 +139,13 @@
     public @interface TypeMask {}
 
     /**
+     * Max size of checksum in bytes.
+     * sizeof(SHA512) == 64 bytes
+     * @hide
+     */
+    public static final int MAX_CHECKSUM_SIZE_BYTES = 64;
+
+    /**
      * Serialize checksum to the stream in binary format.
      * @hide
      */
@@ -276,10 +283,10 @@
     };
 
     @DataClass.Generated(
-            time = 1619810358402L,
+            time = 1700002689652L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/content/pm/Checksum.java",
-            inputSignatures = "public static final  int TYPE_WHOLE_MERKLE_ROOT_4K_SHA256\npublic static final @java.lang.Deprecated int TYPE_WHOLE_MD5\npublic static final @java.lang.Deprecated int TYPE_WHOLE_SHA1\npublic static final @java.lang.Deprecated int TYPE_WHOLE_SHA256\npublic static final @java.lang.Deprecated int TYPE_WHOLE_SHA512\npublic static final  int TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256\npublic static final  int TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512\nprivate final @android.content.pm.Checksum.Type int mType\nprivate final @android.annotation.NonNull byte[] mValue\npublic static  void writeToStream(java.io.DataOutputStream,android.content.pm.Checksum)\npublic static @android.annotation.NonNull android.content.pm.Checksum readFromStream(java.io.DataInputStream)\nclass Checksum extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstDefs=false)")
+            inputSignatures = "public static final  int TYPE_WHOLE_MERKLE_ROOT_4K_SHA256\npublic static final @java.lang.Deprecated int TYPE_WHOLE_MD5\npublic static final @java.lang.Deprecated int TYPE_WHOLE_SHA1\npublic static final @java.lang.Deprecated int TYPE_WHOLE_SHA256\npublic static final @java.lang.Deprecated int TYPE_WHOLE_SHA512\npublic static final  int TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256\npublic static final  int TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512\npublic static final  int MAX_CHECKSUM_SIZE_BYTES\nprivate final @android.content.pm.Checksum.Type int mType\nprivate final @android.annotation.NonNull byte[] mValue\npublic static  void writeToStream(java.io.DataOutputStream,android.content.pm.Checksum)\npublic static @android.annotation.NonNull android.content.pm.Checksum readFromStream(java.io.DataInputStream)\nclass Checksum extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstDefs=false)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 72e1066..38f137a 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -4019,6 +4019,15 @@
 
     /**
      * Feature for {@link #getSystemAvailableFeatures} and
+     * {@link #hasSystemFeature}: The device supports multiple concurrent IME sessions.
+     */
+    @FlaggedApi("android.view.inputmethod.concurrent_input_methods")
+    @SdkConstant(SdkConstantType.FEATURE)
+    public static final String FEATURE_CONCURRENT_INPUT_METHODS =
+            "android.software.concurrent_input_methods";
+
+    /**
+     * Feature for {@link #getSystemAvailableFeatures} and
      * {@link #hasSystemFeature}: The device supports device policy enforcement via device admins.
      */
     @SdkConstant(SdkConstantType.FEATURE)
diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java
index 884d463..f532c4c 100644
--- a/core/java/android/content/pm/UserProperties.java
+++ b/core/java/android/content/pm/UserProperties.java
@@ -62,6 +62,8 @@
             "mediaSharedWithParent";
     private static final String ATTR_CREDENTIAL_SHAREABLE_WITH_PARENT =
             "credentialShareableWithParent";
+    private static final String ATTR_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE =
+            "authAlwaysRequiredToDisableQuietMode";
     private static final String ATTR_DELETE_APP_WITH_PARENT = "deleteAppWithParent";
     private static final String ATTR_ALWAYS_VISIBLE = "alwaysVisible";
 
@@ -80,6 +82,7 @@
             INDEX_DELETE_APP_WITH_PARENT,
             INDEX_ALWAYS_VISIBLE,
             INDEX_HIDE_IN_SETTINGS_IN_QUIET_MODE,
+            INDEX_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE,
     })
     @Retention(RetentionPolicy.SOURCE)
     private @interface PropertyIndex {
@@ -97,6 +100,7 @@
     private static final int INDEX_DELETE_APP_WITH_PARENT = 10;
     private static final int INDEX_ALWAYS_VISIBLE = 11;
     private static final int INDEX_HIDE_IN_SETTINGS_IN_QUIET_MODE = 12;
+    private static final int INDEX_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE = 13;
     /** A bit set, mapping each PropertyIndex to whether it is present (1) or absent (0). */
     private long mPropertiesPresent = 0;
 
@@ -329,6 +333,8 @@
             setShowInSettings(orig.getShowInSettings());
             setHideInSettingsInQuietMode(orig.getHideInSettingsInQuietMode());
             setUseParentsContacts(orig.getUseParentsContacts());
+            setAuthAlwaysRequiredToDisableQuietMode(
+                    orig.isAuthAlwaysRequiredToDisableQuietMode());
         }
         if (hasQueryOrManagePermission) {
             // Add items that require QUERY_USERS or stronger.
@@ -611,6 +617,31 @@
     }
     private boolean mCredentialShareableWithParent;
 
+    /**
+     * Returns whether the profile always requires user authentication to disable from quiet mode.
+     *
+     * <p> Settings this field to true will ensure that the credential confirmation activity is
+     * always shown whenever the user requests to disable quiet mode. The behavior of credential
+     * checks is not guaranteed when the property is false and may vary depending on user types.
+     * @hide
+     */
+    public boolean isAuthAlwaysRequiredToDisableQuietMode() {
+        if (isPresent(INDEX_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE)) {
+            return mAuthAlwaysRequiredToDisableQuietMode;
+        }
+        if (mDefaultProperties != null) {
+            return mDefaultProperties.mAuthAlwaysRequiredToDisableQuietMode;
+        }
+        throw new SecurityException(
+                "You don't have permission to query authAlwaysRequiredToDisableQuietMode");
+    }
+    /** @hide */
+    public void setAuthAlwaysRequiredToDisableQuietMode(boolean val) {
+        this.mAuthAlwaysRequiredToDisableQuietMode = val;
+        setPresent(INDEX_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE);
+    }
+    private boolean mAuthAlwaysRequiredToDisableQuietMode;
+
     /*
      Indicate if {@link com.android.server.pm.CrossProfileIntentFilter}s need to be updated during
      OTA update between user-parent
@@ -693,6 +724,8 @@
                 + getCrossProfileIntentResolutionStrategy()
                 + ", mMediaSharedWithParent=" + isMediaSharedWithParent()
                 + ", mCredentialShareableWithParent=" + isCredentialShareableWithParent()
+                + ", mAuthAlwaysRequiredToDisableQuietMode="
+                + isAuthAlwaysRequiredToDisableQuietMode()
                 + ", mDeleteAppWithParent=" + getDeleteAppWithParent()
                 + ", mAlwaysVisible=" + getAlwaysVisible()
                 + "}";
@@ -720,6 +753,8 @@
         pw.println(prefix + "    mMediaSharedWithParent=" + isMediaSharedWithParent());
         pw.println(prefix + "    mCredentialShareableWithParent="
                 + isCredentialShareableWithParent());
+        pw.println(prefix + "    mAuthAlwaysRequiredToDisableQuietMode="
+                + isAuthAlwaysRequiredToDisableQuietMode());
         pw.println(prefix + "    mDeleteAppWithParent=" + getDeleteAppWithParent());
         pw.println(prefix + "    mAlwaysVisible=" + getAlwaysVisible());
     }
@@ -788,6 +823,9 @@
                 case ATTR_CREDENTIAL_SHAREABLE_WITH_PARENT:
                     setCredentialShareableWithParent(parser.getAttributeBoolean(i));
                     break;
+                case ATTR_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE:
+                    setAuthAlwaysRequiredToDisableQuietMode(parser.getAttributeBoolean(i));
+                    break;
                 case ATTR_DELETE_APP_WITH_PARENT:
                     setDeleteAppWithParent(parser.getAttributeBoolean(i));
                     break;
@@ -853,6 +891,10 @@
             serializer.attributeBoolean(null, ATTR_CREDENTIAL_SHAREABLE_WITH_PARENT,
                     mCredentialShareableWithParent);
         }
+        if (isPresent(INDEX_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE)) {
+            serializer.attributeBoolean(null, ATTR_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE,
+                    mAuthAlwaysRequiredToDisableQuietMode);
+        }
         if (isPresent(INDEX_DELETE_APP_WITH_PARENT)) {
             serializer.attributeBoolean(null, ATTR_DELETE_APP_WITH_PARENT,
                     mDeleteAppWithParent);
@@ -878,6 +920,7 @@
         dest.writeInt(mCrossProfileIntentResolutionStrategy);
         dest.writeBoolean(mMediaSharedWithParent);
         dest.writeBoolean(mCredentialShareableWithParent);
+        dest.writeBoolean(mAuthAlwaysRequiredToDisableQuietMode);
         dest.writeBoolean(mDeleteAppWithParent);
         dest.writeBoolean(mAlwaysVisible);
     }
@@ -901,6 +944,7 @@
         mCrossProfileIntentResolutionStrategy = source.readInt();
         mMediaSharedWithParent = source.readBoolean();
         mCredentialShareableWithParent = source.readBoolean();
+        mAuthAlwaysRequiredToDisableQuietMode = source.readBoolean();
         mDeleteAppWithParent = source.readBoolean();
         mAlwaysVisible = source.readBoolean();
     }
@@ -941,6 +985,7 @@
                 CROSS_PROFILE_INTENT_RESOLUTION_STRATEGY_DEFAULT;
         private boolean mMediaSharedWithParent = false;
         private boolean mCredentialShareableWithParent = false;
+        private boolean mAuthAlwaysRequiredToDisableQuietMode = false;
         private boolean mDeleteAppWithParent = false;
         private boolean mAlwaysVisible = false;
 
@@ -1010,6 +1055,14 @@
             return this;
         }
 
+        /** Sets the value for {@link #mAuthAlwaysRequiredToDisableQuietMode} */
+        public Builder setAuthAlwaysRequiredToDisableQuietMode(
+                boolean authAlwaysRequiredToDisableQuietMode) {
+            mAuthAlwaysRequiredToDisableQuietMode =
+                    authAlwaysRequiredToDisableQuietMode;
+            return this;
+        }
+
         /** Sets the value for {@link #mDeleteAppWithParent}*/
         public Builder setDeleteAppWithParent(boolean deleteAppWithParent) {
             mDeleteAppWithParent = deleteAppWithParent;
@@ -1036,6 +1089,7 @@
                     mCrossProfileIntentResolutionStrategy,
                     mMediaSharedWithParent,
                     mCredentialShareableWithParent,
+                    mAuthAlwaysRequiredToDisableQuietMode,
                     mDeleteAppWithParent,
                     mAlwaysVisible);
         }
@@ -1053,6 +1107,7 @@
             @CrossProfileIntentResolutionStrategy int crossProfileIntentResolutionStrategy,
             boolean mediaSharedWithParent,
             boolean credentialShareableWithParent,
+            boolean authAlwaysRequiredToDisableQuietMode,
             boolean deleteAppWithParent,
             boolean alwaysVisible) {
         mDefaultProperties = null;
@@ -1067,6 +1122,8 @@
         setCrossProfileIntentResolutionStrategy(crossProfileIntentResolutionStrategy);
         setMediaSharedWithParent(mediaSharedWithParent);
         setCredentialShareableWithParent(credentialShareableWithParent);
+        setAuthAlwaysRequiredToDisableQuietMode(
+                authAlwaysRequiredToDisableQuietMode);
         setDeleteAppWithParent(deleteAppWithParent);
         setAlwaysVisible(alwaysVisible);
     }
diff --git a/core/java/android/credentials/flags.aconfig b/core/java/android/credentials/flags.aconfig
index ec96215..bab84aa 100644
--- a/core/java/android/credentials/flags.aconfig
+++ b/core/java/android/credentials/flags.aconfig
@@ -20,3 +20,10 @@
     description: "Enables clearing of Credential Manager sessions when client process dies"
     bug: "308470501"
 }
+
+flag {
+    namespace: "credential_manager"
+    name: "new_settings_intents"
+    description: "Enables settings intents to redirect to new settings page"
+    bug: "307587989"
+}
\ No newline at end of file
diff --git a/core/java/android/database/sqlite/SQLiteStatement.java b/core/java/android/database/sqlite/SQLiteStatement.java
index acdc0fa..d33eadc 100644
--- a/core/java/android/database/sqlite/SQLiteStatement.java
+++ b/core/java/android/database/sqlite/SQLiteStatement.java
@@ -27,7 +27,6 @@
  * <p>
  * This class is not thread-safe.
  * </p>
- * Note that this class is unrelated to {@link SQLiteRawStatement}.
  */
 public final class SQLiteStatement extends SQLiteProgram {
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
diff --git a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
index ea951a5..0a61c32 100644
--- a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
@@ -736,6 +736,9 @@
                     return generateJpegSupportedSizes(
                             extenders.second.getSupportedPostviewResolutions(sz),
                                     streamMap);
+                }  else if (format == ImageFormat.JPEG_R) {
+                    // Jpeg_R/UltraHDR is currently not supported in the basic extension case
+                    return new ArrayList<>();
                 } else {
                     throw new IllegalArgumentException("Unsupported format: " + format);
                 }
@@ -858,6 +861,7 @@
                     switch(format) {
                         case ImageFormat.YUV_420_888:
                         case ImageFormat.JPEG:
+                        case ImageFormat.JPEG_R:
                             break;
                         default:
                             throw new IllegalArgumentException("Unsupported format: " + format);
@@ -890,6 +894,9 @@
                         } else {
                             return generateSupportedSizes(null, format, streamMap);
                         }
+                    } else if (format == ImageFormat.JPEG_R) {
+                        // Jpeg_R/UltraHDR is currently not supported in the basic extension case
+                        return new ArrayList<>();
                     } else {
                         throw new IllegalArgumentException("Unsupported format: " + format);
                     }
diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionUtils.java b/core/java/android/hardware/camera2/impl/CameraExtensionUtils.java
index f4fc472..a8066aa 100644
--- a/core/java/android/hardware/camera2/impl/CameraExtensionUtils.java
+++ b/core/java/android/hardware/camera2/impl/CameraExtensionUtils.java
@@ -47,7 +47,8 @@
 
     public static final int[] SUPPORTED_CAPTURE_OUTPUT_FORMATS = {
             CameraExtensionCharacteristics.PROCESSING_INPUT_FORMAT,
-            ImageFormat.JPEG
+            ImageFormat.JPEG,
+            ImageFormat.JPEG_R
     };
 
     public static class SurfaceInfo {
@@ -92,6 +93,10 @@
                 (dataspace == StreamConfigurationMap.HAL_DATASPACE_V0_JFIF)) {
             surfaceInfo.mFormat = ImageFormat.JPEG;
             return surfaceInfo;
+        } else if ((nativeFormat == StreamConfigurationMap.HAL_PIXEL_FORMAT_BLOB)
+                && (dataspace == StreamConfigurationMap.HAL_DATASPACE_JPEG_R)) {
+            surfaceInfo.mFormat = ImageFormat.JPEG_R;
+            return surfaceInfo;
         }
 
         return surfaceInfo;
diff --git a/core/java/android/hardware/display/VirtualDisplayConfig.java b/core/java/android/hardware/display/VirtualDisplayConfig.java
index 22e3938..7388b5b 100644
--- a/core/java/android/hardware/display/VirtualDisplayConfig.java
+++ b/core/java/android/hardware/display/VirtualDisplayConfig.java
@@ -18,10 +18,12 @@
 
 import static android.view.Display.DEFAULT_DISPLAY;
 
+import android.annotation.FlaggedApi;
 import android.annotation.FloatRange;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.hardware.display.DisplayManager.VirtualDisplayFlag;
 import android.media.projection.MediaProjection;
 import android.os.Handler;
@@ -55,6 +57,7 @@
     private final boolean mWindowManagerMirroringEnabled;
     private ArraySet<String> mDisplayCategories = null;
     private final float mRequestedRefreshRate;
+    private final boolean mIsHomeSupported;
 
     private VirtualDisplayConfig(
             @NonNull String name,
@@ -67,7 +70,8 @@
             int displayIdToMirror,
             boolean windowManagerMirroringEnabled,
             @NonNull ArraySet<String> displayCategories,
-            float requestedRefreshRate) {
+            float requestedRefreshRate,
+            boolean isHomeSupported) {
         mName = name;
         mWidth = width;
         mHeight = height;
@@ -79,6 +83,7 @@
         mWindowManagerMirroringEnabled = windowManagerMirroringEnabled;
         mDisplayCategories = displayCategories;
         mRequestedRefreshRate = requestedRefreshRate;
+        mIsHomeSupported = isHomeSupported;
     }
 
     /**
@@ -157,6 +162,18 @@
     }
 
     /**
+     * Whether this virtual display supports showing home activity and wallpaper.
+     *
+     * @see Builder#setHomeSupported
+     * @hide
+     */
+    @FlaggedApi(android.companion.virtual.flags.Flags.FLAG_VDM_CUSTOM_HOME)
+    @SystemApi
+    public boolean isHomeSupported() {
+        return android.companion.virtual.flags.Flags.vdmCustomHome() && mIsHomeSupported;
+    }
+
+    /**
      * Returns the display categories.
      *
      * @see Builder#setDisplayCategories
@@ -189,6 +206,7 @@
         dest.writeBoolean(mWindowManagerMirroringEnabled);
         dest.writeArraySet(mDisplayCategories);
         dest.writeFloat(mRequestedRefreshRate);
+        dest.writeBoolean(mIsHomeSupported);
     }
 
     @Override
@@ -213,7 +231,8 @@
                 && mDisplayIdToMirror == that.mDisplayIdToMirror
                 && mWindowManagerMirroringEnabled == that.mWindowManagerMirroringEnabled
                 && Objects.equals(mDisplayCategories, that.mDisplayCategories)
-                && mRequestedRefreshRate == that.mRequestedRefreshRate;
+                && mRequestedRefreshRate == that.mRequestedRefreshRate
+                && mIsHomeSupported == that.mIsHomeSupported;
     }
 
     @Override
@@ -221,7 +240,7 @@
         int hashCode = Objects.hash(
                 mName, mWidth, mHeight, mDensityDpi, mFlags, mSurface, mUniqueId,
                 mDisplayIdToMirror, mWindowManagerMirroringEnabled, mDisplayCategories,
-                mRequestedRefreshRate);
+                mRequestedRefreshRate, mIsHomeSupported);
         return hashCode;
     }
 
@@ -240,6 +259,7 @@
                 + " mWindowManagerMirroringEnabled=" + mWindowManagerMirroringEnabled
                 + " mDisplayCategories=" + mDisplayCategories
                 + " mRequestedRefreshRate=" + mRequestedRefreshRate
+                + " mIsHomeSupported=" + mIsHomeSupported
                 + ")";
     }
 
@@ -255,6 +275,7 @@
         mWindowManagerMirroringEnabled = in.readBoolean();
         mDisplayCategories = (ArraySet<String>) in.readArraySet(null);
         mRequestedRefreshRate = in.readFloat();
+        mIsHomeSupported = in.readBoolean();
     }
 
     @NonNull
@@ -286,6 +307,7 @@
         private boolean mWindowManagerMirroringEnabled = false;
         private ArraySet<String> mDisplayCategories = new ArraySet<>();
         private float mRequestedRefreshRate = 0.0f;
+        private boolean mIsHomeSupported = false;
 
         /**
          * Creates a new Builder.
@@ -422,6 +444,27 @@
         }
 
         /**
+         * Sets whether this display supports showing home activities and wallpaper.
+         *
+         * <p>If set to {@code true}, then the home activity relevant to this display will be
+         * automatically launched upon the display creation.</p>
+         *
+         * <p>Note: setting to {@code true} requires the display to be trusted. If the display is
+         * not trusted, this property is ignored.</p>
+         *
+         * @param isHomeSupported whether home activities are supported on the display
+         * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED
+         * @hide
+         */
+        @FlaggedApi(android.companion.virtual.flags.Flags.FLAG_VDM_CUSTOM_HOME)
+        @SystemApi
+        @NonNull
+        public Builder setHomeSupported(boolean isHomeSupported) {
+            mIsHomeSupported = isHomeSupported;
+            return this;
+        }
+
+        /**
          * Builds the {@link VirtualDisplayConfig} instance.
          */
         @NonNull
@@ -437,7 +480,8 @@
                     mDisplayIdToMirror,
                     mWindowManagerMirroringEnabled,
                     mDisplayCategories,
-                    mRequestedRefreshRate);
+                    mRequestedRefreshRate,
+                    mIsHomeSupported);
         }
     }
 }
diff --git a/core/java/android/hardware/input/VirtualDpad.java b/core/java/android/hardware/input/VirtualDpad.java
index 8133472..7f2d8a0 100644
--- a/core/java/android/hardware/input/VirtualDpad.java
+++ b/core/java/android/hardware/input/VirtualDpad.java
@@ -52,8 +52,8 @@
                                     KeyEvent.KEYCODE_DPAD_CENTER)));
 
     /** @hide */
-    public VirtualDpad(IVirtualDevice virtualDevice, IBinder token) {
-        super(virtualDevice, token);
+    public VirtualDpad(VirtualDpadConfig config, IVirtualDevice virtualDevice, IBinder token) {
+        super(config, virtualDevice, token);
     }
 
     /**
diff --git a/core/java/android/hardware/input/VirtualInputDevice.java b/core/java/android/hardware/input/VirtualInputDevice.java
index 772ba8e..931e1ff 100644
--- a/core/java/android/hardware/input/VirtualInputDevice.java
+++ b/core/java/android/hardware/input/VirtualInputDevice.java
@@ -42,9 +42,12 @@
      */
     protected final IBinder mToken;
 
+    protected final VirtualInputDeviceConfig mConfig;
+
     /** @hide */
-    VirtualInputDevice(
+    VirtualInputDevice(VirtualInputDeviceConfig config,
             IVirtualDevice virtualDevice, IBinder token) {
+        mConfig = config;
         mVirtualDevice = virtualDevice;
         mToken = token;
     }
@@ -70,4 +73,9 @@
             throw e.rethrowFromSystemServer();
         }
     }
+
+    @Override
+    public String toString() {
+        return mConfig.toString();
+    }
 }
diff --git a/core/java/android/hardware/input/VirtualInputDeviceConfig.java b/core/java/android/hardware/input/VirtualInputDeviceConfig.java
index d3dacc9..a8caa58 100644
--- a/core/java/android/hardware/input/VirtualInputDeviceConfig.java
+++ b/core/java/android/hardware/input/VirtualInputDeviceConfig.java
@@ -91,6 +91,22 @@
         dest.writeString8(mInputDeviceName);
     }
 
+    @Override
+    public String toString() {
+        return getClass().getName() + "( "
+                + " name=" + mInputDeviceName
+                + " vendorId=" + mVendorId
+                + " productId=" + mProductId
+                + " associatedDisplayId=" + mAssociatedDisplayId
+                + additionalFieldsToString() + ")";
+    }
+
+    /** @hide */
+    @NonNull
+    String additionalFieldsToString() {
+        return "";
+    }
+
     /**
      * A builder for {@link VirtualInputDeviceConfig}
      *
diff --git a/core/java/android/hardware/input/VirtualKeyEvent.java b/core/java/android/hardware/input/VirtualKeyEvent.java
index dc47f08..c0102bf 100644
--- a/core/java/android/hardware/input/VirtualKeyEvent.java
+++ b/core/java/android/hardware/input/VirtualKeyEvent.java
@@ -172,6 +172,7 @@
             KeyEvent.KEYCODE_BREAK,
             KeyEvent.KEYCODE_BACK,
             KeyEvent.KEYCODE_FORWARD,
+            KeyEvent.KEYCODE_LANGUAGE_SWITCH,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface SupportedKeycode {
@@ -205,6 +206,14 @@
         return 0;
     }
 
+    @Override
+    public String toString() {
+        return "VirtualKeyEvent("
+                + " action=" + KeyEvent.actionToString(mAction)
+                + " keyCode=" + KeyEvent.keyCodeToString(mKeyCode)
+                + " eventTime(ns)=" + mEventTimeNanos;
+    }
+
     /**
      * Returns the key code associated with this event.
      */
diff --git a/core/java/android/hardware/input/VirtualKeyboard.java b/core/java/android/hardware/input/VirtualKeyboard.java
index e569dbf..c90f893 100644
--- a/core/java/android/hardware/input/VirtualKeyboard.java
+++ b/core/java/android/hardware/input/VirtualKeyboard.java
@@ -39,8 +39,9 @@
     private final int mUnsupportedKeyCode = KeyEvent.KEYCODE_DPAD_CENTER;
 
     /** @hide */
-    public VirtualKeyboard(IVirtualDevice virtualDevice, IBinder token) {
-        super(virtualDevice, token);
+    public VirtualKeyboard(VirtualKeyboardConfig config,
+            IVirtualDevice virtualDevice, IBinder token) {
+        super(config, virtualDevice, token);
     }
 
     /**
diff --git a/core/java/android/hardware/input/VirtualKeyboardConfig.java b/core/java/android/hardware/input/VirtualKeyboardConfig.java
index 6d03065..96a1a36 100644
--- a/core/java/android/hardware/input/VirtualKeyboardConfig.java
+++ b/core/java/android/hardware/input/VirtualKeyboardConfig.java
@@ -99,6 +99,12 @@
         dest.writeString8(mLayoutType);
     }
 
+    @Override
+    @NonNull
+    String additionalFieldsToString() {
+        return " languageTag=" + mLanguageTag + " layoutType=" + mLayoutType;
+    }
+
     /**
      * Builder for creating a {@link VirtualKeyboardConfig}.
      */
diff --git a/core/java/android/hardware/input/VirtualMouse.java b/core/java/android/hardware/input/VirtualMouse.java
index 7eba2b8..51f3f69 100644
--- a/core/java/android/hardware/input/VirtualMouse.java
+++ b/core/java/android/hardware/input/VirtualMouse.java
@@ -38,8 +38,8 @@
 public class VirtualMouse extends VirtualInputDevice {
 
     /** @hide */
-    public VirtualMouse(IVirtualDevice virtualDevice, IBinder token) {
-        super(virtualDevice, token);
+    public VirtualMouse(VirtualMouseConfig config, IVirtualDevice virtualDevice, IBinder token) {
+        super(config, virtualDevice, token);
     }
 
     /**
diff --git a/core/java/android/hardware/input/VirtualMouseButtonEvent.java b/core/java/android/hardware/input/VirtualMouseButtonEvent.java
index dfdd3b4..fc42b15 100644
--- a/core/java/android/hardware/input/VirtualMouseButtonEvent.java
+++ b/core/java/android/hardware/input/VirtualMouseButtonEvent.java
@@ -110,6 +110,14 @@
         return 0;
     }
 
+    @Override
+    public String toString() {
+        return "VirtualMouseButtonEvent("
+                + " action=" + MotionEvent.actionToString(mAction)
+                + " button=" + MotionEvent.buttonStateToString(mButtonCode)
+                + " eventTime(ns)=" + mEventTimeNanos;
+    }
+
     /**
      * Returns the button code associated with this event.
      */
diff --git a/core/java/android/hardware/input/VirtualMouseRelativeEvent.java b/core/java/android/hardware/input/VirtualMouseRelativeEvent.java
index e6ad118..2a42cfc 100644
--- a/core/java/android/hardware/input/VirtualMouseRelativeEvent.java
+++ b/core/java/android/hardware/input/VirtualMouseRelativeEvent.java
@@ -61,6 +61,14 @@
         return 0;
     }
 
+    @Override
+    public String toString() {
+        return "VirtualMouseRelativeEvent("
+                + " x=" + mRelativeX
+                + " y=" + mRelativeY
+                + " eventTime(ns)=" + mEventTimeNanos;
+    }
+
     /**
      * Returns the relative x-axis movement, in pixels.
      */
diff --git a/core/java/android/hardware/input/VirtualMouseScrollEvent.java b/core/java/android/hardware/input/VirtualMouseScrollEvent.java
index 4d0a157..c89c188 100644
--- a/core/java/android/hardware/input/VirtualMouseScrollEvent.java
+++ b/core/java/android/hardware/input/VirtualMouseScrollEvent.java
@@ -65,6 +65,14 @@
         return 0;
     }
 
+    @Override
+    public String toString() {
+        return "VirtualMouseScrollEvent("
+                + " x=" + mXAxisMovement
+                + " y=" + mYAxisMovement
+                + " eventTime(ns)=" + mEventTimeNanos;
+    }
+
     /**
      * Returns the x-axis scroll movement, normalized from -1.0 to 1.0, inclusive. Positive values
      * indicate scrolling upward; negative values, downward.
diff --git a/core/java/android/hardware/input/VirtualNavigationTouchpad.java b/core/java/android/hardware/input/VirtualNavigationTouchpad.java
index 2854034..61d72e2 100644
--- a/core/java/android/hardware/input/VirtualNavigationTouchpad.java
+++ b/core/java/android/hardware/input/VirtualNavigationTouchpad.java
@@ -40,8 +40,9 @@
 public class VirtualNavigationTouchpad extends VirtualInputDevice {
 
     /** @hide */
-    public VirtualNavigationTouchpad(IVirtualDevice virtualDevice, IBinder token) {
-        super(virtualDevice, token);
+    public VirtualNavigationTouchpad(VirtualNavigationTouchpadConfig config,
+            IVirtualDevice virtualDevice, IBinder token) {
+        super(config, virtualDevice, token);
     }
 
     /**
diff --git a/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java b/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java
index 8935efa..75f7b3e 100644
--- a/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java
+++ b/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java
@@ -70,6 +70,12 @@
         dest.writeInt(mWidth);
     }
 
+    @Override
+    @NonNull
+    String additionalFieldsToString() {
+        return " width=" + mWidth + " height=" + mHeight;
+    }
+
     @NonNull
     public static final Creator<VirtualNavigationTouchpadConfig> CREATOR =
             new Creator<VirtualNavigationTouchpadConfig>() {
diff --git a/core/java/android/hardware/input/VirtualTouchEvent.java b/core/java/android/hardware/input/VirtualTouchEvent.java
index 2695a79..7936dfe 100644
--- a/core/java/android/hardware/input/VirtualTouchEvent.java
+++ b/core/java/android/hardware/input/VirtualTouchEvent.java
@@ -138,6 +138,19 @@
         return 0;
     }
 
+    @Override
+    public String toString() {
+        return "VirtualTouchEvent("
+                + " pointerId=" + mPointerId
+                + " toolType=" + MotionEvent.toolTypeToString(mToolType)
+                + " action=" + MotionEvent.actionToString(mAction)
+                + " x=" + mX
+                + " y=" + mY
+                + " pressure=" + mPressure
+                + " majorAxisSize=" + mMajorAxisSize
+                + " eventTime(ns)=" + mEventTimeNanos;
+    }
+
     /**
      * Returns the pointer id associated with this event.
      */
diff --git a/core/java/android/hardware/input/VirtualTouchscreen.java b/core/java/android/hardware/input/VirtualTouchscreen.java
index 0d07753..4ac439e 100644
--- a/core/java/android/hardware/input/VirtualTouchscreen.java
+++ b/core/java/android/hardware/input/VirtualTouchscreen.java
@@ -34,8 +34,9 @@
 @SystemApi
 public class VirtualTouchscreen extends VirtualInputDevice {
     /** @hide */
-    public VirtualTouchscreen(IVirtualDevice virtualDevice, IBinder token) {
-        super(virtualDevice, token);
+    public VirtualTouchscreen(VirtualTouchscreenConfig config,
+            IVirtualDevice virtualDevice, IBinder token) {
+        super(config, virtualDevice, token);
     }
 
     /**
diff --git a/core/java/android/hardware/input/VirtualTouchscreenConfig.java b/core/java/android/hardware/input/VirtualTouchscreenConfig.java
index aac341cc..6308459 100644
--- a/core/java/android/hardware/input/VirtualTouchscreenConfig.java
+++ b/core/java/android/hardware/input/VirtualTouchscreenConfig.java
@@ -69,6 +69,12 @@
         dest.writeInt(mHeight);
     }
 
+    @Override
+    @NonNull
+    String additionalFieldsToString() {
+        return " width=" + mWidth + " height=" + mHeight;
+    }
+
     @NonNull
     public static final Creator<VirtualTouchscreenConfig> CREATOR =
             new Creator<VirtualTouchscreenConfig>() {
diff --git a/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig b/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig
index 5c5083a..63ae28f 100644
--- a/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig
+++ b/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig
@@ -13,3 +13,10 @@
     description: "Flag incompatible charging on COMPLIANCE_WARNING_INPUT_POWER_LIMITED instead of COMPLIANCE_WARNING_OTHER when enabled"
     bug: "308700954"
 }
+
+flag {
+    name: "enable_report_usb_data_compliance_warning"
+    namespace: "system_sw_usb"
+    description: "Enable reporting USB data compliance warnings from HAL when set"
+    bug: "296119135"
+}
diff --git a/core/java/android/net/INetworkManagementEventObserver.aidl b/core/java/android/net/INetworkManagementEventObserver.aidl
index 0a6be20..eda80c8 100644
--- a/core/java/android/net/INetworkManagementEventObserver.aidl
+++ b/core/java/android/net/INetworkManagementEventObserver.aidl
@@ -85,14 +85,14 @@
     /**
      * Interface data activity status is changed.
      *
-     * @param transportType The transport type of the data activity change.
+     * @param label label of the data activity change.
      * @param active  True if the interface is actively transmitting data, false if it is idle.
      * @param tsNanos Elapsed realtime in nanos when the state of the network interface changed.
      * @param uid Uid of this event. It represents the uid that was responsible for waking the
      *            radio. For those events that are reported by system itself, not from specific uid,
      *            use -1 for the events which means no uid.
      */
-    void interfaceClassDataActivityChanged(int transportType, boolean active, long tsNanos, int uid);
+    void interfaceClassDataActivityChanged(int label, boolean active, long tsNanos, int uid);
 
     /**
      * Information about available DNS servers has been received.
diff --git a/core/java/android/os/DeadObjectException.java b/core/java/android/os/DeadObjectException.java
index e06b0f9..65ed618 100644
--- a/core/java/android/os/DeadObjectException.java
+++ b/core/java/android/os/DeadObjectException.java
@@ -19,7 +19,8 @@
 
 /**
  * The object you are calling has died, because its hosting process
- * no longer exists.
+ * no longer exists. This is also thrown for low-level binder
+ * errors.
  */
 public class DeadObjectException extends RemoteException {
     public DeadObjectException() {
diff --git a/core/java/android/os/DeadSystemRuntimeException.java b/core/java/android/os/DeadSystemRuntimeException.java
index 1e86924..82b1ad8 100644
--- a/core/java/android/os/DeadSystemRuntimeException.java
+++ b/core/java/android/os/DeadSystemRuntimeException.java
@@ -18,9 +18,10 @@
 
 /**
  * Exception thrown when a call into system_server resulted in a
- * DeadObjectException, meaning that the system_server has died.  There's
- * nothing apps can do at this point - the system will automatically restart -
- * so there's no point in catching this.
+ * DeadObjectException, meaning that the system_server has died or
+ * experienced a low-level binder error.  There's * nothing apps can
+ * do at this point - the system will automatically restart - so
+ * there's no point in catching this.
  *
  * @hide
  */
diff --git a/core/java/android/os/IThermalService.aidl b/core/java/android/os/IThermalService.aidl
index c6c8adc..bcffa45 100644
--- a/core/java/android/os/IThermalService.aidl
+++ b/core/java/android/os/IThermalService.aidl
@@ -111,4 +111,9 @@
      *     occur; returns NaN if the headroom or forecast is unavailable
      */
     float getThermalHeadroom(int forecastSeconds);
+
+    /**
+     * @return thermal headroom for each thermal status
+     */
+    float[] getThermalHeadroomThresholds();
 }
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index d2c1755..11bddfb 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -20,6 +20,7 @@
 import android.Manifest.permission;
 import android.annotation.CallbackExecutor;
 import android.annotation.CurrentTimeMillisLong;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
@@ -42,14 +43,17 @@
 
 import com.android.internal.util.Preconditions;
 
+import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.Executor;
@@ -1180,6 +1184,8 @@
 
     private final ArrayMap<OnThermalStatusChangedListener, IThermalStatusListener>
             mListenerMap = new ArrayMap<>();
+    private final Object mThermalHeadroomThresholdsLock = new Object();
+    private float[] mThermalHeadroomThresholds = null;
 
     /**
      * {@hide}
@@ -2636,6 +2642,7 @@
     public static final int THERMAL_STATUS_SHUTDOWN = Temperature.THROTTLING_SHUTDOWN;
 
     /** @hide */
+    @Target(ElementType.TYPE_USE)
     @IntDef(prefix = { "THERMAL_STATUS_" }, value = {
             THERMAL_STATUS_NONE,
             THERMAL_STATUS_LIGHT,
@@ -2800,6 +2807,63 @@
     }
 
     /**
+     * Gets the thermal headroom thresholds for all available thermal throttling status above
+     * {@link #THERMAL_STATUS_NONE}.
+     * <p>
+     * A thermal status key in the returned map is only set if the device manufacturer has the
+     * corresponding threshold defined for at least one of its sensors. If it's set, one should
+     * expect to see that from {@link #getCurrentThermalStatus()} or
+     * {@link OnThermalStatusChangedListener#onThermalStatusChanged(int)}.
+     * <p>
+     * The headroom threshold is used to interpret the possible thermal throttling status based on
+     * the headroom prediction. For example, if the headroom threshold for
+     * {@link #THERMAL_STATUS_LIGHT} is 0.7, and a headroom prediction in 10s returns 0.75
+     * (or {@code getThermalHeadroom(10)=0.75}), one can expect that in 10 seconds the system could
+     * be in lightly throttled state if the workload remains the same. The app can consider
+     * taking actions according to the nearest throttling status the difference between the headroom
+     * and the threshold.
+     * <p>
+     * For new devices it's guaranteed to have a single sensor, but for older devices with multiple
+     * sensors reporting different threshold values, the minimum threshold is taken to be
+     * conservative on predictions. Thus, when reading real-time headroom, it's not guaranteed that
+     * a real-time value of 0.75 (or {@code getThermalHeadroom(0)}=0.75) exceeding the threshold of
+     * 0.7 above will always come with lightly throttled state
+     * (or {@code getCurrentThermalStatus()=THERMAL_STATUS_LIGHT}) but it can be lower
+     * (or {@code getCurrentThermalStatus()=THERMAL_STATUS_NONE}). While it's always guaranteed that
+     * the device won't be throttled heavier than the unmet threshold's state, so a real-time
+     * headroom of 0.75 will never come with {@link #THERMAL_STATUS_MODERATE} but lower, and 0.65
+     * will never come with {@link #THERMAL_STATUS_LIGHT} but {@link #THERMAL_STATUS_NONE}.
+     * <p>
+     * The returned map of thresholds will not change between calls to this function, so it's
+     * best to call this once on initialization. Modifying the result will not change the thresholds
+     * cached by the system, and a new call to the API will get a new copy.
+     *
+     * @return map from each thermal status to its thermal headroom
+     * @throws IllegalStateException if the thermal service is not ready
+     * @throws UnsupportedOperationException if the feature is not enabled
+     */
+    @FlaggedApi(Flags.FLAG_ALLOW_THERMAL_HEADROOM_THRESHOLDS)
+    public @NonNull Map<@ThermalStatus Integer, Float> getThermalHeadroomThresholds() {
+        try {
+            synchronized (mThermalHeadroomThresholdsLock) {
+                if (mThermalHeadroomThresholds == null) {
+                    mThermalHeadroomThresholds = mThermalService.getThermalHeadroomThresholds();
+                }
+                final ArrayMap<Integer, Float> ret = new ArrayMap<>(THERMAL_STATUS_SHUTDOWN);
+                for (int status = THERMAL_STATUS_LIGHT; status <= THERMAL_STATUS_SHUTDOWN;
+                        status++) {
+                    if (!Float.isNaN(mThermalHeadroomThresholds[status])) {
+                        ret.put(status, mThermalHeadroomThresholds[status]);
+                    }
+                }
+                return ret;
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * If true, the doze component is not started until after the screen has been
      * turned off and the screen off animation has been performed.
      * @hide
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index daec1721..13572fb 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -832,10 +832,16 @@
     /**
      * Returns true if the current process is a 64-bit runtime.
      */
+    @android.ravenwood.annotation.RavenwoodReplace
     public static final boolean is64Bit() {
         return VMRuntime.getRuntime().is64Bit();
     }
 
+    /** @hide */
+    public static final boolean is64Bit$ravenwood() {
+        return "amd64".equals(System.getProperty("os.arch"));
+    }
+
     private static SomeArgs sIdentity$ravenwood;
 
     /** @hide */
@@ -906,6 +912,7 @@
      * {@link #myUid()} in that a particular user will have multiple
      * distinct apps running under it each with their own uid.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public static UserHandle myUserHandle() {
         return UserHandle.of(UserHandle.getUserId(myUid()));
     }
@@ -914,6 +921,7 @@
      * Returns whether the given uid belongs to a system core component or not.
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public static boolean isCoreUid(int uid) {
         return UserHandle.isCore(uid);
     }
@@ -924,6 +932,7 @@
      * @return Whether the uid corresponds to an application sandbox running in
      *     a specific user.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public static boolean isApplicationUid(int uid) {
         return UserHandle.isApp(uid);
     }
@@ -931,6 +940,7 @@
     /**
      * Returns whether the current process is in an isolated sandbox.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public static final boolean isIsolated() {
         return isIsolated(myUid());
     }
@@ -942,6 +952,7 @@
     @Deprecated
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.TIRAMISU,
             publicAlternatives = "Use {@link #isIsolatedUid(int)} instead.")
+    @android.ravenwood.annotation.RavenwoodKeep
     public static final boolean isIsolated(int uid) {
         return isIsolatedUid(uid);
     }
@@ -949,6 +960,7 @@
     /**
      * Returns whether the process with the given {@code uid} is an isolated sandbox.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public static final boolean isIsolatedUid(int uid) {
         uid = UserHandle.getAppId(uid);
         return (uid >= FIRST_ISOLATED_UID && uid <= LAST_ISOLATED_UID)
@@ -962,6 +974,7 @@
      */
     @SystemApi(client = MODULE_LIBRARIES)
     @TestApi
+    @android.ravenwood.annotation.RavenwoodKeep
     public static final boolean isSdkSandboxUid(int uid) {
         uid = UserHandle.getAppId(uid);
         return (uid >= FIRST_SDK_SANDBOX_UID && uid <= LAST_SDK_SANDBOX_UID);
@@ -975,6 +988,7 @@
      */
     @SystemApi(client = MODULE_LIBRARIES)
     @TestApi
+    @android.ravenwood.annotation.RavenwoodKeep
     public static final int getAppUidForSdkSandboxUid(int uid) {
         return uid - (FIRST_SDK_SANDBOX_UID - FIRST_APPLICATION_UID);
     }
@@ -987,6 +1001,7 @@
      */
     @SystemApi(client = MODULE_LIBRARIES)
     @TestApi
+    @android.ravenwood.annotation.RavenwoodKeep
     public static final int toSdkSandboxUid(int uid) {
         return uid + (FIRST_SDK_SANDBOX_UID - FIRST_APPLICATION_UID);
     }
@@ -994,6 +1009,7 @@
     /**
      * Returns whether the current process is a sdk sandbox process.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public static final boolean isSdkSandbox() {
         return isSdkSandboxUid(myUid());
     }
diff --git a/core/java/android/os/TEST_MAPPING b/core/java/android/os/TEST_MAPPING
index 2d6e09a..b5029a6 100644
--- a/core/java/android/os/TEST_MAPPING
+++ b/core/java/android/os/TEST_MAPPING
@@ -153,5 +153,11 @@
       "file_patterns": ["Bugreport[^/]*\\.java"],
       "name": "ShellTests"
     }
+  ],
+  "ravenwood-presubmit": [
+    {
+      "name": "CtsOsTestCasesRavenwood",
+      "host": true
+    }
   ]
 }
diff --git a/core/java/android/os/UserHandle.java b/core/java/android/os/UserHandle.java
index cac7f3b..0644ef1 100644
--- a/core/java/android/os/UserHandle.java
+++ b/core/java/android/os/UserHandle.java
@@ -36,6 +36,7 @@
 /**
  * Representation of a user on the device.
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public final class UserHandle implements Parcelable {
     // NOTE: keep logic in sync with system/core/libcutils/multiuser.c
 
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index d9c6bee..2419a4c 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -204,6 +204,8 @@
      * the user in locked state so that a direct boot aware DPC could reset the password.
      * Should not be used together with
      * {@link #QUIET_MODE_DISABLE_ONLY_IF_CREDENTIAL_NOT_REQUIRED} or an exception will be thrown.
+     * This flag is currently only allowed for {@link #isManagedProfile() managed profiles};
+     * usage on other profiles may result in an Exception.
      * @hide
      */
     public static final int QUIET_MODE_DISABLE_DONT_ASK_CREDENTIAL = 0x2;
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index 0809b3b..d405d1d 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -29,6 +29,13 @@
 }
 
 flag {
+    name: "allow_thermal_headroom_thresholds"
+    namespace: "game"
+    description: "Enable thermal headroom thresholds API"
+    bug: "288119641"
+}
+
+flag {
     name: "allow_private_profile"
     namespace: "profile_experiences"
     description: "Guards a new Private Profile type in UserManager - everything from its setup to config to deletion."
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index fb2ac711..dc86e3f5 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -17,6 +17,7 @@
 
 flag {
     name: "role_controller_in_system_server"
+    is_fixed_read_only: true
     namespace: "permissions"
     description: "enable role controller in system server"
     bug: "302562590"
diff --git a/core/java/android/util/TEST_MAPPING b/core/java/android/util/TEST_MAPPING
index 0ae1c15..c681f86 100644
--- a/core/java/android/util/TEST_MAPPING
+++ b/core/java/android/util/TEST_MAPPING
@@ -24,5 +24,11 @@
       ],
       "file_patterns": ["Xml"]
     }
+  ],
+  "ravenwood-presubmit": [
+    {
+      "name": "CtsUtilTestCasesRavenwood",
+      "host": true
+    }
   ]
 }
diff --git a/core/java/android/util/Xml.java b/core/java/android/util/Xml.java
index 33058d8..2a33caa 100644
--- a/core/java/android/util/Xml.java
+++ b/core/java/android/util/Xml.java
@@ -26,6 +26,7 @@
 import com.android.internal.util.ArtBinaryXmlSerializer;
 import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.BinaryXmlPullParser;
 import com.android.modules.utils.BinaryXmlSerializer;
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
@@ -38,6 +39,7 @@
 import org.xml.sax.XMLReader;
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
 import org.xmlpull.v1.XmlSerializer;
 
 import java.io.BufferedInputStream;
@@ -115,6 +117,7 @@
     /**
      * Returns a new pull parser with namespace support.
      */
+    @android.ravenwood.annotation.RavenwoodReplace
     public static XmlPullParser newPullParser() {
         try {
             XmlPullParser parser = XmlObjectFactory.newXmlPullParser();
@@ -126,6 +129,12 @@
         }
     }
 
+    /** @hide */
+    public static XmlPullParser newPullParser$ravenwood() {
+        // TODO: remove once we're linking against libcore
+        return new BinaryXmlPullParser();
+    }
+
     /**
      * Creates a new {@link TypedXmlPullParser} which is optimized for use
      * inside the system, typically by supporting only a basic set of features.
@@ -136,10 +145,17 @@
      * @hide
      */
     @SuppressWarnings("AndroidFrameworkEfficientXml")
+    @android.ravenwood.annotation.RavenwoodReplace
     public static @NonNull TypedXmlPullParser newFastPullParser() {
         return XmlUtils.makeTyped(newPullParser());
     }
 
+    /** @hide */
+    public static TypedXmlPullParser newFastPullParser$ravenwood() {
+        // TODO: remove once we're linking against libcore
+        return new BinaryXmlPullParser();
+    }
+
     /**
      * Creates a new {@link XmlPullParser} that reads XML documents using a
      * custom binary wire protocol which benchmarking has shown to be 8.5x
@@ -148,10 +164,17 @@
      *
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodReplace
     public static @NonNull TypedXmlPullParser newBinaryPullParser() {
         return new ArtBinaryXmlPullParser();
     }
 
+    /** @hide */
+    public static TypedXmlPullParser newBinaryPullParser$ravenwood() {
+        // TODO: remove once we're linking against libcore
+        return new BinaryXmlPullParser();
+    }
+
     /**
      * Creates a new {@link XmlPullParser} which is optimized for use inside the
      * system, typically by supporting only a basic set of features.
@@ -166,6 +189,7 @@
      *
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodReplace
     public static @NonNull TypedXmlPullParser resolvePullParser(@NonNull InputStream in)
             throws IOException {
         final byte[] magic = new byte[4];
@@ -198,13 +222,33 @@
         return xml;
     }
 
+    /** @hide */
+    public static @NonNull TypedXmlPullParser resolvePullParser$ravenwood(@NonNull InputStream in)
+            throws IOException {
+        // TODO: remove once we're linking against libcore
+        final TypedXmlPullParser xml = new BinaryXmlPullParser();
+        try {
+            xml.setInput(in, StandardCharsets.UTF_8.name());
+        } catch (XmlPullParserException e) {
+            throw new IOException(e);
+        }
+        return xml;
+    }
+
     /**
      * Creates a new xml serializer.
      */
+    @android.ravenwood.annotation.RavenwoodReplace
     public static XmlSerializer newSerializer() {
         return XmlObjectFactory.newXmlSerializer();
     }
 
+    /** @hide */
+    public static XmlSerializer newSerializer$ravenwood() {
+        // TODO: remove once we're linking against libcore
+        return new BinaryXmlSerializer();
+    }
+
     /**
      * Creates a new {@link XmlSerializer} which is optimized for use inside the
      * system, typically by supporting only a basic set of features.
@@ -215,10 +259,17 @@
      * @hide
      */
     @SuppressWarnings("AndroidFrameworkEfficientXml")
+    @android.ravenwood.annotation.RavenwoodReplace
     public static @NonNull TypedXmlSerializer newFastSerializer() {
         return XmlUtils.makeTyped(new FastXmlSerializer());
     }
 
+    /** @hide */
+    public static @NonNull TypedXmlSerializer newFastSerializer$ravenwood() {
+        // TODO: remove once we're linking against libcore
+        return new BinaryXmlSerializer();
+    }
+
     /**
      * Creates a new {@link XmlSerializer} that writes XML documents using a
      * custom binary wire protocol which benchmarking has shown to be 4.4x
@@ -227,10 +278,17 @@
      *
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodReplace
     public static @NonNull TypedXmlSerializer newBinarySerializer() {
         return new ArtBinaryXmlSerializer();
     }
 
+    /** @hide */
+    public static @NonNull TypedXmlSerializer newBinarySerializer$ravenwood() {
+        // TODO: remove once we're linking against libcore
+        return new BinaryXmlSerializer();
+    }
+
     /**
      * Creates a new {@link XmlSerializer} which is optimized for use inside the
      * system, typically by supporting only a basic set of features.
@@ -245,6 +303,7 @@
      *
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodReplace
     public static @NonNull TypedXmlSerializer resolveSerializer(@NonNull OutputStream out)
             throws IOException {
         final TypedXmlSerializer xml;
@@ -257,6 +316,15 @@
         return xml;
     }
 
+    /** @hide */
+    public static @NonNull TypedXmlSerializer resolveSerializer$ravenwood(@NonNull OutputStream out)
+            throws IOException {
+        // TODO: remove once we're linking against libcore
+        final TypedXmlSerializer xml = new BinaryXmlSerializer();
+        xml.setOutput(out, StandardCharsets.UTF_8.name());
+        return xml;
+    }
+
     /**
      * Copy the first XML document into the second document.
      * <p>
diff --git a/core/java/android/util/proto/TEST_MAPPING b/core/java/android/util/proto/TEST_MAPPING
index 5b98741..1261743 100644
--- a/core/java/android/util/proto/TEST_MAPPING
+++ b/core/java/android/util/proto/TEST_MAPPING
@@ -6,5 +6,11 @@
     {
       "name": "CtsProtoTestCases"
     }
+  ],
+  "ravenwood-presubmit": [
+    {
+      "name": "CtsProtoTestCasesRavenwood",
+      "host": true
+    }
   ]
 }
diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl
index 02e97da..d6535d4 100644
--- a/core/java/android/view/IWindowSession.aidl
+++ b/core/java/android/view/IWindowSession.aidl
@@ -296,9 +296,11 @@
 
     /**
     * Request the server to call setInputWindowInfo on a given Surface, and return
-    * an input channel where the client can receive input.
+    * an input channel where the client can receive input. For windows, the clientToken should be
+    * the IWindow binder object. For other requests, the token can be any unique IBinder token to
+    * be used as unique identifier.
     */
-    void grantInputChannel(int displayId, in SurfaceControl surface, in IWindow window,
+    void grantInputChannel(int displayId, in SurfaceControl surface, in IBinder clientToken,
             in IBinder hostInputToken, int flags, int privateFlags, int inputFeatures, int type,
             in IBinder windowToken, in IBinder focusGrantToken, String inputHandleName,
             out InputChannel outInputChannel);
diff --git a/core/java/android/view/InputWindowHandle.java b/core/java/android/view/InputWindowHandle.java
index 6a588eb8..59ec605 100644
--- a/core/java/android/view/InputWindowHandle.java
+++ b/core/java/android/view/InputWindowHandle.java
@@ -83,15 +83,11 @@
     public IBinder token;
 
     /**
-     * The {@link IWindow} handle if InputWindowHandle is associated with a window, null otherwise.
+     * The {@link IBinder} handle if InputWindowHandle is associated with a client token,
+     * normally the IWindow token, null otherwise.
      */
     @Nullable
     private IBinder windowToken;
-    /**
-     * Used to cache IWindow from the windowToken so we don't need to convert every time getWindow
-     * is called.
-     */
-    private IWindow window;
 
     // The window name.
     public String name;
@@ -161,6 +157,11 @@
     public Matrix transform;
 
     /**
+     * The alpha value returned from SurfaceFlinger. This will be ignored if passed as input data.
+     */
+    public float alpha;
+
+    /**
      * The input token for the window to which focus should be transferred when this input window
      * can be successfully focused. If null, this input window will not transfer its focus to
      * any other window.
@@ -181,7 +182,6 @@
         inputApplicationHandle = new InputApplicationHandle(other.inputApplicationHandle);
         token = other.token;
         windowToken = other.windowToken;
-        window = other.window;
         name = other.name;
         layoutParamsFlags = other.layoutParamsFlags;
         layoutParamsType = other.layoutParamsType;
@@ -204,6 +204,7 @@
         }
         focusTransferTarget = other.focusTransferTarget;
         contentSize = new Size(other.contentSize.getWidth(), other.contentSize.getHeight());
+        alpha = other.alpha;
     }
 
     @Override
@@ -217,6 +218,7 @@
                 .append(", displayId=").append(displayId)
                 .append(", isClone=").append((inputConfig & InputConfig.CLONE) != 0)
                 .append(", contentSize=").append(contentSize)
+                .append(", alpha=").append(alpha)
                 .toString();
 
     }
@@ -249,23 +251,14 @@
         touchableRegionSurfaceControl = new WeakReference<>(bounds);
     }
 
-    public void setWindowToken(IWindow iwindow) {
-        windowToken = iwindow.asBinder();
-        window = iwindow;
+    public void setWindowToken(IBinder iwindow) {
+        windowToken = iwindow;
     }
 
     public @Nullable IBinder getWindowToken() {
         return windowToken;
     }
 
-    public IWindow getWindow() {
-        if (window != null) {
-            return window;
-        }
-        window = IWindow.Stub.asInterface(windowToken);
-        return window;
-    }
-
     /**
      * Set the provided inputConfig flag values.
      * @param inputConfig the flag values to change
diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java
index ec96167..fb2b8b9 100644
--- a/core/java/android/view/ViewConfiguration.java
+++ b/core/java/android/view/ViewConfiguration.java
@@ -88,6 +88,13 @@
     private static final int DEFAULT_MULTI_PRESS_TIMEOUT = 300;
 
     /**
+     * Defines the default duration in milliseconds between a key being pressed and its first key
+     * repeat event being generated. Historically, Android used the long press timeout as the
+     * key repeat timeout, so its default value is set to long press timeout's default.
+     */
+    private static final int DEFAULT_KEY_REPEAT_TIMEOUT_MS = DEFAULT_LONG_PRESS_TIMEOUT;
+
+    /**
      * Defines the default duration between successive key repeats in milliseconds.
      */
     private static final int DEFAULT_KEY_REPEAT_DELAY_MS = 50;
@@ -719,11 +726,8 @@
      * @return the time before the first key repeat in milliseconds.
      */
     public static int getKeyRepeatTimeout() {
-        // Before the key repeat timeout was introduced, some users relied on changing
-        // LONG_PRESS_TIMEOUT settings to also change the key repeat timeout. To support
-        // this backward compatibility, we'll use the long press timeout as default value.
         return AppGlobals.getIntCoreSetting(Settings.Secure.KEY_REPEAT_TIMEOUT_MS,
-                getLongPressTimeout());
+                DEFAULT_KEY_REPEAT_TIMEOUT_MS);
     }
 
     /**
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 145af2e..f98e1dd 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -81,6 +81,8 @@
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 import static android.view.WindowManager.LayoutParams.TYPE_DRAWN_APPLICATION;
 import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
+import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE;
+import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
 import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL;
 import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
 import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
@@ -12029,7 +12031,8 @@
                 || motionEventAction == MotionEvent.ACTION_MOVE
                 || motionEventAction == MotionEvent.ACTION_UP;
         boolean desiredType = windowType == TYPE_BASE_APPLICATION || windowType == TYPE_APPLICATION
-                || windowType == TYPE_APPLICATION_STARTING || windowType == TYPE_DRAWN_APPLICATION;
+                || windowType == TYPE_APPLICATION_STARTING || windowType == TYPE_DRAWN_APPLICATION
+                || windowType == TYPE_NOTIFICATION_SHADE || windowType == TYPE_STATUS_BAR;
         // use toolkitSetFrameRate flag to gate the change
         return desiredAction && desiredType && sToolkitSetFrameRateReadOnlyFlagValue;
     }
diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java
index 2746340..da31078 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -228,14 +228,14 @@
                 if (mRealWm instanceof IWindowSession.Stub) {
                     mRealWm.grantInputChannel(displayId,
                             new SurfaceControl(sc, "WindowlessWindowManager.addToDisplay"),
-                            window, mHostInputToken, attrs.flags, attrs.privateFlags,
+                            window.asBinder(), mHostInputToken, attrs.flags, attrs.privateFlags,
                             attrs.inputFeatures, attrs.type,
                             attrs.token, state.mInputTransferToken, attrs.getTitle().toString(),
                             outInputChannel);
                 } else {
-                    mRealWm.grantInputChannel(displayId, sc, window, mHostInputToken, attrs.flags,
-                            attrs.privateFlags, attrs.inputFeatures, attrs.type, attrs.token,
-                            state.mInputTransferToken, attrs.getTitle().toString(),
+                    mRealWm.grantInputChannel(displayId, sc, window.asBinder(), mHostInputToken,
+                            attrs.flags, attrs.privateFlags, attrs.inputFeatures, attrs.type,
+                            attrs.token, state.mInputTransferToken, attrs.getTitle().toString(),
                             outInputChannel);
                 }
                 state.mInputChannelToken =
@@ -592,7 +592,7 @@
             List<Rect> unrestrictedRects) {}
 
     @Override
-    public void grantInputChannel(int displayId, SurfaceControl surface, IWindow window,
+    public void grantInputChannel(int displayId, SurfaceControl surface, IBinder clientToken,
             IBinder hostInputToken, int flags, int privateFlags, int inputFeatures, int type,
             IBinder windowToken, IBinder focusGrantToken, String inputHandleName,
             InputChannel outInputChannel) {
diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig
index 1e8718c..7486362 100644
--- a/core/java/android/view/inputmethod/flags.aconfig
+++ b/core/java/android/view/inputmethod/flags.aconfig
@@ -22,4 +22,12 @@
     description: "Feature flag for replacing UserIdInt with UserHandle in some helper IMM functions"
     bug: "301713309"
     is_fixed_read_only: true
-}
\ No newline at end of file
+}
+
+flag {
+    name: "concurrent_input_methods"
+    namespace: "input_method"
+    description: "Feature flag for concurrent multi-session IME"
+    bug: "284527000"
+    is_fixed_read_only: true
+}
diff --git a/core/java/com/android/internal/util/ArtFastDataInput.java b/core/java/com/android/internal/util/ArtFastDataInput.java
index 3e8916c..768ea82 100644
--- a/core/java/com/android/internal/util/ArtFastDataInput.java
+++ b/core/java/com/android/internal/util/ArtFastDataInput.java
@@ -21,6 +21,8 @@
 
 import com.android.modules.utils.FastDataInput;
 
+import dalvik.system.VMRuntime;
+
 import java.io.DataInput;
 import java.io.IOException;
 import java.io.InputStream;
@@ -35,13 +37,14 @@
  */
 public class ArtFastDataInput extends FastDataInput {
     private static AtomicReference<ArtFastDataInput> sInCache = new AtomicReference<>();
+    private static VMRuntime sRuntime = VMRuntime.getRuntime();
 
     private final long mBufferPtr;
 
     public ArtFastDataInput(@NonNull InputStream in, int bufferSize) {
         super(in, bufferSize);
 
-        mBufferPtr = mRuntime.addressOf(mBuffer);
+        mBufferPtr = sRuntime.addressOf(mBuffer);
     }
 
     /**
@@ -66,6 +69,7 @@
      * Release a {@link ArtFastDataInput} to potentially be recycled. You must not
      * interact with the object after releasing it.
      */
+    @Override
     public void release() {
         super.release();
 
@@ -76,6 +80,11 @@
     }
 
     @Override
+    public byte[] newByteArray(int bufferSize) {
+        return (byte[]) sRuntime.newNonMovableArray(byte.class, bufferSize);
+    }
+
+    @Override
     public String readUTF() throws IOException {
         // Attempt to read directly from buffer space if there's enough room,
         // otherwise fall back to chunking into place
@@ -86,9 +95,9 @@
             mBufferPos += len;
             return res;
         } else {
-            final byte[] tmp = (byte[]) mRuntime.newNonMovableArray(byte.class, len + 1);
+            final byte[] tmp = (byte[]) sRuntime.newNonMovableArray(byte.class, len + 1);
             readFully(tmp, 0, len);
-            return CharsetUtils.fromModifiedUtf8Bytes(mRuntime.addressOf(tmp), 0, len);
+            return CharsetUtils.fromModifiedUtf8Bytes(sRuntime.addressOf(tmp), 0, len);
         }
     }
 }
diff --git a/core/java/com/android/internal/util/ArtFastDataOutput.java b/core/java/com/android/internal/util/ArtFastDataOutput.java
index ac595b6..360ddb8 100644
--- a/core/java/com/android/internal/util/ArtFastDataOutput.java
+++ b/core/java/com/android/internal/util/ArtFastDataOutput.java
@@ -21,6 +21,8 @@
 
 import com.android.modules.utils.FastDataOutput;
 
+import dalvik.system.VMRuntime;
+
 import java.io.DataOutput;
 import java.io.IOException;
 import java.io.OutputStream;
@@ -35,13 +37,14 @@
  */
 public class ArtFastDataOutput extends FastDataOutput {
     private static AtomicReference<ArtFastDataOutput> sOutCache = new AtomicReference<>();
+    private static VMRuntime sRuntime = VMRuntime.getRuntime();
 
     private final long mBufferPtr;
 
     public ArtFastDataOutput(@NonNull OutputStream out, int bufferSize) {
         super(out, bufferSize);
 
-        mBufferPtr = mRuntime.addressOf(mBuffer);
+        mBufferPtr = sRuntime.addressOf(mBuffer);
     }
 
     /**
@@ -73,6 +76,11 @@
     }
 
     @Override
+    public byte[] newByteArray(int bufferSize) {
+        return (byte[]) sRuntime.newNonMovableArray(byte.class, bufferSize);
+    }
+
+    @Override
     public void writeUTF(String s) throws IOException {
         // Attempt to write directly to buffer space if there's enough room,
         // otherwise fall back to chunking into place
@@ -94,8 +102,8 @@
             // Negative value indicates buffer was too small and we need to
             // allocate a temporary buffer for encoding
             len = -len;
-            final byte[] tmp = (byte[]) mRuntime.newNonMovableArray(byte.class, len + 1);
-            CharsetUtils.toModifiedUtf8Bytes(s, mRuntime.addressOf(tmp), 0, tmp.length);
+            final byte[] tmp = (byte[]) sRuntime.newNonMovableArray(byte.class, len + 1);
+            CharsetUtils.toModifiedUtf8Bytes(s, sRuntime.addressOf(tmp), 0, tmp.length);
             writeShort(len);
             write(tmp, 0, len);
         }
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index a3e0016..28fd2b4 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -1936,7 +1936,8 @@
      * If the user is not secured, ie doesn't have an LSKF, then decrypt the user's synthetic
      * password and use it to unlock various cryptographic keys associated with the user.  This
      * primarily includes unlocking the user's credential-encrypted (CE) storage.  It also includes
-     * deriving or decrypting the vendor auth secret and sending it to the AuthSecret HAL.
+     * unlocking the user's Keystore super keys, and deriving or decrypting the vendor auth secret
+     * and sending it to the AuthSecret HAL in order to unlock Secure Element firmware updates.
      * <p>
      * These tasks would normally be done when the LSKF is verified.  This method is where these
      * tasks are done when the user doesn't have an LSKF.  It's called when the user is started.
diff --git a/core/java/com/android/server/net/BaseNetworkObserver.java b/core/java/com/android/server/net/BaseNetworkObserver.java
index 139b88b..61e017d 100644
--- a/core/java/com/android/server/net/BaseNetworkObserver.java
+++ b/core/java/com/android/server/net/BaseNetworkObserver.java
@@ -64,7 +64,7 @@
     }
 
     @Override
-    public void interfaceClassDataActivityChanged(int transportType, boolean active, long tsNanos,
+    public void interfaceClassDataActivityChanged(int label, boolean active, long tsNanos,
             int uid) {
         // default no-op
     }
diff --git a/core/jni/android_hardware_input_InputWindowHandle.cpp b/core/jni/android_hardware_input_InputWindowHandle.cpp
index c3d21a4..ae23942 100644
--- a/core/jni/android_hardware_input_InputWindowHandle.cpp
+++ b/core/jni/android_hardware_input_InputWindowHandle.cpp
@@ -74,6 +74,7 @@
     jfieldID transform;
     jfieldID windowToken;
     jfieldID focusTransferTarget;
+    jfieldID alpha;
 } gInputWindowHandleClassInfo;
 
 static struct {
@@ -325,6 +326,8 @@
     env->SetObjectField(inputWindowHandle, gInputWindowHandleClassInfo.windowToken,
                         javaObjectForIBinder(env, windowInfo.windowToken));
 
+    env->SetFloatField(inputWindowHandle, gInputWindowHandleClassInfo.alpha, windowInfo.alpha);
+
     return inputWindowHandle;
 }
 
@@ -446,6 +449,8 @@
     GET_FIELD_ID(gInputWindowHandleClassInfo.touchableRegionSurfaceControl.ctrl, clazz,
             "touchableRegionSurfaceControl", "Ljava/lang/ref/WeakReference;");
 
+    GET_FIELD_ID(gInputWindowHandleClassInfo.alpha, clazz, "alpha", "F");
+
     jclass surfaceControlClazz;
     FIND_CLASS(surfaceControlClazz, "android/view/SurfaceControl");
     GET_FIELD_ID(gInputWindowHandleClassInfo.touchableRegionSurfaceControl.mNativeObject,
diff --git a/core/jni/android_media_AudioRecord.cpp b/core/jni/android_media_AudioRecord.cpp
index b1dab85..f9d00ed 100644
--- a/core/jni/android_media_AudioRecord.cpp
+++ b/core/jni/android_media_AudioRecord.cpp
@@ -212,14 +212,11 @@
             return (jint) AUDIORECORD_ERROR_SETUP_INVALIDFORMAT;
         }
 
-        size_t bytesPerSample = audio_bytes_per_sample(format);
-
         if (buffSizeInBytes == 0) {
             ALOGE("Error creating AudioRecord: frameCount is 0.");
             return (jint) AUDIORECORD_ERROR_SETUP_ZEROFRAMECOUNT;
         }
-        size_t frameSize = channelCount * bytesPerSample;
-        size_t frameCount = buffSizeInBytes / frameSize;
+        size_t frameCount = buffSizeInBytes / audio_bytes_per_frame(channelCount, format);
 
         // create an uninitialized AudioRecord object
         Parcel* parcel = parcelForJavaObject(env, jAttributionSource);
@@ -574,7 +571,7 @@
     if (result != NO_ERROR) {
         return -1;
     }
-    return frameCount * channelCount * audio_bytes_per_sample(format);
+    return frameCount * audio_bytes_per_frame(channelCount, format);
 }
 
 static jboolean android_media_AudioRecord_setInputDevice(
diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml
index 30beee0..eddd81e 100644
--- a/core/res/res/values/colors.xml
+++ b/core/res/res/values/colors.xml
@@ -521,7 +521,7 @@
     <color name="system_surface_dim_dark">#121316</color>
     <color name="system_surface_variant_dark">#44474F</color>
     <color name="system_on_surface_variant_dark">#C4C6D0</color>
-    <color name="system_outline_dark">#72747D</color>
+    <color name="system_outline_dark">#8E9099</color>
     <color name="system_outline_variant_dark">#444746</color>
     <color name="system_error_dark">#FFB4A8</color>
     <color name="system_on_error_dark">#690001</color>
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index dc2b056..f19acbe 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -415,12 +415,6 @@
       "group": "WM_DEBUG_WINDOW_TRANSITIONS",
       "at": "com\/android\/server\/wm\/Transition.java"
     },
-    "-1717147904": {
-      "message": "Current focused window is embeddedWindow. Dispatch KEYCODE_BACK.",
-      "level": "DEBUG",
-      "group": "WM_DEBUG_BACK_PREVIEW",
-      "at": "com\/android\/server\/wm\/BackNavigationController.java"
-    },
     "-1710206702": {
       "message": "Display id=%d is frozen while keyguard locked, return %d",
       "level": "VERBOSE",
@@ -817,6 +811,12 @@
       "group": "WM_DEBUG_STARTING_WINDOW",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
+    "-1325565952": {
+      "message": "Attempted to get home support flag of a display that does not exist: %d",
+      "level": "WARN",
+      "group": "WM_ERROR",
+      "at": "com\/android\/server\/wm\/WindowManagerService.java"
+    },
     "-1323783276": {
       "message": "performEnableScreen: bootFinished() failed.",
       "level": "WARN",
@@ -1213,12 +1213,6 @@
       "group": "WM_DEBUG_STATES",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
-    "-997565097": {
-      "message": "Focused window found using getFocusedWindowToken",
-      "level": "DEBUG",
-      "group": "WM_DEBUG_BACK_PREVIEW",
-      "at": "com\/android\/server\/wm\/BackNavigationController.java"
-    },
     "-993378225": {
       "message": "finishDrawingLocked: mDrawState=COMMIT_DRAW_PENDING %s in %s",
       "level": "VERBOSE",
@@ -2233,6 +2227,12 @@
       "group": "WM_DEBUG_RECENTS_ANIMATIONS",
       "at": "com\/android\/server\/wm\/RecentsAnimation.java"
     },
+    "-98422345": {
+      "message": "Focus window is closing.",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_BACK_PREVIEW",
+      "at": "com\/android\/server\/wm\/BackNavigationController.java"
+    },
     "-91393839": {
       "message": "Set animatingExit: reason=remove\/applyAnimation win=%s",
       "level": "VERBOSE",
@@ -2731,12 +2731,6 @@
       "group": "WM_DEBUG_STATES",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
-    "309039362": {
-      "message": "SURFACE MATRIX [%f,%f,%f,%f]: %s",
-      "level": "INFO",
-      "group": "WM_SHOW_TRANSACTIONS",
-      "at": "com\/android\/server\/wm\/WindowSurfaceController.java"
-    },
     "312030608": {
       "message": "New topFocusedDisplayId=%d",
       "level": "DEBUG",
@@ -3091,12 +3085,6 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
-    "633654009": {
-      "message": "SURFACE POS (setPositionInTransaction) @ (%f,%f): %s",
-      "level": "INFO",
-      "group": "WM_SHOW_TRANSACTIONS",
-      "at": "com\/android\/server\/wm\/WindowSurfaceController.java"
-    },
     "638429464": {
       "message": "\tRemove container=%s",
       "level": "DEBUG",
diff --git a/data/keyboards/Generic.kl b/data/keyboards/Generic.kl
index 51b720d..f9347ee 100644
--- a/data/keyboards/Generic.kl
+++ b/data/keyboards/Generic.kl
@@ -324,7 +324,7 @@
 # key 365 "KEY_EPG"
 key 366   DVR
 # key 367 "KEY_MHP"
-# key 368 "KEY_LANGUAGE"
+key 368   LANGUAGE_SWITCH
 # key 369 "KEY_TITLE"
 key 370   CAPTIONS
 # key 371 "KEY_ANGLE"
diff --git a/keystore/java/android/security/AndroidKeyStoreMaintenance.java b/keystore/java/android/security/AndroidKeyStoreMaintenance.java
index b7ea04f..2beb434 100644
--- a/keystore/java/android/security/AndroidKeyStoreMaintenance.java
+++ b/keystore/java/android/security/AndroidKeyStoreMaintenance.java
@@ -51,7 +51,7 @@
      * @return 0 if successful or a {@code ResponseCode}
      * @hide
      */
-    public static int onUserAdded(@NonNull int userId) {
+    public static int onUserAdded(int userId) {
         StrictMode.noteDiskWrite();
         try {
             getService().onUserAdded(userId);
@@ -66,6 +66,30 @@
     }
 
     /**
+     * Tells Keystore to create a user's super keys and store them encrypted by the given secret.
+     *
+     * @param userId - Android user id of the user
+     * @param password - a secret derived from the user's synthetic password
+     * @param allowExisting - true if the keys already existing should not be considered an error
+     * @return 0 if successful or a {@code ResponseCode}
+     * @hide
+     */
+    public static int initUserSuperKeys(int userId, @NonNull byte[] password,
+            boolean allowExisting) {
+        StrictMode.noteDiskWrite();
+        try {
+            getService().initUserSuperKeys(userId, password, allowExisting);
+            return 0;
+        } catch (ServiceSpecificException e) {
+            Log.e(TAG, "initUserSuperKeys failed", e);
+            return e.errorCode;
+        } catch (Exception e) {
+            Log.e(TAG, "Can not connect to keystore", e);
+            return SYSTEM_ERROR;
+        }
+    }
+
+    /**
      * Informs Keystore 2.0 about removing a user
      *
      * @param userId - Android user id of the user being removed
@@ -110,6 +134,28 @@
     }
 
     /**
+     * Tells Keystore that a user's LSKF is being removed, ie the user's lock screen is changing to
+     * Swipe or None.  Keystore uses this notification to delete the user's auth-bound keys.
+     *
+     * @param userId - Android user id of the user
+     * @return 0 if successful or a {@code ResponseCode}
+     * @hide
+     */
+    public static int onUserLskfRemoved(int userId) {
+        StrictMode.noteDiskWrite();
+        try {
+            getService().onUserLskfRemoved(userId);
+            return 0;
+        } catch (ServiceSpecificException e) {
+            Log.e(TAG, "onUserLskfRemoved failed", e);
+            return e.errorCode;
+        } catch (Exception e) {
+            Log.e(TAG, "Can not connect to keystore", e);
+            return SYSTEM_ERROR;
+        }
+    }
+
+    /**
      * Informs Keystore 2.0 that an app was uninstalled and the corresponding namespace is to
      * be cleared.
      */
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index 29bdd5c..c366ccd 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -35,3 +35,10 @@
     description: "Enables taskbar / navbar unification"
     bug: "309671494"
 }
+
+flag {
+    name: "enable_pip_ui_state_on_entering"
+    namespace: "multitasking"
+    description: "Enables PiP UI state callback on entering"
+    bug: "303718131"
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java
index ab61a48..5143d41 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java
@@ -115,6 +115,19 @@
         b.setParent(sc);
     }
 
+    /**
+     * Re-parents the provided surface to the leash of the provided display.
+     *
+     * @param displayId the display area to reparent to.
+     * @param sc the surface to be reparented.
+     * @param t a {@link SurfaceControl.Transaction} in which to reparent.
+     */
+    public void reparentToDisplayArea(int displayId, SurfaceControl sc,
+                                      SurfaceControl.Transaction t) {
+        final SurfaceControl displayAreaLeash = mLeashes.get(displayId);
+        t.reparent(sc, displayAreaLeash);
+    }
+
     public void setPosition(@NonNull SurfaceControl.Transaction tx, int displayId, int x, int y) {
         final SurfaceControl sc = mLeashes.get(displayId);
         if (sc == null) {
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 47769a8..71bf487 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
@@ -59,6 +59,7 @@
 import com.android.wm.shell.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
 import com.android.wm.shell.desktopmode.DesktopTasksController;
+import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler;
 import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler;
 import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler;
 import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler;
@@ -498,6 +499,7 @@
             EnterDesktopTaskTransitionHandler enterDesktopTransitionHandler,
             ExitDesktopTaskTransitionHandler exitDesktopTransitionHandler,
             ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler,
+            DragToDesktopTransitionHandler dragToDesktopTransitionHandler,
             @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository,
             LaunchAdjacentController launchAdjacentController,
             RecentsTransitionHandler recentsTransitionHandler,
@@ -506,8 +508,19 @@
         return new DesktopTasksController(context, shellInit, shellCommandHandler, shellController,
                 displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer,
                 transitions, enterDesktopTransitionHandler, exitDesktopTransitionHandler,
-                toggleResizeDesktopTaskTransitionHandler, desktopModeTaskRepository,
-                launchAdjacentController, recentsTransitionHandler, mainExecutor);
+                toggleResizeDesktopTaskTransitionHandler, dragToDesktopTransitionHandler,
+                desktopModeTaskRepository, launchAdjacentController, recentsTransitionHandler,
+                mainExecutor);
+    }
+
+    @WMSingleton
+    @Provides
+    static DragToDesktopTransitionHandler provideDragToDesktopTransitionHandler(
+            Context context,
+            Transitions transitions,
+            RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
+        return new DragToDesktopTransitionHandler(context, transitions,
+                rootTaskDisplayAreaOrganizer);
     }
 
     @WMSingleton
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 8e12991..4a9ea6f 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
@@ -59,6 +59,7 @@
 import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener
 import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.TO_DESKTOP_INDICATOR
+import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.DragToDesktopStateListener
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
 import com.android.wm.shell.recents.RecentsTransitionHandler
 import com.android.wm.shell.recents.RecentsTransitionStateListener
@@ -92,6 +93,7 @@
         private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler,
         private val toggleResizeDesktopTaskTransitionHandler:
         ToggleResizeDesktopTaskTransitionHandler,
+        private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler,
         private val desktopModeTaskRepository: DesktopModeTaskRepository,
         private val launchAdjacentController: LaunchAdjacentController,
         private val recentsTransitionHandler: RecentsTransitionHandler,
@@ -110,6 +112,20 @@
             launchAdjacentController.launchAdjacentEnabled = !hasVisibleFreeformTasks
         }
     }
+    private val dragToDesktopStateListener = object : DragToDesktopStateListener {
+        override fun onCommitToDesktopAnimationStart(tx: SurfaceControl.Transaction) {
+            removeVisualIndicator(tx)
+        }
+
+        override fun onCancelToDesktopAnimationEnd(tx: SurfaceControl.Transaction) {
+            removeVisualIndicator(tx)
+        }
+
+        private fun removeVisualIndicator(tx: SurfaceControl.Transaction) {
+            visualIndicator?.releaseVisualIndicator(tx)
+            visualIndicator = null
+        }
+    }
 
     private val transitionAreaHeight
         get() = context.resources.getDimensionPixelSize(
@@ -122,9 +138,7 @@
         )
 
     private var recentsAnimationRunning = false
-
-    // This is public to avoid cyclic dependency; it is set by SplitScreenController
-    lateinit var splitScreenController: SplitScreenController
+    private lateinit var splitScreenController: SplitScreenController
 
     init {
         desktopMode = DesktopModeImpl()
@@ -143,7 +157,7 @@
         )
         transitions.addHandler(this)
         desktopModeTaskRepository.addVisibleTasksListener(taskVisibilityListener, mainExecutor)
-
+        dragToDesktopTransitionHandler.setDragToDesktopStateListener(dragToDesktopStateListener)
         recentsTransitionHandler.addTransitionStateListener(
             object : RecentsTransitionStateListener {
                 override fun onAnimationStateChanged(running: Boolean) {
@@ -158,6 +172,12 @@
         )
     }
 
+    /** Setter needed to avoid cyclic dependency. */
+    fun setSplitScreenController(controller: SplitScreenController) {
+        splitScreenController = controller
+        dragToDesktopTransitionHandler.setSplitScreenController(controller)
+    }
+
     /** Show all tasks, that are part of the desktop, on top of launcher */
     fun showDesktopApps(displayId: Int, remoteTransition: RemoteTransition? = null) {
         KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: showDesktopApps")
@@ -248,56 +268,43 @@
     }
 
     /**
-     * The first part of the animated move to desktop transition. Applies the changes to move task
-     * to desktop mode and sets the taskBounds to the passed in bounds, startBounds. This is
-     * followed with a call to {@link finishMoveToDesktop} or {@link cancelMoveToDesktop}.
+     * The first part of the animated drag to desktop transition. This is
+     * followed with a call to [finalizeDragToDesktop] or [cancelDragToDesktop].
      */
-    fun startMoveToDesktop(
+    fun startDragToDesktop(
             taskInfo: RunningTaskInfo,
-            startBounds: Rect,
-            dragToDesktopValueAnimator: MoveToDesktopAnimator
+            dragToDesktopValueAnimator: MoveToDesktopAnimator,
+            windowDecor: DesktopModeWindowDecoration
     ) {
         KtProtoLog.v(
-            WM_SHELL_DESKTOP_MODE,
-            "DesktopTasksController: startMoveToDesktop taskId=%d",
-            taskInfo.taskId
+                WM_SHELL_DESKTOP_MODE,
+                "DesktopTasksController: startDragToDesktop taskId=%d",
+                taskInfo.taskId
+        )
+        dragToDesktopTransitionHandler.startDragToDesktopTransition(
+                taskInfo.taskId,
+                dragToDesktopValueAnimator,
+                windowDecor
+        )
+    }
+
+    /**
+     * The second part of the animated drag to desktop transition, called after
+     * [startDragToDesktop].
+     */
+    private fun finalizeDragToDesktop(taskInfo: RunningTaskInfo, freeformBounds: Rect) {
+        KtProtoLog.v(
+                WM_SHELL_DESKTOP_MODE,
+                "DesktopTasksController: finalizeDragToDesktop taskId=%d",
+                taskInfo.taskId
         )
         val wct = WindowContainerTransaction()
         exitSplitIfApplicable(wct, taskInfo)
         moveHomeTaskToFront(wct)
-        addMoveToDesktopChanges(wct, taskInfo)
-        wct.setBounds(taskInfo.token, startBounds)
-
-        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-            enterDesktopTaskTransitionHandler.startMoveToDesktop(wct, dragToDesktopValueAnimator,
-                    mOnAnimationFinishedCallback)
-        } else {
-            shellTaskOrganizer.applyTransaction(wct)
-        }
-    }
-
-    /**
-     * The second part of the animated move to desktop transition, called after
-     * {@link startMoveToDesktop}. Brings apps to front and sets freeform task bounds.
-     */
-    private fun finalizeMoveToDesktop(taskInfo: RunningTaskInfo, freeformBounds: Rect) {
-        KtProtoLog.v(
-            WM_SHELL_DESKTOP_MODE,
-            "DesktopTasksController: finalizeMoveToDesktop taskId=%d",
-            taskInfo.taskId
-        )
-        val wct = WindowContainerTransaction()
         bringDesktopAppsToFront(taskInfo.displayId, wct)
         addMoveToDesktopChanges(wct, taskInfo)
         wct.setBounds(taskInfo.token, freeformBounds)
-
-        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-            enterDesktopTaskTransitionHandler.finalizeMoveToDesktop(wct,
-                    mOnAnimationFinishedCallback)
-        } else {
-            shellTaskOrganizer.applyTransaction(wct)
-            releaseVisualIndicator()
-        }
+        dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct)
     }
 
     /**
@@ -353,40 +360,40 @@
     }
 
     private fun exitSplitIfApplicable(wct: WindowContainerTransaction, taskInfo: RunningTaskInfo) {
-        if (taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) {
-            splitScreenController.prepareExitSplitScreen(wct,
-                splitScreenController.getStageOfTask(taskInfo.taskId), EXIT_REASON_ENTER_DESKTOP)
+        if (splitScreenController.isTaskInSplitScreen(taskInfo.taskId)) {
+            splitScreenController.prepareExitSplitScreen(
+                    wct,
+                    splitScreenController.getStageOfTask(taskInfo.taskId),
+                    EXIT_REASON_ENTER_DESKTOP
+            )
+            getOtherSplitTask(taskInfo.taskId)?.let { otherTaskInfo ->
+                wct.removeTask(otherTaskInfo.token)
+            }
         }
     }
 
+    private fun getOtherSplitTask(taskId: Int): RunningTaskInfo? {
+        val remainingTaskPosition: Int =
+                if (splitScreenController.getSplitPosition(taskId)
+                        == SPLIT_POSITION_BOTTOM_OR_RIGHT) {
+                    SPLIT_POSITION_TOP_OR_LEFT
+                } else {
+                    SPLIT_POSITION_BOTTOM_OR_RIGHT
+                }
+        return splitScreenController.getTaskInfo(remainingTaskPosition)
+    }
+
     /**
-     * The second part of the animated move to desktop transition, called after
-     * {@link startMoveToDesktop}. Move a task to fullscreen after being dragged from fullscreen
-     * and released back into status bar area.
+     * The second part of the animated drag to desktop transition, called after
+     * [startDragToDesktop].
      */
-    fun cancelMoveToDesktop(task: RunningTaskInfo, moveToDesktopAnimator: MoveToDesktopAnimator) {
+    fun cancelDragToDesktop(task: RunningTaskInfo) {
         KtProtoLog.v(
             WM_SHELL_DESKTOP_MODE,
-            "DesktopTasksController: cancelMoveToDesktop taskId=%d",
+            "DesktopTasksController: cancelDragToDesktop taskId=%d",
             task.taskId
         )
-        val wct = WindowContainerTransaction()
-        wct.setBounds(task.token, Rect())
-
-        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-            enterDesktopTaskTransitionHandler.startCancelMoveToDesktopMode(wct,
-                    moveToDesktopAnimator) { t ->
-                val callbackWCT = WindowContainerTransaction()
-                visualIndicator?.releaseVisualIndicator(t)
-                visualIndicator = null
-                addMoveToFullscreenChanges(callbackWCT, task)
-                transitions.startTransition(TRANSIT_CHANGE, callbackWCT, null /* handler */)
-            }
-        } else {
-            addMoveToFullscreenChanges(wct, task)
-            shellTaskOrganizer.applyTransaction(wct)
-            releaseVisualIndicator()
-        }
+        dragToDesktopTransitionHandler.cancelDragToDesktopTransition()
     }
 
     private fun moveToFullscreenWithAnimation(task: RunningTaskInfo, position: Point) {
@@ -966,6 +973,11 @@
             visualIndicator = DesktopModeVisualIndicator(syncQueue, taskInfo,
                     displayController, context, taskSurface, shellTaskOrganizer,
                     rootTaskDisplayAreaOrganizer, TO_DESKTOP_INDICATOR)
+            // TODO(b/301106941): don't show the indicator until the drag-to-desktop animation has
+            // started, or it'll be visible too early on top of the task surface, especially in
+            // the cancel-early case. Also because it shouldn't even be shown in the cancel-early
+            // case since its dismissal is tied to the cancel animation end, which doesn't even run
+            // in cancel-early.
             visualIndicator?.createIndicatorWithAnimatedBounds()
         }
         val indicator = visualIndicator ?: return
@@ -988,7 +1000,7 @@
             taskInfo: RunningTaskInfo,
             freeformBounds: Rect
     ) {
-        finalizeMoveToDesktop(taskInfo, freeformBounds)
+        finalizeDragToDesktop(taskInfo, freeformBounds)
     }
 
     private fun getStatusBarHeight(taskInfo: RunningTaskInfo): Int {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
new file mode 100644
index 0000000..75d27d9
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
@@ -0,0 +1,543 @@
+package com.android.wm.shell.desktopmode
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.RectEvaluator
+import android.animation.ValueAnimator
+import android.app.ActivityOptions
+import android.app.ActivityOptions.SourceInfo
+import android.app.PendingIntent
+import android.app.PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT
+import android.app.PendingIntent.FLAG_MUTABLE
+import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.FILL_IN_COMPONENT
+import android.graphics.Rect
+import android.os.IBinder
+import android.os.SystemClock
+import android.view.SurfaceControl
+import android.view.WindowManager.TRANSIT_CLOSE
+import android.window.TransitionInfo
+import android.window.TransitionInfo.Change
+import android.window.TransitionRequestInfo
+import android.window.WindowContainerToken
+import android.window.WindowContainerTransaction
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.splitscreen.SplitScreenController
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP
+import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP
+import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP
+import com.android.wm.shell.transition.Transitions.TransitionHandler
+import com.android.wm.shell.util.TransitionUtil
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
+import com.android.wm.shell.windowdecor.MoveToDesktopAnimator
+import com.android.wm.shell.windowdecor.MoveToDesktopAnimator.Companion.DRAG_FREEFORM_SCALE
+import java.util.function.Supplier
+
+/**
+ * Handles the transition to enter desktop from fullscreen by dragging on the handle bar. It also
+ * handles the cancellation case where the task is dragged back to the status bar area in the same
+ * gesture.
+ */
+class DragToDesktopTransitionHandler(
+        private val context: Context,
+        private val transitions: Transitions,
+        private val taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
+        private val transactionSupplier: Supplier<SurfaceControl.Transaction>
+) : TransitionHandler {
+
+    constructor(
+            context: Context,
+            transitions: Transitions,
+            rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
+    ) : this(
+            context,
+            transitions,
+            rootTaskDisplayAreaOrganizer,
+            Supplier { SurfaceControl.Transaction() }
+    )
+
+    private val rectEvaluator = RectEvaluator(Rect())
+    private val launchHomeIntent = Intent(Intent.ACTION_MAIN)
+            .addCategory(Intent.CATEGORY_HOME)
+
+    private var dragToDesktopStateListener: DragToDesktopStateListener? = null
+    private var splitScreenController: SplitScreenController? = null
+    private var transitionState: TransitionState? = null
+
+    /** Sets a listener to receive callback about events during the transition animation. */
+    fun setDragToDesktopStateListener(listener: DragToDesktopStateListener) {
+        dragToDesktopStateListener = listener
+    }
+
+    /** Setter needed to avoid cyclic dependency. */
+    fun setSplitScreenController(controller: SplitScreenController) {
+        splitScreenController = controller
+    }
+
+    /**
+     * Starts a transition that performs a transient launch of Home so that Home is brought to the
+     * front while still keeping the currently focused task that is being dragged resumed. This
+     * allows the animation handler to reorder the task to the front and to scale it with the
+     * gesture into the desktop area with the Home and wallpaper behind it.
+     *
+     * Note that the transition handler for this transition doesn't call the finish callback until
+     * after one of the "end" or "cancel" transitions is merged into this transition.
+     */
+    fun startDragToDesktopTransition(
+            taskId: Int,
+            dragToDesktopAnimator: MoveToDesktopAnimator,
+            windowDecoration: DesktopModeWindowDecoration
+    ) {
+        if (transitionState != null) {
+            error("A drag to desktop is already in progress")
+        }
+
+        val options = ActivityOptions.makeBasic().apply {
+            setTransientLaunch()
+            setSourceInfo(SourceInfo.TYPE_DESKTOP_ANIMATION, SystemClock.uptimeMillis())
+        }
+        val pendingIntent = PendingIntent.getActivity(
+                context,
+                0 /* requestCode */,
+                launchHomeIntent,
+                FLAG_MUTABLE or FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or FILL_IN_COMPONENT
+        )
+        val wct = WindowContainerTransaction()
+        wct.sendPendingIntent(pendingIntent, launchHomeIntent, options.toBundle())
+        val startTransitionToken = transitions
+                .startTransition(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, wct, this)
+
+        transitionState = if (isSplitTask(taskId)) {
+            TransitionState.FromSplit(
+                    draggedTaskId = taskId,
+                    dragAnimator = dragToDesktopAnimator,
+                    windowDecoration = windowDecoration,
+                    startTransitionToken = startTransitionToken
+            )
+        } else {
+            TransitionState.FromFullscreen(
+                    draggedTaskId = taskId,
+                    dragAnimator = dragToDesktopAnimator,
+                    windowDecoration = windowDecoration,
+                    startTransitionToken = startTransitionToken
+            )
+        }
+    }
+
+    /**
+     * Starts a transition that "finishes" the drag to desktop gesture. This transition is intended
+     * to merge into the "start" transition and is the one that actually applies the bounds and
+     * windowing mode changes to the dragged task. This is called when the dragged task is released
+     * inside the desktop drop zone.
+     */
+    fun finishDragToDesktopTransition(wct: WindowContainerTransaction) {
+        transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this)
+    }
+
+    /**
+     * Starts a transition that "cancels" the drag to desktop gesture. This transition is intended
+     * to merge into the "start" transition and it restores the transient state that was used to
+     * launch the Home task over the dragged task. This is called when the dragged task is released
+     * outside the desktop drop zone and is instead dropped back into the status bar region that
+     * means the user wants to remain in their current windowing mode.
+     */
+    fun cancelDragToDesktopTransition() {
+        val state = requireTransitionState()
+        state.cancelled = true
+        if (state.draggedTaskChange != null) {
+            // Regular case, transient launch of Home happened as is waiting for the cancel
+            // transient to start and merge. Animate the cancellation (scale back to original
+            // bounds) first before actually starting the cancel transition so that the wallpaper
+            // is visible behind the animating task.
+            startCancelAnimation()
+        } else {
+            // There's no dragged task, this can happen when the "cancel" happened too quickly
+            // before the "start" transition is even ready (like on a fling gesture). The
+            // "shrink" animation didn't even start, so there's no need to animate the "cancel".
+            // We also don't want to start the cancel transition yet since we don't have
+            // enough info to restore the order. We'll check for the cancelled state flag when
+            // the "start" animation is ready and cancel from #startAnimation instead.
+        }
+    }
+
+    override fun startAnimation(
+            transition: IBinder,
+            info: TransitionInfo,
+            startTransaction: SurfaceControl.Transaction,
+            finishTransaction: SurfaceControl.Transaction,
+            finishCallback: Transitions.TransitionFinishCallback
+    ): Boolean {
+        val state = requireTransitionState()
+
+        val isStartDragToDesktop = info.type == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP &&
+                transition == state.startTransitionToken
+        if (!isStartDragToDesktop) {
+            return false
+        }
+
+        // Layering: non-wallpaper, non-home tasks excluding the dragged task go at the bottom,
+        // then Home on top of that, wallpaper on top of that and finally the dragged task on top
+        // of everything.
+        val appLayers = info.changes.size
+        val homeLayers = info.changes.size * 2
+        val wallpaperLayers = info.changes.size * 3
+        val dragLayer = wallpaperLayers
+        val leafTaskFilter = TransitionUtil.LeafTaskFilter()
+        info.changes.withIndex().forEach { (i, change) ->
+            if (TransitionUtil.isWallpaper(change)) {
+                val layer = wallpaperLayers - i
+                startTransaction.apply {
+                    setLayer(change.leash, layer)
+                    show(change.leash)
+                }
+            } else if (isHomeChange(change)) {
+                state.homeToken = change.container
+                val layer = homeLayers - i
+                startTransaction.apply {
+                    setLayer(change.leash, layer)
+                    show(change.leash)
+                }
+            } else if (TransitionInfo.isIndependent(change, info)) {
+                // Root.
+                when (state) {
+                    is TransitionState.FromSplit -> {
+                        state.splitRootChange = change
+                        val layer = if (!state.cancelled) {
+                            // Normal case, split root goes to the bottom behind everything else.
+                            appLayers - i
+                        } else {
+                            // Cancel-early case, pretend nothing happened so split root stays top.
+                            dragLayer
+                        }
+                        startTransaction.apply {
+                            setLayer(change.leash, layer)
+                            show(change.leash)
+                        }
+                    }
+                    is TransitionState.FromFullscreen -> {
+                        if (change.taskInfo?.taskId == state.draggedTaskId) {
+                            state.draggedTaskChange = change
+                            val bounds = change.endAbsBounds
+                            startTransaction.apply {
+                                setLayer(change.leash, dragLayer)
+                                setWindowCrop(change.leash, bounds.width(), bounds.height())
+                                show(change.leash)
+                            }
+                        } else {
+                            throw IllegalStateException("Expected root to be dragged task")
+                        }
+                    }
+                }
+            } else if (leafTaskFilter.test(change)) {
+                // When dragging one of the split tasks, the dragged leaf needs to be re-parented
+                // so that it can be layered separately from the rest of the split root/stages.
+                // The split root including the other split side was layered behind the wallpaper
+                // and home while the dragged split needs to be layered in front of them.
+                // Do not do this in the cancel-early case though, since in that case nothing should
+                // happen on screen so the layering will remain the same as if no transition
+                // occurred.
+                if (change.taskInfo?.taskId == state.draggedTaskId && !state.cancelled) {
+                    state.draggedTaskChange = change
+                    taskDisplayAreaOrganizer.reparentToDisplayArea(
+                            change.endDisplayId, change.leash, startTransaction)
+                    val bounds = change.endAbsBounds
+                    startTransaction.apply {
+                        setLayer(change.leash, dragLayer)
+                        setWindowCrop(change.leash, bounds.width(), bounds.height())
+                        show(change.leash)
+                    }
+                }
+            }
+        }
+        state.startTransitionFinishCb = finishCallback
+        state.startTransitionFinishTransaction = finishTransaction
+        startTransaction.apply()
+
+        if (!state.cancelled) {
+            // Normal case, start animation to scale down the dragged task. It'll also be moved to
+            // follow the finger and when released we'll start the next phase/transition.
+            state.dragAnimator.startAnimation()
+        } else {
+            // Cancel-early case, the state was flagged was cancelled already, which means the
+            // gesture ended in the cancel region. This can happen even before the start transition
+            // is ready/animate here when cancelling quickly like with a fling. There's no point
+            // in starting the scale down animation that we would scale up anyway, so just jump
+            // directly into starting the cancel transition to restore WM order. Surfaces should
+            // not move as if no transition happened.
+            startCancelDragToDesktopTransition()
+        }
+        return true
+    }
+
+    override fun mergeAnimation(
+            transition: IBinder,
+            info: TransitionInfo,
+            t: SurfaceControl.Transaction,
+            mergeTarget: IBinder,
+            finishCallback: Transitions.TransitionFinishCallback
+    ) {
+        val state = requireTransitionState()
+        val isCancelTransition = info.type == TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP &&
+                transition == state.cancelTransitionToken &&
+                mergeTarget == state.startTransitionToken
+        val isEndTransition = info.type == TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP &&
+                mergeTarget == state.startTransitionToken
+
+        val startTransactionFinishT = state.startTransitionFinishTransaction
+                ?: error("Start transition expected to be waiting for merge but wasn't")
+        val startTransitionFinishCb = state.startTransitionFinishCb
+                ?: error("Start transition expected to be waiting for merge but wasn't")
+        if (isEndTransition) {
+            info.changes.withIndex().forEach { (i, change) ->
+                if (change.mode == TRANSIT_CLOSE) {
+                    t.hide(change.leash)
+                    startTransactionFinishT.hide(change.leash)
+                } else if (change.taskInfo?.taskId == state.draggedTaskId) {
+                    t.show(change.leash)
+                    startTransactionFinishT.show(change.leash)
+                    state.draggedTaskChange = change
+                } else if (change.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM) {
+                    // Other freeform tasks that are being restored go behind the dragged task.
+                    val draggedTaskLeash = state.draggedTaskChange?.leash
+                            ?: error("Expected dragged leash to be non-null")
+                    t.setRelativeLayer(change.leash, draggedTaskLeash, -i)
+                    startTransactionFinishT.setRelativeLayer(change.leash, draggedTaskLeash, -i)
+                }
+            }
+
+            val draggedTaskChange = state.draggedTaskChange
+                    ?: throw IllegalStateException("Expected non-null change of dragged task")
+            val draggedTaskLeash = draggedTaskChange.leash
+            val startBounds = draggedTaskChange.startAbsBounds
+            val endBounds = draggedTaskChange.endAbsBounds
+
+            // TODO(b/301106941): Instead of forcing-finishing the animation that scales the
+            //  surface down and then starting another that scales it back up to the final size,
+            //  blend the two animations.
+            state.dragAnimator.endAnimator()
+            // Using [DRAG_FREEFORM_SCALE] to calculate animated width/height is possible because
+            // it is known that the animation scale is finished because the animation was
+            // force-ended above. This won't be true when the two animations are blended.
+            val animStartWidth = (startBounds.width() * DRAG_FREEFORM_SCALE).toInt()
+            val animStartHeight = (startBounds.height() * DRAG_FREEFORM_SCALE).toInt()
+            // Using end bounds here to find the left/top also assumes the center animation has
+            // finished and the surface is placed exactly in the center of the screen which matches
+            // the end/default bounds of the now freeform task.
+            val animStartLeft = endBounds.centerX() - (animStartWidth / 2)
+            val animStartTop = endBounds.centerY() - (animStartHeight / 2)
+            val animStartBounds = Rect(
+                    animStartLeft,
+                    animStartTop,
+                    animStartLeft + animStartWidth,
+                    animStartTop + animStartHeight
+            )
+
+
+            dragToDesktopStateListener?.onCommitToDesktopAnimationStart(t)
+            t.apply {
+                setScale(draggedTaskLeash, 1f, 1f)
+                setPosition(
+                        draggedTaskLeash,
+                        animStartBounds.left.toFloat(),
+                        animStartBounds.top.toFloat()
+                )
+                setWindowCrop(
+                        draggedTaskLeash,
+                        animStartBounds.width(),
+                        animStartBounds.height()
+                )
+            }
+            // Accept the merge by applying the merging transaction (applied by #showResizeVeil)
+            // and finish callback. Show the veil and position the task at the first frame before
+            // starting the final animation.
+            state.windowDecoration.showResizeVeil(t, animStartBounds)
+            finishCallback.onTransitionFinished(null /* wct */)
+
+            // Because the task surface was scaled down during the drag, we must use the animated
+            // bounds instead of the [startAbsBounds].
+            val tx: SurfaceControl.Transaction = transactionSupplier.get()
+            ValueAnimator.ofObject(rectEvaluator, animStartBounds, endBounds)
+                    .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS)
+                    .apply {
+                        addUpdateListener { animator ->
+                            val animBounds = animator.animatedValue as Rect
+                            tx.apply {
+                                setScale(draggedTaskLeash, 1f, 1f)
+                                 setPosition(
+                                         draggedTaskLeash,
+                                         animBounds.left.toFloat(),
+                                         animBounds.top.toFloat()
+                                 )
+                                setWindowCrop(
+                                        draggedTaskLeash,
+                                        animBounds.width(),
+                                        animBounds.height()
+                                )
+                            }
+                            state.windowDecoration.updateResizeVeil(tx, animBounds)
+                        }
+                        addListener(object : AnimatorListenerAdapter() {
+                            override fun onAnimationEnd(animation: Animator) {
+                                state.windowDecoration.hideResizeVeil()
+                                startTransitionFinishCb.onTransitionFinished(null /* null */)
+                                clearState()
+                            }
+                        })
+                        start()
+                    }
+        } else if (isCancelTransition) {
+            info.changes.forEach { change ->
+                t.show(change.leash)
+                startTransactionFinishT.show(change.leash)
+            }
+            t.apply()
+            finishCallback.onTransitionFinished(null /* wct */)
+            startTransitionFinishCb.onTransitionFinished(null /* wct */)
+            clearState()
+        }
+    }
+
+    override fun handleRequest(
+            transition: IBinder,
+            request: TransitionRequestInfo
+    ): WindowContainerTransaction? {
+        // Only handle transitions started from shell.
+        return null
+    }
+
+    private fun isHomeChange(change: Change): Boolean {
+        return change.taskInfo?.activityType == ACTIVITY_TYPE_HOME
+    }
+
+    private fun startCancelAnimation() {
+        val state = requireTransitionState()
+        val dragToDesktopAnimator = state.dragAnimator
+
+        val draggedTaskChange = state.draggedTaskChange
+                ?: throw IllegalStateException("Expected non-null task change")
+        val sc = draggedTaskChange.leash
+        // TODO(b/301106941): Don't end the animation and start one to scale it back, merge them
+        //  instead.
+        // End the animation that shrinks the window when task is first dragged from fullscreen
+        dragToDesktopAnimator.endAnimator()
+        // Then animate the scaled window back to its original bounds.
+        val x: Float = dragToDesktopAnimator.position.x
+        val y: Float = dragToDesktopAnimator.position.y
+        val targetX = draggedTaskChange.endAbsBounds.left
+        val targetY = draggedTaskChange.endAbsBounds.top
+        val dx = targetX - x
+        val dy = targetY - y
+        val tx: SurfaceControl.Transaction = transactionSupplier.get()
+        ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE, 1f)
+                .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS)
+                .apply {
+                    addUpdateListener { animator ->
+                        val scale = animator.animatedValue as Float
+                        val fraction = animator.animatedFraction
+                        val animX = x + (dx * fraction)
+                        val animY = y + (dy * fraction)
+                        tx.apply {
+                            setPosition(sc, animX, animY)
+                            setScale(sc, scale, scale)
+                            show(sc)
+                            apply()
+                        }
+                    }
+                    addListener(object : AnimatorListenerAdapter() {
+                        override fun onAnimationEnd(animation: Animator) {
+                            dragToDesktopStateListener?.onCancelToDesktopAnimationEnd(tx)
+                            // Start the cancel transition to restore order.
+                            startCancelDragToDesktopTransition()
+                        }
+                    })
+                    start()
+                }
+    }
+
+    private fun startCancelDragToDesktopTransition() {
+        val state = requireTransitionState()
+        val wct = WindowContainerTransaction()
+        when (state) {
+            is TransitionState.FromFullscreen -> {
+                val wc = state.draggedTaskChange?.container
+                        ?: error("Dragged task should be non-null before cancelling")
+                wct.reorder(wc, true /* toTop */)
+            }
+            is TransitionState.FromSplit -> {
+                val wc = state.splitRootChange?.container
+                        ?: error("Split root should be non-null before cancelling")
+                wct.reorder(wc, true /* toTop */)
+            }
+        }
+        val homeWc = state.homeToken ?: error("Home task should be non-null before cancelling")
+        wct.restoreTransientOrder(homeWc)
+
+        state.cancelTransitionToken = transitions.startTransition(
+                TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP, wct, this)
+    }
+
+    private fun clearState() {
+        transitionState = null
+    }
+
+    private fun isSplitTask(taskId: Int): Boolean {
+        return splitScreenController?.isTaskInSplitScreen(taskId) ?: false
+    }
+
+    private fun requireTransitionState(): TransitionState {
+        return transitionState ?: error("Expected non-null transition state")
+    }
+
+    interface DragToDesktopStateListener {
+        fun onCommitToDesktopAnimationStart(tx: SurfaceControl.Transaction)
+        fun onCancelToDesktopAnimationEnd(tx: SurfaceControl.Transaction)
+    }
+
+    sealed class TransitionState {
+        abstract val draggedTaskId: Int
+        abstract val dragAnimator: MoveToDesktopAnimator
+        abstract val windowDecoration: DesktopModeWindowDecoration
+        abstract val startTransitionToken: IBinder
+        abstract var startTransitionFinishCb: Transitions.TransitionFinishCallback?
+        abstract var startTransitionFinishTransaction: SurfaceControl.Transaction?
+        abstract var cancelTransitionToken: IBinder?
+        abstract var homeToken: WindowContainerToken?
+        abstract var draggedTaskChange: Change?
+        abstract var cancelled: Boolean
+
+        data class FromFullscreen(
+                override val draggedTaskId: Int,
+                override val dragAnimator: MoveToDesktopAnimator,
+                override val windowDecoration: DesktopModeWindowDecoration,
+                override val startTransitionToken: IBinder,
+                override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null,
+                override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null,
+                override var cancelTransitionToken: IBinder? = null,
+                override var homeToken: WindowContainerToken? = null,
+                override var draggedTaskChange: Change? = null,
+                override var cancelled: Boolean = false,
+        ) : TransitionState()
+        data class FromSplit(
+                override val draggedTaskId: Int,
+                override val dragAnimator: MoveToDesktopAnimator,
+                override val windowDecoration: DesktopModeWindowDecoration,
+                override val startTransitionToken: IBinder,
+                override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null,
+                override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null,
+                override var cancelTransitionToken: IBinder? = null,
+                override var homeToken: WindowContainerToken? = null,
+                override var draggedTaskChange: Change? = null,
+                override var cancelled: Boolean = false,
+                var splitRootChange: Change? = null,
+        ) : TransitionState()
+    }
+
+    companion object {
+        /** The duration of the animation to commit or cancel the drag-to-desktop gesture. */
+        private const val DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS = 336L
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
index 024465b..605600f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
@@ -18,12 +18,13 @@
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 
+import static com.android.wm.shell.transition.Transitions.TRANSIT_MOVE_TO_DESKTOP;
+
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.RectEvaluator;
 import android.animation.ValueAnimator;
 import android.app.ActivityManager;
-import android.graphics.PointF;
 import android.graphics.Rect;
 import android.os.IBinder;
 import android.util.Slog;
@@ -38,11 +39,9 @@
 
 import com.android.wm.shell.transition.Transitions;
 import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration;
-import com.android.wm.shell.windowdecor.MoveToDesktopAnimator;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.function.Consumer;
 import java.util.function.Supplier;
 
 /**
@@ -60,8 +59,6 @@
     public static final int FREEFORM_ANIMATION_DURATION = 336;
 
     private final List<IBinder> mPendingTransitionTokens = new ArrayList<>();
-    private Consumer<SurfaceControl.Transaction> mOnAnimationFinishedCallback;
-    private MoveToDesktopAnimator mMoveToDesktopAnimator;
     private DesktopModeWindowDecoration mDesktopModeWindowDecoration;
 
     public EnterDesktopTaskTransitionHandler(
@@ -77,61 +74,6 @@
     }
 
     /**
-     * Starts Transition of a given type
-     * @param type Transition type
-     * @param wct WindowContainerTransaction for transition
-     * @param onAnimationEndCallback to be called after animation
-     */
-    private void startTransition(@WindowManager.TransitionType int type,
-            @NonNull WindowContainerTransaction wct,
-            Consumer<SurfaceControl.Transaction> onAnimationEndCallback) {
-        mOnAnimationFinishedCallback = onAnimationEndCallback;
-        final IBinder token = mTransitions.startTransition(type, wct, this);
-        mPendingTransitionTokens.add(token);
-    }
-
-    /**
-     * Starts Transition of type TRANSIT_START_DRAG_TO_DESKTOP_MODE
-     * @param wct WindowContainerTransaction for transition
-     * @param moveToDesktopAnimator Animator that shrinks and positions task during two part move
-     *                              to desktop animation
-     * @param onAnimationEndCallback to be called after animation
-     */
-    public void startMoveToDesktop(@NonNull WindowContainerTransaction wct,
-            @NonNull MoveToDesktopAnimator moveToDesktopAnimator,
-            Consumer<SurfaceControl.Transaction> onAnimationEndCallback) {
-        mMoveToDesktopAnimator = moveToDesktopAnimator;
-        startTransition(Transitions.TRANSIT_START_DRAG_TO_DESKTOP_MODE, wct,
-                onAnimationEndCallback);
-    }
-
-    /**
-     * Starts Transition of type TRANSIT_FINALIZE_DRAG_TO_DESKTOP_MODE
-     * @param wct WindowContainerTransaction for transition
-     * @param onAnimationEndCallback to be called after animation
-     */
-    public void finalizeMoveToDesktop(@NonNull WindowContainerTransaction wct,
-            Consumer<SurfaceControl.Transaction> onAnimationEndCallback) {
-        startTransition(Transitions.TRANSIT_FINALIZE_DRAG_TO_DESKTOP_MODE, wct,
-                onAnimationEndCallback);
-    }
-
-    /**
-     * Starts Transition of type TRANSIT_CANCEL_ENTERING_DESKTOP_MODE
-     * @param wct WindowContainerTransaction for transition
-     * @param moveToDesktopAnimator Animator that shrinks and positions task during two part move
-     *                              to desktop animation
-     * @param onAnimationEndCallback to be called after animation
-     */
-    public void startCancelMoveToDesktopMode(@NonNull WindowContainerTransaction wct,
-            MoveToDesktopAnimator moveToDesktopAnimator,
-            Consumer<SurfaceControl.Transaction> onAnimationEndCallback) {
-        mMoveToDesktopAnimator = moveToDesktopAnimator;
-        startTransition(Transitions.TRANSIT_CANCEL_DRAG_TO_DESKTOP_MODE, wct,
-                onAnimationEndCallback);
-    }
-
-    /**
      * Starts Transition of type TRANSIT_MOVE_TO_DESKTOP
      * @param wct WindowContainerTransaction for transition
      * @param decor {@link DesktopModeWindowDecoration} of task being animated
@@ -139,8 +81,8 @@
     public void moveToDesktop(@NonNull WindowContainerTransaction wct,
             DesktopModeWindowDecoration decor) {
         mDesktopModeWindowDecoration = decor;
-        startTransition(Transitions.TRANSIT_MOVE_TO_DESKTOP, wct,
-                null /* onAnimationEndCallback */);
+        final IBinder token = mTransitions.startTransition(TRANSIT_MOVE_TO_DESKTOP, wct, this);
+        mPendingTransitionTokens.add(token);
     }
 
     @Override
@@ -182,30 +124,11 @@
         }
 
         final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
-        if (type == Transitions.TRANSIT_MOVE_TO_DESKTOP
+        if (type == TRANSIT_MOVE_TO_DESKTOP
                 && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
             return animateMoveToDesktop(change, startT, finishCallback);
         }
 
-        if (type == Transitions.TRANSIT_START_DRAG_TO_DESKTOP_MODE
-                && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
-            return animateStartDragToDesktopMode(change, startT, finishT, finishCallback);
-        }
-
-        final Rect endBounds = change.getEndAbsBounds();
-        if (type == Transitions.TRANSIT_FINALIZE_DRAG_TO_DESKTOP_MODE
-                && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
-                && !endBounds.isEmpty()) {
-            return animateFinalizeDragToDesktopMode(change, startT, finishT, finishCallback,
-                    endBounds);
-        }
-
-        if (type == Transitions.TRANSIT_CANCEL_DRAG_TO_DESKTOP_MODE
-                && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
-            return animateCancelDragToDesktopMode(change, startT, finishT, finishCallback,
-                    endBounds);
-        }
-
         return false;
     }
 
@@ -248,142 +171,6 @@
         return true;
     }
 
-    private boolean animateStartDragToDesktopMode(
-            @NonNull TransitionInfo.Change change,
-            @NonNull SurfaceControl.Transaction startT,
-            @NonNull SurfaceControl.Transaction finishT,
-            @NonNull Transitions.TransitionFinishCallback finishCallback) {
-        // Transitioning to freeform but keeping fullscreen bounds, so the crop is set
-        // to null and we don't require an animation
-        final SurfaceControl sc = change.getLeash();
-        startT.setWindowCrop(sc, null);
-
-        if (mMoveToDesktopAnimator == null
-                || mMoveToDesktopAnimator.getTaskId() != change.getTaskInfo().taskId) {
-            Slog.e(TAG, "No animator available for this transition");
-            return false;
-        }
-
-        // Calculate and set position of the task
-        final PointF position = mMoveToDesktopAnimator.getPosition();
-        startT.setPosition(sc, position.x, position.y);
-        finishT.setPosition(sc, position.x, position.y);
-
-        startT.apply();
-
-        mTransitions.getMainExecutor().execute(() -> finishCallback.onTransitionFinished(null));
-
-        return true;
-    }
-
-    private boolean animateFinalizeDragToDesktopMode(
-            @NonNull TransitionInfo.Change change,
-            @NonNull SurfaceControl.Transaction startT,
-            @NonNull SurfaceControl.Transaction finishT,
-            @NonNull Transitions.TransitionFinishCallback finishCallback,
-            @NonNull Rect endBounds) {
-        // This Transition animates a task to freeform bounds after being dragged into freeform
-        // mode and brings the remaining freeform tasks to front
-        final SurfaceControl sc = change.getLeash();
-        startT.setWindowCrop(sc, endBounds.width(),
-                endBounds.height());
-        startT.apply();
-
-        // End the animation that shrinks the window when task is first dragged from fullscreen
-        if (mMoveToDesktopAnimator != null) {
-            mMoveToDesktopAnimator.endAnimator();
-        }
-
-        // We want to find the scale of the current bounds relative to the end bounds. The
-        // task is currently scaled to DRAG_FREEFORM_SCALE and the final bounds will be
-        // scaled to FINAL_FREEFORM_SCALE. So, it is scaled to
-        // DRAG_FREEFORM_SCALE / FINAL_FREEFORM_SCALE relative to the freeform bounds
-        final ValueAnimator animator =
-                ValueAnimator.ofFloat(
-                        MoveToDesktopAnimator.DRAG_FREEFORM_SCALE / FINAL_FREEFORM_SCALE, 1f);
-        animator.setDuration(FREEFORM_ANIMATION_DURATION);
-        final SurfaceControl.Transaction t = mTransactionSupplier.get();
-        animator.addUpdateListener(animation -> {
-            final float animationValue = (float) animation.getAnimatedValue();
-            t.setScale(sc, animationValue, animationValue);
-
-            final float animationWidth = endBounds.width() * animationValue;
-            final float animationHeight = endBounds.height() * animationValue;
-            final int animationX = endBounds.centerX() - (int) (animationWidth / 2);
-            final int animationY = endBounds.centerY() - (int) (animationHeight / 2);
-
-            t.setPosition(sc, animationX, animationY);
-            t.apply();
-        });
-
-        animator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                if (mOnAnimationFinishedCallback != null) {
-                    mOnAnimationFinishedCallback.accept(finishT);
-                }
-                mTransitions.getMainExecutor().execute(
-                        () -> finishCallback.onTransitionFinished(null));
-            }
-        });
-
-        animator.start();
-        return true;
-    }
-    private boolean animateCancelDragToDesktopMode(
-            @NonNull TransitionInfo.Change change,
-            @NonNull SurfaceControl.Transaction startT,
-            @NonNull SurfaceControl.Transaction finishT,
-            @NonNull Transitions.TransitionFinishCallback finishCallback,
-            @NonNull Rect endBounds) {
-        // This Transition animates a task to fullscreen after being dragged from the status
-        // bar and then released back into the status bar area
-        final SurfaceControl sc = change.getLeash();
-        // Hide the first (fullscreen) frame because the animation will start from the smaller
-        // scale size.
-        startT.hide(sc)
-                .setWindowCrop(sc, endBounds.width(), endBounds.height())
-                .apply();
-
-        if (mMoveToDesktopAnimator == null
-                || mMoveToDesktopAnimator.getTaskId() != change.getTaskInfo().taskId) {
-            Slog.e(TAG, "No animator available for this transition");
-            return false;
-        }
-
-        // End the animation that shrinks the window when task is first dragged from fullscreen
-        mMoveToDesktopAnimator.endAnimator();
-
-        final ValueAnimator animator = new ValueAnimator();
-        animator.setFloatValues(MoveToDesktopAnimator.DRAG_FREEFORM_SCALE, 1f);
-        animator.setDuration(FREEFORM_ANIMATION_DURATION);
-        final SurfaceControl.Transaction t = mTransactionSupplier.get();
-
-        // Get position of the task
-        final float x = mMoveToDesktopAnimator.getPosition().x;
-        final float y = mMoveToDesktopAnimator.getPosition().y;
-
-        animator.addUpdateListener(animation -> {
-            final float scale = (float) animation.getAnimatedValue();
-            t.setPosition(sc, x * (1 - scale), y * (1 - scale))
-                    .setScale(sc, scale, scale)
-                    .show(sc)
-                    .apply();
-        });
-        animator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                if (mOnAnimationFinishedCallback != null) {
-                    mOnAnimationFinishedCallback.accept(finishT);
-                }
-                mTransitions.getMainExecutor().execute(
-                        () -> finishCallback.onTransitionFinished(null));
-            }
-        });
-        animator.start();
-        return true;
-    }
-
     @Nullable
     @Override
     public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java
index fc34772..63cef9e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java
@@ -107,7 +107,7 @@
     private static final int POST_INTERACTION_DISMISS_DELAY = 2000;
     private static final long MENU_SHOW_ON_EXPAND_START_DELAY = 30;
 
-    private static final float MENU_BACKGROUND_ALPHA = 0.3f;
+    private static final float MENU_BACKGROUND_ALPHA = 0.54f;
     private static final float DISABLED_ACTION_ALPHA = 0.54f;
 
     private int mMenuState;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index ab5c063..41ec33c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -150,19 +150,19 @@
     /** Transition type for maximize to freeform transition. */
     public static final int TRANSIT_RESTORE_FROM_MAXIMIZE = WindowManager.TRANSIT_FIRST_CUSTOM + 9;
 
-    /** Transition type for starting the move to desktop mode. */
-    public static final int TRANSIT_START_DRAG_TO_DESKTOP_MODE =
+    /** Transition type for starting the drag to desktop mode. */
+    public static final int TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP =
             WindowManager.TRANSIT_FIRST_CUSTOM + 10;
 
-    /** Transition type for finalizing the move to desktop mode. */
-    public static final int TRANSIT_FINALIZE_DRAG_TO_DESKTOP_MODE =
+    /** Transition type for finalizing the drag to desktop mode. */
+    public static final int TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP =
             WindowManager.TRANSIT_FIRST_CUSTOM + 11;
 
     /** Transition type to fullscreen from desktop mode. */
     public static final int TRANSIT_EXIT_DESKTOP_MODE = WindowManager.TRANSIT_FIRST_CUSTOM + 12;
 
-    /** Transition type to animate back to fullscreen when drag to freeform is cancelled. */
-    public static final int TRANSIT_CANCEL_DRAG_TO_DESKTOP_MODE =
+    /** Transition type to cancel the drag to desktop mode. */
+    public static final int TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP =
             WindowManager.TRANSIT_FIRST_CUSTOM + 13;
 
     /** Transition type to animate the toggle resize between the max and default desktop sizes. */
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 e206039..3add6f4 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
@@ -268,13 +268,19 @@
             @NonNull TransitionInfo info,
             @NonNull TransitionInfo.Change change) {
         if (change.getMode() == WindowManager.TRANSIT_CHANGE
-                && (info.getType() == Transitions.TRANSIT_FINALIZE_DRAG_TO_DESKTOP_MODE
-                || info.getType() == Transitions.TRANSIT_CANCEL_DRAG_TO_DESKTOP_MODE
-                || info.getType() == Transitions.TRANSIT_EXIT_DESKTOP_MODE
+                && (info.getType() == Transitions.TRANSIT_EXIT_DESKTOP_MODE
                 || info.getType() == Transitions.TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE
                 || info.getType() == Transitions.TRANSIT_MOVE_TO_DESKTOP)) {
             mWindowDecorByTaskId.get(change.getTaskInfo().taskId)
                     .addTransitionPausingRelayout(transition);
+        } else if (change.getMode() == WindowManager.TRANSIT_TO_BACK
+                && info.getType() == Transitions.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP
+                && change.getTaskInfo() != null) {
+            final DesktopModeWindowDecoration decor =
+                    mWindowDecorByTaskId.get(change.getTaskInfo().taskId);
+            if (decor != null) {
+                decor.addTransitionPausingRelayout(transition);
+            }
         }
     }
 
@@ -765,10 +771,8 @@
                         mMoveToDesktopAnimator = null;
                         return;
                     } else if (mMoveToDesktopAnimator != null) {
-                        relevantDecor.incrementRelayoutBlock();
                         mDesktopTasksController.ifPresent(
-                                c -> c.cancelMoveToDesktop(relevantDecor.mTaskInfo,
-                                        mMoveToDesktopAnimator));
+                                c -> c.cancelDragToDesktop(relevantDecor.mTaskInfo));
                         mMoveToDesktopAnimator = null;
                         return;
                     }
@@ -790,15 +794,24 @@
                             relevantDecor.mTaskInfo.displayId);
                     if (ev.getY() > statusBarHeight) {
                         if (mMoveToDesktopAnimator == null) {
-                            closeOtherSplitTask(relevantDecor.mTaskInfo.taskId);
                             mMoveToDesktopAnimator = new MoveToDesktopAnimator(
-                                    mDragToDesktopAnimationStartBounds, relevantDecor.mTaskInfo,
-                                    relevantDecor.mTaskSurface);
+                                    mContext, mDragToDesktopAnimationStartBounds,
+                                    relevantDecor.mTaskInfo, relevantDecor.mTaskSurface);
                             mDesktopTasksController.ifPresent(
-                                    c -> c.startMoveToDesktop(relevantDecor.mTaskInfo,
-                                            mDragToDesktopAnimationStartBounds,
-                                            mMoveToDesktopAnimator));
-                            mMoveToDesktopAnimator.startAnimation();
+                                    c -> {
+                                        final int taskId = relevantDecor.mTaskInfo.taskId;
+                                        relevantDecor.incrementRelayoutBlock();
+                                        if (isTaskInSplitScreen(taskId)) {
+                                            final DesktopModeWindowDecoration otherDecor =
+                                                    mWindowDecorByTaskId.get(
+                                                            getOtherSplitTask(taskId).taskId);
+                                            if (otherDecor != null) {
+                                                otherDecor.incrementRelayoutBlock();
+                                            }
+                                        }
+                                        c.startDragToDesktop(relevantDecor.mTaskInfo,
+                                                mMoveToDesktopAnimator, relevantDecor);
+                                    });
                         }
                     }
                     if (mMoveToDesktopAnimator != null) {
@@ -837,7 +850,6 @@
      */
     private void animateToDesktop(DesktopModeWindowDecoration relevantDecor,
             MotionEvent ev) {
-        relevantDecor.incrementRelayoutBlock();
         centerAndMoveToDesktopWithAnimation(relevantDecor, ev);
     }
 
@@ -853,15 +865,15 @@
         final SurfaceControl sc = relevantDecor.mTaskSurface;
         final Rect endBounds = calculateFreeformBounds(ev.getDisplayId(), DRAG_FREEFORM_SCALE);
         final Transaction t = mTransactionFactory.get();
-        final float diffX = endBounds.centerX() - ev.getX();
-        final float diffY = endBounds.top - ev.getY();
-        final float startingX = ev.getX() - DRAG_FREEFORM_SCALE
+        final float diffX = endBounds.centerX() - ev.getRawX();
+        final float diffY = endBounds.top - ev.getRawY();
+        final float startingX = ev.getRawX() - DRAG_FREEFORM_SCALE
                 * mDragToDesktopAnimationStartBounds.width() / 2;
 
         animator.addUpdateListener(animation -> {
             final float animatorValue = (float) animation.getAnimatedValue();
             final float x = startingX + diffX * animatorValue;
-            final float y = ev.getY() + diffY * animatorValue;
+            final float y = ev.getRawY() + diffY * animatorValue;
             t.setPosition(sc, x, y);
             t.apply();
         });
@@ -869,9 +881,11 @@
             @Override
             public void onAnimationEnd(Animator animation) {
                 mDesktopTasksController.ifPresent(
-                        c -> c.onDragPositioningEndThroughStatusBar(
-                                relevantDecor.mTaskInfo,
-                                calculateFreeformBounds(ev.getDisplayId(), FINAL_FREEFORM_SCALE)));
+                        c -> {
+                            c.onDragPositioningEndThroughStatusBar(relevantDecor.mTaskInfo,
+                                    calculateFreeformBounds(ev.getDisplayId(),
+                                            FINAL_FREEFORM_SCALE));
+                        });
             }
         });
         animator.start();
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 7c6fb99..518f4b8 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
@@ -120,7 +120,7 @@
             mWindowSession.grantInputChannel(
                     mDisplayId,
                     mDecorationSurface,
-                    mFakeWindow,
+                    mFakeWindow.asBinder(),
                     null /* hostInputToken */,
                     FLAG_NOT_FOCUSABLE,
                     PRIVATE_FLAG_TRUSTED_OVERLAY,
@@ -155,7 +155,7 @@
             mWindowSession.grantInputChannel(
                     mDisplayId,
                     mInputSinkSurface,
-                    mFakeSinkWindow,
+                    mFakeSinkWindow.asBinder(),
                     null /* hostInputToken */,
                     FLAG_NOT_FOCUSABLE,
                     0 /* privateFlags */,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt
index b2267dd..af05523 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt
@@ -2,10 +2,12 @@
 
 import android.animation.ValueAnimator
 import android.app.ActivityManager.RunningTaskInfo
+import android.content.Context
 import android.graphics.PointF
 import android.graphics.Rect
 import android.view.MotionEvent
 import android.view.SurfaceControl
+import com.android.internal.policy.ScreenDecorationsUtils
 
 /**
  * Creates an animator to shrink and position task after a user drags a fullscreen task from
@@ -14,6 +16,7 @@
  * accessed by the EnterDesktopTaskTransitionHandler.
  */
 class MoveToDesktopAnimator @JvmOverloads constructor(
+        private val context: Context,
         private val startBounds: Rect,
         private val taskInfo: RunningTaskInfo,
         private val taskSurface: SurfaceControl,
@@ -33,9 +36,11 @@
             .setDuration(ANIMATION_DURATION.toLong())
             .apply {
                 val t = SurfaceControl.Transaction()
+                val cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
                 addUpdateListener { animation ->
                     val animatorValue = animation.animatedValue as Float
                     t.setScale(taskSurface, animatorValue, animatorValue)
+                            .setCornerRadius(taskSurface, cornerRadius)
                             .apply()
                 }
             }
@@ -44,19 +49,40 @@
     val position: PointF = PointF(0.0f, 0.0f)
 
     /**
+     * Whether motion events from the drag gesture should affect the dragged surface or not. Used
+     * to disallow moving the surface's position prematurely since it should not start moving at
+     * all until the drag-to-desktop transition is ready to animate and the wallpaper/home are
+     * ready to be revealed behind the dragged/scaled task.
+     */
+    private var allowSurfaceChangesOnMove = false
+
+    /**
      * Starts the animation that scales the task down.
      */
     fun startAnimation() {
+        allowSurfaceChangesOnMove = true
         dragToDesktopAnimator.start()
     }
 
     /**
-     * Uses the position of the motion event and the current scale of the task as defined by the
-     * ValueAnimator to update the local position variable and set the task surface's position
+     * Uses the position of the motion event of the drag-to-desktop gesture to update the dragged
+     * task's position on screen to follow the touch point. Note that the position change won't
+     * be applied immediately always, such as near the beginning where it waits until the wallpaper
+     * or home are visible behind it. Once they're visible the surface will catch-up to the most
+     * recent touch position.
      */
     fun updatePosition(ev: MotionEvent) {
-        position.x = ev.x - animatedTaskWidth / 2
-        position.y = ev.y
+        // Using rawX/Y because when dragging a task in split, the local X/Y is relative to the
+        // split stages, but the split task surface is re-parented to the task display area to
+        // allow dragging beyond its stage across any region of the display. Because of that, the
+        // rawX/Y are more true to where the gesture is on screen and where the surface should be
+        // positioned.
+        position.x = ev.rawX - animatedTaskWidth / 2
+        position.y = ev.rawY
+
+        if (!allowSurfaceChangesOnMove) {
+            return
+        }
 
         val t = transactionFactory()
         t.setPosition(taskSurface, position.x, position.y)
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/Android.bp b/libs/WindowManager/Shell/tests/flicker/pip/Android.bp
index 4d11dfb..386983c 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/Android.bp
+++ b/libs/WindowManager/Shell/tests/flicker/pip/Android.bp
@@ -124,6 +124,10 @@
         ":WMShellFlickerTestsPipCommon-src",
     ],
     static_libs: ["WMShellFlickerTestsBase"],
+    test_suites: [
+        "device-tests",
+        "csuite",
+    ],
 }
 
 csuite_test {
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml
index 6df6539..89ecc29 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml
@@ -71,9 +71,9 @@
     <!-- Enable mocking GPS location by the test app -->
     <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
         <option name="run-command"
-                value="appops set com.android.wm.shell.flicker.pip.apps android:mock_location allow"/>
+                value="appops set com.android.shell android:mock_location allow"/>
         <option name="teardown-command"
-                value="appops set com.android.wm.shell.flicker.pip.apps android:mock_location deny"/>
+                value="appops set com.android.shell android:mock_location deny"/>
     </target_preparer>
 
     <!-- Needed for pushing the trace config file -->
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt
index bd8b005..42191d1 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt
@@ -34,7 +34,7 @@
     @FlickerBuilderProvider
     override fun buildFlicker(): FlickerBuilder {
         return FlickerBuilder(instrumentation).apply {
-            withoutScreenRecorder()
+            instrumentation.uiAutomation.adoptShellPermissionIdentity()
             setup { flicker.scenario.setIsTablet(tapl.isTablet) }
             transition()
         }
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 ebcb640..fde6acb 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
@@ -100,6 +100,7 @@
     @Mock lateinit var enterDesktopTransitionHandler: EnterDesktopTaskTransitionHandler
     @Mock lateinit var mToggleResizeDesktopTaskTransitionHandler:
             ToggleResizeDesktopTaskTransitionHandler
+    @Mock lateinit var dragToDesktopTransitionHandler: DragToDesktopTransitionHandler
     @Mock lateinit var launchAdjacentController: LaunchAdjacentController
     @Mock lateinit var desktopModeWindowDecoration: DesktopModeWindowDecoration
     @Mock lateinit var splitScreenController: SplitScreenController
@@ -127,7 +128,7 @@
         whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
 
         controller = createController()
-        controller.splitScreenController = splitScreenController
+        controller.setSplitScreenController(splitScreenController)
 
         shellInit.init()
 
@@ -150,6 +151,7 @@
             enterDesktopTransitionHandler,
             exitDesktopTransitionHandler,
             mToggleResizeDesktopTaskTransitionHandler,
+            dragToDesktopTransitionHandler,
             desktopModeTaskRepository,
             launchAdjacentController,
             recentsTransitionHandler,
@@ -757,6 +759,7 @@
 
     private fun setUpSplitScreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
         val task = createSplitScreenTask(displayId)
+        whenever(splitScreenController.isTaskInSplitScreen(task.taskId)).thenReturn(true)
         whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
         runningTasks.add(task)
         return task
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
new file mode 100644
index 0000000..a5629c8
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
@@ -0,0 +1,211 @@
+package com.android.wm.shell.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
+import android.app.WindowConfiguration.WindowingMode
+import android.graphics.PointF
+import android.os.IBinder
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.view.SurfaceControl
+import android.window.TransitionInfo
+import android.window.TransitionInfo.FLAG_IS_WALLPAPER
+import androidx.test.filters.SmallTest
+import com.android.server.testutils.any
+import com.android.server.testutils.mock
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestRunningTaskInfoBuilder
+import com.android.wm.shell.splitscreen.SplitScreenController
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP
+import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP
+import com.android.wm.shell.windowdecor.MoveToDesktopAnimator
+import java.util.function.Supplier
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyZeroInteractions
+import org.mockito.kotlin.whenever
+
+/** Tests of [DragToDesktopTransitionHandler]. */
+@SmallTest
+@RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class DragToDesktopTransitionHandlerTest : ShellTestCase() {
+
+    @Mock private lateinit var transitions: Transitions
+    @Mock private lateinit var taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
+    @Mock private lateinit var splitScreenController: SplitScreenController
+
+    private val transactionSupplier = Supplier { mock<SurfaceControl.Transaction>() }
+
+    private lateinit var handler: DragToDesktopTransitionHandler
+
+    @Before
+    fun setUp() {
+        handler =
+            DragToDesktopTransitionHandler(
+                    context,
+                    transitions,
+                    taskDisplayAreaOrganizer,
+                    transactionSupplier
+                )
+                .apply { setSplitScreenController(splitScreenController) }
+    }
+
+    @Test
+    fun startDragToDesktop_animateDragWhenReady() {
+        val task = createTask()
+        val dragAnimator = mock<MoveToDesktopAnimator>()
+        // Simulate transition is started.
+        val transition = startDragToDesktopTransition(task, dragAnimator)
+
+        // Now it's ready to animate.
+        handler.startAnimation(
+            transition = transition,
+            info =
+                createTransitionInfo(
+                    type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP,
+                    draggedTask = task
+                ),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+            finishCallback = {}
+        )
+
+        verify(dragAnimator).startAnimation()
+    }
+
+    @Test
+    fun startDragToDesktop_cancelledBeforeReady_startCancelTransition() {
+        val task = createTask()
+        val dragAnimator = mock<MoveToDesktopAnimator>()
+        // Simulate transition is started and is ready to animate.
+        val transition = startDragToDesktopTransition(task, dragAnimator)
+
+        handler.cancelDragToDesktopTransition()
+
+        handler.startAnimation(
+            transition = transition,
+            info =
+                createTransitionInfo(
+                    type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP,
+                    draggedTask = task
+                ),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+            finishCallback = {}
+        )
+
+        // Don't even animate the "drag" since it was already cancelled.
+        verify(dragAnimator, never()).startAnimation()
+        // Instead, start the cancel transition.
+        verify(transitions)
+            .startTransition(eq(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP), any(), eq(handler))
+    }
+
+    @Test
+    fun cancelDragToDesktop_startWasReady_cancel() {
+        val task = createTask()
+        val dragAnimator = mock<MoveToDesktopAnimator>()
+        whenever(dragAnimator.position).thenReturn(PointF())
+        // Simulate transition is started and is ready to animate.
+        val transition = startDragToDesktopTransition(task, dragAnimator)
+        handler.startAnimation(
+            transition = transition,
+            info =
+                createTransitionInfo(
+                    type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP,
+                    draggedTask = task
+                ),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+            finishCallback = {}
+        )
+
+        // Then user cancelled after it had already started.
+        handler.cancelDragToDesktopTransition()
+
+        // Cancel animation should run since it had already started.
+        verify(dragAnimator).endAnimator()
+    }
+
+    @Test
+    fun cancelDragToDesktop_startWasNotReady_animateCancel() {
+        val task = createTask()
+        val dragAnimator = mock<MoveToDesktopAnimator>()
+        // Simulate transition is started and is ready to animate.
+        startDragToDesktopTransition(task, dragAnimator)
+
+        // Then user cancelled before the transition was ready and animated.
+        handler.cancelDragToDesktopTransition()
+
+        // No need to animate the cancel since the start animation couldn't even start.
+        verifyZeroInteractions(dragAnimator)
+    }
+
+    private fun startDragToDesktopTransition(
+        task: RunningTaskInfo,
+        dragAnimator: MoveToDesktopAnimator
+    ): IBinder {
+        val token = mock<IBinder>()
+        whenever(
+                transitions.startTransition(
+                    eq(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP),
+                    any(),
+                    eq(handler)
+                )
+            )
+            .thenReturn(token)
+        handler.startDragToDesktopTransition(task.taskId, dragAnimator, mock())
+        return token
+    }
+
+    private fun createTask(
+        @WindowingMode windowingMode: Int = WINDOWING_MODE_FULLSCREEN,
+        isHome: Boolean = false,
+    ): RunningTaskInfo {
+        return TestRunningTaskInfoBuilder()
+            .setActivityType(if (isHome) ACTIVITY_TYPE_HOME else ACTIVITY_TYPE_STANDARD)
+            .setWindowingMode(windowingMode)
+            .build()
+            .also {
+                whenever(splitScreenController.isTaskInSplitScreen(it.taskId))
+                    .thenReturn(windowingMode == WINDOWING_MODE_MULTI_WINDOW)
+            }
+    }
+
+    private fun createTransitionInfo(type: Int, draggedTask: RunningTaskInfo): TransitionInfo {
+        return TransitionInfo(type, 0 /* flags */).apply {
+            addChange( // Home.
+                TransitionInfo.Change(mock(), mock()).apply {
+                    parent = null
+                    taskInfo =
+                        TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build()
+                    flags = flags or FLAG_IS_WALLPAPER
+                }
+            )
+            addChange( // Dragged Task.
+                TransitionInfo.Change(mock(), mock()).apply {
+                    parent = null
+                    taskInfo = draggedTask
+                }
+            )
+            addChange( // Wallpaper.
+                TransitionInfo.Change(mock(), mock()).apply {
+                    parent = null
+                    taskInfo = null
+                    flags = flags or FLAG_IS_WALLPAPER
+                }
+            )
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java
deleted file mode 100644
index 772d97d..0000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.desktopmode;
-
-import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
-
-import static androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread;
-
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-
-import android.annotation.NonNull;
-import android.app.ActivityManager;
-import android.app.WindowConfiguration;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.os.IBinder;
-import android.view.SurfaceControl;
-import android.view.WindowManager;
-import android.window.IWindowContainerToken;
-import android.window.TransitionInfo;
-import android.window.WindowContainerToken;
-import android.window.WindowContainerTransaction;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.transition.Transitions;
-import com.android.wm.shell.windowdecor.MoveToDesktopAnimator;
-
-import junit.framework.AssertionFailedError;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.function.Supplier;
-
-/** Tests of {@link com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler} */
-@SmallTest
-public class EnterDesktopTaskTransitionHandlerTest {
-
-    @Mock
-    private Transitions mTransitions;
-    @Mock
-    IBinder mToken;
-    @Mock
-    Supplier<SurfaceControl.Transaction> mTransactionFactory;
-    @Mock
-    SurfaceControl.Transaction mStartT;
-    @Mock
-    SurfaceControl.Transaction mFinishT;
-    @Mock
-    SurfaceControl.Transaction mAnimationT;
-    @Mock
-    Transitions.TransitionFinishCallback mTransitionFinishCallback;
-    @Mock
-    ShellExecutor mExecutor;
-    @Mock
-    SurfaceControl mSurfaceControl;
-    @Mock
-    MoveToDesktopAnimator mMoveToDesktopAnimator;
-    @Mock
-    PointF mPosition;
-
-    private EnterDesktopTaskTransitionHandler mEnterDesktopTaskTransitionHandler;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-
-        doReturn(mExecutor).when(mTransitions).getMainExecutor();
-        doReturn(mAnimationT).when(mTransactionFactory).get();
-        doReturn(mPosition).when(mMoveToDesktopAnimator).getPosition();
-
-        mEnterDesktopTaskTransitionHandler = new EnterDesktopTaskTransitionHandler(mTransitions,
-                mTransactionFactory);
-    }
-
-    @Test
-    public void testEnterFreeformAnimation() {
-        final int taskId = 1;
-        WindowContainerTransaction wct = new WindowContainerTransaction();
-        doReturn(mToken).when(mTransitions)
-                .startTransition(Transitions.TRANSIT_START_DRAG_TO_DESKTOP_MODE, wct,
-                        mEnterDesktopTaskTransitionHandler);
-        doReturn(taskId).when(mMoveToDesktopAnimator).getTaskId();
-
-        mEnterDesktopTaskTransitionHandler.startMoveToDesktop(wct,
-                mMoveToDesktopAnimator, null);
-
-        TransitionInfo.Change change =
-                createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FREEFORM);
-        TransitionInfo info = createTransitionInfo(Transitions.TRANSIT_START_DRAG_TO_DESKTOP_MODE,
-                change);
-
-
-        assertTrue(mEnterDesktopTaskTransitionHandler
-                .startAnimation(mToken, info, mStartT, mFinishT, mTransitionFinishCallback));
-
-        verify(mStartT).setWindowCrop(mSurfaceControl, null);
-        verify(mStartT).apply();
-    }
-
-    @Test
-    public void testTransitEnterDesktopModeAnimation() throws Throwable {
-        final int transitionType = Transitions.TRANSIT_FINALIZE_DRAG_TO_DESKTOP_MODE;
-        final int taskId = 1;
-        WindowContainerTransaction wct = new WindowContainerTransaction();
-        doReturn(mToken).when(mTransitions)
-                .startTransition(transitionType, wct, mEnterDesktopTaskTransitionHandler);
-        mEnterDesktopTaskTransitionHandler.finalizeMoveToDesktop(wct, null);
-
-        TransitionInfo.Change change =
-                createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FREEFORM);
-        change.setEndAbsBounds(new Rect(0, 0, 1, 1));
-        TransitionInfo info = createTransitionInfo(
-                Transitions.TRANSIT_FINALIZE_DRAG_TO_DESKTOP_MODE, change);
-
-        runOnUiThread(() -> {
-            try {
-                assertTrue(mEnterDesktopTaskTransitionHandler
-                                .startAnimation(mToken, info, mStartT, mFinishT,
-                                        mTransitionFinishCallback));
-            } catch (Exception e) {
-                throw new AssertionFailedError(e.getMessage());
-            }
-        });
-
-        verify(mStartT).setWindowCrop(mSurfaceControl, change.getEndAbsBounds().width(),
-                change.getEndAbsBounds().height());
-        verify(mStartT).apply();
-    }
-
-    private TransitionInfo.Change createChange(@WindowManager.TransitionType int type, int taskId,
-            @WindowConfiguration.WindowingMode int windowingMode) {
-        final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
-        taskInfo.taskId = taskId;
-        taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode);
-        final TransitionInfo.Change change = new TransitionInfo.Change(
-                new WindowContainerToken(mock(IWindowContainerToken.class)), mSurfaceControl);
-        change.setMode(type);
-        change.setTaskInfo(taskInfo);
-        return change;
-    }
-
-    private static TransitionInfo createTransitionInfo(
-            @WindowManager.TransitionType int type, @NonNull TransitionInfo.Change change) {
-        TransitionInfo info = new TransitionInfo(type, 0);
-        info.addChange(change);
-        return info;
-    }
-
-}
diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java
index 7faa13c..447d3bb 100644
--- a/media/java/android/media/AudioRecord.java
+++ b/media/java/android/media/AudioRecord.java
@@ -1176,6 +1176,7 @@
             case AudioFormat.ENCODING_PCM_FLOAT:
             case AudioFormat.ENCODING_PCM_16BIT:
             case AudioFormat.ENCODING_PCM_8BIT:
+            case AudioFormat.ENCODING_E_AC3_JOC:
                 mAudioFormat = audioFormat;
                 break;
             default:
@@ -1188,20 +1189,12 @@
 
 
     // Convenience method for the contructor's audio buffer size check.
-    // preconditions:
-    //    mChannelCount is valid
-    //    mAudioFormat is AudioFormat.ENCODING_PCM_8BIT, AudioFormat.ENCODING_PCM_16BIT,
-    //                 or AudioFormat.ENCODING_PCM_FLOAT
     // postcondition:
     //    mNativeBufferSizeInBytes is valid (multiple of frame size, positive)
     private void audioBuffSizeCheck(int audioBufferSize) throws IllegalArgumentException {
-        // NB: this section is only valid with PCM data.
-        // To update when supporting compressed formats
-        int frameSizeInBytes = mChannelCount
-            * (AudioFormat.getBytesPerSample(mAudioFormat));
-        if ((audioBufferSize % frameSizeInBytes != 0) || (audioBufferSize < 1)) {
+        if ((audioBufferSize % getFormat().getFrameSizeInBytes() != 0) || (audioBufferSize < 1)) {
             throw new IllegalArgumentException("Invalid audio buffer size " + audioBufferSize
-                    + " (frame size " + frameSizeInBytes + ")");
+                    + " (frame size " + getFormat().getFrameSizeInBytes() + ")");
         }
 
         mNativeBufferSizeInBytes = audioBufferSize;
diff --git a/media/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp
index feb914f..757e9f8 100644
--- a/media/jni/android_media_tv_Tuner.cpp
+++ b/media/jni/android_media_tv_Tuner.cpp
@@ -630,7 +630,6 @@
 
     const DemuxFilterMediaEvent &mediaEvent = event.get<DemuxFilterEvent::Tag::media>();
     ScopedLocalRef<jobject> audioDescriptor(env);
-    gAudioPresentationFields.init(env);
     ScopedLocalRef presentationsJObj(env, JAudioPresentationInfo::asJobject(
         env, gAudioPresentationFields));
     switch (mediaEvent.extraMetaData.getTag()) {
@@ -3731,6 +3730,7 @@
     gFields.linearBlockInitID = env->GetMethodID(linearBlockClazz, "<init>", "()V");
     gFields.linearBlockSetInternalStateID =
             env->GetMethodID(linearBlockClazz, "setInternalStateLocked", "(JZ)V");
+    gAudioPresentationFields.init(env);
 }
 
 static void android_media_tv_Tuner_native_setup(JNIEnv *env, jobject thiz) {
diff --git a/native/android/TEST_MAPPING b/native/android/TEST_MAPPING
index fd394fc..7c71098 100644
--- a/native/android/TEST_MAPPING
+++ b/native/android/TEST_MAPPING
@@ -22,5 +22,15 @@
        ],
        "file_patterns": ["performance_hint.cpp"]
     }
+  ],
+  "postsubmit": [
+    {
+      "name": "CtsThermalTestCases",
+       "file_patterns": ["thermal.cpp"]
+    },
+    {
+      "name": "NativeThermalUnitTestCases",
+       "file_patterns": ["thermal.cpp"]
+    }
   ]
 }
diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt
index f4be33c7..9f2a9ac 100644
--- a/native/android/libandroid.map.txt
+++ b/native/android/libandroid.map.txt
@@ -327,6 +327,7 @@
     AThermal_registerThermalStatusListener; # introduced=30
     AThermal_unregisterThermalStatusListener; # introduced=30
     AThermal_getThermalHeadroom; # introduced=31
+    AThermal_getThermalHeadroomThresholds; # introduced=VanillaIceCream
     APerformanceHint_getManager; # introduced=Tiramisu
     APerformanceHint_createSession; # introduced=Tiramisu
     APerformanceHint_getPreferredUpdateRateNanos; # introduced=Tiramisu
@@ -348,6 +349,7 @@
 
 LIBANDROID_PLATFORM {
   global:
+    AThermal_setIThermalServiceForTesting;
     APerformanceHint_setIHintManagerForTesting;
     APerformanceHint_sendHint;
     APerformanceHint_getThreadIds;
diff --git a/native/android/tests/thermal/Android.bp b/native/android/tests/thermal/Android.bp
new file mode 100644
index 0000000..8540d83
--- /dev/null
+++ b/native/android/tests/thermal/Android.bp
@@ -0,0 +1,65 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+cc_test {
+    name: "NativeThermalUnitTestCases",
+
+    multilib: {
+        lib32: {
+            suffix: "32",
+        },
+        lib64: {
+            suffix: "64",
+        },
+    },
+
+    srcs: ["NativeThermalUnitTest.cpp"],
+
+    shared_libs: [
+        "libandroid",
+        "liblog",
+        "libbinder",
+        "libpowermanager",
+        "libutils",
+    ],
+
+    static_libs: [
+        "libbase",
+        "libgmock",
+        "libgtest",
+    ],
+    stl: "c++_shared",
+
+    test_suites: [
+        "device-tests",
+    ],
+
+    cflags: [
+        "-Werror",
+        "-Wall",
+    ],
+
+    header_libs: [
+        "libandroid_headers_private",
+    ],
+}
diff --git a/native/android/tests/thermal/NativeThermalUnitTest.cpp b/native/android/tests/thermal/NativeThermalUnitTest.cpp
new file mode 100644
index 0000000..6d6861a
--- /dev/null
+++ b/native/android/tests/thermal/NativeThermalUnitTest.cpp
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "NativeThermalUnitTest"
+
+#include <android/os/IThermalService.h>
+#include <android/thermal.h>
+#include <binder/IBinder.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <thermal_private.h>
+
+using android::binder::Status;
+
+using namespace testing;
+using namespace android;
+using namespace android::os;
+
+class MockIThermalService : public IThermalService {
+public:
+    MOCK_METHOD(Status, registerThermalEventListener,
+                (const ::android::sp<::android::os::IThermalEventListener>& listener,
+                 bool* _aidl_return),
+                (override));
+    MOCK_METHOD(Status, registerThermalEventListenerWithType,
+                (const ::android::sp<::android::os::IThermalEventListener>& listener, int32_t type,
+                 bool* _aidl_return),
+                (override));
+    MOCK_METHOD(Status, unregisterThermalEventListener,
+                (const ::android::sp<::android::os::IThermalEventListener>& listener,
+                 bool* _aidl_return),
+                (override));
+    MOCK_METHOD(Status, getCurrentTemperatures,
+                (::std::vector<::android::os::Temperature> * _aidl_return), (override));
+    MOCK_METHOD(Status, getCurrentTemperaturesWithType,
+                (int32_t type, ::std::vector<::android::os::Temperature>* _aidl_return),
+                (override));
+    MOCK_METHOD(Status, registerThermalStatusListener,
+                (const ::android::sp<::android::os::IThermalStatusListener>& listener,
+                 bool* _aidl_return),
+                (override));
+    MOCK_METHOD(Status, unregisterThermalStatusListener,
+                (const ::android::sp<::android::os::IThermalStatusListener>& listener,
+                 bool* _aidl_return),
+                (override));
+    MOCK_METHOD(Status, getCurrentThermalStatus, (int32_t * _aidl_return), (override));
+    MOCK_METHOD(Status, getCurrentCoolingDevices,
+                (::std::vector<::android::os::CoolingDevice> * _aidl_return), (override));
+    MOCK_METHOD(Status, getCurrentCoolingDevicesWithType,
+                (int32_t type, ::std::vector<::android::os::CoolingDevice>* _aidl_return),
+                (override));
+    MOCK_METHOD(Status, getThermalHeadroom, (int32_t forecastSeconds, float* _aidl_return),
+                (override));
+    MOCK_METHOD(Status, getThermalHeadroomThresholds, (::std::vector<float> * _aidl_return),
+                (override));
+    MOCK_METHOD(IBinder*, onAsBinder, (), (override));
+};
+
+class NativeThermalUnitTest : public Test {
+public:
+    void SetUp() override {
+        mMockIThermalService = new StrictMock<MockIThermalService>();
+        AThermal_setIThermalServiceForTesting(mMockIThermalService);
+        mThermalManager = AThermal_acquireManager();
+    }
+
+    void TearDown() override {
+        AThermal_setIThermalServiceForTesting(nullptr);
+        AThermal_releaseManager(mThermalManager);
+    }
+
+    StrictMock<MockIThermalService>* mMockIThermalService = nullptr;
+    AThermalManager* mThermalManager = nullptr;
+};
+
+static void checkThermalHeadroomThresholds(const std::vector<float>& expected,
+                                           const AThermalHeadroomThreshold* thresholds,
+                                           size_t size) {
+    if (thresholds == nullptr) {
+        FAIL() << "Unexpected null thresholds pointer";
+    }
+    for (int i = 0; i < (int)size; i++) {
+        auto t = thresholds[i];
+        ASSERT_EQ(i, t.thermalStatus) << "threshold " << i << " should have status " << i;
+        ASSERT_EQ(expected[i], t.headroom)
+                << "threshold " << i << " should have headroom " << expected[i];
+    }
+}
+
+TEST_F(NativeThermalUnitTest, TestGetThermalHeadroomThresholds) {
+    std::vector<float> expected = {1, 2, 3, 4, 5, 6, 7, 8, 9};
+    EXPECT_CALL(*mMockIThermalService, getThermalHeadroomThresholds(_))
+            .Times(Exactly(1))
+            .WillRepeatedly(DoAll(SetArgPointee<0>(expected), Return(Status())));
+    const AThermalHeadroomThreshold* thresholds1 = nullptr;
+    size_t size1;
+    ASSERT_EQ(OK, AThermal_getThermalHeadroomThresholds(mThermalManager, &thresholds1, &size1));
+    checkThermalHeadroomThresholds(expected, thresholds1, size1);
+    // following calls should be cached
+    EXPECT_CALL(*mMockIThermalService, getThermalHeadroomThresholds(_)).Times(0);
+
+    const AThermalHeadroomThreshold* thresholds2 = nullptr;
+    size_t size2;
+    ASSERT_EQ(OK, AThermal_getThermalHeadroomThresholds(mThermalManager, &thresholds2, &size2));
+    checkThermalHeadroomThresholds(expected, thresholds2, size2);
+}
+
+TEST_F(NativeThermalUnitTest, TestGetThermalHeadroomThresholdsFailedWithServerError) {
+    const AThermalHeadroomThreshold* thresholds = nullptr;
+    size_t size;
+    EXPECT_CALL(*mMockIThermalService, getThermalHeadroomThresholds(_))
+            .Times(Exactly(1))
+            .WillOnce(Return(
+                    Status::fromExceptionCode(binder::Status::Exception::EX_ILLEGAL_ARGUMENT)));
+    ASSERT_EQ(EPIPE, AThermal_getThermalHeadroomThresholds(mThermalManager, &thresholds, &size));
+    ASSERT_EQ(nullptr, thresholds);
+}
+
+TEST_F(NativeThermalUnitTest, TestGetThermalHeadroomThresholdsFailedWithFeatureDisabled) {
+    const AThermalHeadroomThreshold* thresholds = nullptr;
+    size_t size;
+    EXPECT_CALL(*mMockIThermalService, getThermalHeadroomThresholds(_))
+            .Times(Exactly(1))
+            .WillOnce(Return(Status::fromExceptionCode(
+                    binder::Status::Exception::EX_UNSUPPORTED_OPERATION)));
+    ASSERT_EQ(ENOSYS, AThermal_getThermalHeadroomThresholds(mThermalManager, &thresholds, &size));
+    ASSERT_EQ(nullptr, thresholds);
+}
+
+TEST_F(NativeThermalUnitTest, TestGetThermalHeadroomThresholdsFailedWithNullPtr) {
+    const AThermalHeadroomThreshold* thresholds = nullptr;
+    size_t size;
+    size_t* nullSize = nullptr;
+    ASSERT_EQ(EINVAL,
+              AThermal_getThermalHeadroomThresholds(mThermalManager, &thresholds, nullSize));
+    ASSERT_EQ(nullptr, thresholds);
+    ASSERT_EQ(EINVAL, AThermal_getThermalHeadroomThresholds(mThermalManager, nullptr, &size));
+}
+
+TEST_F(NativeThermalUnitTest, TestGetThermalHeadroomThresholdsFailedWithNonEmptyPtr) {
+    const AThermalHeadroomThreshold* initialized = new AThermalHeadroomThreshold[1];
+    size_t size;
+    ASSERT_EQ(EINVAL, AThermal_getThermalHeadroomThresholds(mThermalManager, &initialized, &size));
+    delete[] initialized;
+}
diff --git a/native/android/tests/thermal/OWNERS b/native/android/tests/thermal/OWNERS
new file mode 100644
index 0000000..e3bbee92
--- /dev/null
+++ b/native/android/tests/thermal/OWNERS
@@ -0,0 +1 @@
+include /ADPF_OWNERS
diff --git a/native/android/thermal.cpp b/native/android/thermal.cpp
index 1f6ef47..b43f2f16 100644
--- a/native/android/thermal.cpp
+++ b/native/android/thermal.cpp
@@ -16,27 +16,32 @@
 
 #define LOG_TAG "thermal"
 
-#include <cerrno>
-#include <thread>
-#include <limits>
-
-#include <android/thermal.h>
+#include <android-base/thread_annotations.h>
 #include <android/os/BnThermalStatusListener.h>
 #include <android/os/IThermalService.h>
+#include <android/thermal.h>
 #include <binder/IServiceManager.h>
+#include <thermal_private.h>
 #include <utils/Log.h>
 
+#include <cerrno>
+#include <limits>
+#include <thread>
+
 using android::sp;
 
 using namespace android;
 using namespace android::os;
 
 struct ThermalServiceListener : public BnThermalStatusListener {
-    public:
-        virtual binder::Status onStatusChange(int32_t status) override;
-        ThermalServiceListener(AThermalManager *manager) {mMgr = manager;}
-    private:
-        AThermalManager *mMgr;
+public:
+    virtual binder::Status onStatusChange(int32_t status) override;
+    ThermalServiceListener(AThermalManager *manager) {
+        mMgr = manager;
+    }
+
+private:
+    AThermalManager *mMgr;
 };
 
 struct ListenerCallback {
@@ -44,22 +49,29 @@
     void* data;
 };
 
+static IThermalService *gIThermalServiceForTesting = nullptr;
+
 struct AThermalManager {
-   public:
-        static AThermalManager* createAThermalManager();
-        AThermalManager() = delete;
-        ~AThermalManager();
-        status_t notifyStateChange(int32_t status);
-        status_t getCurrentThermalStatus(int32_t *status);
-        status_t addListener(AThermal_StatusCallback, void *data);
-        status_t removeListener(AThermal_StatusCallback, void *data);
-        status_t getThermalHeadroom(int32_t forecastSeconds, float *result);
-   private:
-       AThermalManager(sp<IThermalService> service);
-       sp<IThermalService> mThermalSvc;
-       sp<ThermalServiceListener> mServiceListener;
-       std::vector<ListenerCallback> mListeners;
-       std::mutex mMutex;
+public:
+    static AThermalManager *createAThermalManager();
+    AThermalManager() = delete;
+    ~AThermalManager();
+    status_t notifyStateChange(int32_t status);
+    status_t getCurrentThermalStatus(int32_t *status);
+    status_t addListener(AThermal_StatusCallback, void *data);
+    status_t removeListener(AThermal_StatusCallback, void *data);
+    status_t getThermalHeadroom(int32_t forecastSeconds, float *result);
+    status_t getThermalHeadroomThresholds(const AThermalHeadroomThreshold **, size_t *size);
+
+private:
+    AThermalManager(sp<IThermalService> service);
+    sp<IThermalService> mThermalSvc;
+    std::mutex mListenerMutex;
+    sp<ThermalServiceListener> mServiceListener GUARDED_BY(mListenerMutex);
+    std::vector<ListenerCallback> mListeners GUARDED_BY(mListenerMutex);
+    std::mutex mThresholdsMutex;
+    const AThermalHeadroomThreshold *mThresholds = nullptr; // GUARDED_BY(mThresholdsMutex)
+    size_t mThresholdsCount GUARDED_BY(mThresholdsMutex);
 };
 
 binder::Status ThermalServiceListener::onStatusChange(int32_t status) {
@@ -70,6 +82,9 @@
 }
 
 AThermalManager* AThermalManager::createAThermalManager() {
+    if (gIThermalServiceForTesting) {
+        return new AThermalManager(gIThermalServiceForTesting);
+    }
     sp<IBinder> binder =
             defaultServiceManager()->checkService(String16("thermalservice"));
 
@@ -81,12 +96,10 @@
 }
 
 AThermalManager::AThermalManager(sp<IThermalService> service)
-    : mThermalSvc(service),
-      mServiceListener(nullptr) {
-}
+      : mThermalSvc(std::move(service)), mServiceListener(nullptr) {}
 
 AThermalManager::~AThermalManager() {
-    std::unique_lock<std::mutex> lock(mMutex);
+    std::unique_lock<std::mutex> listenerLock(mListenerMutex);
 
     mListeners.clear();
     if (mServiceListener != nullptr) {
@@ -94,10 +107,13 @@
         mThermalSvc->unregisterThermalStatusListener(mServiceListener, &success);
         mServiceListener = nullptr;
     }
+    listenerLock.unlock();
+    std::unique_lock<std::mutex> lock(mThresholdsMutex);
+    delete[] mThresholds;
 }
 
 status_t AThermalManager::notifyStateChange(int32_t status) {
-    std::unique_lock<std::mutex> lock(mMutex);
+    std::unique_lock<std::mutex> lock(mListenerMutex);
     AThermalStatus thermalStatus = static_cast<AThermalStatus>(status);
 
     for (auto listener : mListeners) {
@@ -107,7 +123,7 @@
 }
 
 status_t AThermalManager::addListener(AThermal_StatusCallback callback, void *data) {
-    std::unique_lock<std::mutex> lock(mMutex);
+    std::unique_lock<std::mutex> lock(mListenerMutex);
 
     if (callback == nullptr) {
         // Callback can not be nullptr
@@ -141,7 +157,7 @@
 }
 
 status_t AThermalManager::removeListener(AThermal_StatusCallback callback, void *data) {
-    std::unique_lock<std::mutex> lock(mMutex);
+    std::unique_lock<std::mutex> lock(mListenerMutex);
 
     auto it = std::remove_if(mListeners.begin(),
                              mListeners.end(),
@@ -198,6 +214,32 @@
     return OK;
 }
 
+status_t AThermalManager::getThermalHeadroomThresholds(const AThermalHeadroomThreshold **result,
+                                                       size_t *size) {
+    std::unique_lock<std::mutex> lock(mThresholdsMutex);
+    if (mThresholds == nullptr) {
+        auto thresholds = std::make_unique<std::vector<float>>();
+        binder::Status ret = mThermalSvc->getThermalHeadroomThresholds(thresholds.get());
+        if (!ret.isOk()) {
+            if (ret.exceptionCode() == binder::Status::EX_UNSUPPORTED_OPERATION) {
+                // feature is not enabled
+                return ENOSYS;
+            }
+            return EPIPE;
+        }
+        mThresholdsCount = thresholds->size();
+        auto t = new AThermalHeadroomThreshold[mThresholdsCount];
+        for (int i = 0; i < (int)mThresholdsCount; i++) {
+            t[i].headroom = (*thresholds)[i];
+            t[i].thermalStatus = static_cast<AThermalStatus>(i);
+        }
+        mThresholds = t;
+    }
+    *size = mThresholdsCount;
+    *result = mThresholds;
+    return OK;
+}
+
 /**
   * Acquire an instance of the thermal manager. This must be freed using
   * {@link AThermal_releaseManager}.
@@ -291,14 +333,24 @@
  *  	   threshold. Returns NaN if the device does not support this functionality or if
  * 	       this function is called significantly faster than once per second.
  */
-float AThermal_getThermalHeadroom(AThermalManager *manager,
-        int forecastSeconds) {
+float AThermal_getThermalHeadroom(AThermalManager *manager, int forecastSeconds) {
     float result = 0.0f;
     status_t ret = manager->getThermalHeadroom(forecastSeconds, &result);
-
     if (ret != OK) {
         result = std::numeric_limits<float>::quiet_NaN();
     }
-
     return result;
 }
+
+int AThermal_getThermalHeadroomThresholds(AThermalManager *manager,
+                                          const AThermalHeadroomThreshold **outThresholds,
+                                          size_t *size) {
+    if (outThresholds == nullptr || *outThresholds != nullptr || size == nullptr) {
+        return EINVAL;
+    }
+    return manager->getThermalHeadroomThresholds(outThresholds, size);
+}
+
+void AThermal_setIThermalServiceForTesting(void *iThermalService) {
+    gIThermalServiceForTesting = static_cast<IThermalService *>(iThermalService);
+}
diff --git a/packages/PackageInstaller/Android.bp b/packages/PackageInstaller/Android.bp
index 58224b8..38bd7d5 100644
--- a/packages/PackageInstaller/Android.bp
+++ b/packages/PackageInstaller/Android.bp
@@ -46,6 +46,9 @@
         "xz-java",
         "androidx.leanback_leanback",
         "androidx.annotation_annotation",
+        "androidx.fragment_fragment",
+        "androidx.lifecycle_lifecycle-livedata",
+        "androidx.lifecycle_lifecycle-extensions",
     ],
 
     lint: {
@@ -69,6 +72,9 @@
     static_libs: [
         "xz-java",
         "androidx.leanback_leanback",
+        "androidx.fragment_fragment",
+        "androidx.lifecycle_lifecycle-livedata",
+        "androidx.lifecycle_lifecycle-extensions",
     ],
     aaptflags: ["--product tablet"],
 
@@ -94,6 +100,9 @@
         "xz-java",
         "androidx.leanback_leanback",
         "androidx.annotation_annotation",
+        "androidx.fragment_fragment",
+        "androidx.lifecycle_lifecycle-livedata",
+        "androidx.lifecycle_lifecycle-extensions",
     ],
     aaptflags: ["--product tv"],
 
diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml
index a16f9f5..35f5772 100644
--- a/packages/PackageInstaller/AndroidManifest.xml
+++ b/packages/PackageInstaller/AndroidManifest.xml
@@ -43,6 +43,11 @@
             </intent-filter>
         </receiver>
 
+        <activity android:name=".v2.ui.InstallLaunch"
+            android:configChanges="orientation|keyboardHidden|screenSize"
+            android:theme="@style/Theme.AlertDialogActivity"
+            android:exported="true"/>
+
         <activity android:name=".InstallStart"
                 android:theme="@style/Theme.AlertDialogActivity"
                 android:exported="true"
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
index 736e0ef..e2107eb 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
@@ -40,7 +40,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-
+import com.android.packageinstaller.v2.ui.InstallLaunch;
 import java.util.Arrays;
 
 /**
@@ -57,9 +57,23 @@
 
     private final boolean mLocalLOGV = false;
 
+    // TODO (sumedhsen): Replace with an Android Feature Flag once implemented
+    private static final boolean USE_PIA_V2 = false;
+
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+
+        if (USE_PIA_V2) {
+            Intent piaV2 = new Intent(getIntent());
+            piaV2.putExtra(InstallLaunch.EXTRA_CALLING_PKG_NAME, getCallingPackage());
+            piaV2.putExtra(InstallLaunch.EXTRA_CALLING_PKG_UID, getLaunchedFromUid());
+            piaV2.setClass(this, InstallLaunch.class);
+            piaV2.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
+            startActivity(piaV2);
+            finish();
+            return;
+        }
         mPackageManager = getPackageManager();
         mUserManager = getSystemService(UserManager.class);
 
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.java
new file mode 100644
index 0000000..03af951
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.packageinstaller.v2.model;
+
+import static com.android.packageinstaller.v2.model.PackageUtil.canPackageQuery;
+import static com.android.packageinstaller.v2.model.PackageUtil.isCallerSessionOwner;
+import static com.android.packageinstaller.v2.model.PackageUtil.isInstallPermissionGrantedOrRequested;
+import static com.android.packageinstaller.v2.model.PackageUtil.isPermissionGranted;
+import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.ABORT_REASON_INTERNAL_ERROR;
+import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.ABORT_REASON_POLICY;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.admin.DevicePolicyManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.SessionInfo;
+import android.content.pm.PackageManager;
+import android.content.res.AssetFileDescriptor;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.UserManager;
+import android.text.TextUtils;
+import android.util.EventLog;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.MutableLiveData;
+import com.android.packageinstaller.v2.model.installstagedata.InstallAborted;
+import com.android.packageinstaller.v2.model.installstagedata.InstallReady;
+import com.android.packageinstaller.v2.model.installstagedata.InstallStage;
+import com.android.packageinstaller.v2.model.installstagedata.InstallStaging;
+import java.io.IOException;
+
+public class InstallRepository {
+
+    private static final String SCHEME_PACKAGE = "package";
+    private static final String TAG = InstallRepository.class.getSimpleName();
+    private final Context mContext;
+    private final PackageManager mPackageManager;
+    private final PackageInstaller mPackageInstaller;
+    private final UserManager mUserManager;
+    private final DevicePolicyManager mDevicePolicyManager;
+    private final MutableLiveData<InstallStage> mStagingResult = new MutableLiveData<>();
+    private final boolean mLocalLOGV = false;
+    private Intent mIntent;
+    private boolean mIsSessionInstall;
+    private boolean mIsTrustedSource;
+    /**
+     * Session ID for a session created when caller uses PackageInstaller APIs
+     */
+    private int mSessionId;
+    /**
+     * Session ID for a session created by this app
+     */
+    private int mStagedSessionId = SessionInfo.INVALID_ID;
+    private int mCallingUid;
+    private String mCallingPackage;
+    private SessionStager mSessionStager;
+
+    public InstallRepository(Context context) {
+        mContext = context;
+        mPackageManager = context.getPackageManager();
+        mPackageInstaller = mPackageManager.getPackageInstaller();
+        mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class);
+        mUserManager = context.getSystemService(UserManager.class);
+    }
+
+    /**
+     * Extracts information from the incoming install intent, checks caller's permission to install
+     * packages, verifies that the caller is the install session owner (in case of a session based
+     * install) and checks if the current user has restrictions set that prevent app installation,
+     *
+     * @param intent the incoming {@link Intent} object for installing a package
+     * @param callerInfo {@link CallerInfo} that holds the callingUid and callingPackageName
+     * @return <p>{@link InstallAborted} if there are errors while performing the checks</p>
+     *     <p>{@link InstallStaging} after successfully performing the checks</p>
+     */
+    public InstallStage performPreInstallChecks(Intent intent, CallerInfo callerInfo) {
+        mIntent = intent;
+
+        String callingAttributionTag = null;
+
+        mIsSessionInstall =
+            PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(intent.getAction())
+                || PackageInstaller.ACTION_CONFIRM_INSTALL.equals(intent.getAction());
+
+        mSessionId = mIsSessionInstall
+            ? intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, SessionInfo.INVALID_ID)
+            : SessionInfo.INVALID_ID;
+
+        mCallingPackage = callerInfo.getPackageName();
+
+        if (mCallingPackage == null && mSessionId != SessionInfo.INVALID_ID) {
+            PackageInstaller.SessionInfo sessionInfo = mPackageInstaller.getSessionInfo(mSessionId);
+            mCallingPackage = (sessionInfo != null) ? sessionInfo.getInstallerPackageName() : null;
+            callingAttributionTag =
+                (sessionInfo != null) ? sessionInfo.getInstallerAttributionTag() : null;
+        }
+
+        // Uid of the source package, coming from ActivityManager
+        mCallingUid = callerInfo.getUid();
+        if (mCallingUid == Process.INVALID_UID) {
+            Log.e(TAG, "Could not determine the launching uid.");
+        }
+        final ApplicationInfo sourceInfo = getSourceInfo(mCallingPackage);
+        // Uid of the source package, with a preference to uid from ApplicationInfo
+        final int originatingUid = sourceInfo != null ? sourceInfo.uid : mCallingUid;
+
+        if (mCallingUid == Process.INVALID_UID && sourceInfo == null) {
+            // Caller's identity could not be determined. Abort the install
+            return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
+        }
+
+        if (!isCallerSessionOwner(mPackageInstaller, originatingUid, mSessionId)) {
+            return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
+        }
+
+        mIsTrustedSource = isInstallRequestFromTrustedSource(sourceInfo, mIntent, originatingUid);
+
+        if (!isInstallPermissionGrantedOrRequested(mContext, mCallingUid, originatingUid,
+            mIsTrustedSource)) {
+            return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
+        }
+
+        String restriction = getDevicePolicyRestrictions();
+        if (restriction != null) {
+            InstallAborted.Builder abortedBuilder =
+                new InstallAborted.Builder(ABORT_REASON_POLICY).setMessage(restriction);
+            final Intent adminSupportDetailsIntent =
+                mDevicePolicyManager.createAdminSupportIntent(restriction);
+            if (adminSupportDetailsIntent != null) {
+                abortedBuilder.setResultIntent(adminSupportDetailsIntent);
+            }
+            return abortedBuilder.build();
+        }
+
+        maybeRemoveInvalidInstallerPackageName(callerInfo);
+
+        return new InstallStaging();
+    }
+
+    /**
+     * @return the ApplicationInfo for the installation source (the calling package), if available
+     */
+    @Nullable
+    private ApplicationInfo getSourceInfo(@Nullable String callingPackage) {
+        if (callingPackage == null) {
+            return null;
+        }
+        try {
+            return mPackageManager.getApplicationInfo(callingPackage, 0);
+        } catch (PackageManager.NameNotFoundException ignored) {
+            return null;
+        }
+    }
+
+    private boolean isInstallRequestFromTrustedSource(ApplicationInfo sourceInfo, Intent intent,
+        int originatingUid) {
+        boolean isNotUnknownSource = intent.getBooleanExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false);
+        return sourceInfo != null && sourceInfo.isPrivilegedApp()
+            && (isNotUnknownSource
+            || isPermissionGranted(mContext, Manifest.permission.INSTALL_PACKAGES, originatingUid));
+    }
+
+    private String getDevicePolicyRestrictions() {
+        final String[] restrictions = new String[]{
+            UserManager.DISALLOW_INSTALL_APPS,
+            UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES,
+            UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY
+        };
+
+        for (String restriction : restrictions) {
+            if (!mUserManager.hasUserRestrictionForUser(restriction, Process.myUserHandle())) {
+                continue;
+            }
+            return restriction;
+        }
+        return null;
+    }
+
+    private void maybeRemoveInvalidInstallerPackageName(CallerInfo callerInfo) {
+        final String installerPackageNameFromIntent =
+            mIntent.getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME);
+        if (installerPackageNameFromIntent == null) {
+            return;
+        }
+        if (!TextUtils.equals(installerPackageNameFromIntent, callerInfo.getPackageName())
+            && !isPermissionGranted(mPackageManager, Manifest.permission.INSTALL_PACKAGES,
+            callerInfo.getPackageName())) {
+            Log.e(TAG, "The given installer package name " + installerPackageNameFromIntent
+                + " is invalid. Remove it.");
+            EventLog.writeEvent(0x534e4554, "236687884", callerInfo.getUid(),
+                "Invalid EXTRA_INSTALLER_PACKAGE_NAME");
+            mIntent.removeExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME);
+        }
+    }
+
+    public void stageForInstall() {
+        Uri uri = mIntent.getData();
+        if (mIsSessionInstall || (uri != null && SCHEME_PACKAGE.equals(uri.getScheme()))) {
+            // For a session based install or installing with a package:// URI, there is no file
+            // for us to stage. Setting the mStagingResult as null will signal InstallViewModel to
+            // proceed with user confirmation stage.
+            mStagingResult.setValue(new InstallReady());
+            return;
+        }
+        if (uri != null
+            && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())
+            && canPackageQuery(mContext, mCallingUid, uri)) {
+
+            if (mStagedSessionId > 0) {
+                final PackageInstaller.SessionInfo info =
+                    mPackageInstaller.getSessionInfo(mStagedSessionId);
+                if (info == null || !info.isActive() || info.getResolvedBaseApkPath() == null) {
+                    Log.w(TAG, "Session " + mStagedSessionId + " in funky state; ignoring");
+                    if (info != null) {
+                        cleanupStagingSession();
+                    }
+                    mStagedSessionId = 0;
+                }
+            }
+
+            // Session does not exist, or became invalid.
+            if (mStagedSessionId <= 0) {
+                // Create session here to be able to show error.
+                try (final AssetFileDescriptor afd =
+                    mContext.getContentResolver().openAssetFileDescriptor(uri, "r")) {
+                    ParcelFileDescriptor pfd = afd != null ? afd.getParcelFileDescriptor() : null;
+                    PackageInstaller.SessionParams params =
+                        createSessionParams(mIntent, pfd, uri.toString());
+                    mStagedSessionId = mPackageInstaller.createSession(params);
+                } catch (IOException e) {
+                    Log.w(TAG, "Failed to create a staging session", e);
+                    mStagingResult.setValue(
+                        new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR)
+                            .setResultIntent(new Intent().putExtra(Intent.EXTRA_INSTALL_RESULT,
+                                PackageManager.INSTALL_FAILED_INVALID_APK))
+                            .setActivityResultCode(Activity.RESULT_FIRST_USER)
+                            .build());
+                    return;
+                }
+            }
+
+            SessionStageListener listener = new SessionStageListener() {
+                @Override
+                public void onStagingSuccess(SessionInfo info) {
+                    //TODO: Verify if the returned sessionInfo should be used anywhere
+                    mStagingResult.setValue(new InstallReady());
+                }
+
+                @Override
+                public void onStagingFailure() {
+                    cleanupStagingSession();
+                    mStagingResult.setValue(
+                        new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR)
+                            .setResultIntent(new Intent().putExtra(Intent.EXTRA_INSTALL_RESULT,
+                                PackageManager.INSTALL_FAILED_INVALID_APK))
+                            .setActivityResultCode(Activity.RESULT_FIRST_USER)
+                            .build());
+                }
+            };
+            if (mSessionStager != null) {
+                mSessionStager.cancel(true);
+            }
+            mSessionStager = new SessionStager(mContext, uri, mStagedSessionId, listener);
+            mSessionStager.execute();
+        }
+    }
+
+    private void cleanupStagingSession() {
+        if (mStagedSessionId > 0) {
+            try {
+                mPackageInstaller.abandonSession(mStagedSessionId);
+            } catch (SecurityException ignored) {
+            }
+            mStagedSessionId = 0;
+        }
+    }
+
+    private PackageInstaller.SessionParams createSessionParams(@NonNull Intent intent,
+        @Nullable ParcelFileDescriptor pfd, @NonNull String debugPathName) {
+        PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
+            PackageInstaller.SessionParams.MODE_FULL_INSTALL);
+        final Uri referrerUri = intent.getParcelableExtra(Intent.EXTRA_REFERRER, Uri.class);
+        params.setPackageSource(
+            referrerUri != null ? PackageInstaller.PACKAGE_SOURCE_DOWNLOADED_FILE
+                : PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE);
+        params.setInstallAsInstantApp(false);
+        params.setReferrerUri(referrerUri);
+        params.setOriginatingUri(
+            intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI, Uri.class));
+        params.setOriginatingUid(intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID,
+            Process.INVALID_UID));
+        params.setInstallerPackageName(intent.getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME));
+        params.setInstallReason(PackageManager.INSTALL_REASON_USER);
+        // Disable full screen intent usage by for sideloads.
+        params.setPermissionState(Manifest.permission.USE_FULL_SCREEN_INTENT,
+            PackageInstaller.SessionParams.PERMISSION_STATE_DENIED);
+
+        if (pfd != null) {
+            try {
+                final PackageInstaller.InstallInfo result = mPackageInstaller.readInstallInfo(pfd,
+                    debugPathName, 0);
+                params.setAppPackageName(result.getPackageName());
+                params.setInstallLocation(result.getInstallLocation());
+                params.setSize(result.calculateInstalledSize(params, pfd));
+            } catch (PackageInstaller.PackageParsingException e) {
+                Log.e(TAG, "Cannot parse package " + debugPathName + ". Assuming defaults.", e);
+                params.setSize(pfd.getStatSize());
+            } catch (IOException e) {
+                Log.e(TAG,
+                    "Cannot calculate installed size " + debugPathName
+                        + ". Try only apk size.", e);
+            }
+        } else {
+            Log.e(TAG, "Cannot parse package " + debugPathName + ". Assuming defaults.");
+        }
+        return params;
+    }
+
+    public MutableLiveData<Integer> getStagingProgress() {
+        if (mSessionStager != null) {
+            return mSessionStager.getProgress();
+        }
+        return new MutableLiveData<>(0);
+    }
+
+    public MutableLiveData<InstallStage> getStagingResult() {
+        return mStagingResult;
+    }
+
+    public interface SessionStageListener {
+
+        void onStagingSuccess(SessionInfo info);
+
+        void onStagingFailure();
+    }
+
+    public static class CallerInfo {
+
+        private final String mPackageName;
+        private final int mUid;
+
+        public CallerInfo(String packageName, int uid) {
+            mPackageName = packageName;
+            mUid = uid;
+        }
+
+        public String getPackageName() {
+            return mPackageName;
+        }
+
+        public int getUid() {
+            return mUid;
+        }
+    }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java
new file mode 100644
index 0000000..82a8c95
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.packageinstaller.v2.model;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.SessionInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Process;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import java.util.Arrays;
+
+public class PackageUtil {
+
+    private static final String TAG = InstallRepository.class.getSimpleName();
+    private static final String DOWNLOADS_AUTHORITY = "downloads";
+
+    /**
+     * Determines if the UID belongs to the system downloads provider and returns the
+     * {@link ApplicationInfo} of the provider
+     *
+     * @param uid UID of the caller
+     * @return {@link ApplicationInfo} of the provider if a downloads provider exists, it is a
+     *     system app, and its UID matches with the passed UID, null otherwise.
+     */
+    public static ApplicationInfo getSystemDownloadsProviderInfo(PackageManager pm, int uid) {
+        final ProviderInfo providerInfo = pm.resolveContentProvider(
+            DOWNLOADS_AUTHORITY, 0);
+        if (providerInfo == null) {
+            // There seems to be no currently enabled downloads provider on the system.
+            return null;
+        }
+        ApplicationInfo appInfo = providerInfo.applicationInfo;
+        if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 && uid == appInfo.uid) {
+            return appInfo;
+        }
+        return null;
+    }
+
+    /**
+     * Get the maximum target sdk for a UID.
+     *
+     * @param context The context to use
+     * @param uid The UID requesting the install/uninstall
+     * @return The maximum target SDK or -1 if the uid does not match any packages.
+     */
+    public static int getMaxTargetSdkVersionForUid(@NonNull Context context, int uid) {
+        PackageManager pm = context.getPackageManager();
+        final String[] packages = pm.getPackagesForUid(uid);
+        int targetSdkVersion = -1;
+        if (packages != null) {
+            for (String packageName : packages) {
+                try {
+                    ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
+                    targetSdkVersion = Math.max(targetSdkVersion, info.targetSdkVersion);
+                } catch (PackageManager.NameNotFoundException e) {
+                    // Ignore and try the next package
+                }
+            }
+        }
+        return targetSdkVersion;
+    }
+
+    public static boolean canPackageQuery(Context context, int callingUid, Uri packageUri) {
+        PackageManager pm = context.getPackageManager();
+        ProviderInfo info = pm.resolveContentProvider(packageUri.getAuthority(),
+            PackageManager.ComponentInfoFlags.of(0));
+        if (info == null) {
+            return false;
+        }
+        String targetPackage = info.packageName;
+
+        String[] callingPackages = pm.getPackagesForUid(callingUid);
+        if (callingPackages == null) {
+            return false;
+        }
+        for (String callingPackage : callingPackages) {
+            try {
+                if (pm.canPackageQuery(callingPackage, targetPackage)) {
+                    return true;
+                }
+            } catch (PackageManager.NameNotFoundException e) {
+                // no-op
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @param context the {@link Context} object
+     * @param permission the permission name to check
+     * @param callingUid the UID of the caller who's permission is being checked
+     * @return {@code true} if the callingUid is granted the said permission
+     */
+    public static boolean isPermissionGranted(Context context, String permission, int callingUid) {
+        return context.checkPermission(permission, -1, callingUid)
+            == PackageManager.PERMISSION_GRANTED;
+    }
+
+    /**
+     * @param pm the {@link PackageManager} object
+     * @param permission the permission name to check
+     * @param packageName the name of the package who's permission is being checked
+     * @return {@code true} if the package is granted the said permission
+     */
+    public static boolean isPermissionGranted(PackageManager pm, String permission,
+        String packageName) {
+        return pm.checkPermission(permission, packageName) == PackageManager.PERMISSION_GRANTED;
+    }
+
+    /**
+     * @param context the {@link Context} object
+     * @param callingUid the UID of the caller who's permission is being checked
+     * @param originatingUid the UID from where install is being originated. This could be same as
+     * callingUid or it will be the UID of the package performing a session based install
+     * @param isTrustedSource whether install request is coming from a privileged app or an app that
+     * has {@link Manifest.permission.INSTALL_PACKAGES} permission granted
+     * @return {@code true} if the package is granted the said permission
+     */
+    public static boolean isInstallPermissionGrantedOrRequested(Context context, int callingUid,
+        int originatingUid, boolean isTrustedSource) {
+        boolean isDocumentsManager =
+            isPermissionGranted(context, Manifest.permission.MANAGE_DOCUMENTS, callingUid);
+        boolean isSystemDownloadsProvider =
+            getSystemDownloadsProviderInfo(context.getPackageManager(), callingUid) != null;
+
+        if (!isTrustedSource && !isSystemDownloadsProvider && !isDocumentsManager) {
+
+            final int targetSdkVersion = getMaxTargetSdkVersionForUid(context, originatingUid);
+            if (targetSdkVersion < 0) {
+                // Invalid originating uid supplied. Abort install.
+                Log.w(TAG, "Cannot get target sdk version for uid " + originatingUid);
+                return false;
+            } else if (targetSdkVersion >= Build.VERSION_CODES.O
+                && !isUidRequestingPermission(context.getPackageManager(), originatingUid,
+                Manifest.permission.REQUEST_INSTALL_PACKAGES)) {
+                Log.e(TAG, "Requesting uid " + originatingUid + " needs to declare permission "
+                    + Manifest.permission.REQUEST_INSTALL_PACKAGES);
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * @param pm the {@link PackageManager} object
+     * @param uid the UID of the caller who's permission is being checked
+     * @param permission the permission name to check
+     * @return {@code true} if the caller is requesting the said permission in its Manifest
+     */
+    public static boolean isUidRequestingPermission(PackageManager pm, int uid, String permission) {
+        final String[] packageNames = pm.getPackagesForUid(uid);
+        if (packageNames == null) {
+            return false;
+        }
+        for (final String packageName : packageNames) {
+            final PackageInfo packageInfo;
+            try {
+                packageInfo = pm.getPackageInfo(packageName,
+                    PackageManager.GET_PERMISSIONS);
+            } catch (PackageManager.NameNotFoundException e) {
+                // Ignore and try the next package
+                continue;
+            }
+            if (packageInfo.requestedPermissions != null
+                && Arrays.asList(packageInfo.requestedPermissions).contains(permission)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @param pi the {@link PackageInstaller} object to use
+     * @param originatingUid the UID of the package performing a session based install
+     * @param sessionId ID of the install session
+     * @return {@code true} if the caller is the session owner
+     */
+    public static boolean isCallerSessionOwner(PackageInstaller pi, int originatingUid,
+        int sessionId) {
+        if (sessionId == SessionInfo.INVALID_ID) {
+            return false;
+        }
+        if (originatingUid == Process.ROOT_UID) {
+            return true;
+        }
+        PackageInstaller.SessionInfo sessionInfo = pi.getSessionInfo(sessionId);
+        if (sessionInfo == null) {
+            return false;
+        }
+        int installerUid = sessionInfo.getInstallerUid();
+        return originatingUid == installerUid;
+    }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/SessionStager.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/SessionStager.java
new file mode 100644
index 0000000..a2c81f1
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/SessionStager.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.packageinstaller.v2.model;
+
+import static android.content.res.AssetFileDescriptor.UNKNOWN_LENGTH;
+
+import android.content.Context;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.SessionInfo;
+import android.content.res.AssetFileDescriptor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.util.Log;
+import androidx.lifecycle.MutableLiveData;
+import com.android.packageinstaller.v2.model.InstallRepository.SessionStageListener;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class SessionStager extends AsyncTask<Void, Integer, SessionInfo> {
+
+    private static final String TAG = SessionStager.class.getSimpleName();
+    private final Context mContext;
+    private final Uri mUri;
+    private final int mStagedSessionId;
+    private final MutableLiveData<Integer> mProgressLiveData = new MutableLiveData<>(0);
+    private final SessionStageListener mListener;
+
+    SessionStager(Context context, Uri uri, int stagedSessionId, SessionStageListener listener) {
+        mContext = context;
+        mUri = uri;
+        mStagedSessionId = stagedSessionId;
+        mListener = listener;
+    }
+
+    @Override
+    protected PackageInstaller.SessionInfo doInBackground(Void... params) {
+        PackageInstaller pi = mContext.getPackageManager().getPackageInstaller();
+        try (PackageInstaller.Session session = pi.openSession(mStagedSessionId);
+            InputStream in = mContext.getContentResolver().openInputStream(mUri)) {
+            session.setStagingProgress(0);
+
+            if (in == null) {
+                return null;
+            }
+            final long sizeBytes = getContentSizeBytes();
+            mProgressLiveData.postValue(sizeBytes > 0 ? 0 : -1);
+
+            long totalRead = 0;
+            try (OutputStream out = session.openWrite("PackageInstaller", 0, sizeBytes)) {
+                byte[] buffer = new byte[1024 * 1024];
+                while (true) {
+                    int numRead = in.read(buffer);
+
+                    if (numRead == -1) {
+                        session.fsync(out);
+                        break;
+                    }
+
+                    if (isCancelled()) {
+                        break;
+                    }
+
+                    out.write(buffer, 0, numRead);
+                    if (sizeBytes > 0) {
+                        totalRead += numRead;
+                        float fraction = ((float) totalRead / (float) sizeBytes);
+                        session.setStagingProgress(fraction);
+                        publishProgress((int) (fraction * 100.0));
+                    }
+                }
+            }
+            return pi.getSessionInfo(mStagedSessionId);
+        } catch (IOException | SecurityException | IllegalStateException
+                 | IllegalArgumentException e) {
+            Log.w(TAG, "Error staging apk from content URI", e);
+            return null;
+        }
+    }
+
+    private long getContentSizeBytes() {
+        try (AssetFileDescriptor afd = mContext.getContentResolver()
+            .openAssetFileDescriptor(mUri, "r")) {
+            return afd != null ? afd.getLength() : UNKNOWN_LENGTH;
+        } catch (IOException e) {
+            Log.w(TAG, "Failed to open asset file descriptor", e);
+            return UNKNOWN_LENGTH;
+        }
+    }
+
+    public MutableLiveData<Integer> getProgress() {
+        return mProgressLiveData;
+    }
+
+    @Override
+    protected void onProgressUpdate(Integer... progress) {
+        if (progress != null && progress.length > 0) {
+            mProgressLiveData.setValue(progress[0]);
+        }
+    }
+
+    @Override
+    protected void onPostExecute(SessionInfo sessionInfo) {
+        if (sessionInfo == null || !sessionInfo.isActive()
+            || sessionInfo.getResolvedBaseApkPath() == null) {
+            Log.w(TAG, "Session info is invalid: " + sessionInfo);
+            mListener.onStagingFailure();
+            return;
+        }
+        mListener.onStagingSuccess(sessionInfo);
+    }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallAborted.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallAborted.java
new file mode 100644
index 0000000..cc9857d
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallAborted.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.packageinstaller.v2.model.installstagedata;
+
+
+import android.app.Activity;
+import android.content.Intent;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class InstallAborted extends InstallStage {
+
+    public static final int ABORT_REASON_INTERNAL_ERROR = 0;
+    public static final int ABORT_REASON_POLICY = 1;
+    private final int mStage = InstallStage.STAGE_ABORTED;
+    private final int mAbortReason;
+
+    /**
+     * It will hold the restriction name, when the restriction was enforced by the system, and not
+     * a device admin.
+     */
+    @NonNull
+    private final String mMessage;
+    /**
+     * <p>If abort reason is ABORT_REASON_POLICY, then this will hold the Intent
+     * to display a support dialog when a feature was disabled by an admin. It will be
+     * {@code null} if the feature is disabled by the system. In this case, the restriction name
+     * will be set in {@link #mMessage} </p>
+     *
+     * <p>If the abort reason is ABORT_REASON_INTERNAL_ERROR, it <b>may</b> hold an
+     * intent to be sent as a result to the calling activity.</p>
+     */
+    @Nullable
+    private final Intent mIntent;
+    private final int mActivityResultCode;
+
+    private InstallAborted(int reason, @NonNull String message, @Nullable Intent intent,
+        int activityResultCode) {
+        mAbortReason = reason;
+        mMessage = message;
+        mIntent = intent;
+        mActivityResultCode = activityResultCode;
+    }
+
+    public int getAbortReason() {
+        return mAbortReason;
+    }
+
+    @NonNull
+    public String getMessage() {
+        return mMessage;
+    }
+
+    @Nullable
+    public Intent getResultIntent() {
+        return mIntent;
+    }
+
+    public int getActivityResultCode() {
+        return mActivityResultCode;
+    }
+
+    @Override
+    public int getStageCode() {
+        return mStage;
+    }
+
+    public static class Builder {
+
+        private final int mAbortReason;
+        private String mMessage = "";
+        private Intent mIntent = null;
+        private int mActivityResultCode = Activity.RESULT_CANCELED;
+
+        public Builder(int reason) {
+            mAbortReason = reason;
+        }
+
+        public Builder setMessage(@NonNull String message) {
+            mMessage = message;
+            return this;
+        }
+
+        public Builder setResultIntent(@NonNull Intent intent) {
+            mIntent = intent;
+            return this;
+        }
+
+        public Builder setActivityResultCode(int resultCode) {
+            mActivityResultCode = resultCode;
+            return this;
+        }
+
+        public InstallAborted build() {
+            return new InstallAborted(mAbortReason, mMessage, mIntent, mActivityResultCode);
+        }
+    }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallReady.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallReady.java
new file mode 100644
index 0000000..548f2c5
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallReady.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.packageinstaller.v2.model.installstagedata;
+
+public class InstallReady extends InstallStage{
+
+    private final int mStage = InstallStage.STAGE_READY;
+
+    @Override
+    public int getStageCode() {
+        return mStage;
+    }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStage.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStage.java
new file mode 100644
index 0000000..f91e64b
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStage.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.packageinstaller.v2.model.installstagedata;
+
+public abstract class InstallStage {
+
+    public static final int STAGE_DEFAULT = -1;
+    public static final int STAGE_ABORTED = 0;
+    public static final int STAGE_STAGING = 1;
+    public static final int STAGE_READY = 2;
+    public static final int STAGE_USER_ACTION_REQUIRED = 3;
+    public static final int STAGE_INSTALLING = 4;
+    public static final int STAGE_SUCCESS = 5;
+    public static final int STAGE_FAILED = 6;
+
+    /**
+     * @return the integer value representing current install stage.
+     */
+    public abstract int getStageCode();
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStaging.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStaging.java
new file mode 100644
index 0000000..a979cf8
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStaging.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.packageinstaller.v2.model.installstagedata;
+
+public class InstallStaging extends InstallStage {
+
+    private final int mStage = InstallStage.STAGE_STAGING;
+
+    @Override
+    public int getStageCode() {
+        return mStage;
+    }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.java
new file mode 100644
index 0000000..ba5a0cd
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.packageinstaller.v2.ui;
+
+import static android.os.Process.INVALID_UID;
+import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.ABORT_REASON_INTERNAL_ERROR;
+import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.ABORT_REASON_POLICY;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.UserManager;
+import android.util.Log;
+import android.view.Window;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.ViewModelProvider;
+import com.android.packageinstaller.R;
+import com.android.packageinstaller.v2.model.InstallRepository;
+import com.android.packageinstaller.v2.model.InstallRepository.CallerInfo;
+import com.android.packageinstaller.v2.model.installstagedata.InstallAborted;
+import com.android.packageinstaller.v2.model.installstagedata.InstallStage;
+import com.android.packageinstaller.v2.ui.fragments.InstallStagingFragment;
+import com.android.packageinstaller.v2.ui.fragments.SimpleErrorFragment;
+import com.android.packageinstaller.v2.viewmodel.InstallViewModel;
+import com.android.packageinstaller.v2.viewmodel.InstallViewModelFactory;
+
+public class InstallLaunch extends FragmentActivity {
+
+    public static final String EXTRA_CALLING_PKG_UID =
+            InstallLaunch.class.getPackageName() + ".callingPkgUid";
+    public static final String EXTRA_CALLING_PKG_NAME =
+            InstallLaunch.class.getPackageName() + ".callingPkgName";
+    private static final String TAG = InstallLaunch.class.getSimpleName();
+    private static final String TAG_DIALOG = "dialog";
+    private final boolean mLocalLOGV = false;
+    private InstallViewModel mInstallViewModel;
+    private InstallRepository mInstallRepository;
+
+    private FragmentManager mFragmentManager;
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        this.requestWindowFeature(Window.FEATURE_NO_TITLE);
+
+        mFragmentManager = getSupportFragmentManager();
+        mInstallRepository = new InstallRepository(getApplicationContext());
+        mInstallViewModel = new ViewModelProvider(this,
+                new InstallViewModelFactory(this.getApplication(), mInstallRepository)).get(
+                InstallViewModel.class);
+
+        Intent intent = getIntent();
+        CallerInfo info = new CallerInfo(
+                intent.getStringExtra(EXTRA_CALLING_PKG_NAME),
+                intent.getIntExtra(EXTRA_CALLING_PKG_UID, INVALID_UID));
+        mInstallViewModel.preprocessIntent(intent, info);
+
+        mInstallViewModel.getCurrentInstallStage().observe(this, this::onInstallStageChange);
+    }
+
+    /**
+     * Main controller of the UI. This method shows relevant dialogs based on the install stage
+     */
+    private void onInstallStageChange(InstallStage installStage) {
+        if (installStage.getStageCode() == InstallStage.STAGE_STAGING) {
+            InstallStagingFragment stagingDialog = new InstallStagingFragment();
+            showDialogInner(stagingDialog);
+            mInstallViewModel.getStagingProgress().observe(this, stagingDialog::setProgress);
+        } else if (installStage.getStageCode() == InstallStage.STAGE_ABORTED) {
+            InstallAborted aborted = (InstallAborted) installStage;
+            switch (aborted.getAbortReason()) {
+                // TODO: check if any dialog is to be shown for ABORT_REASON_INTERNAL_ERROR
+                case ABORT_REASON_INTERNAL_ERROR -> setResult(RESULT_CANCELED, true);
+                case ABORT_REASON_POLICY -> showPolicyRestrictionDialog(aborted);
+                default -> setResult(RESULT_CANCELED, true);
+            }
+        } else {
+            Log.d(TAG, "Unimplemented stage: " + installStage.getStageCode());
+            showDialogInner(null);
+        }
+    }
+
+    private void showPolicyRestrictionDialog(InstallAborted aborted) {
+        String restriction = aborted.getMessage();
+        Intent adminSupportIntent = aborted.getResultIntent();
+        boolean shouldFinish;
+
+        // If the given restriction is set by an admin, display information about the
+        // admin enforcing the restriction for the affected user. If not enforced by the admin,
+        // show the system dialog.
+        if (adminSupportIntent != null) {
+            if (mLocalLOGV) {
+                Log.i(TAG, "Restriction set by admin, starting " + adminSupportIntent);
+            }
+            startActivity(adminSupportIntent);
+            // Finish the package installer app since the next dialog will not be shown by this app
+            shouldFinish = true;
+        } else {
+            if (mLocalLOGV) {
+                Log.i(TAG, "Restriction set by system: " + restriction);
+            }
+            DialogFragment blockedByPolicyDialog = createDevicePolicyRestrictionDialog(restriction);
+            // Don't finish the package installer app since the next dialog
+            // will be shown by this app
+            shouldFinish = false;
+            showDialogInner(blockedByPolicyDialog);
+        }
+        setResult(RESULT_CANCELED, shouldFinish);
+    }
+
+    /**
+     * Create a new dialog based on the install restriction enforced.
+     *
+     * @param restriction The restriction to create the dialog for
+     * @return The dialog
+     */
+    private DialogFragment createDevicePolicyRestrictionDialog(String restriction) {
+        if (mLocalLOGV) {
+            Log.i(TAG, "createDialog(" + restriction + ")");
+        }
+        return switch (restriction) {
+            case UserManager.DISALLOW_INSTALL_APPS ->
+                new SimpleErrorFragment(R.string.install_apps_user_restriction_dlg_text);
+            case UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES,
+                UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY ->
+                new SimpleErrorFragment(R.string.unknown_apps_user_restriction_dlg_text);
+            default -> null;
+        };
+    }
+
+    /**
+     * Replace any visible dialog by the dialog returned by InstallRepository
+     *
+     * @param newDialog The new dialog to display
+     */
+    private void showDialogInner(@Nullable DialogFragment newDialog) {
+        DialogFragment currentDialog = (DialogFragment) mFragmentManager.findFragmentByTag(
+            TAG_DIALOG);
+        if (currentDialog != null) {
+            currentDialog.dismissAllowingStateLoss();
+        }
+        if (newDialog != null) {
+            newDialog.show(mFragmentManager, TAG_DIALOG);
+        }
+    }
+
+    public void setResult(int resultCode, boolean shouldFinish) {
+        // TODO: This is incomplete. We need to send RESULT_FIRST_USER, RESULT_OK etc
+        //  for relevant use cases. Investigate when to send what result.
+        super.setResult(resultCode);
+        if (shouldFinish) {
+            finish();
+        }
+    }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallStagingFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallStagingFragment.java
new file mode 100644
index 0000000..feb2428
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallStagingFragment.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.packageinstaller.v2.ui.fragments;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.ProgressBar;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import com.android.packageinstaller.R;
+
+public class InstallStagingFragment extends DialogFragment {
+
+    private static final String TAG = InstallStagingFragment.class.getSimpleName();
+    private ProgressBar mProgressBar;
+    private AlertDialog mDialog;
+
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+        View dialogView = getLayoutInflater().inflate(R.layout.install_content_view, null);
+        dialogView.requireViewById(R.id.staging).setVisibility(View.VISIBLE);
+
+        mDialog = new AlertDialog.Builder(requireContext())
+            .setTitle(getString(R.string.app_name_unknown))
+            .setIcon(R.drawable.ic_file_download)
+            .setView(dialogView)
+            .setNegativeButton(R.string.cancel, null)
+            .setCancelable(false)
+            .create();
+
+        mDialog.setCanceledOnTouchOutside(false);
+        return mDialog;
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        mDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setEnabled(false);
+        mProgressBar = mDialog.requireViewById(R.id.progress_indeterminate);
+        mProgressBar.setProgress(0);
+        mProgressBar.setMax(100);
+        mProgressBar.setIndeterminate(false);
+    }
+
+    public void setProgress(int progress) {
+        if (mProgressBar != null) {
+            mProgressBar.setProgress(progress);
+        }
+    }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/SimpleErrorFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/SimpleErrorFragment.java
new file mode 100644
index 0000000..dce0b9a
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/SimpleErrorFragment.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.packageinstaller.v2.ui.fragments;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.fragment.app.DialogFragment;
+import com.android.packageinstaller.R;
+
+public class SimpleErrorFragment extends DialogFragment {
+
+    private static final String TAG = SimpleErrorFragment.class.getSimpleName();
+    private final int mMessageResId;
+
+    public SimpleErrorFragment(int messageResId) {
+        mMessageResId = messageResId;
+    }
+
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        return new AlertDialog.Builder(getActivity())
+            .setMessage(mMessageResId)
+            .setPositiveButton(R.string.ok, (dialog, which) -> getActivity().finish())
+            .create();
+    }
+
+    @Override
+    public void onCancel(DialogInterface dialog) {
+        getActivity().setResult(Activity.RESULT_CANCELED);
+        getActivity().finish();
+    }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.java
new file mode 100644
index 0000000..42b3023
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.packageinstaller.v2.viewmodel;
+
+import android.app.Application;
+import android.content.Intent;
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.MediatorLiveData;
+import androidx.lifecycle.MutableLiveData;
+import com.android.packageinstaller.v2.model.InstallRepository;
+import com.android.packageinstaller.v2.model.InstallRepository.CallerInfo;
+import com.android.packageinstaller.v2.model.installstagedata.InstallStage;
+import com.android.packageinstaller.v2.model.installstagedata.InstallStaging;
+
+
+public class InstallViewModel extends AndroidViewModel {
+
+    private static final String TAG = InstallViewModel.class.getSimpleName();
+    private final InstallRepository mRepository;
+    private final MediatorLiveData<InstallStage> mCurrentInstallStage = new MediatorLiveData<>(
+            new InstallStaging());
+
+    public InstallViewModel(@NonNull Application application, InstallRepository repository) {
+        super(application);
+        mRepository = repository;
+    }
+
+    public MutableLiveData<InstallStage> getCurrentInstallStage() {
+        return mCurrentInstallStage;
+    }
+
+    public void preprocessIntent(Intent intent, CallerInfo callerInfo) {
+        InstallStage stage = mRepository.performPreInstallChecks(intent, callerInfo);
+        if (stage.getStageCode() == InstallStage.STAGE_ABORTED) {
+            mCurrentInstallStage.setValue(stage);
+        } else {
+            // Since staging is an async operation, we will get the staging result later in time.
+            // Result of the file staging will be set in InstallRepository#mStagingResult.
+            // As such, mCurrentInstallStage will need to add another MutableLiveData
+            // as a data source
+            mRepository.stageForInstall();
+            mCurrentInstallStage.addSource(mRepository.getStagingResult(), installStage -> {
+                if (installStage.getStageCode() != InstallStage.STAGE_READY) {
+                    mCurrentInstallStage.setValue(installStage);
+                } else {
+                    // Proceed with user confirmation here.
+                }
+            });
+        }
+    }
+
+    public MutableLiveData<Integer> getStagingProgress() {
+        return mRepository.getStagingProgress();
+    }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModelFactory.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModelFactory.java
new file mode 100644
index 0000000..ef459e6
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModelFactory.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.packageinstaller.v2.viewmodel;
+
+import android.app.Application;
+import androidx.annotation.NonNull;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+import com.android.packageinstaller.v2.model.InstallRepository;
+
+public class InstallViewModelFactory extends ViewModelProvider.AndroidViewModelFactory {
+
+    private final InstallRepository mRepository;
+    private final Application mApplication;
+
+    public InstallViewModelFactory(Application application, InstallRepository repository) {
+        // Calling super class' ctor ensures that create method is called correctly and the right
+        // ctor of InstallViewModel is used. If we fail to do that, the default ctor:
+        // InstallViewModel(application) is used, and repository isn't initialized in the viewmodel
+        super(application);
+        mApplication = application;
+        mRepository = repository;
+    }
+
+    @NonNull
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
+        return (T) new InstallViewModel(mApplication, mRepository);
+    }
+}
diff --git a/packages/SettingsLib/Spa/gradle/libs.versions.toml b/packages/SettingsLib/Spa/gradle/libs.versions.toml
index fdb0471..905640f 100644
--- a/packages/SettingsLib/Spa/gradle/libs.versions.toml
+++ b/packages/SettingsLib/Spa/gradle/libs.versions.toml
@@ -15,7 +15,7 @@
 #
 
 [versions]
-agp = "8.1.2"
+agp = "8.1.3"
 compose-compiler = "1.5.1"
 dexmaker-mockito = "2.28.3"
 jvm = "17"
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt
index 47660bc..5a1120e 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt
@@ -20,6 +20,7 @@
 import androidx.compose.ui.unit.dp
 
 object SettingsDimension {
+    val paddingTiny = 2.dp
     val paddingSmall = 4.dp
 
     val itemIconSize = 24.dp
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt
index f4b2843..b4a6a0d 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt
@@ -39,6 +39,7 @@
 fun SettingsTitle(title: String, useMediumWeight: Boolean = false) {
     Text(
         text = title,
+        modifier = Modifier.padding(vertical = SettingsDimension.paddingTiny),
         color = MaterialTheme.colorScheme.onSurface,
         style = MaterialTheme.typography.titleMedium.withWeight(useMediumWeight),
     )
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index fa8c1fb..0ffcc45 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -594,6 +594,16 @@
                 || cachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO);
     }
 
+    /**
+     * Check if the Bluetooth device is an active LE Audio device
+     *
+     * @param cachedDevice the CachedBluetoothDevice
+     * @return if the Bluetooth device is an active LE Audio device
+     */
+    public static boolean isActiveLeAudioDevice(CachedBluetoothDevice cachedDevice) {
+        return cachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO);
+    }
+
     private static boolean isDeviceConnected(CachedBluetoothDevice cachedDevice) {
         if (cachedDevice == null) {
             return false;
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 80fd516..f1ffa66 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -246,11 +246,9 @@
     srcs: [
         /* Status bar fakes */
         "tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt",
-        "tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt",
-        "tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt",
-        "tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt",
         "tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt",
         "tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt",
+        "tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeSubscriptionManagerProxy.kt",
 
         /* QS fakes */
         "tests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt",
@@ -263,6 +261,7 @@
     srcs: [
         /* Keyguard converted tests */
         // data
+        "tests/src/com/android/systemui/bouncer/data/repository/SimBouncerRepositoryTest.kt",
         "tests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt",
         "tests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt",
         "tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt",
@@ -285,6 +284,7 @@
         "tests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt",
         "tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt",
         "tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt",
+        "tests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt",
         "tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt",
         "tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt",
         "tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt",
@@ -294,6 +294,8 @@
         "tests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt",
         "tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt",
         "tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt",
+        "tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt",
+        "tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt",
         "tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt",
         "tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt",
         // Keyguard helper
@@ -616,6 +618,9 @@
 
     instrumentation_for: "SystemUIRobo-stub",
     java_resource_dirs: ["tests/robolectric/config"],
+    plugins: [
+        "dagger2-compiler",
+    ],
 }
 
 // Opt-out config for optimizing the SystemUI target using R8.
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 5881631..e218308 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -982,6 +982,24 @@
             android:excludeFromRecents="true"
             android:visibleToInstantApps="true"/>
 
+        <activity android:name="com.android.systemui.communal.widgets.WidgetPickerActivity"
+            android:theme="@style/Theme.EditWidgetsActivity"
+            android:excludeFromRecents="true"
+            android:autoRemoveFromRecents="true"
+            android:showOnLockScreen="true"
+            android:launchMode="singleTop"
+            android:exported="false">
+        </activity>
+
+        <activity android:name="com.android.systemui.communal.widgets.EditWidgetsActivity"
+            android:theme="@style/Theme.EditWidgetsActivity"
+            android:excludeFromRecents="true"
+            android:autoRemoveFromRecents="true"
+            android:showOnLockScreen="true"
+            android:launchMode="singleTop"
+            android:exported="false">
+        </activity>
+
         <!-- Doze with notifications, run in main sysui process for every user  -->
         <service
             android:name=".doze.DozeService"
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
index 53e437a..0b13383 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
@@ -28,6 +28,7 @@
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.TextField
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
@@ -63,11 +64,13 @@
     val isImeVisible by rememberUpdatedState(WindowInsets.imeAnimationTarget.getBottom(density) > 0)
     LaunchedEffect(isImeVisible) { viewModel.onImeVisibilityChanged(isImeVisible) }
 
-    LaunchedEffect(Unit) {
+    DisposableEffect(Unit) {
+        viewModel.onShown()
+
         // When the UI comes up, request focus on the TextField to bring up the software keyboard.
         focusRequester.requestFocus()
-        // Also, report that the UI is shown to let the view-model run some logic.
-        viewModel.onShown()
+
+        onDispose { viewModel.onHidden() }
     }
 
     LaunchedEffect(animateFailure) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
index 03efbe0..2bbe9b8 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
@@ -26,6 +26,7 @@
 import androidx.compose.foundation.gestures.detectDragGestures
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
@@ -65,8 +66,10 @@
     viewModel: PatternBouncerViewModel,
     modifier: Modifier = Modifier,
 ) {
-    // Report that the UI is shown to let the view-model run some logic.
-    LaunchedEffect(Unit) { viewModel.onShown() }
+    DisposableEffect(Unit) {
+        viewModel.onShown()
+        onDispose { viewModel.onHidden() }
+    }
 
     val colCount = viewModel.columnCount
     val rowCount = viewModel.rowCount
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
index 243751fa..59617c9 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
@@ -31,6 +31,7 @@
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
@@ -69,8 +70,10 @@
     viewModel: PinBouncerViewModel,
     modifier: Modifier = Modifier,
 ) {
-    // Report that the UI is shown to let the view-model run some logic.
-    LaunchedEffect(Unit) { viewModel.onShown() }
+    DisposableEffect(Unit) {
+        viewModel.onShown()
+        onDispose { viewModel.onHidden() }
+    }
 
     val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
     val backspaceButtonAppearance by viewModel.backspaceButtonAppearance.collectAsState()
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt
index 814ea31..1a97912 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt
@@ -18,6 +18,11 @@
 
 package com.android.systemui.bouncer.ui.composable
 
+import android.app.AlertDialog
+import android.app.Dialog
+import android.view.Gravity
+import android.view.WindowManager
+import android.widget.TextView
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.VectorConverter
 import androidx.compose.animation.core.tween
@@ -26,11 +31,16 @@
 import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
 import androidx.compose.animation.graphics.vector.AnimatedImageVector
 import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
@@ -41,14 +51,21 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.runtime.toMutableStateList
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.ColorFilter
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.colorResource
 import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import com.android.compose.PlatformOutlinedButton
 import com.android.compose.animation.Easings
 import com.android.keyguard.PinShapeAdapter
 import com.android.systemui.bouncer.ui.viewmodel.EntryToken.Digit
@@ -189,6 +206,10 @@
     shapeAnimations: ShapeAnimations,
     modifier: Modifier = Modifier,
 ) {
+    if (viewModel.isSimAreaVisible) {
+        SimArea(viewModel = viewModel)
+    }
+
     // Holds all currently [VisiblePinEntry] composables. This cannot be simply derived from
     // `viewModel.pinInput` at composition, since deleting a pin entry needs to play a remove
     // animation, thus the composable to be removed has to remain in the composition until fully
@@ -234,6 +255,94 @@
     pinInputRow.Content(modifier)
 }
 
+@Composable
+private fun SimArea(viewModel: PinBouncerViewModel) {
+    val isLockedEsim by viewModel.isLockedEsim.collectAsState()
+    val isSimUnlockingDialogVisible by viewModel.isSimUnlockingDialogVisible.collectAsState()
+    val errorDialogMessage by viewModel.errorDialogMessage.collectAsState()
+    var unlockDialog: Dialog? by remember { mutableStateOf(null) }
+    var errorDialog: Dialog? by remember { mutableStateOf(null) }
+    val context = LocalView.current.context
+
+    DisposableEffect(isSimUnlockingDialogVisible) {
+        if (isSimUnlockingDialogVisible) {
+            val builder =
+                AlertDialog.Builder(context).apply {
+                    setMessage(context.getString(R.string.kg_sim_unlock_progress_dialog_message))
+                    setCancelable(false)
+                }
+            unlockDialog =
+                builder.create().apply {
+                    window?.setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG)
+                    show()
+                    findViewById<TextView>(android.R.id.message)?.gravity = Gravity.CENTER
+                }
+        } else {
+            unlockDialog?.hide()
+            unlockDialog = null
+        }
+
+        onDispose {
+            unlockDialog?.hide()
+            unlockDialog = null
+        }
+    }
+
+    DisposableEffect(errorDialogMessage) {
+        if (errorDialogMessage != null) {
+            val builder = AlertDialog.Builder(context)
+            builder.setMessage(errorDialogMessage)
+            builder.setCancelable(false)
+            builder.setNeutralButton(R.string.ok, null)
+            errorDialog =
+                builder.create().apply {
+                    window?.setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG)
+                    setOnDismissListener { viewModel.onErrorDialogDismissed() }
+                    show()
+                }
+        } else {
+            errorDialog?.hide()
+            errorDialog = null
+        }
+
+        onDispose {
+            errorDialog?.hide()
+            errorDialog = null
+        }
+    }
+
+    Box(modifier = Modifier.padding(bottom = 20.dp)) {
+        // If isLockedEsim is null, then we do not show anything.
+        if (isLockedEsim == true) {
+            PlatformOutlinedButton(
+                onClick = { viewModel.onDisableEsimButtonClicked() },
+            ) {
+                Row(
+                    horizontalArrangement = Arrangement.spacedBy(10.dp),
+                    verticalAlignment = Alignment.CenterVertically
+                ) {
+                    Image(
+                        painter = painterResource(id = R.drawable.ic_no_sim),
+                        contentDescription = null,
+                        colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface)
+                    )
+                    Text(
+                        text = stringResource(R.string.disable_carrier_button_text),
+                        style = MaterialTheme.typography.bodyMedium,
+                        color = MaterialTheme.colorScheme.onSurface,
+                    )
+                }
+            }
+        } else if (isLockedEsim == false) {
+            Image(
+                painter = painterResource(id = R.drawable.ic_lockscreen_sim),
+                contentDescription = null,
+                colorFilter = ColorFilter.tint(colorResource(id = R.color.background_protected))
+            )
+        }
+    }
+}
+
 private class PinInputRow(
     val shapeAnimations: ShapeAnimations,
 ) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 6e18cb9..c80902e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -45,6 +45,7 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
@@ -89,11 +90,8 @@
                 )
             }
         }
-        IconButton(onClick = viewModel::onOpenWidgetPicker) {
-            Icon(
-                Icons.Default.Add,
-                LocalContext.current.getString(R.string.button_to_open_widget_picker)
-            )
+        IconButton(onClick = viewModel::onOpenWidgetEditor) {
+            Icon(Icons.Default.Add, stringResource(R.string.button_to_open_widget_editor))
         }
 
         // This spacer covers the edge of the LazyHorizontalGrid and prevents it from receiving
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index 0da562b..4eb9089 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -18,7 +18,6 @@
 
 package com.android.systemui.scene.ui.composable
 
-import android.os.SystemProperties
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.material3.Text
@@ -84,7 +83,6 @@
     val currentDestinations: Map<UserAction, SceneModel> by
         currentScene.destinationScenes.collectAsState()
     val state = remember { SceneTransitionLayoutState(currentSceneKey.toTransitionSceneKey()) }
-    val isRibbonEnabled = remember { SystemProperties.getBoolean("flexi.ribbon", false) }
 
     DisposableEffect(viewModel, state) {
         viewModel.setTransitionState(state.observableTransitionState().map { it.toModel() })
@@ -137,17 +135,15 @@
             }
         }
 
-        if (isRibbonEnabled) {
-            BottomRightCornerRibbon(
-                content = {
-                    Text(
-                        text = "flexi\uD83E\uDD43",
-                        color = Color.White,
-                    )
-                },
-                modifier = Modifier.align(Alignment.BottomEnd),
-            )
-        }
+        BottomRightCornerRibbon(
+            content = {
+                Text(
+                    text = "flexi\uD83E\uDD43",
+                    color = Color.White,
+                )
+            },
+            modifier = Modifier.align(Alignment.BottomEnd),
+        )
     }
 }
 
diff --git a/packages/SystemUI/compose/scene/Android.bp b/packages/SystemUI/compose/scene/Android.bp
index 050d1d5..3424085 100644
--- a/packages/SystemUI/compose/scene/Android.bp
+++ b/packages/SystemUI/compose/scene/Android.bp
@@ -21,12 +21,19 @@
     default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
 }
 
+filegroup {
+    name: "PlatformComposeSceneTransitionLayout-srcs",
+    srcs: [
+        "src/**/*.kt",
+    ],
+}
+
 android_library {
     name: "PlatformComposeSceneTransitionLayout",
     manifest: "AndroidManifest.xml",
 
     srcs: [
-        "src/**/*.kt",
+        ":PlatformComposeSceneTransitionLayout-srcs",
     ],
 
     static_libs: [
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index 6153e19..3b999e30 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -17,18 +17,14 @@
 package com.android.compose.animation.scene
 
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.movableContentOf
 import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.runtime.snapshots.SnapshotStateMap
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
 import androidx.compose.ui.draw.drawWithContent
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.isSpecified
@@ -39,6 +35,7 @@
 import androidx.compose.ui.layout.Measurable
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.layout.intermediateLayout
+import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.IntSize
@@ -46,6 +43,7 @@
 import com.android.compose.animation.scene.transformation.PropertyTransformation
 import com.android.compose.animation.scene.transformation.SharedElementTransformation
 import com.android.compose.ui.util.lerp
+import kotlinx.coroutines.launch
 
 /** An element on screen, that can be composed in one or more scenes. */
 internal class Element(val key: ElementKey) {
@@ -92,13 +90,20 @@
     }
 
     /** The target values of this element in a given scene. */
-    class TargetValues {
+    class TargetValues(val scene: SceneKey) {
         val lastValues = Values()
 
         var targetSize by mutableStateOf(SizeUnspecified)
         var targetOffset by mutableStateOf(Offset.Unspecified)
 
         val sharedValues = SnapshotStateMap<ValueKey, SharedValue<*>>()
+
+        /**
+         * The attached [ElementNode] a Modifier.element() for a given element and scene. During
+         * composition, this set could have 0 to 2 elements. After composition and after all
+         * modifier nodes have been attached/detached, this set should contain exactly 1 element.
+         */
+        val nodes = mutableSetOf<ElementNode>()
     }
 
     /** A shared value of this element. */
@@ -125,50 +130,31 @@
     layoutImpl: SceneTransitionLayoutImpl,
     scene: Scene,
     key: ElementKey,
-): Modifier = composed {
-    val sceneValues = remember(scene, key) { Element.TargetValues() }
-    val element =
-        // Get the element associated to [key] if it was already composed in another scene,
-        // otherwise create it and add it to our Map<ElementKey, Element>. This is done inside a
-        // withoutReadObservation() because there is no need to recompose when that map is mutated.
-        Snapshot.withoutReadObservation {
-            val element =
-                layoutImpl.elements[key] ?: Element(key).also { layoutImpl.elements[key] = it }
-            val previousValues = element.sceneValues[scene.key]
-            if (previousValues == null) {
-                element.sceneValues[scene.key] = sceneValues
-            } else if (previousValues != sceneValues) {
-                error("$key was composed multiple times in $scene")
-            }
+): Modifier {
+    val element: Element
+    val sceneValues: Element.TargetValues
 
-            element
-        }
-
-    DisposableEffect(scene, sceneValues, element) {
-        onDispose {
-            element.sceneValues.remove(scene.key)
-
-            // This was the last scene this element was in, so remove it from the map.
-            if (element.sceneValues.isEmpty()) {
-                layoutImpl.elements.remove(element.key)
-            }
-        }
+    // Get the element associated to [key] if it was already composed in another scene,
+    // otherwise create it and add it to our Map<ElementKey, Element>. This is done inside a
+    // withoutReadObservation() because there is no need to recompose when that map is mutated.
+    Snapshot.withoutReadObservation {
+        element = layoutImpl.elements[key] ?: Element(key).also { layoutImpl.elements[key] = it }
+        sceneValues =
+            element.sceneValues[scene.key]
+                ?: Element.TargetValues(scene.key).also { element.sceneValues[scene.key] = it }
     }
 
-    val drawScale by
-        remember(layoutImpl, element, scene, sceneValues) {
-            derivedStateOf { getDrawScale(layoutImpl, element, scene, sceneValues) }
-        }
-
-    drawWithContent {
+    return this.then(ElementModifier(layoutImpl, element, sceneValues))
+        .drawWithContent {
             if (shouldDrawElement(layoutImpl, scene, element)) {
+                val drawScale = getDrawScale(layoutImpl, element, scene, sceneValues)
                 if (drawScale == Scale.Default) {
-                    this@drawWithContent.drawContent()
+                    drawContent()
                 } else {
                     scale(
                         drawScale.scaleX,
                         drawScale.scaleY,
-                        if (drawScale.pivot.isUnspecified) center else drawScale.pivot
+                        if (drawScale.pivot.isUnspecified) center else drawScale.pivot,
                     ) {
                         this@drawWithContent.drawContent()
                     }
@@ -186,6 +172,84 @@
         .testTag(key.testTag)
 }
 
+/**
+ * An element associated to [ElementNode]. Note that this element does not support updates as its
+ * arguments should always be the same.
+ */
+private data class ElementModifier(
+    private val layoutImpl: SceneTransitionLayoutImpl,
+    private val element: Element,
+    private val sceneValues: Element.TargetValues,
+) : ModifierNodeElement<ElementNode>() {
+    override fun create(): ElementNode = ElementNode(layoutImpl, element, sceneValues)
+
+    override fun update(node: ElementNode) {
+        node.update(layoutImpl, element, sceneValues)
+    }
+}
+
+internal class ElementNode(
+    layoutImpl: SceneTransitionLayoutImpl,
+    element: Element,
+    sceneValues: Element.TargetValues,
+) : Modifier.Node() {
+    private var layoutImpl: SceneTransitionLayoutImpl = layoutImpl
+    private var element: Element = element
+    private var sceneValues: Element.TargetValues = sceneValues
+
+    override fun onAttach() {
+        super.onAttach()
+        addNodeToSceneValues()
+    }
+
+    private fun addNodeToSceneValues() {
+        sceneValues.nodes.add(this)
+
+        coroutineScope.launch {
+            // At this point all [CodeLocationNode] have been attached or detached, which means that
+            // [sceneValues.codeLocations] should have exactly 1 element, otherwise this means that
+            // this element was composed multiple times in the same scene.
+            val nCodeLocations = sceneValues.nodes.size
+            if (nCodeLocations != 1 || !sceneValues.nodes.contains(this@ElementNode)) {
+                error("${element.key} was composed $nCodeLocations times in ${sceneValues.scene}")
+            }
+        }
+    }
+
+    override fun onDetach() {
+        super.onDetach()
+        removeNodeFromSceneValues()
+    }
+
+    private fun removeNodeFromSceneValues() {
+        sceneValues.nodes.remove(this)
+
+        // If element is not composed from this scene anymore, remove the scene values. This works
+        // because [onAttach] is called before [onDetach], so if an element is moved from the UI
+        // tree we will first add the new code location then remove the old one.
+        if (sceneValues.nodes.isEmpty()) {
+            element.sceneValues.remove(sceneValues.scene)
+        }
+
+        // If the element is not composed in any scene, remove it from the elements map.
+        if (element.sceneValues.isEmpty()) {
+            layoutImpl.elements.remove(element.key)
+        }
+    }
+
+    fun update(
+        layoutImpl: SceneTransitionLayoutImpl,
+        element: Element,
+        sceneValues: Element.TargetValues,
+    ) {
+        removeNodeFromSceneValues()
+        this.layoutImpl = layoutImpl
+        this.element = element
+        this.sceneValues = sceneValues
+        addNodeToSceneValues()
+    }
+}
+
 private fun shouldDrawElement(
     layoutImpl: SceneTransitionLayoutImpl,
     scene: Scene,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
index bc015ee..5b752eb 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
@@ -43,7 +43,10 @@
     name: String,
     identity: Any = Object(),
 ) : Key(name, identity) {
-    @VisibleForTesting val testTag: String = "scene:$name"
+    @VisibleForTesting
+    // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can
+    // access internal members.
+    val testTag: String = "scene:$name"
 
     /** The unique [ElementKey] identifying this scene's root element. */
     val rootElementKey = ElementKey(name, identity)
@@ -64,7 +67,10 @@
      */
     val isBackground: Boolean = false,
 ) : Key(name, identity), ElementMatcher {
-    @VisibleForTesting val testTag: String = "element:$name"
+    @VisibleForTesting
+    // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can
+    // access internal members.
+    val testTag: String = "element:$name"
 
     override fun matches(key: ElementKey, scene: SceneKey): Boolean {
         return key == this
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
index 1a79522..857a596 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
@@ -76,6 +76,8 @@
     private val layoutImpl: SceneTransitionLayoutImpl,
     private val scene: Scene,
 ) : SceneScope {
+    override val layoutState: SceneTransitionLayoutState = layoutImpl.state
+
     override fun Modifier.element(key: ElementKey): Modifier {
         return element(layoutImpl, scene, key)
     }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
index 838cb3b..c51287a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
@@ -17,7 +17,6 @@
 package com.android.compose.animation.scene
 
 import android.util.Log
-import androidx.annotation.VisibleForTesting
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.Spring
 import androidx.compose.animation.core.spring
@@ -37,8 +36,7 @@
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.launch
 
-@VisibleForTesting
-class SceneGestureHandler(
+internal class SceneGestureHandler(
     internal val layoutImpl: SceneTransitionLayoutImpl,
     internal val orientation: Orientation,
     private val coroutineScope: CoroutineScope,
@@ -63,12 +61,10 @@
     internal val currentScene: Scene
         get() = layoutImpl.scene(transitionState.currentScene)
 
-    @VisibleForTesting
-    val isDrivingTransition
+    internal val isDrivingTransition
         get() = transitionState == swipeTransition
 
-    @VisibleForTesting
-    var isAnimatingOffset
+    internal var isAnimatingOffset
         get() = swipeTransition.isAnimatingOffset
         private set(value) {
             swipeTransition.isAnimatingOffset = value
@@ -81,7 +77,7 @@
      * The velocity threshold at which the intent of the user is to swipe up or down. It is the same
      * as SwipeableV2Defaults.VelocityThreshold.
      */
-    @VisibleForTesting val velocityThreshold = with(layoutImpl.density) { 125.dp.toPx() }
+    internal val velocityThreshold = with(layoutImpl.density) { 125.dp.toPx() }
 
     /**
      * The positional threshold at which the intent of the user is to swipe to the next scene. It is
@@ -533,8 +529,7 @@
     }
 }
 
-@VisibleForTesting
-class SceneNestedScrollHandler(
+internal class SceneNestedScrollHandler(
     private val gestureHandler: SceneGestureHandler,
     private val startBehavior: NestedScrollBehavior,
     private val endBehavior: NestedScrollBehavior,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 9c31445..30d13df 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -57,30 +57,17 @@
     @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f,
     scenes: SceneTransitionLayoutScope.() -> Unit,
 ) {
-    val density = LocalDensity.current
-    val coroutineScope = rememberCoroutineScope()
-    val layoutImpl = remember {
-        SceneTransitionLayoutImpl(
-            onChangeScene = onChangeScene,
-            builder = scenes,
-            transitions = transitions,
-            state = state,
-            density = density,
-            edgeDetector = edgeDetector,
-            transitionInterceptionThreshold = transitionInterceptionThreshold,
-            coroutineScope = coroutineScope,
-        )
-    }
-
-    layoutImpl.onChangeScene = onChangeScene
-    layoutImpl.transitions = transitions
-    layoutImpl.density = density
-    layoutImpl.edgeDetector = edgeDetector
-    layoutImpl.transitionInterceptionThreshold = transitionInterceptionThreshold
-
-    layoutImpl.setScenes(scenes)
-    layoutImpl.setCurrentScene(currentScene)
-    layoutImpl.Content(modifier)
+    SceneTransitionLayoutForTesting(
+        currentScene,
+        onChangeScene,
+        transitions,
+        state,
+        edgeDetector,
+        transitionInterceptionThreshold,
+        modifier,
+        onLayoutImpl = null,
+        scenes,
+    )
 }
 
 interface SceneTransitionLayoutScope {
@@ -108,6 +95,9 @@
 
 @ElementDsl
 interface SceneScope {
+    /** The state of the [SceneTransitionLayout] in which this scene is contained. */
+    val layoutState: SceneTransitionLayoutState
+
     /**
      * Tag an element identified by [key].
      *
@@ -228,3 +218,47 @@
     Left(Orientation.Horizontal),
     Right(Orientation.Horizontal),
 }
+
+/**
+ * An internal version of [SceneTransitionLayout] to be used for tests.
+ *
+ * Important: You should use this only in tests and if you need to access the underlying
+ * [SceneTransitionLayoutImpl]. In other cases, you should use [SceneTransitionLayout].
+ */
+@Composable
+internal fun SceneTransitionLayoutForTesting(
+    currentScene: SceneKey,
+    onChangeScene: (SceneKey) -> Unit,
+    transitions: SceneTransitions,
+    state: SceneTransitionLayoutState,
+    edgeDetector: EdgeDetector,
+    transitionInterceptionThreshold: Float,
+    modifier: Modifier,
+    onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)?,
+    scenes: SceneTransitionLayoutScope.() -> Unit,
+) {
+    val density = LocalDensity.current
+    val coroutineScope = rememberCoroutineScope()
+    val layoutImpl = remember {
+        SceneTransitionLayoutImpl(
+                onChangeScene = onChangeScene,
+                builder = scenes,
+                transitions = transitions,
+                state = state,
+                density = density,
+                edgeDetector = edgeDetector,
+                transitionInterceptionThreshold = transitionInterceptionThreshold,
+                coroutineScope = coroutineScope,
+            )
+            .also { onLayoutImpl?.invoke(it) }
+    }
+
+    layoutImpl.onChangeScene = onChangeScene
+    layoutImpl.transitions = transitions
+    layoutImpl.density = density
+    layoutImpl.edgeDetector = edgeDetector
+
+    layoutImpl.setScenes(scenes)
+    layoutImpl.setCurrentScene(currentScene)
+    layoutImpl.Content(modifier)
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 94f2737..60f385a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -17,7 +17,6 @@
 package com.android.compose.animation.scene
 
 import androidx.activity.compose.BackHandler
-import androidx.annotation.VisibleForTesting
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.layout.Box
 import androidx.compose.runtime.Composable
@@ -42,8 +41,7 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.Channel
 
-@VisibleForTesting
-class SceneTransitionLayoutImpl(
+internal class SceneTransitionLayoutImpl(
     onChangeScene: (SceneKey) -> Unit,
     builder: SceneTransitionLayoutScope.() -> Unit,
     transitions: SceneTransitions,
@@ -260,8 +258,7 @@
 
     internal fun isSceneReady(scene: SceneKey): Boolean = readyScenes.containsKey(scene)
 
-    @VisibleForTesting
-    fun setScenesTargetSizeForTest(size: IntSize) {
+    internal fun setScenesTargetSizeForTest(size: IntSize) {
         scenes.values.forEach { it.targetSize = size }
     }
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index b9f83c5..64c9775 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -30,6 +30,22 @@
      */
     var transitionState: TransitionState by mutableStateOf(TransitionState.Idle(initialScene))
         internal set
+
+    /**
+     * Whether we are transitioning, optionally restricting the check to the transition between
+     * [from] and [to].
+     */
+    fun isTransitioning(from: SceneKey? = null, to: SceneKey? = null): Boolean {
+        val transition = transitionState as? TransitionState.Transition ?: return false
+
+        // TODO(b/310915136): Remove this check.
+        if (transition.fromScene == transition.toScene) {
+            return false
+        }
+
+        return (from == null || transition.fromScene == from) &&
+            (to == null || transition.toScene == to)
+    }
 }
 
 sealed interface TransitionState {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
index 72a2d61..2172ed3 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
@@ -16,7 +16,6 @@
 
 package com.android.compose.animation.scene
 
-import androidx.annotation.VisibleForTesting
 import androidx.compose.animation.core.AnimationSpec
 import androidx.compose.animation.core.snap
 import androidx.compose.ui.geometry.Offset
@@ -38,12 +37,11 @@
 
 /** The transitions configuration of a [SceneTransitionLayout]. */
 class SceneTransitions(
-    @get:VisibleForTesting val transitionSpecs: List<TransitionSpec>,
+    internal val transitionSpecs: List<TransitionSpec>,
 ) {
     private val cache = mutableMapOf<SceneKey, MutableMap<SceneKey, TransitionSpec>>()
 
-    @VisibleForTesting
-    fun transitionSpec(from: SceneKey, to: SceneKey): TransitionSpec {
+    internal fun transitionSpec(from: SceneKey, to: SceneKey): TransitionSpec {
         return cache.getOrPut(from) { mutableMapOf() }.getOrPut(to) { findSpec(from, to) }
     }
 
diff --git a/packages/SystemUI/compose/scene/tests/Android.bp b/packages/SystemUI/compose/scene/tests/Android.bp
index 6de7550..13df35b 100644
--- a/packages/SystemUI/compose/scene/tests/Android.bp
+++ b/packages/SystemUI/compose/scene/tests/Android.bp
@@ -30,10 +30,13 @@
 
     srcs: [
         "src/**/*.kt",
+
+        // TODO(b/240432457): Depend on PlatformComposeSceneTransitionLayout
+        // directly once Kotlin tests can access internal declarations.
+        ":PlatformComposeSceneTransitionLayout-srcs",
     ],
 
     static_libs: [
-        "PlatformComposeSceneTransitionLayout",
         "PlatformComposeSceneTransitionLayoutTestsUtils",
 
         "androidx.test.runner",
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 6401bb3..cc7a0b8 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -18,9 +18,15 @@
 
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.intermediateLayout
@@ -29,6 +35,7 @@
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -209,4 +216,215 @@
             }
         }
     }
+
+    @Test
+    fun elementIsReusedInSameSceneAndBetweenScenes() {
+        var currentScene by mutableStateOf(TestScenes.SceneA)
+        var sceneCState by mutableStateOf(0)
+        var sceneDState by mutableStateOf(0)
+        val key = TestElements.Foo
+        var nullableLayoutImpl: SceneTransitionLayoutImpl? = null
+
+        rule.setContent {
+            SceneTransitionLayoutForTesting(
+                currentScene = currentScene,
+                onChangeScene = { currentScene = it },
+                transitions = remember { transitions {} },
+                state = remember { SceneTransitionLayoutState(currentScene) },
+                edgeDetector = DefaultEdgeDetector,
+                modifier = Modifier,
+                transitionInterceptionThreshold = 0f,
+                onLayoutImpl = { nullableLayoutImpl = it },
+            ) {
+                scene(TestScenes.SceneA) { /* Nothing */}
+                scene(TestScenes.SceneB) { Box(Modifier.element(key)) }
+                scene(TestScenes.SceneC) {
+                    when (sceneCState) {
+                        0 -> Row(Modifier.element(key)) {}
+                        1 -> Column(Modifier.element(key)) {}
+                        else -> {
+                            /* Nothing */
+                        }
+                    }
+                }
+                scene(TestScenes.SceneD) {
+                    // We should be able to extract the modifier before assigning it to different
+                    // nodes.
+                    val childModifier = Modifier.element(key)
+                    when (sceneDState) {
+                        0 -> Row(childModifier) {}
+                        1 -> Column(childModifier) {}
+                        else -> {
+                            /* Nothing */
+                        }
+                    }
+                }
+            }
+        }
+
+        assertThat(nullableLayoutImpl).isNotNull()
+        val layoutImpl = nullableLayoutImpl!!
+
+        // Scene A: no elements in the elements map.
+        rule.waitForIdle()
+        assertThat(layoutImpl.elements).isEmpty()
+
+        // Scene B: element is in the map.
+        currentScene = TestScenes.SceneB
+        rule.waitForIdle()
+
+        assertThat(layoutImpl.elements.keys).containsExactly(key)
+        val element = layoutImpl.elements.getValue(key)
+        assertThat(element.sceneValues.keys).containsExactly(TestScenes.SceneB)
+
+        // Scene C, state 0: the same element is reused.
+        currentScene = TestScenes.SceneC
+        sceneCState = 0
+        rule.waitForIdle()
+
+        assertThat(layoutImpl.elements.keys).containsExactly(key)
+        assertThat(layoutImpl.elements.getValue(key)).isSameInstanceAs(element)
+        assertThat(element.sceneValues.keys).containsExactly(TestScenes.SceneC)
+
+        // Scene C, state 1: the same element is reused.
+        sceneCState = 1
+        rule.waitForIdle()
+
+        assertThat(layoutImpl.elements.keys).containsExactly(key)
+        assertThat(layoutImpl.elements.getValue(key)).isSameInstanceAs(element)
+        assertThat(element.sceneValues.keys).containsExactly(TestScenes.SceneC)
+
+        // Scene D, state 0: the same element is reused.
+        currentScene = TestScenes.SceneD
+        sceneDState = 0
+        rule.waitForIdle()
+
+        assertThat(layoutImpl.elements.keys).containsExactly(key)
+        assertThat(layoutImpl.elements.getValue(key)).isSameInstanceAs(element)
+        assertThat(element.sceneValues.keys).containsExactly(TestScenes.SceneD)
+
+        // Scene D, state 1: the same element is reused.
+        sceneDState = 1
+        rule.waitForIdle()
+
+        assertThat(layoutImpl.elements.keys).containsExactly(key)
+        assertThat(layoutImpl.elements.getValue(key)).isSameInstanceAs(element)
+        assertThat(element.sceneValues.keys).containsExactly(TestScenes.SceneD)
+
+        // Scene D, state 2: the element is removed from the map.
+        sceneDState = 2
+        rule.waitForIdle()
+
+        assertThat(element.sceneValues).isEmpty()
+        assertThat(layoutImpl.elements).isEmpty()
+    }
+
+    @Test
+    fun throwsExceptionWhenElementIsComposedMultipleTimes() {
+        val key = TestElements.Foo
+
+        assertThrows(IllegalStateException::class.java) {
+            rule.setContent {
+                TestSceneScope {
+                    Column {
+                        Box(Modifier.element(key))
+                        Box(Modifier.element(key))
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun throwsExceptionWhenElementIsComposedMultipleTimes_childModifier() {
+        val key = TestElements.Foo
+
+        assertThrows(IllegalStateException::class.java) {
+            rule.setContent {
+                TestSceneScope {
+                    Column {
+                        val childModifier = Modifier.element(key)
+                        Box(childModifier)
+                        Box(childModifier)
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun throwsExceptionWhenElementIsComposedMultipleTimes_childModifier_laterDuplication() {
+        val key = TestElements.Foo
+
+        assertThrows(IllegalStateException::class.java) {
+            var nElements by mutableStateOf(1)
+            rule.setContent {
+                TestSceneScope {
+                    Column {
+                        val childModifier = Modifier.element(key)
+                        repeat(nElements) { Box(childModifier) }
+                    }
+                }
+            }
+
+            nElements = 2
+            rule.waitForIdle()
+        }
+    }
+
+    @Test
+    fun throwsExceptionWhenElementIsComposedMultipleTimes_updatedNode() {
+        assertThrows(IllegalStateException::class.java) {
+            var key by mutableStateOf(TestElements.Foo)
+            rule.setContent {
+                TestSceneScope {
+                    Column {
+                        Box(Modifier.element(key))
+                        Box(Modifier.element(TestElements.Bar))
+                    }
+                }
+            }
+
+            key = TestElements.Bar
+            rule.waitForIdle()
+        }
+    }
+
+    @Test
+    fun elementModifierSupportsUpdates() {
+        var key by mutableStateOf(TestElements.Foo)
+        var nullableLayoutImpl: SceneTransitionLayoutImpl? = null
+
+        rule.setContent {
+            SceneTransitionLayoutForTesting(
+                currentScene = TestScenes.SceneA,
+                onChangeScene = {},
+                transitions = remember { transitions {} },
+                state = remember { SceneTransitionLayoutState(TestScenes.SceneA) },
+                edgeDetector = DefaultEdgeDetector,
+                modifier = Modifier,
+                transitionInterceptionThreshold = 0f,
+                onLayoutImpl = { nullableLayoutImpl = it },
+            ) {
+                scene(TestScenes.SceneA) { Box(Modifier.element(key)) }
+            }
+        }
+
+        assertThat(nullableLayoutImpl).isNotNull()
+        val layoutImpl = nullableLayoutImpl!!
+
+        // There is only Foo in the elements map.
+        assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Foo)
+        val fooElement = layoutImpl.elements.getValue(TestElements.Foo)
+        assertThat(fooElement.sceneValues.keys).containsExactly(TestScenes.SceneA)
+
+        key = TestElements.Bar
+
+        // There is only Bar in the elements map and foo scene values was cleaned up.
+        rule.waitForIdle()
+        assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Bar)
+        val barElement = layoutImpl.elements.getValue(TestElements.Bar)
+        assertThat(barElement.sceneValues.keys).containsExactly(TestScenes.SceneA)
+        assertThat(fooElement.sceneValues).isEmpty()
+    }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
new file mode 100644
index 0000000..94c51ca
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SceneTransitionLayoutStateTest {
+    @get:Rule val rule = createComposeRule()
+
+    @Test
+    fun isTransitioningTo_idle() {
+        val state = SceneTransitionLayoutState(TestScenes.SceneA)
+
+        assertThat(state.isTransitioning()).isFalse()
+        assertThat(state.isTransitioning(from = TestScenes.SceneA)).isFalse()
+        assertThat(state.isTransitioning(to = TestScenes.SceneB)).isFalse()
+        assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB))
+            .isFalse()
+    }
+
+    @Test
+    fun isTransitioningTo_fromSceneEqualToToScene() {
+        val state = SceneTransitionLayoutState(TestScenes.SceneA)
+        state.transitionState = transition(from = TestScenes.SceneA, to = TestScenes.SceneA)
+
+        assertThat(state.isTransitioning()).isFalse()
+        assertThat(state.isTransitioning(from = TestScenes.SceneA)).isFalse()
+        assertThat(state.isTransitioning(to = TestScenes.SceneB)).isFalse()
+        assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB))
+            .isFalse()
+    }
+
+    @Test
+    fun isTransitioningTo_transition() {
+        val state = SceneTransitionLayoutState(TestScenes.SceneA)
+        state.transitionState = transition(from = TestScenes.SceneA, to = TestScenes.SceneB)
+
+        assertThat(state.isTransitioning()).isTrue()
+        assertThat(state.isTransitioning(from = TestScenes.SceneA)).isTrue()
+        assertThat(state.isTransitioning(from = TestScenes.SceneB)).isFalse()
+        assertThat(state.isTransitioning(to = TestScenes.SceneB)).isTrue()
+        assertThat(state.isTransitioning(to = TestScenes.SceneA)).isFalse()
+        assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)).isTrue()
+    }
+
+    private fun transition(from: SceneKey, to: SceneKey): TransitionState.Transition {
+        return object : TransitionState.Transition {
+            override val currentScene: SceneKey = from
+            override val fromScene: SceneKey = from
+            override val toScene: SceneKey = to
+            override val progress: Float = 0f
+            override val isInitiatedByUserInput: Boolean = false
+            override val isUserInputOngoing: Boolean = false
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestValues.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestValues.kt
index b4c393e..b83705a 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestValues.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestValues.kt
@@ -26,6 +26,7 @@
     val SceneA = SceneKey("SceneA")
     val SceneB = SceneKey("SceneB")
     val SceneC = SceneKey("SceneC")
+    val SceneD = SceneKey("SceneD")
 }
 
 /** Element keys that can be reused by tests. */
diff --git a/packages/SystemUI/flag_check.py b/packages/SystemUI/flag_check.py
index 5db27d8..bac3553 100755
--- a/packages/SystemUI/flag_check.py
+++ b/packages/SystemUI/flag_check.py
@@ -14,7 +14,7 @@
 The Flag: stanza is regex matched and should describe whether your change is behind a flag or flags.
 
 As a CL author, you'll have a consistent place to describe the risk of the proposed change by explicitly calling out the name of the
-flag in addition to its state (ENABLED|DISABLED|DEVELOPMENT|TEAMFOOD|TRUNKFOOD|NEXTFOOD).
+flag in addition to its state (ENABLED|DISABLED|DEVELOPMENT|STAGING|TEAMFOOD|TRUNKFOOD|NEXTFOOD).
 
 Some examples below:
 
@@ -74,11 +74,11 @@
     #common_typos_disable
     flagName = '([a-zA-z0-9_.])+'
 
-    #[state:ENABLED|DISABLED|DEVELOPMENT|TEAM*(TEAMFOOD)|TRUNK*(TRUNK_STAGING, TRUNK_FOOD)|NEXT*(NEXTFOOD)]
-    stateExpression = '\s*(ENABLED|DISABLED|DEVELOPMENT|TEAM[a-zA-z]*|TRUNK[a-zA-z]*|NEXT[a-zA-z]*)'
+    #[state:ENABLED|DISABLED|DEVELOPMENT|TEAM*(TEAMFOOD)|STAGING|TRUNK*(TRUNK_STAGING, TRUNK_FOOD)|NEXT*(NEXTFOOD)]
+    stateExpression = '\s*(ENABLED|DISABLED|DEVELOPMENT|TEAM[a-zA-z]*|STAGING|TRUNK[a-zA-z]*|NEXT[a-zA-z]*)'
     #common_typos_enable
 
-    readableRegexMsg = '\n\tFlag: (NONE|NA)\n\tFlag: LEGACY|ACONFIG FlagName|packageName.flagName ENABLED|DISABLED|DEVELOPMENT|TEAMFOOD|TRUNKFOOD|NEXTFOOD'
+    readableRegexMsg = '\n\tFlag: (NONE|NA)\n\tFlag: LEGACY|ACONFIG FlagName|packageName.flagName ENABLED|DISABLED|DEVELOPMENT|TEAMFOOD|STAGING|TRUNKFOOD|NEXTFOOD'
 
     flagRegex = fr'^{field}: .*$'
     check_flag = re.compile(flagRegex) #Flag:
diff --git a/packages/SystemUI/res/layout/edit_widgets.xml b/packages/SystemUI/res/layout/edit_widgets.xml
new file mode 100644
index 0000000..182e651
--- /dev/null
+++ b/packages/SystemUI/res/layout/edit_widgets.xml
@@ -0,0 +1,32 @@
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/edit_widgets"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <Button
+        style="@android:Widget.DeviceDefault.Button.Colored"
+        android:id="@+id/add_widget"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:textSize="28sp"
+        android:text="@string/hub_mode_add_widget_button_text"/>
+
+</FrameLayout>
diff --git a/packages/SystemUI/res/layout/widget_picker.xml b/packages/SystemUI/res/layout/widget_picker.xml
new file mode 100644
index 0000000..827bd5d
--- /dev/null
+++ b/packages/SystemUI/res/layout/widget_picker.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/widgets_container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:padding="64dp"
+    android:gravity="center_vertical"
+    android:orientation="horizontal">
+
+</LinearLayout>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 3163533..daf6cb3 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1056,10 +1056,12 @@
     <!-- Indicator on keyguard to start the communal tutorial. [CHAR LIMIT=100] -->
     <string name="communal_tutorial_indicator_text">Swipe left to start the communal tutorial</string>
 
-    <!-- Description for the button that opens the widget picker on click. [CHAR LIMIT=50] -->
-    <string name="button_to_open_widget_picker">Open the widget picker</string>
+    <!-- Description for the button that opens the widget editor on click. [CHAR LIMIT=50] -->
+    <string name="button_to_open_widget_editor">Open the widget editor</string>
     <!-- Description for the button that removes a widget on click. [CHAR LIMIT=50] -->
     <string name="button_to_remove_widget">Remove a widget</string>
+    <!-- Text for the button that launches the hub mode widget picker. [CHAR LIMIT=50] -->
+    <string name="hub_mode_add_widget_button_text">Add Widget</string>
 
     <!-- Related to user switcher --><skip/>
 
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 7ce530f..2117714 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -943,6 +943,11 @@
         <item name="android:windowLightStatusBar">true</item>
     </style>
 
+    <style name="Theme.EditWidgetsActivity"
+        parent="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen">
+        <item name="android:windowBackground">@android:color/white</item>
+    </style>
+
     <style name="TextAppearance.Control">
         <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
     </style>
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/ScrimLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/ScrimLogger.kt
new file mode 100644
index 0000000..a068769
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/logging/ScrimLogger.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keyguard.logging
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.ScrimLog
+import com.google.errorprone.annotations.CompileTimeConstant
+import javax.inject.Inject
+
+/**
+ * A logger to log scrim state.
+ *
+ * To enable logcat echoing for this buffer use this command:
+ * ```
+ * $ adb shell cmd statusbar echo -b ScrimLog:VERBOSE
+ * ```
+ */
+class ScrimLogger
+@Inject
+constructor(
+    @ScrimLog val buffer: LogBuffer,
+) {
+    companion object {
+        val TAG = ScrimLogger::class.simpleName!!
+    }
+
+    fun d(
+        tag: String,
+        @CompileTimeConstant msg: String,
+        arg: Any,
+    ) = log("$tag::$TAG", LogLevel.DEBUG, msg, arg)
+
+    fun log(
+        tag: String,
+        level: LogLevel,
+        @CompileTimeConstant msg: String,
+        arg: Any,
+    ) =
+        buffer.log(
+            tag,
+            level,
+            {
+                str1 = msg
+                str2 = arg.toString()
+            },
+            { "$str1: $str2" }
+        )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java
similarity index 88%
rename from packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java
rename to packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java
index 59b85d1..b704f3c 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java
@@ -59,8 +59,8 @@
  * when {@code IStatusBar#requestWindowMagnificationConnection(boolean)} is called.
  */
 @SysUISingleton
-public class WindowMagnification implements CoreStartable, CommandQueue.Callbacks {
-    private static final String TAG = "WindowMagnification";
+public class Magnification implements CoreStartable, CommandQueue.Callbacks {
+    private static final String TAG = "Magnification";
 
     private final ModeSwitchesController mModeSwitchesController;
     private final Context mContext;
@@ -154,7 +154,7 @@
     DisplayIdIndexSupplier<MagnificationSettingsController> mMagnificationSettingsSupplier;
 
     @Inject
-    public WindowMagnification(Context context, @Main Handler mainHandler,
+    public Magnification(Context context, @Main Handler mainHandler,
             CommandQueue commandQueue, ModeSwitchesController modeSwitchesController,
             SysUiState sysUiState, OverviewProxyService overviewProxyService,
             SecureSettings secureSettings, DisplayTracker displayTracker,
@@ -366,49 +366,53 @@
     @VisibleForTesting
     final MagnificationSettingsController.Callback mMagnificationSettingsControllerCallback =
             new MagnificationSettingsController.Callback() {
-        @Override
-        public void onSetMagnifierSize(int displayId, int index) {
-            mHandler.post(() -> onSetMagnifierSizeInternal(displayId, index));
-            mA11yLogger.logWithPosition(
-                    MagnificationSettingsEvent.MAGNIFICATION_SETTINGS_WINDOW_SIZE_SELECTED,
-                    index
-            );
-        }
+                @Override
+                public void onSetMagnifierSize(int displayId, int index) {
+                    mHandler.post(() -> onSetMagnifierSizeInternal(displayId, index));
+                    mA11yLogger.logWithPosition(
+                            MagnificationSettingsEvent.MAGNIFICATION_SETTINGS_WINDOW_SIZE_SELECTED,
+                            index
+                    );
+                }
 
-        @Override
-        public void onSetDiagonalScrolling(int displayId, boolean enable) {
-            mHandler.post(() -> onSetDiagonalScrollingInternal(displayId, enable));
-        }
+                @Override
+                public void onSetDiagonalScrolling(int displayId, boolean enable) {
+                    mHandler.post(() -> onSetDiagonalScrollingInternal(displayId, enable));
+                }
 
-        @Override
-        public void onEditMagnifierSizeMode(int displayId, boolean enable) {
-            mHandler.post(() -> onEditMagnifierSizeModeInternal(displayId, enable));
-            mA11yLogger.log(enable
-                    ? MagnificationSettingsEvent.MAGNIFICATION_SETTINGS_SIZE_EDITING_ACTIVATED
-                    : MagnificationSettingsEvent.MAGNIFICATION_SETTINGS_SIZE_EDITING_DEACTIVATED);
-        }
+                @Override
+                public void onEditMagnifierSizeMode(int displayId, boolean enable) {
+                    mHandler.post(() -> onEditMagnifierSizeModeInternal(displayId, enable));
+                    mA11yLogger.log(enable
+                            ?
+                            MagnificationSettingsEvent
+                                    .MAGNIFICATION_SETTINGS_SIZE_EDITING_ACTIVATED
+                            : MagnificationSettingsEvent
+                                    .MAGNIFICATION_SETTINGS_SIZE_EDITING_DEACTIVATED);
+                }
 
-        @Override
-        public void onMagnifierScale(int displayId, float scale, boolean updatePersistence) {
-            if (mWindowMagnificationConnectionImpl != null) {
-                mWindowMagnificationConnectionImpl.onPerformScaleAction(
-                        displayId, scale, updatePersistence);
-            }
-            mA11yLogger.logThrottled(
-                    MagnificationSettingsEvent.MAGNIFICATION_SETTINGS_ZOOM_SLIDER_CHANGED
-            );
-        }
+                @Override
+                public void onMagnifierScale(int displayId, float scale,
+                        boolean updatePersistence) {
+                    if (mWindowMagnificationConnectionImpl != null) {
+                        mWindowMagnificationConnectionImpl.onPerformScaleAction(
+                                displayId, scale, updatePersistence);
+                    }
+                    mA11yLogger.logThrottled(
+                            MagnificationSettingsEvent.MAGNIFICATION_SETTINGS_ZOOM_SLIDER_CHANGED
+                    );
+                }
 
-        @Override
-        public void onModeSwitch(int displayId, int newMode) {
-            mHandler.post(() -> onModeSwitchInternal(displayId, newMode));
-        }
+                @Override
+                public void onModeSwitch(int displayId, int newMode) {
+                    mHandler.post(() -> onModeSwitchInternal(displayId, newMode));
+                }
 
-        @Override
-        public void onSettingsPanelVisibilityChanged(int displayId, boolean shown) {
-            mHandler.post(() -> onSettingsPanelVisibilityChangedInternal(displayId, shown));
-        }
-    };
+                @Override
+                public void onSettingsPanelVisibilityChanged(int displayId, boolean shown) {
+                    mHandler.post(() -> onSettingsPanelVisibilityChangedInternal(displayId, shown));
+                }
+            };
 
     @MainThread
     private void onSetMagnifierSizeInternal(int displayId, int index) {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java
index ee7781d..b4530ac 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java
@@ -34,7 +34,7 @@
  * A class to control {@link WindowMagnificationSettings} and receive settings panel callbacks by
  * {@link WindowMagnificationSettingsCallback}.
  * The settings panel callbacks will be delegated through
- * {@link MagnificationSettingsController.Callback} to {@link WindowMagnification}.
+ * {@link MagnificationSettingsController.Callback} to {@link Magnification}.
  */
 
 public class MagnificationSettingsController implements ComponentCallbacks {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationConnectionImpl.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationConnectionImpl.java
index 928445b..5666851 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationConnectionImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationConnectionImpl.java
@@ -37,12 +37,12 @@
     private static final String TAG = "WindowMagnificationConnectionImpl";
 
     private IWindowMagnificationConnectionCallback mConnectionCallback;
-    private final WindowMagnification mWindowMagnification;
+    private final Magnification mMagnification;
     private final Handler mHandler;
 
-    WindowMagnificationConnectionImpl(@NonNull WindowMagnification windowMagnification,
+    WindowMagnificationConnectionImpl(@NonNull Magnification magnification,
             @Main Handler mainHandler) {
-        mWindowMagnification = windowMagnification;
+        mMagnification = magnification;
         mHandler = mainHandler;
     }
 
@@ -51,56 +51,56 @@
             float magnificationFrameOffsetRatioX, float magnificationFrameOffsetRatioY,
             IRemoteMagnificationAnimationCallback callback) {
         mHandler.post(
-                () -> mWindowMagnification.enableWindowMagnification(displayId, scale, centerX,
+                () -> mMagnification.enableWindowMagnification(displayId, scale, centerX,
                         centerY, magnificationFrameOffsetRatioX,
                         magnificationFrameOffsetRatioY, callback));
     }
 
     @Override
     public void setScale(int displayId, float scale) {
-        mHandler.post(() -> mWindowMagnification.setScale(displayId, scale));
+        mHandler.post(() -> mMagnification.setScale(displayId, scale));
     }
 
     @Override
     public void disableWindowMagnification(int displayId,
             IRemoteMagnificationAnimationCallback callback) {
-        mHandler.post(() -> mWindowMagnification.disableWindowMagnification(displayId,
+        mHandler.post(() -> mMagnification.disableWindowMagnification(displayId,
                 callback));
     }
 
     @Override
     public void moveWindowMagnifier(int displayId, float offsetX, float offsetY) {
         mHandler.post(
-                () -> mWindowMagnification.moveWindowMagnifier(displayId, offsetX, offsetY));
+                () -> mMagnification.moveWindowMagnifier(displayId, offsetX, offsetY));
     }
 
     @Override
     public void moveWindowMagnifierToPosition(int displayId, float positionX, float positionY,
             IRemoteMagnificationAnimationCallback callback) {
-        mHandler.post(() -> mWindowMagnification.moveWindowMagnifierToPositionInternal(
+        mHandler.post(() -> mMagnification.moveWindowMagnifierToPositionInternal(
                 displayId, positionX, positionY, callback));
     }
 
     @Override
     public void showMagnificationButton(int displayId, int magnificationMode) {
         mHandler.post(
-                () -> mWindowMagnification.showMagnificationButton(displayId, magnificationMode));
+                () -> mMagnification.showMagnificationButton(displayId, magnificationMode));
     }
 
     @Override
     public void removeMagnificationButton(int displayId) {
         mHandler.post(
-                () -> mWindowMagnification.removeMagnificationButton(displayId));
+                () -> mMagnification.removeMagnificationButton(displayId));
     }
 
     @Override
     public void removeMagnificationSettingsPanel(int display) {
-        mHandler.post(() -> mWindowMagnification.hideMagnificationSettingsPanel(display));
+        mHandler.post(() -> mMagnification.hideMagnificationSettingsPanel(display));
     }
 
     @Override
     public void onUserMagnificationScaleChanged(int userId, int displayId, float scale) {
-        mHandler.post(() -> mWindowMagnification.setUserMagnificationScale(
+        mHandler.post(() -> mMagnification.setUserMagnificationScale(
                 userId, displayId, scale));
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
index 7769dd9..a42c0ae 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
 import com.android.systemui.user.data.repository.UserRepository
 import com.android.systemui.util.kotlin.pairwise
 import com.android.systemui.util.time.SystemClock
@@ -108,6 +109,9 @@
     /** The minimal length of a pattern. */
     val minPatternLength: Int
 
+    /** The minimal length of a password. */
+    val minPasswordLength: Int
+
     /** Whether the "enhanced PIN privacy" setting is enabled for the current user. */
     val isPinEnhancedPrivacyEnabled: StateFlow<Boolean>
 
@@ -168,6 +172,7 @@
     private val userRepository: UserRepository,
     private val lockPatternUtils: LockPatternUtils,
     broadcastDispatcher: BroadcastDispatcher,
+    mobileConnectionsRepository: MobileConnectionsRepository,
 ) : AuthenticationRepository {
 
     override val isAutoConfirmFeatureEnabled: StateFlow<Boolean> =
@@ -192,9 +197,11 @@
         get() = getSelectedUserInfo().id
 
     override val authenticationMethod: Flow<AuthenticationMethodModel> =
-        userRepository.selectedUserInfo
-            .map { it.id }
-            .distinctUntilChanged()
+        combine(userRepository.selectedUserInfo, mobileConnectionsRepository.isAnySimSecure) {
+                selectedUserInfo,
+                _ ->
+                selectedUserInfo.id
+            }
             .flatMapLatest { selectedUserId ->
                 broadcastDispatcher
                     .broadcastFlow(
@@ -212,9 +219,12 @@
                     blockingAuthenticationMethodInternal(selectedUserId)
                 }
             }
+            .distinctUntilChanged()
 
     override val minPatternLength: Int = LockPatternUtils.MIN_LOCK_PATTERN_SIZE
 
+    override val minPasswordLength: Int = LockPatternUtils.MIN_LOCK_PASSWORD_SIZE
+
     override val isPinEnhancedPrivacyEnabled: StateFlow<Boolean> =
         refreshingFlow(
             initialValue = true,
@@ -354,9 +364,9 @@
         userId: Int,
     ): AuthenticationMethodModel {
         return when (getSecurityMode.apply(userId)) {
-            KeyguardSecurityModel.SecurityMode.PIN,
+            KeyguardSecurityModel.SecurityMode.PIN -> AuthenticationMethodModel.Pin
             KeyguardSecurityModel.SecurityMode.SimPin,
-            KeyguardSecurityModel.SecurityMode.SimPuk -> AuthenticationMethodModel.Pin
+            KeyguardSecurityModel.SecurityMode.SimPuk -> AuthenticationMethodModel.Sim
             KeyguardSecurityModel.SecurityMode.Password -> AuthenticationMethodModel.Password
             KeyguardSecurityModel.SecurityMode.Pattern -> AuthenticationMethodModel.Pattern
             KeyguardSecurityModel.SecurityMode.None -> AuthenticationMethodModel.None
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
index 5eefbf5..c297486 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -200,9 +200,8 @@
                 // We're being throttled, the UI layer should not have called this; skip the
                 // attempt.
                 isThrottled.value -> true
-                // The pattern is too short; skip the attempt.
-                authMethod == AuthenticationMethodModel.Pattern &&
-                    input.size < repository.minPatternLength -> true
+                // The input is too short; skip the attempt.
+                input.isTooShort(authMethod) -> true
                 // Auto-confirm attempt when the feature is not enabled; skip the attempt.
                 tryAutoConfirm && !isAutoConfirmEnabled.value -> true
                 // Auto-confirm should skip the attempt if the pin entered is too short.
@@ -247,6 +246,14 @@
         }
     }
 
+    private fun List<Any>.isTooShort(authMethod: AuthenticationMethodModel): Boolean {
+        return when (authMethod) {
+            AuthenticationMethodModel.Pattern -> size < repository.minPatternLength
+            AuthenticationMethodModel.Password -> size < repository.minPasswordLength
+            else -> false
+        }
+    }
+
     /** Starts refreshing the throttling state every second. */
     private suspend fun startThrottlingCountdown() {
         cancelThrottlingCountdown()
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
index bb5b81d..3552a19 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
@@ -37,4 +37,6 @@
     object Password : AuthenticationMethodModel(isSecure = true)
 
     object Pattern : AuthenticationMethodModel(isSecure = true)
+
+    object Sim : AuthenticationMethodModel(isSecure = true)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 4175937..b064391 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -83,6 +83,7 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter;
 import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels;
 import com.android.systemui.log.SessionTracker;
@@ -170,6 +171,7 @@
     @NonNull private final SelectedUserInteractor mSelectedUserInteractor;
     @NonNull private final FpsUnlockTracker mFpsUnlockTracker;
     private final boolean mIgnoreRefreshRate;
+    private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
 
     // Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple
     // sensors, this, in addition to a lot of the code here, will be updated.
@@ -283,8 +285,8 @@
                         mPrimaryBouncerInteractor,
                         mAlternateBouncerInteractor,
                         mUdfpsKeyguardAccessibilityDelegate,
-                        mUdfpsKeyguardViewModels,
-                            mSelectedUserInteractor
+                        mKeyguardTransitionInteractor,
+                        mSelectedUserInteractor
                     )));
         }
 
@@ -649,7 +651,8 @@
             @NonNull UdfpsKeyguardAccessibilityDelegate udfpsKeyguardAccessibilityDelegate,
             @NonNull Provider<UdfpsKeyguardViewModels> udfpsKeyguardViewModelsProvider,
             @NonNull SelectedUserInteractor selectedUserInteractor,
-            @NonNull FpsUnlockTracker fpsUnlockTracker) {
+            @NonNull FpsUnlockTracker fpsUnlockTracker,
+            @NonNull KeyguardTransitionInteractor keyguardTransitionInteractor) {
         mContext = context;
         mExecution = execution;
         mVibrator = vibrator;
@@ -695,6 +698,7 @@
         mSelectedUserInteractor = selectedUserInteractor;
         mFpsUnlockTracker = fpsUnlockTracker;
         mFpsUnlockTracker.startTracking();
+        mKeyguardTransitionInteractor = keyguardTransitionInteractor;
 
         mTouchProcessor = singlePointerTouchProcessor;
         mSessionTracker = sessionTracker;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index 934f9f9..a5bd89a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -48,11 +48,11 @@
 import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter
-import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
@@ -63,7 +63,6 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import javax.inject.Provider
 
 private const val TAG = "UdfpsControllerOverlay"
 
@@ -102,7 +101,7 @@
     private val alternateBouncerInteractor: AlternateBouncerInteractor,
     private val isDebuggable: Boolean = Build.IS_DEBUGGABLE,
     private val udfpsKeyguardAccessibilityDelegate: UdfpsKeyguardAccessibilityDelegate,
-    private val udfpsKeyguardViewModels: Provider<UdfpsKeyguardViewModels>,
+    private val transitionInteractor: KeyguardTransitionInteractor,
     private val selectedUserInteractor: SelectedUserInteractor,
 ) {
     /** The view, when [isShowing], or null. */
@@ -238,7 +237,7 @@
                 )
             }
             REASON_AUTH_KEYGUARD -> {
-                if (featureFlags.isEnabled(REFACTOR_UDFPS_KEYGUARD_VIEWS)) {
+                if (DeviceEntryUdfpsRefactor.isEnabled) {
                     // note: empty controller, currently shows no visual affordance
                     // instead SysUI will show the fingerprint icon in its DeviceEntryIconView
                     UdfpsBpViewController(
@@ -264,11 +263,11 @@
                         dialogManager,
                         controller,
                         activityLaunchAnimator,
-                        featureFlags,
                         primaryBouncerInteractor,
                         alternateBouncerInteractor,
                         udfpsKeyguardAccessibilityDelegate,
                         selectedUserInteractor,
+                        transitionInteractor,
                     )
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
index d7df0e5..35c3ded 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.biometrics
 
-import android.animation.ValueAnimator
 import android.content.res.Configuration
 import android.util.MathUtils
 import android.view.View
@@ -27,17 +26,17 @@
 import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.biometrics.UdfpsKeyguardViewLegacy.ANIMATION_UNLOCKED_SCREEN_OFF
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
 import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.notification.stack.StackStateAnimator
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.KeyguardViewManagerCallback
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.OccludingAppBiometricUI
@@ -48,10 +47,14 @@
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import java.io.PrintWriter
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.launch
 
 /** Class that coordinates non-HBM animations during keyguard authentication. */
+@ExperimentalCoroutinesApi
 open class UdfpsKeyguardViewControllerLegacy(
     private val view: UdfpsKeyguardViewLegacy,
     statusBarStateController: StatusBarStateController,
@@ -65,11 +68,11 @@
     systemUIDialogManager: SystemUIDialogManager,
     private val udfpsController: UdfpsController,
     private val activityLaunchAnimator: ActivityLaunchAnimator,
-    featureFlags: FeatureFlags,
     primaryBouncerInteractor: PrimaryBouncerInteractor,
     private val alternateBouncerInteractor: AlternateBouncerInteractor,
     private val udfpsKeyguardAccessibilityDelegate: UdfpsKeyguardAccessibilityDelegate,
     private val selectedUserInteractor: SelectedUserInteractor,
+    private val transitionInteractor: KeyguardTransitionInteractor,
 ) :
     UdfpsAnimationViewController<UdfpsKeyguardViewLegacy>(
         view,
@@ -91,44 +94,10 @@
     private var launchTransitionFadingAway = false
     private var isLaunchingActivity = false
     private var activityLaunchProgress = 0f
-    private val unlockedScreenOffDozeAnimator =
-        ValueAnimator.ofFloat(0f, 1f).apply {
-            duration = StackStateAnimator.ANIMATION_DURATION_STANDARD.toLong()
-            interpolator = Interpolators.ALPHA_IN
-            addUpdateListener { animation ->
-                view.onDozeAmountChanged(
-                    animation.animatedFraction,
-                    animation.animatedValue as Float,
-                    UdfpsKeyguardViewLegacy.ANIMATION_UNLOCKED_SCREEN_OFF
-                )
-            }
-        }
     private var inputBouncerExpansion = 0f
 
     private val stateListener: StatusBarStateController.StateListener =
         object : StatusBarStateController.StateListener {
-            override fun onDozeAmountChanged(linear: Float, eased: Float) {
-                if (lastDozeAmount < linear) {
-                    showUdfpsBouncer(false)
-                }
-                unlockedScreenOffDozeAnimator.cancel()
-                val animatingFromUnlockedScreenOff =
-                    unlockedScreenOffAnimationController.isAnimationPlaying()
-                if (animatingFromUnlockedScreenOff && linear != 0f) {
-                    // we manually animate the fade in of the UDFPS icon since the unlocked
-                    // screen off animation prevents the doze amounts to be incrementally eased in
-                    unlockedScreenOffDozeAnimator.start()
-                } else {
-                    view.onDozeAmountChanged(
-                        linear,
-                        eased,
-                        UdfpsKeyguardViewLegacy.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN
-                    )
-                }
-                lastDozeAmount = linear
-                updatePauseAuth()
-            }
-
             override fun onStateChanged(statusBarState: Int) {
                 this@UdfpsKeyguardViewControllerLegacy.statusBarState = statusBarState
                 updateAlpha()
@@ -215,6 +184,7 @@
     }
 
     init {
+        com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor.assertInLegacyMode()
         view.repeatWhenAttached {
             // repeatOnLifecycle CREATED (as opposed to STARTED) because the Bouncer expansion
             // can make the view not visible; and we still want to listen for events
@@ -222,11 +192,39 @@
             repeatOnLifecycle(Lifecycle.State.CREATED) {
                 listenForBouncerExpansion(this)
                 listenForAlternateBouncerVisibility(this)
+                listenForGoneToAodTransition(this)
+                listenForLockscreenAodTransitions(this)
             }
         }
     }
 
     @VisibleForTesting
+    suspend fun listenForGoneToAodTransition(scope: CoroutineScope): Job {
+        return scope.launch {
+            transitionInteractor.goneToAodTransition.collect { transitionStep ->
+                view.onDozeAmountChanged(
+                    transitionStep.value,
+                    transitionStep.value,
+                    ANIMATION_UNLOCKED_SCREEN_OFF,
+                )
+            }
+        }
+    }
+
+    @VisibleForTesting
+    suspend fun listenForLockscreenAodTransitions(scope: CoroutineScope): Job {
+        return scope.launch {
+            transitionInteractor.dozeAmountTransition.collect { transitionStep ->
+                  view.onDozeAmountChanged(
+                      transitionStep.value,
+                      transitionStep.value,
+                      UdfpsKeyguardViewLegacy.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN,
+                  )
+              }
+        }
+    }
+
+    @VisibleForTesting
     override suspend fun listenForBouncerExpansion(scope: CoroutineScope): Job {
         return scope.launch {
             primaryBouncerInteractor.bouncerExpansion.collect { bouncerExpansion: Float ->
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacy.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacy.java
index 95e3a76..f4ed8ce 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacy.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacy.java
@@ -73,9 +73,6 @@
     // AOD anti-burn-in offsets
     private final int mMaxBurnInOffsetX;
     private final int mMaxBurnInOffsetY;
-    private float mBurnInOffsetX;
-    private float mBurnInOffsetY;
-    private float mBurnInProgress;
     private float mInterpolatedDarkAmount;
     private int mAnimationType = ANIMATION_NONE;
     private boolean mFullyInflated;
@@ -138,20 +135,22 @@
         // AoD-burn in location, else we need to translate the icon from LS => AoD.
         final float darkAmountForAnimation = mAnimationType == ANIMATION_UNLOCKED_SCREEN_OFF
                 ? 1f : mInterpolatedDarkAmount;
-        mBurnInOffsetX = MathUtils.lerp(0f,
+        final float burnInOffsetX = MathUtils.lerp(0f,
             getBurnInOffset(mMaxBurnInOffsetX * 2, true /* xAxis */)
                 - mMaxBurnInOffsetX, darkAmountForAnimation);
-        mBurnInOffsetY = MathUtils.lerp(0f,
+        final float burnInOffsetY = MathUtils.lerp(0f,
             getBurnInOffset(mMaxBurnInOffsetY * 2, false /* xAxis */)
                 - mMaxBurnInOffsetY, darkAmountForAnimation);
-        mBurnInProgress = MathUtils.lerp(0f, getBurnInProgressOffset(), darkAmountForAnimation);
+        final float burnInProgress = MathUtils.lerp(0f, getBurnInProgressOffset(),
+                darkAmountForAnimation);
 
         if (mAnimationType == ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN && !mPauseAuth) {
-            mLockScreenFp.setTranslationX(mBurnInOffsetX);
-            mLockScreenFp.setTranslationY(mBurnInOffsetY);
+            mLockScreenFp.setTranslationX(burnInOffsetX);
+            mLockScreenFp.setTranslationY(burnInOffsetY);
             mBgProtection.setAlpha(1f - mInterpolatedDarkAmount);
             mLockScreenFp.setAlpha(1f - mInterpolatedDarkAmount);
         } else if (darkAmountForAnimation == 0f) {
+            // we're on the lockscreen and should use mAlpha (changes based on shade expansion)
             mLockScreenFp.setTranslationX(0);
             mLockScreenFp.setTranslationY(0);
             mBgProtection.setAlpha(mAlpha / 255f);
@@ -162,9 +161,9 @@
         }
         mLockScreenFp.setProgress(1f - mInterpolatedDarkAmount);
 
-        mAodFp.setTranslationX(mBurnInOffsetX);
-        mAodFp.setTranslationY(mBurnInOffsetY);
-        mAodFp.setProgress(mBurnInProgress);
+        mAodFp.setTranslationX(burnInOffsetX);
+        mAodFp.setTranslationY(burnInOffsetY);
+        mAodFp.setProgress(burnInProgress);
         mAodFp.setAlpha(mInterpolatedDarkAmount);
 
         // done animating
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/model/SimBouncerModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/model/SimBouncerModel.kt
new file mode 100644
index 0000000..5fc5101
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/model/SimBouncerModel.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.data.model
+
+/** Represents the locked sim card in the Bouncer. */
+data class SimBouncerModel(val isSimPukLocked: Boolean, val subscriptionId: Int)
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/model/SimPukInputModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/model/SimPukInputModel.kt
new file mode 100644
index 0000000..3cd88d6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/model/SimPukInputModel.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.data.model
+
+/**
+ * Represents the user flow for unlocking a PUK locked sim card.
+ *
+ * After entering the puk code, we need to enter and confirm a new pin code for the sim card.
+ */
+data class SimPukInputModel(
+    val enteredSimPuk: String? = null,
+    val enteredSimPin: String? = null,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepositoryModule.kt
new file mode 100644
index 0000000..ff6321c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepositoryModule.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.bouncer.data.repository
+
+import dagger.Binds
+import dagger.Module
+
+@Module
+interface BouncerRepositoryModule {
+    @Binds
+    fun provideSimRepository(simRepositoryImpl: SimBouncerRepositoryImpl): SimBouncerRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/SimBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/SimBouncerRepository.kt
new file mode 100644
index 0000000..269878b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/SimBouncerRepository.kt
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.data.repository
+
+import android.annotation.SuppressLint
+import android.content.IntentFilter
+import android.content.res.Resources
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyManager
+import android.telephony.euicc.EuiccManager
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.systemui.bouncer.data.model.SimBouncerModel
+import com.android.systemui.bouncer.data.model.SimPukInputModel
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxy
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+/** Handles data layer logic for locked sim cards. */
+interface SimBouncerRepository {
+    /** The subscription id of the current locked sim card. */
+    val subscriptionId: StateFlow<Int>
+    /** The active subscription of the current subscription id. */
+    val activeSubscriptionInfo: StateFlow<SubscriptionInfo?>
+    /**
+     * Determines if current sim card is an esim and is locked.
+     *
+     * A null value indicates that we do not know if we are esim locked or not.
+     */
+    val isLockedEsim: StateFlow<Boolean?>
+    /**
+     * Determines whether the current sim is locked requiring a PUK (Personal Unlocking Key) code.
+     */
+    val isSimPukLocked: StateFlow<Boolean>
+    /**
+     * The error message that should be displayed in an alert dialog.
+     *
+     * A null value indicates that the error dialog is not showing.
+     */
+    val errorDialogMessage: StateFlow<String?>
+    /** The state of the user flow on the SimPuk screen. */
+    val simPukInputModel: SimPukInputModel
+    /** Sets the state of the user flow on the SimPuk screen. */
+    fun setSimPukUserInput(enteredSimPuk: String? = null, enteredSimPin: String? = null)
+    /**
+     * Sets the error message when failing sim verification.
+     *
+     * A null value indicates that there is no error message to show.
+     */
+    fun setSimVerificationErrorMessage(msg: String?)
+}
+
+@SysUISingleton
+class SimBouncerRepositoryImpl
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    @Main resources: Resources,
+    keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    private val subscriptionManager: SubscriptionManagerProxy,
+    broadcastDispatcher: BroadcastDispatcher,
+    euiccManager: EuiccManager,
+) : SimBouncerRepository {
+    private val isPukScreenAvailable: Boolean =
+        resources.getBoolean(com.android.internal.R.bool.config_enable_puk_unlock_screen)
+
+    private val simBouncerModel: Flow<SimBouncerModel?> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : KeyguardUpdateMonitorCallback() {
+                        override fun onSimStateChanged(subId: Int, slotId: Int, simState: Int) {
+                            trySend(Unit)
+                        }
+                    }
+                keyguardUpdateMonitor.registerCallback(callback)
+                awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
+            }
+            .map {
+                // Check to see if there is a locked sim puk card.
+                val pukLockedSubId =
+                    withContext(backgroundDispatcher) {
+                        keyguardUpdateMonitor.getNextSubIdForState(
+                            TelephonyManager.SIM_STATE_PUK_REQUIRED
+                        )
+                    }
+                if (
+                    isPukScreenAvailable &&
+                        subscriptionManager.isValidSubscriptionId(pukLockedSubId)
+                ) {
+                    return@map (SimBouncerModel(isSimPukLocked = true, pukLockedSubId))
+                }
+
+                // If there is no locked sim puk card, check to see if there is a locked sim card.
+                val pinLockedSubId =
+                    withContext(backgroundDispatcher) {
+                        keyguardUpdateMonitor.getNextSubIdForState(
+                            TelephonyManager.SIM_STATE_PIN_REQUIRED
+                        )
+                    }
+                if (subscriptionManager.isValidSubscriptionId(pinLockedSubId)) {
+                    return@map SimBouncerModel(isSimPukLocked = false, pinLockedSubId)
+                }
+
+                return@map null // There is no locked sim.
+            }
+
+    override val subscriptionId: StateFlow<Int> =
+        simBouncerModel
+            .map { state -> state?.subscriptionId ?: INVALID_SUBSCRIPTION_ID }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = INVALID_SUBSCRIPTION_ID,
+            )
+
+    @SuppressLint("MissingPermission")
+    override val activeSubscriptionInfo: StateFlow<SubscriptionInfo?> =
+        subscriptionId
+            .map {
+                withContext(backgroundDispatcher) {
+                    subscriptionManager.getActiveSubscriptionInfo(it)
+                }
+            }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.Eagerly,
+                initialValue = null,
+            )
+
+    @SuppressLint("MissingPermission")
+    override val isLockedEsim: StateFlow<Boolean?> =
+        activeSubscriptionInfo
+            .map { info -> info?.let { euiccManager.isEnabled && info.isEmbedded } }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.Eagerly,
+                initialValue = null,
+            )
+
+    override val isSimPukLocked: StateFlow<Boolean> =
+        simBouncerModel
+            .map { it?.isSimPukLocked == true }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.Eagerly,
+                initialValue = false,
+            )
+
+    private val disableEsimErrorMessage: Flow<String?> =
+        broadcastDispatcher.broadcastFlow(filter = IntentFilter(ACTION_DISABLE_ESIM)) { _, receiver
+            ->
+            if (receiver.resultCode != EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_OK) {
+                resources.getString(R.string.error_disable_esim_msg)
+            } else {
+                null
+            }
+        }
+
+    private val simVerificationErrorMessage: MutableStateFlow<String?> = MutableStateFlow(null)
+
+    override val errorDialogMessage: StateFlow<String?> =
+        merge(disableEsimErrorMessage, simVerificationErrorMessage)
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = null,
+            )
+
+    private var _simPukInputModel: SimPukInputModel = SimPukInputModel()
+    override val simPukInputModel: SimPukInputModel
+        get() = _simPukInputModel
+
+    override fun setSimPukUserInput(enteredSimPuk: String?, enteredSimPin: String?) {
+        _simPukInputModel = SimPukInputModel(enteredSimPuk, enteredSimPin)
+    }
+
+    override fun setSimVerificationErrorMessage(msg: String?) {
+        simVerificationErrorMessage.value = msg
+    }
+
+    companion object {
+        const val ACTION_DISABLE_ESIM = "com.android.keyguard.disable_esim"
+        const val INVALID_SUBSCRIPTION_ID = SubscriptionManager.INVALID_SUBSCRIPTION_ID
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
index 138a76c..d5ac483 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
@@ -54,6 +54,7 @@
     flags: SceneContainerFlags,
     private val falsingInteractor: FalsingInteractor,
     private val powerInteractor: PowerInteractor,
+    private val simBouncerInteractor: SimBouncerInteractor,
 ) {
 
     /** The user-facing message to show in the bouncer. */
@@ -148,6 +149,10 @@
         )
     }
 
+    fun setMessage(message: String?) {
+        repository.setMessage(message)
+    }
+
     /**
      * Resets the user-facing message back to the default according to the current authentication
      * method.
@@ -186,6 +191,12 @@
         if (input.isEmpty()) {
             return AuthenticationResult.SKIPPED
         }
+
+        if (authenticationInteractor.getAuthenticationMethod() == AuthenticationMethodModel.Sim) {
+            // We authenticate sim in SimInteractor
+            return AuthenticationResult.SKIPPED
+        }
+
         // Switching to the application scope here since this method is often called from
         // view-models, whose lifecycle (and thus scope) is shorter than this interactor.
         // This allows the task to continue running properly even when the calling scope has been
@@ -223,6 +234,7 @@
 
     private fun promptMessage(authMethod: AuthenticationMethodModel): String {
         return when (authMethod) {
+            is AuthenticationMethodModel.Sim -> simBouncerInteractor.getDefaultMessage()
             is AuthenticationMethodModel.Pin ->
                 applicationContext.getString(R.string.keyguard_enter_your_pin)
             is AuthenticationMethodModel.Password ->
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorModule.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorModule.kt
index e398c93..efa7792 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorModule.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.content.Intent
 import android.telecom.TelecomManager
+import android.telephony.euicc.EuiccManager
 import com.android.internal.util.EmergencyAffordanceManager
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -47,4 +48,9 @@
     ): EmergencyAffordanceManager {
         return EmergencyAffordanceManager(applicationContext)
     }
+
+    @Provides
+    fun provideEuiccManager(@Application applicationContext: Context): EuiccManager {
+        return applicationContext.getSystemService(Context.EUICC_SERVICE) as EuiccManager
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt
new file mode 100644
index 0000000..99d1f13
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.domain.interactor
+
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import android.os.UserHandle
+import android.telephony.PinResult
+import android.telephony.SubscriptionInfo
+import android.telephony.TelephonyManager
+import android.telephony.euicc.EuiccManager
+import android.text.TextUtils
+import android.util.Log
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.bouncer.data.repository.SimBouncerRepository
+import com.android.systemui.bouncer.data.repository.SimBouncerRepositoryImpl
+import com.android.systemui.bouncer.data.repository.SimBouncerRepositoryImpl.Companion.ACTION_DISABLE_ESIM
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
+import com.android.systemui.util.icuMessageFormat
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/** Handles domain layer logic for locked sim cards. */
+@SuppressLint("WrongConstant")
+@SysUISingleton
+class SimBouncerInteractor
+@Inject
+constructor(
+    @Application private val applicationContext: Context,
+    @Application private val applicationScope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val repository: SimBouncerRepository,
+    private val telephonyManager: TelephonyManager,
+    @Main private val resources: Resources,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    private val euiccManager: EuiccManager,
+    // TODO(b/307977401): Replace this with `MobileConnectionsInteractor` when available.
+    mobileConnectionsRepository: MobileConnectionsRepository,
+) {
+    val subId: StateFlow<Int> = repository.subscriptionId
+    val isAnySimSecure: Flow<Boolean> = mobileConnectionsRepository.isAnySimSecure
+    val isLockedEsim: StateFlow<Boolean?> = repository.isLockedEsim
+    val errorDialogMessage: StateFlow<String?> = repository.errorDialogMessage
+
+    /** Returns the default message for the sim pin screen. */
+    fun getDefaultMessage(): String {
+        val isEsimLocked = repository.isLockedEsim.value ?: false
+        val isPuk: Boolean = repository.isSimPukLocked.value
+        val subscriptionId = repository.subscriptionId.value
+
+        if (subscriptionId == INVALID_SUBSCRIPTION_ID) {
+            Log.e(TAG, "Trying to get default message from unknown sub id")
+            return ""
+        }
+
+        var count = telephonyManager.activeModemCount
+        val info: SubscriptionInfo? = repository.activeSubscriptionInfo.value
+        val displayName = info?.displayName
+        var msg: String =
+            when {
+                count < 2 && isPuk -> resources.getString(R.string.kg_puk_enter_puk_hint)
+                count < 2 -> resources.getString(R.string.kg_sim_pin_instructions)
+                else -> {
+                    when {
+                        !TextUtils.isEmpty(displayName) && isPuk ->
+                            resources.getString(R.string.kg_puk_enter_puk_hint_multi, displayName)
+                        !TextUtils.isEmpty(displayName) ->
+                            resources.getString(R.string.kg_sim_pin_instructions_multi, displayName)
+                        isPuk -> resources.getString(R.string.kg_puk_enter_puk_hint)
+                        else -> resources.getString(R.string.kg_sim_pin_instructions)
+                    }
+                }
+            }
+
+        if (isEsimLocked) {
+            msg = resources.getString(R.string.kg_sim_lock_esim_instructions, msg)
+        }
+
+        return msg
+    }
+
+    /** Resets the user flow when the sim screen is puk locked. */
+    fun resetSimPukUserInput() {
+        repository.setSimPukUserInput()
+        // Force a garbage collection in an attempt to erase any sim pin or sim puk codes left in
+        // memory. Do it asynchronously with a 5-sec delay to avoid making the keyguard
+        // dismiss animation janky.
+
+        applicationScope.launch(backgroundDispatcher) {
+            delay(5000)
+            System.gc()
+            System.runFinalization()
+            System.gc()
+        }
+    }
+
+    /** Disables the locked esim card so user can bypass the sim pin screen. */
+    fun disableEsim() {
+        val activeSubscription = repository.activeSubscriptionInfo.value
+        if (activeSubscription == null) {
+            val subId = repository.subscriptionId.value
+            Log.e(TAG, "No active subscription with subscriptionId: $subId")
+            return
+        }
+        val intent = Intent(ACTION_DISABLE_ESIM)
+        intent.setPackage(applicationContext.packageName)
+        val callbackIntent =
+            PendingIntent.getBroadcastAsUser(
+                applicationContext,
+                0 /* requestCode */,
+                intent,
+                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE_UNAUDITED,
+                UserHandle.SYSTEM
+            )
+        applicationScope.launch(backgroundDispatcher) {
+            euiccManager.switchToSubscription(
+                INVALID_SUBSCRIPTION_ID,
+                activeSubscription.portIndex,
+                callbackIntent,
+            )
+        }
+    }
+
+    /** Update state when error dialog is dismissed by the user. */
+    fun onErrorDialogDismissed() {
+        repository.setSimVerificationErrorMessage(null)
+    }
+
+    /**
+     * Based on sim state, unlock the locked sim with the given credentials.
+     *
+     * @return Any message that should show associated with the provided input. Null means that no
+     *   message needs to be shown.
+     */
+    suspend fun verifySim(input: List<Any>): String? {
+        if (repository.isSimPukLocked.value) {
+            return verifySimPuk(input.joinToString(separator = ""))
+        }
+
+        return verifySimPin(input.joinToString(separator = ""))
+    }
+
+    /**
+     * Verifies the input and unlocks the locked sim with a 4-8 digit pin code.
+     *
+     * @return Any message that should show associated with the provided input. Null means that no
+     *   message needs to be shown.
+     */
+    private suspend fun verifySimPin(input: String): String? {
+        val subscriptionId = repository.subscriptionId.value
+        // A SIM PIN is 4 to 8 decimal digits according to
+        // GSM 02.17 version 5.0.1, Section 5.6 PIN Management
+        if (input.length < MIN_SIM_PIN_LENGTH || input.length > MAX_SIM_PIN_LENGTH) {
+            return resources.getString(R.string.kg_invalid_sim_pin_hint)
+        }
+        val result =
+            withContext(backgroundDispatcher) {
+                val telephonyManager: TelephonyManager =
+                    telephonyManager.createForSubscriptionId(subscriptionId)
+                telephonyManager.supplyIccLockPin(input)
+            }
+        when (result.result) {
+            PinResult.PIN_RESULT_TYPE_SUCCESS ->
+                keyguardUpdateMonitor.reportSimUnlocked(subscriptionId)
+            PinResult.PIN_RESULT_TYPE_INCORRECT -> {
+                if (result.attemptsRemaining <= CRITICAL_NUM_OF_ATTEMPTS) {
+                    // Show a dialog to display the remaining number of attempts to verify the sim
+                    // pin to the user.
+                    repository.setSimVerificationErrorMessage(
+                        getPinPasswordErrorMessage(result.attemptsRemaining)
+                    )
+                } else {
+                    return getPinPasswordErrorMessage(result.attemptsRemaining)
+                }
+            }
+        }
+
+        return null
+    }
+
+    /**
+     * Verifies the input and unlocks the locked sim with a puk code instead of pin.
+     *
+     * This occurs after incorrectly verifying the sim pin multiple times.
+     *
+     * @return Any message that should show associated with the provided input. Null means that no
+     *   message needs to be shown.
+     */
+    private suspend fun verifySimPuk(entry: String): String? {
+        val (enteredSimPuk, enteredSimPin) = repository.simPukInputModel
+        val subscriptionId: Int = repository.subscriptionId.value
+
+        // Stage 1: Enter the sim puk code of the sim card.
+        if (enteredSimPuk == null) {
+            if (entry.length >= MIN_SIM_PUK_LENGTH) {
+                repository.setSimPukUserInput(enteredSimPuk = entry)
+                return resources.getString(R.string.kg_puk_enter_pin_hint)
+            } else {
+                return resources.getString(R.string.kg_invalid_sim_puk_hint)
+            }
+        }
+
+        // Stage 2: Set a new sim pin to lock the sim card.
+        if (enteredSimPin == null) {
+            if (entry.length in MIN_SIM_PIN_LENGTH..MAX_SIM_PIN_LENGTH) {
+                repository.setSimPukUserInput(
+                    enteredSimPuk = enteredSimPuk,
+                    enteredSimPin = entry,
+                )
+                return resources.getString(R.string.kg_enter_confirm_pin_hint)
+            } else {
+                return resources.getString(R.string.kg_invalid_sim_pin_hint)
+            }
+        }
+
+        // Stage 3: Confirm the newly set sim pin.
+        if (repository.simPukInputModel.enteredSimPin != entry) {
+            // The entered sim pins do not match. Enter desired sim pin again to confirm.
+            repository.setSimVerificationErrorMessage(
+                resources.getString(R.string.kg_invalid_confirm_pin_hint)
+            )
+            repository.setSimPukUserInput(enteredSimPuk = enteredSimPuk)
+            return resources.getString(R.string.kg_puk_enter_pin_hint)
+        }
+
+        val result =
+            withContext(backgroundDispatcher) {
+                val telephonyManager = telephonyManager.createForSubscriptionId(subscriptionId)
+                telephonyManager.supplyIccLockPuk(enteredSimPuk, enteredSimPin)
+            }
+        resetSimPukUserInput()
+
+        when (result.result) {
+            PinResult.PIN_RESULT_TYPE_SUCCESS ->
+                keyguardUpdateMonitor.reportSimUnlocked(subscriptionId)
+            PinResult.PIN_RESULT_TYPE_INCORRECT ->
+                if (result.attemptsRemaining <= CRITICAL_NUM_OF_ATTEMPTS) {
+                    // Show a dialog to display the remaining number of attempts to verify the sim
+                    // puk to the user.
+                    repository.setSimVerificationErrorMessage(
+                        getPukPasswordErrorMessage(
+                            result.attemptsRemaining,
+                            isDefault = false,
+                            isEsimLocked = repository.isLockedEsim.value == true
+                        )
+                    )
+                } else {
+                    return getPukPasswordErrorMessage(
+                        result.attemptsRemaining,
+                        isDefault = false,
+                        isEsimLocked = repository.isLockedEsim.value == true
+                    )
+                }
+            else -> return resources.getString(R.string.kg_password_puk_failed)
+        }
+
+        return null
+    }
+
+    private fun getPinPasswordErrorMessage(attemptsRemaining: Int): String {
+        var displayMessage: String =
+            if (attemptsRemaining == 0) {
+                resources.getString(R.string.kg_password_wrong_pin_code_pukked)
+            } else if (attemptsRemaining > 0) {
+                val msgId = R.string.kg_password_default_pin_message
+                icuMessageFormat(resources, msgId, attemptsRemaining)
+            } else {
+                val msgId = R.string.kg_sim_pin_instructions
+                resources.getString(msgId)
+            }
+        if (repository.isLockedEsim.value == true) {
+            displayMessage =
+                resources.getString(R.string.kg_sim_lock_esim_instructions, displayMessage)
+        }
+        return displayMessage
+    }
+
+    private fun getPukPasswordErrorMessage(
+        attemptsRemaining: Int,
+        isDefault: Boolean,
+        isEsimLocked: Boolean,
+    ): String {
+        var displayMessage: String =
+            if (attemptsRemaining == 0) {
+                resources.getString(R.string.kg_password_wrong_puk_code_dead)
+            } else if (attemptsRemaining > 0) {
+                val msgId =
+                    if (isDefault) R.string.kg_password_default_puk_message
+                    else R.string.kg_password_wrong_puk_code
+                icuMessageFormat(resources, msgId, attemptsRemaining)
+            } else {
+                val msgId =
+                    if (isDefault) R.string.kg_puk_enter_puk_hint
+                    else R.string.kg_password_puk_failed
+                resources.getString(msgId)
+            }
+        if (isEsimLocked) {
+            displayMessage =
+                resources.getString(R.string.kg_sim_lock_esim_instructions, displayMessage)
+        }
+        return displayMessage
+    }
+
+    companion object {
+        private const val TAG = "BouncerSimInteractor"
+        const val INVALID_SUBSCRIPTION_ID = SimBouncerRepositoryImpl.INVALID_SUBSCRIPTION_ID
+        const val MIN_SIM_PIN_LENGTH = 4
+        const val MAX_SIM_PIN_LENGTH = 8
+        const val MIN_SIM_PUK_LENGTH = 8
+        const val CRITICAL_NUM_OF_ATTEMPTS = 2
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
index f46574c..8024874 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
@@ -62,6 +62,13 @@
 
     /** Notifies that the UI has been shown to the user. */
     fun onShown() {
+        interactor.resetMessage()
+    }
+
+    /**
+     * Notifies that the UI has been hidden from the user (after any transitions have completed).
+     */
+    fun onHidden() {
         clearInput()
         interactor.resetMessage()
     }
@@ -113,8 +120,6 @@
             }
             _animateFailure.value = authenticationResult != AuthenticationResult.SUCCEEDED
 
-            // TODO(b/291528545): On success, this should only be cleared after the view is animated
-            //  away).
             clearInput()
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
index 09c94c8..44ddd97 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
 import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
@@ -64,6 +65,7 @@
     users: Flow<List<UserViewModel>>,
     userSwitcherMenu: Flow<List<UserActionViewModel>>,
     actionButtonInteractor: BouncerActionButtonInteractor,
+    private val simBouncerInteractor: SimBouncerInteractor,
 ) {
     val selectedUserImage: StateFlow<Bitmap?> =
         selectedUser
@@ -259,6 +261,17 @@
                     viewModelScope = newViewModelScope,
                     interactor = bouncerInteractor,
                     isInputEnabled = isInputEnabled,
+                    simBouncerInteractor = simBouncerInteractor,
+                    authenticationMethod = authenticationMethod
+                )
+            is AuthenticationMethodModel.Sim ->
+                PinBouncerViewModel(
+                    applicationContext = applicationContext,
+                    viewModelScope = newViewModelScope,
+                    interactor = bouncerInteractor,
+                    isInputEnabled = isInputEnabled,
+                    simBouncerInteractor = simBouncerInteractor,
+                    authenticationMethod = authenticationMethod,
                 )
             is AuthenticationMethodModel.Password ->
                 PasswordBouncerViewModel(
@@ -316,6 +329,7 @@
         flags: SceneContainerFlags,
         userSwitcherViewModel: UserSwitcherViewModel,
         actionButtonInteractor: BouncerActionButtonInteractor,
+        simBouncerInteractor: SimBouncerInteractor,
     ): BouncerViewModel {
         return BouncerViewModel(
             applicationContext = applicationContext,
@@ -328,6 +342,7 @@
             users = userSwitcherViewModel.users,
             userSwitcherMenu = userSwitcherViewModel.menu,
             actionButtonInteractor = actionButtonInteractor,
+            simBouncerInteractor = simBouncerInteractor,
         )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index b2b8049..e25e82f 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -14,20 +14,26 @@
  * limitations under the License.
  */
 
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
 package com.android.systemui.bouncer.ui.viewmodel
 
 import android.content.Context
 import com.android.keyguard.PinShapeAdapter
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
 import com.android.systemui.res.R
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
 
 /** Holds UI state and handles user input for the PIN code bouncer UI. */
 class PinBouncerViewModel(
@@ -35,13 +41,23 @@
     viewModelScope: CoroutineScope,
     interactor: BouncerInteractor,
     isInputEnabled: StateFlow<Boolean>,
+    private val simBouncerInteractor: SimBouncerInteractor,
+    authenticationMethod: AuthenticationMethodModel,
 ) :
     AuthMethodBouncerViewModel(
         viewModelScope = viewModelScope,
         interactor = interactor,
         isInputEnabled = isInputEnabled,
     ) {
-
+    /**
+     * Whether the sim-related UI in the pin view is showing.
+     *
+     * This UI is used to unlock a locked sim.
+     */
+    val isSimAreaVisible = authenticationMethod == AuthenticationMethodModel.Sim
+    val isLockedEsim: StateFlow<Boolean?> = simBouncerInteractor.isLockedEsim
+    val errorDialogMessage: StateFlow<String?> = simBouncerInteractor.errorDialogMessage
+    val isSimUnlockingDialogVisible: MutableStateFlow<Boolean> = MutableStateFlow(false)
     val pinShapes = PinShapeAdapter(applicationContext)
     private val mutablePinInput = MutableStateFlow(PinInputViewModel.empty())
 
@@ -49,7 +65,13 @@
     val pinInput: StateFlow<PinInputViewModel> = mutablePinInput
 
     /** The length of the PIN for which we should show a hint. */
-    val hintedPinLength: StateFlow<Int?> = interactor.hintedPinLength
+    val hintedPinLength: StateFlow<Int?> =
+        if (isSimAreaVisible) {
+                flowOf(null)
+            } else {
+                interactor.hintedPinLength
+            }
+            .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
 
     /** Appearance of the backspace button. */
     val backspaceButtonAppearance: StateFlow<ActionButtonAppearance> =
@@ -80,10 +102,19 @@
                 initialValue = ActionButtonAppearance.Hidden,
             )
 
-    override val authenticationMethod = AuthenticationMethodModel.Pin
+    override val authenticationMethod: AuthenticationMethodModel = authenticationMethod
 
     override val throttlingMessageId = R.string.kg_too_many_failed_pin_attempts_dialog_message
 
+    init {
+        viewModelScope.launch { simBouncerInteractor.subId.collect { onResetSimFlow() } }
+    }
+
+    /** Notifies that the user dismissed the sim pin error dialog. */
+    fun onErrorDialogDismissed() {
+        viewModelScope.launch { simBouncerInteractor.onErrorDialogDismissed() }
+    }
+
     /**
      * Whether the digit buttons should be animated when touched. Note that this doesn't affect the
      * delete or enter buttons; those should always animate.
@@ -123,7 +154,28 @@
 
     /** Notifies that the user clicked the "enter" button. */
     fun onAuthenticateButtonClicked() {
-        tryAuthenticate(useAutoConfirm = false)
+        if (authenticationMethod == AuthenticationMethodModel.Sim) {
+            viewModelScope.launch {
+                isSimUnlockingDialogVisible.value = true
+                val msg = simBouncerInteractor.verifySim(getInput())
+                interactor.setMessage(msg)
+                isSimUnlockingDialogVisible.value = false
+                clearInput()
+            }
+        } else {
+            tryAuthenticate(useAutoConfirm = false)
+        }
+    }
+
+    fun onDisableEsimButtonClicked() {
+        viewModelScope.launch { simBouncerInteractor.disableEsim() }
+    }
+
+    /** Resets the sim screen and shows a default message. */
+    private fun onResetSimFlow() {
+        simBouncerInteractor.resetSimPukUserInput()
+        interactor.resetMessage()
+        clearInput()
     }
 
     override fun clearInput() {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
index 273adcf..847b98e 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
@@ -16,12 +16,17 @@
 
 package com.android.systemui.communal.dagger
 
+import android.content.Context
 import com.android.systemui.communal.data.db.CommunalDatabaseModule
 import com.android.systemui.communal.data.repository.CommunalMediaRepositoryModule
 import com.android.systemui.communal.data.repository.CommunalRepositoryModule
 import com.android.systemui.communal.data.repository.CommunalTutorialRepositoryModule
 import com.android.systemui.communal.data.repository.CommunalWidgetRepositoryModule
+import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
+import com.android.systemui.communal.widgets.EditWidgetsActivityStarterImpl
+import com.android.systemui.dagger.qualifiers.Application
 import dagger.Module
+import dagger.Provides
 
 @Module(
     includes =
@@ -33,4 +38,11 @@
             CommunalDatabaseModule::class,
         ]
 )
-class CommunalModule
+class CommunalModule {
+    @Provides
+    fun provideEditWidgetsActivityStarter(
+        @Application context: Context
+    ): EditWidgetsActivityStarter {
+        return EditWidgetsActivityStarterImpl(context)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 771dfbc..7391a5e 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.communal.domain.model.CommunalContentModel
 import com.android.systemui.communal.shared.model.CommunalContentSize
 import com.android.systemui.communal.shared.model.CommunalSceneKey
+import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.smartspace.data.repository.SmartspaceRepository
 import javax.inject.Inject
@@ -48,6 +49,7 @@
     smartspaceRepository: SmartspaceRepository,
     tutorialInteractor: CommunalTutorialInteractor,
     private val appWidgetHost: AppWidgetHost,
+    private val editWidgetsActivityStarter: EditWidgetsActivityStarter
 ) {
 
     /** Whether communal features are enabled. */
@@ -72,6 +74,11 @@
         communalRepository.setDesiredScene(newScene)
     }
 
+    /** Show the widget editor Activity. */
+    fun showWidgetEditor() {
+        editWidgetsActivityStarter.startActivity()
+    }
+
     /** Add a widget at the specified position. */
     fun addWidget(componentName: ComponentName, priority: Int) =
         widgetRepository.addWidget(componentName, priority)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index 5efe6ce..14edc8e 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.communal.ui.viewmodel
 
-import android.content.ComponentName
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.domain.model.CommunalContentModel
 import com.android.systemui.communal.shared.model.CommunalSceneKey
@@ -46,19 +45,6 @@
     /** Delete a widget by id. */
     fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id)
 
-    /** Open the widget picker */
-    fun onOpenWidgetPicker() {
-        // STOPSHIP(b/306500486): refactor this when integrating with the widget picker.
-        // Eventually clicking on this button will bring up the widget picker and inside
-        // the widget picker, addWidget will be called to add the user selected widget.
-        // For now, a stopwatch widget will be added to the end of the grid.
-        communalInteractor.addWidget(
-            componentName =
-                ComponentName(
-                    "com.google.android.deskclock",
-                    "com.android.alarmclock.StopwatchAppWidgetProvider"
-                ),
-            priority = 0
-        )
-    }
+    /** Open the widget editor */
+    fun onOpenWidgetEditor() = communalInteractor.showWidgetEditor()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
new file mode 100644
index 0000000..78e85db
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.widgets
+
+import android.appwidget.AppWidgetProviderInfo
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import android.view.View
+import androidx.activity.ComponentActivity
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+/** An Activity for editing the widgets that appear in hub mode. */
+class EditWidgetsActivity @Inject constructor(private val communalInteractor: CommunalInteractor) :
+    ComponentActivity() {
+    companion object {
+        /**
+         * Intent extra name for the {@link AppWidgetProviderInfo} of a widget to add to hub mode.
+         */
+        const val ADD_WIDGET_INFO = "add_widget_info"
+        private const val TAG = "EditWidgetsActivity"
+    }
+
+    private val addWidgetActivityLauncher: ActivityResultLauncher<Intent> =
+        registerForActivityResult(StartActivityForResult()) { result ->
+            when (result.resultCode) {
+                RESULT_OK -> {
+                    result.data
+                        ?.let {
+                            it.getParcelableExtra(
+                                ADD_WIDGET_INFO,
+                                AppWidgetProviderInfo::class.java
+                            )
+                        }
+                        ?.let { communalInteractor.addWidget(it.provider, 0) }
+                        ?: run { Log.w(TAG, "No AppWidgetProviderInfo found in result.") }
+                }
+                else ->
+                    Log.w(
+                        TAG,
+                        "Failed to receive result from widget picker, code=${result.resultCode}"
+                    )
+            }
+            this@EditWidgetsActivity.finish()
+        }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        setShowWhenLocked(true)
+        setContentView(R.layout.edit_widgets)
+
+        val addWidgetsButton = findViewById<View>(R.id.add_widget)
+        addWidgetsButton?.setOnClickListener({
+            addWidgetActivityLauncher.launch(
+                Intent(applicationContext, WidgetPickerActivity::class.java)
+            )
+        })
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarter.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarter.kt
new file mode 100644
index 0000000..846e300
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarter.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.widgets
+
+import android.content.Context
+import android.content.Intent
+import com.android.systemui.dagger.qualifiers.Application
+
+interface EditWidgetsActivityStarter {
+    fun startActivity()
+}
+
+class EditWidgetsActivityStarterImpl(@Application private val applicationContext: Context) :
+    EditWidgetsActivityStarter {
+    override fun startActivity() {
+        applicationContext.startActivity(
+            Intent(applicationContext, EditWidgetsActivity::class.java)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetPickerActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetPickerActivity.kt
new file mode 100644
index 0000000..3e6dbd5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetPickerActivity.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.widgets
+
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProviderInfo
+import android.content.Intent
+import android.os.Bundle
+import android.util.DisplayMetrics
+import android.util.Log
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.LinearLayout
+import androidx.activity.ComponentActivity
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+/**
+ * An Activity responsible for displaying a list of widgets to add to the hub mode grid. This is
+ * essentially a placeholder until Launcher's widget picker can be used.
+ */
+class WidgetPickerActivity
+@Inject
+constructor(
+    private val appWidgetManager: AppWidgetManager,
+) : ComponentActivity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        setContentView(R.layout.widget_picker)
+        setShowWhenLocked(true)
+
+        loadWidgets()
+    }
+
+    private fun loadWidgets() {
+        val containerView: ViewGroup? = findViewById(R.id.widgets_container)
+        containerView?.apply {
+            try {
+                appWidgetManager
+                    .getInstalledProviders(AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD)
+                    ?.stream()
+                    ?.limit(5)
+                    ?.forEach { widgetInfo ->
+                        val activity = this@WidgetPickerActivity
+                        val widgetPreview =
+                            widgetInfo.loadPreviewImage(activity, DisplayMetrics.DENSITY_HIGH)
+                        val widgetView = ImageView(activity)
+                        val lp = LinearLayout.LayoutParams(WIDGET_PREVIEW_SIZE, WIDGET_PREVIEW_SIZE)
+                        widgetView.setLayoutParams(lp)
+                        widgetView.setImageDrawable(widgetPreview)
+                        widgetView.setOnClickListener({
+                            setResult(
+                                RESULT_OK,
+                                Intent().putExtra(EditWidgetsActivity.ADD_WIDGET_INFO, widgetInfo)
+                            )
+                            finish()
+                        })
+
+                        addView(widgetView)
+                    }
+            } catch (e: RuntimeException) {
+                Log.e(TAG, "Exception fetching widget providers", e)
+            }
+        }
+    }
+
+    companion object {
+        private const val WIDGET_PREVIEW_SIZE = 400
+        private const val TAG = "WidgetPickerActivity"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
index 32e40c9..4b27af1 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
@@ -19,6 +19,8 @@
 import android.app.Activity;
 
 import com.android.systemui.ForegroundServicesDialog;
+import com.android.systemui.communal.widgets.EditWidgetsActivity;
+import com.android.systemui.communal.widgets.WidgetPickerActivity;
 import com.android.systemui.contrast.ContrastDialogActivity;
 import com.android.systemui.keyguard.WorkLockActivity;
 import com.android.systemui.people.PeopleSpaceActivity;
@@ -150,7 +152,17 @@
     @ClassKey(SensorUseStartedActivity.class)
     public abstract Activity bindSensorUseStartedActivity(SensorUseStartedActivity activity);
 
+    /** Inject into EditWidgetsActivity. */
+    @Binds
+    @IntoMap
+    @ClassKey(EditWidgetsActivity.class)
+    public abstract Activity bindEditWidgetsActivity(EditWidgetsActivity activity);
 
+    /** Inject into WidgetPickerActivity. */
+    @Binds
+    @IntoMap
+    @ClassKey(WidgetPickerActivity.class)
+    public abstract Activity bindWidgetPickerActivity(WidgetPickerActivity activity);
 
     /** Inject into SwitchToManagedProfileForCallActivity. */
     @Binds
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index d8ff535..a0e944b 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -22,7 +22,7 @@
 import com.android.systemui.ScreenDecorations
 import com.android.systemui.SliceBroadcastRelayHandler
 import com.android.systemui.accessibility.SystemActions
-import com.android.systemui.accessibility.WindowMagnification
+import com.android.systemui.accessibility.Magnification
 import com.android.systemui.back.domain.interactor.BackActionInteractor
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.biometrics.BiometricNotificationService
@@ -254,11 +254,11 @@
     @ClassKey(VolumeUI::class)
     abstract fun bindVolumeUI(sysui: VolumeUI): CoreStartable
 
-    /** Inject into WindowMagnification.  */
+    /** Inject into Magnification.  */
     @Binds
     @IntoMap
-    @ClassKey(WindowMagnification::class)
-    abstract fun bindWindowMagnification(sysui: WindowMagnification): CoreStartable
+    @ClassKey(Magnification::class)
+    abstract fun bindMagnification(sysui: Magnification): CoreStartable
 
     /** Inject into WMShell.  */
     @Binds
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 1dcc540..5f54a98 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -37,13 +37,14 @@
 import com.android.systemui.biometrics.UdfpsDisplayModeProvider;
 import com.android.systemui.biometrics.dagger.BiometricsModule;
 import com.android.systemui.biometrics.domain.BiometricsDomainLayerModule;
+import com.android.systemui.bouncer.data.repository.BouncerRepositoryModule;
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractorModule;
 import com.android.systemui.bouncer.ui.BouncerViewModule;
 import com.android.systemui.classifier.FalsingModule;
 import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule;
+import com.android.systemui.common.CommonModule;
 import com.android.systemui.communal.dagger.CommunalModule;
 import com.android.systemui.complication.dagger.ComplicationComponent;
-import com.android.systemui.common.CommonModule;
 import com.android.systemui.controls.dagger.ControlsModule;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.SystemUser;
@@ -129,6 +130,7 @@
 import com.android.systemui.unfold.SysUIUnfoldModule;
 import com.android.systemui.user.UserModule;
 import com.android.systemui.user.domain.UserDomainLayerModule;
+import com.android.systemui.util.EventLogModule;
 import com.android.systemui.util.concurrency.SysUIConcurrencyModule;
 import com.android.systemui.util.dagger.UtilModule;
 import com.android.systemui.util.kotlin.CoroutinesModule;
@@ -171,6 +173,7 @@
         BiometricsModule.class,
         BiometricsDomainLayerModule.class,
         BouncerInteractorModule.class,
+        BouncerRepositoryModule.class,
         BouncerViewModule.class,
         ClipboardOverlayModule.class,
         ClockRegistryModule.class,
@@ -184,6 +187,7 @@
         DisableFlagsModule.class,
         DisplayModule.class,
         DreamModule.class,
+        EventLogModule.class,
         FalsingModule.class,
         FlagsModule.class,
         FlagDependenciesModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
index 298811b..715fb17 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
@@ -74,7 +74,8 @@
                 repository.isUnlocked,
                 authenticationInteractor.authenticationMethod,
             ) { isUnlocked, authenticationMethod ->
-                !authenticationMethod.isSecure || isUnlocked
+                (!authenticationMethod.isSecure || isUnlocked) &&
+                    authenticationMethod != AuthenticationMethodModel.Sim
             }
             .stateIn(
                 scope = applicationScope,
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/shared/DeviceEntryUdfpsRefactor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/shared/DeviceEntryUdfpsRefactor.kt
new file mode 100644
index 0000000..b5d5803
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/shared/DeviceEntryUdfpsRefactor.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.deviceentry.shared
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the device entry udfps refactor flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object DeviceEntryUdfpsRefactor {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR
+
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
+    /** Is the refactor enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.deviceEntryUdfpsRefactor()
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/HideComplicationTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/HideComplicationTouchHandler.java
index 410a0c5..ee48ee5 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/HideComplicationTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/HideComplicationTouchHandler.java
@@ -71,6 +71,7 @@
     private final Runnable mRestoreComplications = new Runnable() {
         @Override
         public void run() {
+            Log.d(TAG, "Restoring complications...");
             mVisibilityController.setVisibility(View.VISIBLE);
             mHidden = false;
         }
@@ -83,6 +84,7 @@
                 // Avoid interfering with the exit animations.
                 return;
             }
+            Log.d(TAG, "Hiding complications...");
             mVisibilityController.setVisibility(View.INVISIBLE);
             mHidden = true;
             if (mHiddenCallback != null) {
@@ -136,9 +138,7 @@
             final MotionEvent motionEvent = (MotionEvent) ev;
 
             if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
-                if (DEBUG) {
-                    Log.d(TAG, "ACTION_DOWN received");
-                }
+                Log.i(TAG, "ACTION_DOWN received");
 
                 final ListenableFuture<Boolean> touchCheck = mTouchInsetManager
                         .checkWithinTouchRegion(Math.round(motionEvent.getX()),
@@ -163,6 +163,8 @@
                 }, mExecutor);
             } else if (motionEvent.getAction() == MotionEvent.ACTION_CANCEL
                     || motionEvent.getAction() == MotionEvent.ACTION_UP) {
+                Log.i(TAG, "ACTION_CANCEL|ACTION_UP received");
+
                 // End session and initiate delayed reappearance of the complications.
                 session.pop();
                 runAfterHidden(() -> mCancelCallbacks.add(
@@ -179,8 +181,10 @@
     private void runAfterHidden(Runnable runnable) {
         mExecutor.execute(() -> {
             if (mHidden) {
+                Log.i(TAG, "Executing after hidden runnable immediately...");
                 runnable.run();
             } else {
+                Log.i(TAG, "Queuing after hidden runnable...");
                 mHiddenCallback = runnable;
             }
         });
diff --git a/packages/SystemUI/src/com/android/systemui/flags/ConditionalRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/ConditionalRestarter.kt
index dd58604..906896f 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/ConditionalRestarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/ConditionalRestarter.kt
@@ -17,9 +17,9 @@
 package com.android.systemui.flags
 
 import android.util.Log
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.flags.ConditionalRestarter.Condition
-import com.android.systemui.util.kotlin.UnflaggedApplication
-import com.android.systemui.util.kotlin.UnflaggedBackground
 import java.util.concurrent.TimeUnit
 import javax.inject.Inject
 import javax.inject.Named
@@ -39,8 +39,8 @@
     private val systemExitRestarter: SystemExitRestarter,
     private val conditions: Set<@JvmSuppressWildcards Condition>,
     @Named(RESTART_DELAY) private val restartDelaySec: Long,
-    @UnflaggedApplication private val applicationScope: CoroutineScope,
-    @UnflaggedBackground private val backgroundDispatcher: CoroutineContext,
+    @Application private val applicationScope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineContext,
 ) : Restarter {
 
     private var pendingReason = ""
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index ad3d6d6..093319f 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -135,14 +135,6 @@
             "lockscreen_custom_clocks"
         )
 
-    // TODO(b/286092087): Tracking Bug
-    @JvmField
-    val ENABLE_SYSTEM_UI_DREAM_CONTROLLER = unreleasedFlag("enable_system_ui_dream_controller")
-
-    // TODO(b/288287730): Tracking Bug
-    @JvmField
-    val ENABLE_SYSTEM_UI_DREAM_HOSTING = unreleasedFlag("enable_system_ui_dream_hosting")
-
     /**
      * Whether the clock on a wide lock screen should use the new "stepping" animation for moving
      * the digits when the clock moves.
@@ -232,11 +224,6 @@
     val WALLPAPER_PICKER_GRID_APPLY_BUTTON =
             unreleasedFlag("wallpaper_picker_grid_apply_button")
 
-    /** Whether to run the new udfps keyguard refactor code. */
-    // TODO(b/279440316): Tracking bug.
-    @JvmField
-    val REFACTOR_UDFPS_KEYGUARD_VIEWS = unreleasedFlag("refactor_udfps_keyguard_views")
-
     /** Provide new auth messages on the bouncer. */
     // TODO(b/277961132): Tracking bug.
     @JvmField val REVAMPED_BOUNCER_MESSAGES = unreleasedFlag("revamped_bouncer_messages")
@@ -293,11 +280,6 @@
             R.bool.flag_stop_pulsing_face_scanning_animation,
             "stop_pulsing_face_scanning_animation")
 
-    /** Flag to use a separate view for the alternate bouncer. */
-    // TODO(b/300440924): Tracking bug
-    @JvmField
-    val ALTERNATE_BOUNCER_VIEW: UnreleasedFlag = unreleasedFlag("alternate_bouncer_view")
-
     // 300 - power menu
     // TODO(b/254512600): Tracking Bug
     @JvmField val POWER_MENU_LITE = releasedFlag("power_menu_lite")
@@ -622,10 +604,6 @@
     val WARN_ON_BLOCKING_BINDER_TRANSACTIONS =
         unreleasedFlag("warn_on_blocking_binder_transactions")
 
-    @JvmField
-    val COROUTINE_TRACING =
-        unreleasedFlag("coroutine_tracing")
-
     // TODO(b/283071711): Tracking bug
     @JvmField
     val TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK =
@@ -721,12 +699,6 @@
     @JvmField
     val USE_REPOS_FOR_BOUNCER_SHOWING = releasedFlag("use_repos_for_bouncer_showing")
 
-    // 3100 - Haptic interactions
-
-    // TODO(b/290213663): Tracking Bug
-    @JvmField
-    val ONE_WAY_HAPTICS_API_MIGRATION = releasedFlag("oneway_haptics_api_migration")
-
     /** TODO(b/296223317): Enables the new keyguard presentation containing a clock. */
     @JvmField
     val ENABLE_CLOCK_KEYGUARD_PRESENTATION = releasedFlag("enable_clock_keyguard_presentation")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 1037b0e..017dac2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -31,8 +31,8 @@
 import com.android.systemui.common.ui.ConfigurationState
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
+import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
 import com.android.systemui.flags.FeatureFlagsClassic
-import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.ui.binder.KeyguardBlueprintViewBinder
 import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder
 import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder
@@ -134,7 +134,7 @@
         val indicationArea = KeyguardIndicationArea(context, null)
         keyguardIndicationController.setIndicationArea(indicationArea)
 
-        if (!featureFlags.isEnabled(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS)) {
+        if (!DeviceEntryUdfpsRefactor.isEnabled) {
             lockIconViewController.get().setLockIconView(LockIconView(context, null))
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt
index 54031dc..cb0f186 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt
@@ -22,6 +22,7 @@
 import android.graphics.Point
 import androidx.core.animation.Animator
 import androidx.core.animation.ValueAnimator
+import com.android.keyguard.logging.ScrimLogger
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
@@ -33,6 +34,8 @@
 import com.android.systemui.statusbar.LiftReveal
 import com.android.systemui.statusbar.LightRevealEffect
 import com.android.systemui.statusbar.PowerButtonReveal
+import javax.inject.Inject
+import kotlin.math.max
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
@@ -42,8 +45,6 @@
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
-import javax.inject.Inject
-import kotlin.math.max
 
 val DEFAULT_REVEAL_EFFECT = LiftReveal
 
@@ -72,8 +73,13 @@
     keyguardRepository: KeyguardRepository,
     val context: Context,
     powerInteractor: PowerInteractor,
+    private val scrimLogger: ScrimLogger,
 ) : LightRevealScrimRepository {
 
+    companion object {
+        val TAG = LightRevealScrimRepository::class.simpleName!!
+    }
+
     /** The reveal effect used if the device was locked/unlocked via the power button. */
     private val powerButtonRevealEffect: Flow<LightRevealEffect?> =
         flowOf(
@@ -120,25 +126,25 @@
 
     /** The reveal effect we'll use for the next non-biometric unlock (tap, power button, etc). */
     private val nonBiometricRevealEffect: Flow<LightRevealEffect?> =
-        powerInteractor
-                .detailedWakefulness
-                .flatMapLatest { wakefulnessModel ->
-                    when {
-                        wakefulnessModel.isAwakeOrAsleepFrom(WakeSleepReason.POWER_BUTTON) ->
-                            powerButtonRevealEffect
-                        wakefulnessModel.isAwakeFrom(TAP) ->
-                            tapRevealEffect
-                        else ->
-                            flowOf(LiftReveal)
-                    }
-                }
+        powerInteractor.detailedWakefulness.flatMapLatest { wakefulnessModel ->
+            when {
+                wakefulnessModel.isAwakeOrAsleepFrom(WakeSleepReason.POWER_BUTTON) ->
+                    powerButtonRevealEffect
+                wakefulnessModel.isAwakeFrom(TAP) -> tapRevealEffect
+                else -> flowOf(LiftReveal)
+            }
+        }
 
     private val revealAmountAnimator = ValueAnimator.ofFloat(0f, 1f).apply { duration = 500 }
 
     override val revealAmount: Flow<Float> = callbackFlow {
         val updateListener =
             Animator.AnimatorUpdateListener {
-                trySend((it as ValueAnimator).animatedValue as Float)
+                val value = (it as ValueAnimator).animatedValue
+                trySend(value as Float)
+                if (value <= 0.0f || value >= 1.0f) {
+                    scrimLogger.d(TAG, "revealAmount", value)
+                }
             }
         revealAmountAnimator.addUpdateListener(updateListener)
         awaitClose { revealAmountAnimator.removeUpdateListener(updateListener) }
@@ -146,6 +152,7 @@
 
     override fun startRevealAmountAnimator(reveal: Boolean) {
         if (reveal) revealAmountAnimator.start() else revealAmountAnimator.reverse()
+        scrimLogger.d(TAG, "startRevealAmountAnimator, reveal", reveal)
     }
 
     override val revealEffect =
@@ -156,13 +163,21 @@
             ) { biometricUnlockState, biometricReveal, nonBiometricReveal ->
 
                 // Use the biometric reveal for any flavor of wake and unlocking.
-                when (biometricUnlockState) {
-                    BiometricUnlockModel.WAKE_AND_UNLOCK,
-                    BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING,
-                    BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM -> biometricReveal
-                    else -> nonBiometricReveal
-                }
-                    ?: DEFAULT_REVEAL_EFFECT
+                val revealEffect =
+                    when (biometricUnlockState) {
+                        BiometricUnlockModel.WAKE_AND_UNLOCK,
+                        BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING,
+                        BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM -> biometricReveal
+                        else -> nonBiometricReveal
+                    }
+                        ?: DEFAULT_REVEAL_EFFECT
+
+                scrimLogger.d(
+                    TAG,
+                    "revealEffect",
+                    "$revealEffect, biometricUnlockState: ${biometricUnlockState.name}"
+                )
+                return@combine revealEffect
             }
             .distinctUntilChanged()
 
@@ -173,8 +188,7 @@
                 x,
                 y,
                 startRadius = 0,
-                endRadius =
-                    max(max(x, display.width - x), max(y, display.height - y)),
+                endRadius = max(max(x, display.width - x), max(y, display.height - y)),
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
index ca882e5..0b6b971 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
@@ -54,7 +54,10 @@
 
     fun startToLockscreenTransition() {
         scope.launch {
-            if (transitionInteractor.startedKeyguardState.value == KeyguardState.DREAMING) {
+            if (
+                transitionInteractor.startedKeyguardState.replayCache.last() ==
+                    KeyguardState.DREAMING
+            ) {
                 startTransitionTo(KeyguardState.LOCKSCREEN)
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt
index efbe261..922baa3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt
@@ -24,6 +24,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
@@ -40,18 +41,20 @@
 
     @OptIn(ExperimentalCoroutinesApi::class)
     val viewParams: Flow<KeyguardSurfaceBehindModel> =
-        transitionInteractor.isInTransitionToAnyState.flatMapLatest { isInTransition ->
-            if (!isInTransition) {
-                defaultParams
-            } else {
-                combine(
-                    transitionSpecificViewParams,
-                    defaultParams,
-                ) { transitionParams, defaultParams ->
-                    transitionParams ?: defaultParams
+        transitionInteractor.isInTransitionToAnyState
+            .flatMapLatest { isInTransition ->
+                if (!isInTransition) {
+                    defaultParams
+                } else {
+                    combine(
+                        transitionSpecificViewParams,
+                        defaultParams,
+                    ) { transitionParams, defaultParams ->
+                        transitionParams ?: defaultParams
+                    }
                 }
             }
-        }
+            .distinctUntilChanged()
 
     val isAnimatingSurface = repository.isAnimatingSurface
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index b0b8577..4da48f6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -37,14 +37,14 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.shareIn
 
 /** Encapsulates business-logic related to the keyguard transitions. */
 @SysUISingleton
@@ -171,16 +171,16 @@
         repository.transitions.filter { step -> step.transitionState == TransitionState.FINISHED }
 
     /** The destination state of the last started transition. */
-    val startedKeyguardState: StateFlow<KeyguardState> =
+    val startedKeyguardState: SharedFlow<KeyguardState> =
         startedKeyguardTransitionStep
             .map { step -> step.to }
-            .stateIn(scope, SharingStarted.Eagerly, OFF)
+            .shareIn(scope, SharingStarted.Eagerly, replay = 1)
 
     /** The last completed [KeyguardState] transition */
-    val finishedKeyguardState: StateFlow<KeyguardState> =
+    val finishedKeyguardState: SharedFlow<KeyguardState> =
         finishedKeyguardTransitionStep
             .map { step -> step.to }
-            .stateIn(scope, SharingStarted.Eagerly, LOCKSCREEN)
+            .shareIn(scope, SharingStarted.Eagerly, replay = 1)
 
     /**
      * Whether we're currently in a transition to a new [KeyguardState] and haven't yet completed
@@ -227,14 +227,13 @@
      * state.
      */
     fun startDismissKeyguardTransition() {
-        when (startedKeyguardState.value) {
+        when (val startedState = startedKeyguardState.replayCache.last()) {
             LOCKSCREEN -> fromLockscreenTransitionInteractor.get().dismissKeyguard()
             PRIMARY_BOUNCER -> fromPrimaryBouncerTransitionInteractor.get().dismissPrimaryBouncer()
             else ->
                 Log.e(
                     "KeyguardTransitionInteractor",
-                    "We don't know how to dismiss keyguard from state " +
-                        "${startedKeyguardState.value}"
+                    "We don't know how to dismiss keyguard from state $startedState."
                 )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
index 6115d90..2d43897 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import com.android.keyguard.logging.ScrimLogger
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.data.repository.LightRevealScrimRepository
@@ -37,6 +38,7 @@
     private val transitionInteractor: KeyguardTransitionInteractor,
     private val lightRevealScrimRepository: LightRevealScrimRepository,
     @Application private val scope: CoroutineScope,
+    private val scrimLogger: ScrimLogger,
 ) {
 
     init {
@@ -46,6 +48,7 @@
     private fun listenForStartedKeyguardTransitionStep() {
         scope.launch {
             transitionInteractor.startedKeyguardTransitionStep.collect {
+                scrimLogger.d(TAG, "listenForStartedKeyguardTransitionStep", it)
                 if (willTransitionChangeEndState(it)) {
                     lightRevealScrimRepository.startRevealAmountAnimator(
                         willBeRevealedInState(it.to)
@@ -100,5 +103,7 @@
                 KeyguardState.OCCLUDED -> true
             }
         }
+
+        val TAG = LightRevealScrimInteractor::class.simpleName!!
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NaturalScrollingSettingObserver.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NaturalScrollingSettingObserver.kt
new file mode 100644
index 0000000..508fb59
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NaturalScrollingSettingObserver.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.content.Context
+import android.database.ContentObserver
+import android.os.Handler
+import android.os.UserHandle
+import android.provider.Settings
+import android.provider.Settings.SettingNotFoundException
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import javax.inject.Inject
+
+@SysUISingleton
+class NaturalScrollingSettingObserver
+@Inject
+constructor(
+    @Main private val handler: Handler,
+    private val context: Context,
+) {
+    var isNaturalScrollingEnabled = true
+        get() {
+            if (!isInitialized) {
+                isInitialized = true
+                update()
+            }
+            return field
+        }
+
+    private var isInitialized = false
+
+    private val contentObserver = object : ContentObserver(handler) {
+        override fun onChange(selfChange: Boolean) {
+            update()
+        }
+    }
+
+    init {
+        context.contentResolver.registerContentObserver(
+                Settings.System.getUriFor(Settings.System.TOUCHPAD_NATURAL_SCROLLING), false,
+                contentObserver)
+    }
+
+    private fun update() {
+        isNaturalScrollingEnabled = try {
+            Settings.System.getIntForUser(context.contentResolver,
+                    Settings.System.TOUCHPAD_NATURAL_SCROLLING, UserHandle.USER_CURRENT) == 1
+        } catch (e: SettingNotFoundException) {
+            true
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
index 7601808..d5ac283 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
@@ -53,16 +53,16 @@
         modeOnCanceled: TransitionModeOnCanceled = TransitionModeOnCanceled.LAST_VALUE
     ): UUID? {
         if (
-            fromState != transitionInteractor.startedKeyguardState.value &&
-                fromState != transitionInteractor.finishedKeyguardState.value
+            fromState != transitionInteractor.startedKeyguardState.replayCache.last() &&
+                fromState != transitionInteractor.finishedKeyguardState.replayCache.last()
         ) {
             Log.e(
                 name,
                 "startTransition: We were asked to transition from " +
                     "$fromState to $toState, however we last finished a transition to " +
-                    "${transitionInteractor.finishedKeyguardState.value}, " +
+                    "${transitionInteractor.finishedKeyguardState.replayCache.last()}, " +
                     "and last started a transition to " +
-                    "${transitionInteractor.startedKeyguardState.value}. " +
+                    "${transitionInteractor.startedKeyguardState.replayCache.last()}. " +
                     "Ignoring startTransition, but this should never happen."
             )
             return null
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
index 41af9e8..cb5813e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
@@ -23,18 +23,17 @@
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.flags.FeatureFlagsClassic
-import com.android.systemui.flags.Flags
+import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.res.R
 import com.android.systemui.scrim.ScrimView
 import com.android.systemui.shade.NotificationShadeWindowView
 import com.android.systemui.statusbar.NotificationShadeWindowController
+import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.launch
-import javax.inject.Inject
 
 @ExperimentalCoroutinesApi
 @SysUISingleton
@@ -42,13 +41,12 @@
 @Inject
 constructor(
     private val notificationShadeWindowView: NotificationShadeWindowView,
-    private val featureFlags: FeatureFlagsClassic,
     private val alternateBouncerViewModel: AlternateBouncerViewModel,
     @Application private val scope: CoroutineScope,
     private val notificationShadeWindowController: NotificationShadeWindowController,
 ) : CoreStartable {
     override fun start() {
-        if (!featureFlags.isEnabled(Flags.ALTERNATE_BOUNCER_VIEW)) {
+        if (!DeviceEntryUdfpsRefactor.isEnabled) {
             return
         }
 
@@ -79,6 +77,7 @@
         scope: CoroutineScope,
         notificationShadeWindowController: NotificationShadeWindowController,
     ) {
+        DeviceEntryUdfpsRefactor.isUnexpectedlyInLegacyMode()
         scope.launch {
             // forcePluginOpen is necessary to show over occluded apps.
             // This cannot be tied to the view's lifecycle because setting this allows the view
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
index a8b28bc..4b4a19e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
@@ -23,6 +23,7 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.common.ui.view.LongPressHandlingView
+import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
 import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
 import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryBackgroundViewModel
 import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel
@@ -51,6 +52,7 @@
         bgViewModel: DeviceEntryBackgroundViewModel,
         falsingManager: FalsingManager,
     ) {
+        DeviceEntryUdfpsRefactor.isUnexpectedlyInLegacyMode()
         val longPressHandlingView = view.longPressHandlingView
         val fgIconView = view.iconView
         val bgView = view.bgView
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt
index b7a165c..55df466 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt
@@ -26,14 +26,13 @@
 import androidx.constraintlayout.widget.ConstraintSet.RIGHT
 import androidx.constraintlayout.widget.ConstraintSet.TOP
 import com.android.systemui.Flags.keyguardBottomAreaRefactor
-import com.android.systemui.res.R
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.flags.FeatureFlagsClassic
-import com.android.systemui.flags.Flags
+import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
 import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
 import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.KeyguardIndicationController
 import com.android.systemui.statusbar.VibratorHelper
 import javax.inject.Inject
@@ -48,7 +47,6 @@
     private val falsingManager: FalsingManager,
     private val indicationController: KeyguardIndicationController,
     private val vibratorHelper: VibratorHelper,
-    private val featureFlags: FeatureFlagsClassic,
 ) : BaseShortcutSection() {
     override fun addViews(constraintLayout: ConstraintLayout) {
         if (keyguardBottomAreaRefactor()) {
@@ -86,11 +84,12 @@
         val width = resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width)
         val height = resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height)
 
-        val lockIconViewId = if (featureFlags.isEnabled(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS)) {
-            R.id.device_entry_icon_view
-        } else {
-            R.id.lock_icon_view
-        }
+        val lockIconViewId =
+            if (DeviceEntryUdfpsRefactor.isEnabled) {
+                R.id.device_entry_icon_view
+            } else {
+                R.id.lock_icon_view
+            }
 
         constraintSet.apply {
             constrainWidth(R.id.start_button, width)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSection.kt
index 13ea8ff..790ddd5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSection.kt
@@ -31,6 +31,7 @@
 import com.android.keyguard.LockIconViewController
 import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.biometrics.AuthController
+import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.shared.model.KeyguardSection
@@ -65,10 +66,7 @@
     private val deviceEntryIconViewId = R.id.device_entry_icon_view
 
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (
-            !keyguardBottomAreaRefactor() &&
-                !featureFlags.isEnabled(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS)
-        ) {
+        if (!keyguardBottomAreaRefactor() && !DeviceEntryUdfpsRefactor.isEnabled) {
             return
         }
 
@@ -77,7 +75,7 @@
         }
 
         val view =
-            if (featureFlags.isEnabled(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS)) {
+            if (DeviceEntryUdfpsRefactor.isEnabled) {
                 DeviceEntryIconView(context, null).apply { id = deviceEntryIconViewId }
             } else {
                 // keyguardBottomAreaRefactor()
@@ -87,7 +85,7 @@
     }
 
     override fun bindData(constraintLayout: ConstraintLayout) {
-        if (featureFlags.isEnabled(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS)) {
+        if (DeviceEntryUdfpsRefactor.isEnabled) {
             constraintLayout.findViewById<DeviceEntryIconView?>(deviceEntryIconViewId)?.let {
                 DeviceEntryIconViewBinder.bind(
                     it,
@@ -140,7 +138,7 @@
     }
 
     override fun removeViews(constraintLayout: ConstraintLayout) {
-        if (featureFlags.isEnabled(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS)) {
+        if (DeviceEntryUdfpsRefactor.isEnabled) {
             constraintLayout.removeView(deviceEntryIconViewId)
         } else {
             constraintLayout.removeView(R.id.lock_icon_view)
@@ -160,7 +158,7 @@
             }
 
         val iconId =
-            if (featureFlags.isEnabled(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS)) {
+            if (DeviceEntryUdfpsRefactor.isEnabled) {
                 deviceEntryIconViewId
             } else {
                 R.id.lock_icon_view
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
index 165ee36..7512e51 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
@@ -24,6 +24,7 @@
 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
 import androidx.constraintlayout.widget.ConstraintSet.START
 import androidx.constraintlayout.widget.ConstraintSet.TOP
+import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
@@ -81,7 +82,7 @@
             connect(R.id.nssl_placeholder, END, PARENT_ID, END)
 
             val lockId =
-                if (featureFlags.isEnabled(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS)) {
+                if (DeviceEntryUdfpsRefactor.isEnabled) {
                     R.id.device_entry_icon_view
                 } else {
                     R.id.lock_icon_view
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
index 2c45da6..f2559ba 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
@@ -24,6 +24,7 @@
 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
 import androidx.constraintlayout.widget.ConstraintSet.START
 import androidx.constraintlayout.widget.ConstraintSet.TOP
+import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
@@ -81,7 +82,7 @@
             connect(R.id.nssl_placeholder, END, PARENT_ID, END)
 
             val lockId =
-                if (featureFlags.isEnabled(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS)) {
+                if (DeviceEntryUdfpsRefactor.isEnabled) {
                     R.id.device_entry_icon_view
                 } else {
                     R.id.lock_icon_view
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 17ff1b1..0d81940 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -517,6 +517,16 @@
     }
 
     /**
+     * Provides a {@link LogBuffer} for Scrims like LightRevealScrim.
+     */
+    @Provides
+    @SysUISingleton
+    @ScrimLog
+    public static LogBuffer provideScrimLogBuffer(LogBufferFactory factory) {
+        return factory.create("ScrimLog", 100);
+    }
+
+    /**
      * Provides a {@link LogBuffer} for dream-related logs.
      */
     @Provides
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/ScrimLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/ScrimLog.kt
new file mode 100644
index 0000000..e78a162
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/ScrimLog.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.log.dagger
+
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.log.LogBuffer] for Scrims like LightRevealScrim */
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class ScrimLog
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
index b1ff708..9d6e9b4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
@@ -46,8 +46,7 @@
 import com.android.systemui.media.dream.MediaDreamComplication
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.res.R
-import com.android.systemui.shade.ShadeStateEvents
-import com.android.systemui.shade.ShadeStateEvents.ShadeStateEventsListener
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.CrossFadeHelper
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
@@ -103,7 +102,7 @@
     private val communalInteractor: CommunalInteractor,
     configurationController: ConfigurationController,
     wakefulnessLifecycle: WakefulnessLifecycle,
-    panelEventsEvents: ShadeStateEvents,
+    shadeInteractor: ShadeInteractor,
     private val secureSettings: SecureSettings,
     @Main private val handler: Handler,
     @Application private val coroutineScope: CoroutineScope,
@@ -545,14 +544,12 @@
             mediaHosts.forEach { it?.updateViewVisibility() }
         }
 
-        panelEventsEvents.addShadeStateEventsListener(
-            object : ShadeStateEventsListener {
-                override fun onExpandImmediateChanged(isExpandImmediateEnabled: Boolean) {
-                    skipQqsOnExpansion = isExpandImmediateEnabled
-                    updateDesiredLocation()
-                }
+        coroutineScope.launch {
+            shadeInteractor.isQsBypassingShade.collect { isExpandImmediateEnabled ->
+                skipQqsOnExpansion = isExpandImmediateEnabled
+                updateDesiredLocation()
             }
-        )
+        }
 
         val settingsObserver: ContentObserver =
             object : ContentObserver(handler) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
index 78f2da5..789a1e4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
@@ -279,6 +279,11 @@
     }
 
     @Override
+    public void onNullBinding(ComponentName name) {
+        executeSetBindService(false);
+    }
+
+    @Override
     public void onServiceDisconnected(ComponentName name) {
         if (DEBUG) Log.d(TAG, "onServiceDisconnected " + name);
         handleDeath();
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index ca2828b..8def457 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -19,7 +19,10 @@
 package com.android.systemui.scene.domain.startable
 
 import com.android.systemui.CoreStartable
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.classifier.FalsingCollectorActual
 import com.android.systemui.dagger.SysUISingleton
@@ -72,6 +75,8 @@
     private val sceneLogger: SceneLogger,
     @FalsingCollectorActual private val falsingCollector: FalsingCollector,
     private val powerInteractor: PowerInteractor,
+    private val simBouncerInteractor: SimBouncerInteractor,
+    private val authenticationInteractor: AuthenticationInteractor,
 ) : CoreStartable {
 
     override fun start() {
@@ -132,6 +137,33 @@
             }
         }
         applicationScope.launch {
+            simBouncerInteractor.isAnySimSecure.collect { isAnySimLocked ->
+                val canSwipeToEnter = deviceEntryInteractor.canSwipeToEnter.value
+                val isUnlocked = deviceEntryInteractor.isUnlocked.value
+
+                when {
+                    isAnySimLocked -> {
+                        switchToScene(
+                            targetSceneKey = SceneKey.Bouncer,
+                            loggingReason = "Need to authenticate locked sim card."
+                        )
+                    }
+                    isUnlocked && !canSwipeToEnter -> {
+                        switchToScene(
+                            targetSceneKey = SceneKey.Gone,
+                            loggingReason = "Sim cards are unlocked."
+                        )
+                    }
+                    else -> {
+                        switchToScene(
+                            targetSceneKey = SceneKey.Lockscreen,
+                            loggingReason = "Sim cards are unlocked."
+                        )
+                    }
+                }
+            }
+        }
+        applicationScope.launch {
             deviceEntryInteractor.isUnlocked
                 .mapNotNull { isUnlocked ->
                     val renderedScenes =
@@ -206,6 +238,14 @@
                                 "device is waking up while unlocked without the ability" +
                                     " to swipe up on lockscreen to enter.",
                         )
+                    } else if (
+                        authenticationInteractor.getAuthenticationMethod() ==
+                            AuthenticationMethodModel.Sim
+                    ) {
+                        switchToScene(
+                            targetSceneKey = SceneKey.Bouncer,
+                            loggingReason = "device is starting to wake up with a locked sim"
+                        )
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 7201b35..a44b4b4 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -134,6 +134,7 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingObserver;
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl;
 import com.android.systemui.keyguard.shared.model.TransitionState;
 import com.android.systemui.keyguard.shared.model.TransitionStep;
@@ -355,6 +356,7 @@
     private final NotificationGutsManager mGutsManager;
     private final AlternateBouncerInteractor mAlternateBouncerInteractor;
     private final QuickSettingsController mQsController;
+    private final NaturalScrollingSettingObserver mNaturalScrollingSettingObserver;
     private final TouchHandler mTouchHandler = new TouchHandler();
 
     private long mDownTime;
@@ -407,6 +409,7 @@
     private float mOverStretchAmount;
     private float mDownX;
     private float mDownY;
+    private boolean mIsTrackpadReverseScroll;
     private int mDisplayTopInset = 0; // in pixels
     private int mDisplayRightInset = 0; // in pixels
     private int mDisplayLeftInset = 0; // in pixels
@@ -775,7 +778,8 @@
             KeyguardFaceAuthInteractor keyguardFaceAuthInteractor,
             SplitShadeStateController splitShadeStateController,
             PowerInteractor powerInteractor,
-            KeyguardClockPositionAlgorithm keyguardClockPositionAlgorithm) {
+            KeyguardClockPositionAlgorithm keyguardClockPositionAlgorithm,
+            NaturalScrollingSettingObserver naturalScrollingSettingObserver) {
         keyguardStateController.addCallback(new KeyguardStateController.Callback() {
             @Override
             public void onKeyguardFadingAwayChanged() {
@@ -804,6 +808,7 @@
         mPowerInteractor = powerInteractor;
         mKeyguardViewConfigurator = keyguardViewConfigurator;
         mClockPositionAlgorithm = keyguardClockPositionAlgorithm;
+        mNaturalScrollingSettingObserver = naturalScrollingSettingObserver;
         mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
             @Override
             public void onViewAttachedToWindow(View v) {
@@ -3682,7 +3687,7 @@
      */
     private boolean isDirectionUpwards(float x, float y) {
         float xDiff = x - mInitialExpandX;
-        float yDiff = y - mInitialExpandY;
+        float yDiff = (mIsTrackpadReverseScroll ? -1 : 1) * (y - mInitialExpandY);
         if (yDiff >= 0) {
             return false;
         }
@@ -3719,7 +3724,7 @@
                 || (!isFullyExpanded() && !isFullyCollapsed())
                 || event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) {
             mVelocityTracker.computeCurrentVelocity(1000);
-            float vel = mVelocityTracker.getYVelocity();
+            float vel = (mIsTrackpadReverseScroll ? -1 : 1) * mVelocityTracker.getYVelocity();
             float vectorVel = (float) Math.hypot(
                     mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
 
@@ -3758,8 +3763,9 @@
                 mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_UNLOCK, heightDp, velocityDp);
                 mLockscreenGestureLogger.log(LockscreenUiEvent.LOCKSCREEN_UNLOCK);
             }
+            float dy = (mIsTrackpadReverseScroll ? -1 : 1) * (y - mInitialExpandY);
             @Classifier.InteractionType int interactionType = vel == 0 ? GENERIC
-                    : y - mInitialExpandY > 0 ? QUICK_SETTINGS
+                    : dy > 0 ? QUICK_SETTINGS
                             : (mKeyguardStateController.canDismissLockScreen()
                                     ? UNLOCK : BOUNCER_UNLOCK);
 
@@ -3786,7 +3792,7 @@
 
     private float getCurrentExpandVelocity() {
         mVelocityTracker.computeCurrentVelocity(1000);
-        return mVelocityTracker.getYVelocity();
+        return (mIsTrackpadReverseScroll ? -1 : 1) * mVelocityTracker.getYVelocity();
     }
 
     private void endClosing() {
@@ -4827,6 +4833,10 @@
                                 + " mAnimatingOnDown: true, mClosing: true");
                         return true;
                     }
+
+                    mIsTrackpadReverseScroll =
+                            !mNaturalScrollingSettingObserver.isNaturalScrollingEnabled()
+                                    && isTrackpadScroll(mTrackpadGestureFeaturesEnabled, event);
                     if (!isTracking() || isFullyCollapsed()) {
                         mInitialExpandY = y;
                         mInitialExpandX = x;
@@ -4869,7 +4879,7 @@
                     }
                     break;
                 case MotionEvent.ACTION_MOVE:
-                    final float h = y - mInitialExpandY;
+                    final float h = (mIsTrackpadReverseScroll ? -1 : 1) * (y - mInitialExpandY);
                     addMovement(event);
                     final boolean openShadeWithoutHun =
                             mPanelClosedOnDown && !mCollapsedAndHeadsUpOnDown;
@@ -5133,7 +5143,7 @@
                     if (!isFullyCollapsed()) {
                         maybeVibrateOnOpening(true /* openingWithTouch */);
                     }
-                    float h = y - mInitialExpandY;
+                    float h = (mIsTrackpadReverseScroll ? -1 : 1) * (y - mInitialExpandY);
 
                     // If the panel was collapsed when touching, we only need to check for the
                     // y-component of the gesture, as we have no conflicting horizontal gesture.
@@ -5182,6 +5192,7 @@
                             mQsController.cancelJankMonitoring();
                         }
                     }
+                    mIsTrackpadReverseScroll = false;
                     break;
             }
             return !mGestureWaitForTouchSlop || isTracking();
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index d0f2784..cf1dfdc 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -46,6 +46,7 @@
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel;
 import com.android.systemui.compose.ComposeFacade;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlagsClassic;
@@ -448,7 +449,7 @@
                 }
 
                 boolean bouncerShowing;
-                if (mFeatureFlagsClassic.isEnabled(Flags.ALTERNATE_BOUNCER_VIEW)) {
+                if (DeviceEntryUdfpsRefactor.isEnabled()) {
                     bouncerShowing = mPrimaryBouncerInteractor.isBouncerShowing()
                             || mAlternateBouncerInteractor.isVisibleState();
                 } else {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
index 4ca763f..335e65e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
@@ -207,12 +207,6 @@
 
     /** Indicates QS is at its max height */
     private boolean mFullyExpanded;
-    /**
-     * Determines if QS should be already expanded when expanding shade.
-     * Used for split shade, two finger gesture as well as accessibility shortcut to QS.
-     * It needs to be set when movement starts as it resets at the end of expansion/collapse.
-     */
-    private boolean mExpandImmediate;
     private boolean mExpandedWhenExpandingStarted;
     private boolean mAnimatingHiddenFromCollapsed;
     private boolean mVisible;
@@ -512,7 +506,7 @@
     /** */
     @VisibleForTesting
     boolean isExpandImmediate() {
-        return mExpandImmediate;
+        return mShadeRepository.getLegacyExpandImmediate().getValue();
     }
 
     float getInitialTouchY() {
@@ -602,7 +596,7 @@
         // close the whole shade with one motion. Also this will be always true when closing
         // split shade as there QS are always expanded so every collapsing motion is motion from
         // expanded QS to closed panel
-        return mExpandImmediate || (getExpanded()
+        return isExpandImmediate() || (getExpanded()
                 && !isTracking() && !isExpansionAnimating()
                 && !mExpansionFromOverscroll);
     }
@@ -792,7 +786,7 @@
                 && mBarState == SHADE) {
             Log.wtf(TAG,
                     "setting QS height to 0 in split shade while shade is open(ing). "
-                            + "Value of mExpandImmediate = " + mExpandImmediate);
+                            + "Value of isExpandImmediate() = " + isExpandImmediate());
         }
         int maxHeight = getMaxExpansionHeight();
         height = Math.min(Math.max(
@@ -941,10 +935,9 @@
     }
 
     void setExpandImmediate(boolean expandImmediate) {
-        if (expandImmediate != mExpandImmediate) {
+        if (expandImmediate != isExpandImmediate()) {
             mShadeLog.logQsExpandImmediateChanged(expandImmediate);
-            mExpandImmediate = expandImmediate;
-            mShadeExpansionStateManager.notifyExpandImmediateChange(expandImmediate);
+            mShadeRepository.setLegacyExpandImmediate(expandImmediate);
         }
     }
 
@@ -980,6 +973,7 @@
 
     void updateQsState() {
         boolean qsFullScreen = getExpanded() && !mSplitShadeEnabled;
+        mShadeRepository.setLegacyQsFullscreen(qsFullScreen);
         mNotificationStackScrollLayoutController.setQsFullScreen(qsFullScreen);
         mNotificationStackScrollLayoutController.setScrollingEnabled(
                 mBarState != KEYGUARD && (!qsFullScreen || mExpansionFromOverscroll));
@@ -996,7 +990,7 @@
     public void updateExpansion() {
         if (mQs == null) return;
         final float squishiness;
-        if ((mExpandImmediate || getExpanded()) && !mSplitShadeEnabled) {
+        if ((isExpandImmediate() || getExpanded()) && !mSplitShadeEnabled) {
             squishiness = 1;
         } else if (mTransitioningToFullShadeProgress > 0.0f) {
             squishiness = mLockscreenShadeTransitionController.getQsSquishTransitionFraction();
@@ -2077,8 +2071,8 @@
         ipw.println(getExpanded());
         ipw.print("mFullyExpanded=");
         ipw.println(mFullyExpanded);
-        ipw.print("mExpandImmediate=");
-        ipw.println(mExpandImmediate);
+        ipw.print("isExpandImmediate()=");
+        ipw.println(isExpandImmediate());
         ipw.print("mExpandedWhenExpandingStarted=");
         ipw.println(mExpandedWhenExpandingStarted);
         ipw.print("mAnimatingHiddenFromCollapsed=");
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt
index 7a803867..53eccfd 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.shade
 
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.domain.interactor.ShadeInteractorEmptyImpl
 import dagger.Binds
 import dagger.Module
 
@@ -30,4 +32,8 @@
     @Binds
     @SysUISingleton
     abstract fun bindsShadeController(sc: ShadeControllerEmptyImpl): ShadeController
+
+    @Binds
+    @SysUISingleton
+    abstract fun bindsShadeInteractor(si: ShadeInteractorEmptyImpl): ShadeInteractor
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
index fca98f5..e20534c 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
@@ -169,12 +169,6 @@
         }
     }
 
-    fun notifyExpandImmediateChange(expandImmediateEnabled: Boolean) {
-        for (cb in shadeStateEventsListeners) {
-            cb.onExpandImmediateChanged(expandImmediateEnabled)
-        }
-    }
-
     private fun debugLog(msg: String) {
         if (!DEBUG) return
         Log.v(TAG, msg)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
index 89aaaaf..54467cf 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
@@ -17,13 +17,40 @@
 package com.android.systemui.shade
 
 import com.android.systemui.dagger.SysUISingleton
-
+import com.android.systemui.scene.shared.flag.SceneContainerFlags
+import com.android.systemui.shade.domain.interactor.BaseShadeInteractor
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl
+import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl
+import com.android.systemui.shade.domain.interactor.ShadeInteractorSceneContainerImpl
 import dagger.Binds
 import dagger.Module
+import dagger.Provides
+import javax.inject.Provider
 
 /** Module for classes related to the notification shade. */
 @Module(includes = [StartShadeModule::class, ShadeViewProviderModule::class])
 abstract class ShadeModule {
+    companion object {
+        @Provides
+        @SysUISingleton
+        fun provideBaseShadeInteractor(
+            sceneContainerFlags: SceneContainerFlags,
+            sceneContainerOn: Provider<ShadeInteractorSceneContainerImpl>,
+            sceneContainerOff: Provider<ShadeInteractorLegacyImpl>
+        ): BaseShadeInteractor {
+            return if (sceneContainerFlags.isEnabled()) {
+                sceneContainerOn.get()
+            } else {
+                sceneContainerOff.get()
+            }
+        }
+    }
+
+    @Binds
+    @SysUISingleton
+    abstract fun bindsShadeInteractor(si: ShadeInteractorImpl): ShadeInteractor
+
     @Binds
     @SysUISingleton
     abstract fun bindsShadeViewController(
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeStateEvents.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateEvents.kt
index 5804040..c8511d7 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeStateEvents.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateEvents.kt
@@ -35,16 +35,5 @@
          * Invoked when the notification panel starts or stops launching an [android.app.Activity].
          */
         fun onLaunchingActivityChanged(isLaunchingActivity: Boolean) {}
-
-        /**
-         * Invoked when the "expand immediate" attribute changes.
-         *
-         * An example of expanding immediately is when swiping down from the top with two fingers.
-         * Instead of going to QQS, we immediately expand to full QS.
-         *
-         * Another example is when full QS is showing, and we swipe up from the bottom. Instead of
-         * going to QQS, the panel fully collapses.
-         */
-        fun onExpandImmediateChanged(isExpandImmediateEnabled: Boolean) {}
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
index 37073a6..374e871 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
@@ -22,6 +22,7 @@
 import android.view.LayoutInflater
 import android.view.ViewStub
 import androidx.constraintlayout.motion.widget.MotionLayout
+import com.android.keyguard.logging.ScrimLogger
 import com.android.systemui.battery.BatteryMeterView
 import com.android.systemui.battery.BatteryMeterViewController
 import com.android.systemui.biometrics.AuthRippleView
@@ -140,8 +141,14 @@
         @SysUISingleton
         fun providesLightRevealScrim(
             notificationShadeWindowView: NotificationShadeWindowView,
+            scrimLogger: ScrimLogger,
         ): LightRevealScrim {
-            return notificationShadeWindowView.requireViewById(R.id.light_reveal_scrim)
+            val scrim =
+                notificationShadeWindowView.requireViewById<LightRevealScrim>(
+                    R.id.light_reveal_scrim
+                )
+            scrim.scrimLogger = scrimLogger
+            return scrim
         }
 
         @Provides
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
index 8bab669..47b08fe 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
@@ -93,6 +93,29 @@
      */
     @Deprecated("Use ShadeInteractor instead") val legacyIsQsExpanded: StateFlow<Boolean>
 
+    /**
+     * QuickSettingsController.mExpandImmediate as a flow. Indicates that Quick Settings is being
+     * expanded without first expanding the Shade or Quick Settings is being collapsed without first
+     * collapsing to shade, i.e. expanding with 2-finger swipe or collapsing by flinging from the
+     * bottom of the screen. Replaced by ShadeInteractor.isQsBypassingShade.
+     */
+    @Deprecated("Use ShadeInteractor.isQsBypassingShade instead")
+    val legacyExpandImmediate: StateFlow<Boolean>
+
+    /** True when QS is taking up the entire screen, i.e. fully expanded on a non-unfolded phone. */
+    @Deprecated("Use ShadeInteractor instead") val legacyQsFullscreen: StateFlow<Boolean>
+
+    /**  */
+    @Deprecated("Use ShadeInteractor instead")
+    fun setLegacyQsFullscreen(legacyQsFullscreen: Boolean)
+
+    /**
+     * Sets whether Quick Settings is being expanded without first expanding the Shade or Quick
+     * Settings is being collapsed without first collapsing to shade.
+     */
+    @Deprecated("Use ShadeInteractor instead")
+    fun setLegacyExpandImmediate(legacyExpandImmediate: Boolean)
+
     /** Sets whether QS is expanded. */
     @Deprecated("Use ShadeInteractor instead")
     fun setLegacyIsQsExpanded(legacyIsQsExpanded: Boolean)
@@ -105,12 +128,13 @@
     fun setLegacyExpandedOrAwaitingInputTransfer(legacyExpandedOrAwaitingInputTransfer: Boolean)
 
     /** Sets whether the user is moving Quick Settings with a pointer */
-    fun setLegacyQsTracking(legacyQsTracking: Boolean)
+    @Deprecated("Use ShadeInteractor instead") fun setLegacyQsTracking(legacyQsTracking: Boolean)
 
     /** Sets whether the user is moving the shade with a pointer */
-    fun setLegacyShadeTracking(tracking: Boolean)
+    @Deprecated("Use ShadeInteractor instead") fun setLegacyShadeTracking(tracking: Boolean)
 
     /** Sets whether the user is moving the shade with a pointer, on lockscreen only */
+    @Deprecated("Use ShadeInteractor instead")
     fun setLegacyLockscreenShadeTracking(tracking: Boolean)
 
     /** Amount shade has expanded with regard to the UDFPS location */
@@ -199,6 +223,22 @@
     @Deprecated("Use ShadeInteractor instead")
     override val legacyIsQsExpanded: StateFlow<Boolean> = _legacyIsQsExpanded.asStateFlow()
 
+    private val _legacyExpandImmediate = MutableStateFlow(false)
+    @Deprecated("Use ShadeInteractor instead")
+    override val legacyExpandImmediate: StateFlow<Boolean> = _legacyExpandImmediate.asStateFlow()
+
+    private val _legacyQsFullscreen = MutableStateFlow(false)
+    @Deprecated("Use ShadeInteractor instead")
+    override val legacyQsFullscreen: StateFlow<Boolean> = _legacyQsFullscreen.asStateFlow()
+
+    override fun setLegacyQsFullscreen(legacyQsFullscreen: Boolean) {
+        _legacyQsFullscreen.value = legacyQsFullscreen
+    }
+
+    override fun setLegacyExpandImmediate(legacyExpandImmediate: Boolean) {
+        _legacyExpandImmediate.value = legacyExpandImmediate
+    }
+
     @Deprecated("Use ShadeInteractor instead")
     override fun setLegacyIsQsExpanded(legacyIsQsExpanded: Boolean) {
         _legacyIsQsExpanded.value = legacyIsQsExpanded
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
index d687ef6..6a9757f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
@@ -16,149 +16,42 @@
 
 package com.android.systemui.shade.domain.interactor
 
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.keyguard.data.repository.KeyguardRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.DozeStateModel
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.StatusBarState
-import com.android.systemui.power.domain.interactor.PowerInteractor
-import com.android.systemui.scene.domain.interactor.SceneInteractor
-import com.android.systemui.scene.shared.flag.SceneContainerFlags
-import com.android.systemui.scene.shared.model.ObservableTransitionState
-import com.android.systemui.scene.shared.model.SceneKey
-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.pipeline.mobile.data.repository.UserSetupRepository
-import com.android.systemui.statusbar.policy.data.repository.DeviceProvisioningRepository
-import com.android.systemui.user.domain.interactor.UserSwitcherInteractor
-import javax.inject.Inject
-import javax.inject.Provider
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.currentCoroutineContext
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.isActive
 
 /** Business logic for shade interactions. */
-@OptIn(ExperimentalCoroutinesApi::class)
-@SysUISingleton
-class ShadeInteractor
-@Inject
-constructor(
-    @Application scope: CoroutineScope,
-    deviceProvisioningRepository: DeviceProvisioningRepository,
-    disableFlagsRepository: DisableFlagsRepository,
-    dozeParams: DozeParameters,
-    sceneContainerFlags: SceneContainerFlags,
-    // TODO(b/300258424) convert to direct reference instead of provider
-    sceneInteractorProvider: Provider<SceneInteractor>,
-    keyguardRepository: KeyguardRepository,
-    keyguardTransitionInteractor: KeyguardTransitionInteractor,
-    powerInteractor: PowerInteractor,
-    userSetupRepository: UserSetupRepository,
-    userSwitcherInteractor: UserSwitcherInteractor,
-    sharedNotificationContainerInteractor: SharedNotificationContainerInteractor,
-    private val repository: ShadeRepository,
-) {
+interface ShadeInteractor : BaseShadeInteractor {
     /** Emits true if the shade is currently allowed and false otherwise. */
-    val isShadeEnabled: StateFlow<Boolean> =
-        disableFlagsRepository.disableFlags
-            .map { it.isShadeEnabled() }
-            .stateIn(scope, SharingStarted.Eagerly, initialValue = false)
-
-    /**
-     * Whether split shade, the combined notifications and quick settings shade used for large
-     * screens, is enabled.
-     */
-    val isSplitShadeEnabled: Flow<Boolean> =
-        sharedNotificationContainerInteractor.configurationBasedDimensions
-            .map { dimens -> dimens.useSplitShade }
-            .distinctUntilChanged()
-
-    /** The amount [0-1] that the shade has been opened */
-    val shadeExpansion: Flow<Float> =
-        if (sceneContainerFlags.isEnabled()) {
-            sceneBasedExpansion(sceneInteractorProvider.get(), SceneKey.Shade)
-        } else {
-            combine(
-                    repository.lockscreenShadeExpansion,
-                    keyguardRepository.statusBarState,
-                    repository.legacyShadeExpansion,
-                    repository.qsExpansion,
-                    isSplitShadeEnabled
-                ) {
-                    lockscreenShadeExpansion,
-                    statusBarState,
-                    legacyShadeExpansion,
-                    qsExpansion,
-                    splitShadeEnabled ->
-                    when (statusBarState) {
-                        // legacyShadeExpansion is 1 instead of 0 when QS is expanded
-                        StatusBarState.SHADE ->
-                            if (!splitShadeEnabled && qsExpansion > 0f) 0f else legacyShadeExpansion
-                        StatusBarState.KEYGUARD -> lockscreenShadeExpansion
-                        // dragDownAmount, which drives lockscreenShadeExpansion resets to 0f when
-                        // the pointer is lifted and the lockscreen shade is fully expanded
-                        StatusBarState.SHADE_LOCKED -> 1f
-                    }
-                }
-                .distinctUntilChanged()
-        }
-
-    /**
-     * The amount [0-1] QS has been opened. Normal shade with notifications (QQS) visible will
-     * report 0f. If split shade is enabled, value matches shadeExpansion.
-     */
-    val qsExpansion: StateFlow<Float> =
-        if (sceneContainerFlags.isEnabled()) {
-            val qsExp = sceneBasedExpansion(sceneInteractorProvider.get(), SceneKey.QuickSettings)
-            combine(isSplitShadeEnabled, shadeExpansion, qsExp) {
-                    isSplitShadeEnabled,
-                    shadeExp,
-                    qsExp ->
-                    if (isSplitShadeEnabled) {
-                        shadeExp
-                    } else {
-                        qsExp
-                    }
-                }
-                .stateIn(scope, SharingStarted.Eagerly, 0f)
-        } else {
-            repository.qsExpansion
-        }
-
-    /** Whether Quick Settings is expanded a non-zero amount. */
-    val isQsExpanded: StateFlow<Boolean> =
-        if (sceneContainerFlags.isEnabled()) {
-            qsExpansion
-                .map { it > 0 }
-                .distinctUntilChanged()
-                .stateIn(scope, SharingStarted.Eagerly, false)
-        } else {
-            repository.legacyIsQsExpanded
-        }
-
-    /** The amount [0-1] either QS or the shade has been opened. */
-    val anyExpansion: StateFlow<Float> =
-        combine(shadeExpansion, qsExpansion) { shadeExp, qsExp -> maxOf(shadeExp, qsExp) }
-            .stateIn(scope, SharingStarted.Eagerly, 0f)
+    val isShadeEnabled: StateFlow<Boolean>
 
     /** Whether either the shade or QS is fully expanded. */
-    val isAnyFullyExpanded: Flow<Boolean> = anyExpansion.map { it >= 1f }.distinctUntilChanged()
+    val isAnyFullyExpanded: Flow<Boolean>
+
+    /** Whether the Shade is fully expanded. */
+    val isShadeFullyExpanded: Flow<Boolean>
+
+    /**
+     * Whether the user is expanding or collapsing either the shade or quick settings with user
+     * input (i.e. dragging a pointer). This will be true even if the user's input gesture had ended
+     * but a transition they initiated is still animating.
+     */
+    val isUserInteracting: Flow<Boolean>
+
+    /** Are touches allowed on the notification panel? */
+    val isShadeTouchable: Flow<Boolean>
+
+    /** Emits true if the shade can be expanded from QQS to QS and false otherwise. */
+    val isExpandToQsEnabled: Flow<Boolean>
+}
+
+/** ShadeInteractor methods with implementations that differ between non-empty impls. */
+interface BaseShadeInteractor {
+    /** The amount [0-1] either QS or the shade has been opened. */
+    val anyExpansion: StateFlow<Float>
 
     /**
      * Whether either the shade or QS is partially or fully expanded, i.e. not fully collapsed. At
@@ -169,149 +62,53 @@
      *
      * TODO(b/300258424) remove all but the first sentence of this comment
      */
-    val isAnyExpanded: StateFlow<Boolean> =
-        if (sceneContainerFlags.isEnabled()) {
-                anyExpansion.map { it > 0f }.distinctUntilChanged()
-            } else {
-                repository.legacyExpandedOrAwaitingInputTransfer
-            }
-            .stateIn(scope, SharingStarted.Eagerly, false)
+    val isAnyExpanded: StateFlow<Boolean>
+
+    /** The amount [0-1] that the shade has been opened. */
+    val shadeExpansion: Flow<Float>
+
+    /**
+     * The amount [0-1] QS has been opened. Normal shade with notifications (QQS) visible will
+     * report 0f. If split shade is enabled, value matches shadeExpansion.
+     */
+    val qsExpansion: StateFlow<Float>
+
+    /** Whether Quick Settings is expanded a non-zero amount. */
+    val isQsExpanded: StateFlow<Boolean>
+
+    /**
+     * Emits true whenever Quick Settings is being expanded without first expanding the Shade or if
+     * if Quick Settings is being collapsed without first collapsing to shade, i.e. expanding with
+     * 2-finger swipe or collapsing by flinging from the bottom of the screen. This concept was
+     * previously called "expand immediate" in the legacy codebase.
+     */
+    val isQsBypassingShade: Flow<Boolean>
+
+    /**
+     * Emits true when QS is displayed over the entire screen of the device. Currently, this only
+     * happens on phones that are not unfolded when QS expansion is equal to 1.
+     */
+    val isQsFullscreen: Flow<Boolean>
 
     /**
      * Whether the user is expanding or collapsing the shade with user input. This will be true even
      * if the user's input gesture has ended but a transition they initiated is animating.
      */
-    val isUserInteractingWithShade: Flow<Boolean> =
-        if (sceneContainerFlags.isEnabled()) {
-            sceneBasedInteracting(sceneInteractorProvider.get(), SceneKey.Shade)
-        } else {
-            combine(
-                userInteractingFlow(
-                    repository.legacyShadeTracking,
-                    repository.legacyShadeExpansion
-                ),
-                repository.legacyLockscreenShadeTracking
-            ) { legacyShadeTracking, legacyLockscreenShadeTracking ->
-                legacyShadeTracking || legacyLockscreenShadeTracking
-            }
-        }
+    val isUserInteractingWithShade: Flow<Boolean>
 
     /**
      * Whether the user is expanding or collapsing quick settings with user input. This will be true
      * even if the user's input gesture has ended but a transition they initiated is still
      * animating.
      */
-    val isUserInteractingWithQs: Flow<Boolean> =
-        if (sceneContainerFlags.isEnabled()) {
-            sceneBasedInteracting(sceneInteractorProvider.get(), SceneKey.QuickSettings)
-        } else {
-            userInteractingFlow(repository.legacyQsTracking, repository.qsExpansion)
-        }
+    val isUserInteractingWithQs: Flow<Boolean>
+}
 
-    /**
-     * Whether the user is expanding or collapsing either the shade or quick settings with user
-     * input (i.e. dragging a pointer). This will be true even if the user's input gesture had ended
-     * but a transition they initiated is still animating.
-     */
-    val isUserInteracting: Flow<Boolean> =
-        combine(isUserInteractingWithShade, isUserInteractingWithQs) { shade, qs -> shade || qs }
-            .distinctUntilChanged()
-
-    /** Are touches allowed on the notification panel? */
-    val isShadeTouchable: Flow<Boolean> =
-        combine(
-            powerInteractor.isAsleep,
-            keyguardTransitionInteractor.isInTransitionToStateWhere { it == KeyguardState.AOD },
-            keyguardRepository.dozeTransitionModel.map { it.to == DozeStateModel.DOZE_PULSING },
-            deviceProvisioningRepository.isFactoryResetProtectionActive,
-        ) { isAsleep, goingToSleep, isPulsing, isFrpActive ->
-            when {
-                // Touches are disabled when Factory Reset Protection is active
-                isFrpActive -> false
-                // If the device is going to sleep, only accept touches if we're still
-                // animating
-                goingToSleep -> dozeParams.shouldControlScreenOff()
-                // If the device is asleep, only accept touches if there's a pulse
-                isAsleep -> isPulsing
-                else -> true
-            }
-        }
-
-    /** Emits true if the shade can be expanded from QQS to QS and false otherwise. */
-    val isExpandToQsEnabled: Flow<Boolean> =
-        combine(
-            disableFlagsRepository.disableFlags,
-            isShadeEnabled,
-            keyguardRepository.isDozing,
-            userSetupRepository.isUserSetupFlow,
-            deviceProvisioningRepository.isDeviceProvisioned,
-        ) { disableFlags, isShadeEnabled, isDozing, isUserSetup, isDeviceProvisioned ->
-            isDeviceProvisioned &&
-                // Disallow QS during setup if it's a simple user switcher. (The user intends to
-                // use the lock screen user switcher, QS is not needed.)
-                (isUserSetup || !userSwitcherInteractor.isSimpleUserSwitcher) &&
-                isShadeEnabled &&
-                disableFlags.isQuickSettingsEnabled() &&
-                !isDozing
-        }
-
-    fun sceneBasedExpansion(sceneInteractor: SceneInteractor, sceneKey: SceneKey) =
-        sceneInteractor.transitionState
-            .flatMapLatest { state ->
-                when (state) {
-                    is ObservableTransitionState.Idle ->
-                        if (state.scene == sceneKey) {
-                            flowOf(1f)
-                        } else {
-                            flowOf(0f)
-                        }
-                    is ObservableTransitionState.Transition ->
-                        if (state.toScene == sceneKey) {
-                            state.progress
-                        } else if (state.fromScene == sceneKey) {
-                            state.progress.map { progress -> 1 - progress }
-                        } else {
-                            flowOf(0f)
-                        }
-                }
-            }
-            .distinctUntilChanged()
-
-    fun sceneBasedInteracting(sceneInteractor: SceneInteractor, sceneKey: SceneKey) =
-        sceneInteractor.transitionState
-            .map { state ->
-                when (state) {
-                    is ObservableTransitionState.Idle -> false
-                    is ObservableTransitionState.Transition ->
-                        state.isInitiatedByUserInput &&
-                            (state.toScene == sceneKey || state.fromScene == sceneKey)
-                }
-            }
-            .distinctUntilChanged()
-
-    /**
-     * Return a flow for whether a user is interacting with an expandable shade component using
-     * tracking and expansion flows. NOTE: expansion must be a `StateFlow` to guarantee that
-     * [expansion.first] checks the current value of the flow.
-     */
-    private fun userInteractingFlow(
-        tracking: Flow<Boolean>,
-        expansion: StateFlow<Float>
-    ): Flow<Boolean> {
-        return flow {
-            // initial value is false
-            emit(false)
-            while (currentCoroutineContext().isActive) {
-                // wait for tracking to become true
-                tracking.first { it }
-                emit(true)
-                // wait for tracking to become false
-                tracking.first { !it }
-                // wait for expansion to complete in either direction
-                expansion.first { it <= 0f || it >= 1f }
-                // interaction complete
-                emit(false)
-            }
-        }
-    }
+fun createAnyExpansionFlow(
+    scope: CoroutineScope,
+    shadeExpansion: Flow<Float>,
+    qsExpansion: Flow<Float>
+): StateFlow<Float> {
+    return combine(shadeExpansion, qsExpansion) { shadeExp, qsExp -> maxOf(shadeExp, qsExp) }
+        .stateIn(scope, SharingStarted.Eagerly, 0f)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
new file mode 100644
index 0000000..d41c5a6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.shade.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+/** Empty implementation of ShadeInteractor for System UI variants with no shade. */
+@SysUISingleton
+class ShadeInteractorEmptyImpl @Inject constructor() : ShadeInteractor {
+    private val inactiveFlowBoolean = MutableStateFlow(false)
+    private val inactiveFlowFloat = MutableStateFlow(0f)
+    override val isShadeEnabled: StateFlow<Boolean> = inactiveFlowBoolean
+    override val shadeExpansion: Flow<Float> = inactiveFlowFloat
+    override val qsExpansion: StateFlow<Float> = inactiveFlowFloat
+    override val isQsExpanded: StateFlow<Boolean> = inactiveFlowBoolean
+    override val isQsBypassingShade: Flow<Boolean> = inactiveFlowBoolean
+    override val isQsFullscreen: Flow<Boolean> = inactiveFlowBoolean
+    override val anyExpansion: StateFlow<Float> = inactiveFlowFloat
+    override val isAnyFullyExpanded: Flow<Boolean> = inactiveFlowBoolean
+    override val isShadeFullyExpanded: Flow<Boolean> = inactiveFlowBoolean
+    override val isAnyExpanded: StateFlow<Boolean> = inactiveFlowBoolean
+    override val isUserInteractingWithShade: Flow<Boolean> = inactiveFlowBoolean
+    override val isUserInteractingWithQs: Flow<Boolean> = inactiveFlowBoolean
+    override val isUserInteracting: Flow<Boolean> = inactiveFlowBoolean
+    override val isShadeTouchable: Flow<Boolean> = inactiveFlowBoolean
+    override val isExpandToQsEnabled: Flow<Boolean> = inactiveFlowBoolean
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
new file mode 100644
index 0000000..68600e9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.shade.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.DozeStateModel
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
+import com.android.systemui.statusbar.phone.DozeParameters
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
+import com.android.systemui.statusbar.policy.data.repository.DeviceProvisioningRepository
+import com.android.systemui.user.domain.interactor.UserSwitcherInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/** The non-empty SceneInteractor implementation. */
+@SysUISingleton
+class ShadeInteractorImpl
+@Inject
+constructor(
+    @Application val scope: CoroutineScope,
+    deviceProvisioningRepository: DeviceProvisioningRepository,
+    disableFlagsRepository: DisableFlagsRepository,
+    dozeParams: DozeParameters,
+    keyguardRepository: KeyguardRepository,
+    keyguardTransitionInteractor: KeyguardTransitionInteractor,
+    powerInteractor: PowerInteractor,
+    userSetupRepository: UserSetupRepository,
+    userSwitcherInteractor: UserSwitcherInteractor,
+    private val baseShadeInteractor: BaseShadeInteractor,
+) : ShadeInteractor, BaseShadeInteractor by baseShadeInteractor {
+    override val isShadeEnabled: StateFlow<Boolean> =
+        disableFlagsRepository.disableFlags
+            .map { it.isShadeEnabled() }
+            .stateIn(scope, SharingStarted.Eagerly, initialValue = false)
+
+    override val isAnyFullyExpanded: Flow<Boolean> =
+        anyExpansion.map { it >= 1f }.distinctUntilChanged()
+
+    override val isShadeFullyExpanded: Flow<Boolean> =
+        baseShadeInteractor.shadeExpansion.map { it >= 1f }.distinctUntilChanged()
+
+    override val isUserInteracting: Flow<Boolean> =
+        combine(isUserInteractingWithShade, isUserInteractingWithQs) { shade, qs -> shade || qs }
+            .distinctUntilChanged()
+
+    override val isShadeTouchable: Flow<Boolean> =
+        combine(
+            powerInteractor.isAsleep,
+            keyguardTransitionInteractor.isInTransitionToStateWhere { it == KeyguardState.AOD },
+            keyguardRepository.dozeTransitionModel.map { it.to == DozeStateModel.DOZE_PULSING },
+            deviceProvisioningRepository.isFactoryResetProtectionActive,
+        ) { isAsleep, goingToSleep, isPulsing, isFrpActive ->
+            when {
+                // Touches are disabled when Factory Reset Protection is active
+                isFrpActive -> false
+                // If the device is going to sleep, only accept touches if we're still
+                // animating
+                goingToSleep -> dozeParams.shouldControlScreenOff()
+                // If the device is asleep, only accept touches if there's a pulse
+                isAsleep -> isPulsing
+                else -> true
+            }
+        }
+
+    override val isExpandToQsEnabled: Flow<Boolean> =
+        combine(
+            disableFlagsRepository.disableFlags,
+            isShadeEnabled,
+            keyguardRepository.isDozing,
+            userSetupRepository.isUserSetupFlow,
+            deviceProvisioningRepository.isDeviceProvisioned,
+        ) { disableFlags, isShadeEnabled, isDozing, isUserSetup, isDeviceProvisioned ->
+            isDeviceProvisioned &&
+                // Disallow QS during setup if it's a simple user switcher. (The user intends to
+                // use the lock screen user switcher, QS is not needed.)
+                (isUserSetup || !userSwitcherInteractor.isSimpleUserSwitcher) &&
+                isShadeEnabled &&
+                disableFlags.isQuickSettingsEnabled() &&
+                !isDozing
+        }
+}
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
new file mode 100644
index 0000000..2ac3193
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.shade.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.keyguard.shared.model.StatusBarState
+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
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.isActive
+
+/** ShadeInteractor implementation for the legacy codebase, e.g. NPVC. */
+@SysUISingleton
+class ShadeInteractorLegacyImpl
+@Inject
+constructor(
+    @Application val scope: CoroutineScope,
+    keyguardRepository: KeyguardRepository,
+    sharedNotificationContainerInteractor: SharedNotificationContainerInteractor,
+    repository: ShadeRepository,
+) : BaseShadeInteractor {
+    /** The amount [0-1] that the shade has been opened */
+    override val shadeExpansion: Flow<Float> =
+        combine(
+                repository.lockscreenShadeExpansion,
+                keyguardRepository.statusBarState,
+                repository.legacyShadeExpansion,
+                repository.qsExpansion,
+                sharedNotificationContainerInteractor.isSplitShadeEnabled
+            ) {
+                lockscreenShadeExpansion,
+                statusBarState,
+                legacyShadeExpansion,
+                qsExpansion,
+                splitShadeEnabled ->
+                when (statusBarState) {
+                    // legacyShadeExpansion is 1 instead of 0 when QS is expanded
+                    StatusBarState.SHADE ->
+                        if (!splitShadeEnabled && qsExpansion > 0f) 0f else legacyShadeExpansion
+                    StatusBarState.KEYGUARD -> lockscreenShadeExpansion
+                    // dragDownAmount, which drives lockscreenShadeExpansion resets to 0f when
+                    // the pointer is lifted and the lockscreen shade is fully expanded
+                    StatusBarState.SHADE_LOCKED -> 1f
+                }
+            }
+            .distinctUntilChanged()
+
+    override val qsExpansion: StateFlow<Float> = repository.qsExpansion
+
+    override val isQsExpanded: StateFlow<Boolean> = repository.legacyIsQsExpanded
+
+    override val isQsBypassingShade: Flow<Boolean> = repository.legacyExpandImmediate
+    override val isQsFullscreen: Flow<Boolean> = repository.legacyQsFullscreen
+
+    override val anyExpansion: StateFlow<Float> =
+        createAnyExpansionFlow(scope, shadeExpansion, qsExpansion)
+
+    override val isAnyExpanded =
+        repository.legacyExpandedOrAwaitingInputTransfer.stateIn(
+            scope,
+            SharingStarted.Eagerly,
+            false
+        )
+
+    override val isUserInteractingWithShade: Flow<Boolean> =
+        combine(
+            userInteractingFlow(repository.legacyShadeTracking, repository.legacyShadeExpansion),
+            repository.legacyLockscreenShadeTracking
+        ) { legacyShadeTracking, legacyLockscreenShadeTracking ->
+            legacyShadeTracking || legacyLockscreenShadeTracking
+        }
+
+    override val isUserInteractingWithQs: Flow<Boolean> =
+        userInteractingFlow(repository.legacyQsTracking, repository.qsExpansion)
+
+    /**
+     * Return a flow for whether a user is interacting with an expandable shade component using
+     * tracking and expansion flows. NOTE: expansion must be a `StateFlow` to guarantee that
+     * [expansion.first] checks the current value of the flow.
+     */
+    private fun userInteractingFlow(
+        tracking: Flow<Boolean>,
+        expansion: StateFlow<Float>
+    ): Flow<Boolean> {
+        return flow {
+            // initial value is false
+            emit(false)
+            while (currentCoroutineContext().isActive) {
+                // wait for tracking to become true
+                tracking.first { it }
+                emit(true)
+                // wait for tracking to become false
+                tracking.first { !it }
+                // wait for expansion to complete in either direction
+                expansion.first { it <= 0f || it >= 1f }
+                // interaction complete
+                emit(false)
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
new file mode 100644
index 0000000..7cff8ea
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.shade.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.ObservableTransitionState
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/** ShadeInteractor implementation for Scene Container. */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class ShadeInteractorSceneContainerImpl
+@Inject
+constructor(
+    @Application scope: CoroutineScope,
+    sceneInteractor: SceneInteractor,
+    sharedNotificationContainerInteractor: SharedNotificationContainerInteractor,
+) : BaseShadeInteractor {
+    override val shadeExpansion: Flow<Float> = sceneBasedExpansion(sceneInteractor, SceneKey.Shade)
+
+    private val sceneBasedQsExpansion = sceneBasedExpansion(sceneInteractor, SceneKey.QuickSettings)
+
+    override val qsExpansion: StateFlow<Float> =
+        combine(
+                sharedNotificationContainerInteractor.isSplitShadeEnabled,
+                shadeExpansion,
+                sceneBasedQsExpansion,
+            ) { isSplitShadeEnabled, shadeExpansion, qsExpansion ->
+                if (isSplitShadeEnabled) {
+                    shadeExpansion
+                } else {
+                    qsExpansion
+                }
+            }
+            .stateIn(scope, SharingStarted.Eagerly, 0f)
+
+    override val isQsExpanded: StateFlow<Boolean> =
+        qsExpansion
+            .map { it > 0 }
+            .distinctUntilChanged()
+            .stateIn(scope, SharingStarted.Eagerly, false)
+
+    override val isQsBypassingShade: Flow<Boolean> =
+        sceneInteractor.transitionState
+            .flatMapLatest { state ->
+                when (state) {
+                    is ObservableTransitionState.Idle -> flowOf(false)
+                    is ObservableTransitionState.Transition ->
+                        flowOf(
+                            state.toScene == SceneKey.QuickSettings &&
+                                state.fromScene != SceneKey.Shade
+                        )
+                }
+            }
+            .distinctUntilChanged()
+
+    override val isQsFullscreen: Flow<Boolean> =
+        sceneInteractor.transitionState
+            .map { state ->
+                when (state) {
+                    is ObservableTransitionState.Idle -> state.scene == SceneKey.QuickSettings
+                    is ObservableTransitionState.Transition -> false
+                }
+            }
+            .distinctUntilChanged()
+
+    override val anyExpansion: StateFlow<Float> =
+        createAnyExpansionFlow(scope, shadeExpansion, qsExpansion)
+
+    override val isAnyExpanded =
+        anyExpansion
+            .map { it > 0f }
+            .distinctUntilChanged()
+            .stateIn(scope, SharingStarted.Eagerly, false)
+
+    override val isUserInteractingWithShade: Flow<Boolean> =
+        sceneBasedInteracting(sceneInteractor, SceneKey.Shade)
+
+    override val isUserInteractingWithQs: Flow<Boolean> =
+        sceneBasedInteracting(sceneInteractor, SceneKey.QuickSettings)
+
+    /**
+     * Returns a flow that uses scene transition progress to and from a scene that is pulled down
+     * from the top of the screen to a 0-1 expansion amount float.
+     */
+    internal fun sceneBasedExpansion(sceneInteractor: SceneInteractor, sceneKey: SceneKey) =
+        sceneInteractor.transitionState
+            .flatMapLatest { state ->
+                when (state) {
+                    is ObservableTransitionState.Idle ->
+                        if (state.scene == sceneKey) {
+                            flowOf(1f)
+                        } else {
+                            flowOf(0f)
+                        }
+                    is ObservableTransitionState.Transition ->
+                        if (state.toScene == sceneKey) {
+                            state.progress
+                        } else if (state.fromScene == sceneKey) {
+                            state.progress.map { progress -> 1 - progress }
+                        } else {
+                            flowOf(0f)
+                        }
+                }
+            }
+            .distinctUntilChanged()
+
+    /**
+     * Returns a flow that uses scene transition data to determine whether the user is interacting
+     * with a scene that is pulled down from the top of the screen.
+     */
+    internal fun sceneBasedInteracting(sceneInteractor: SceneInteractor, sceneKey: SceneKey) =
+        sceneInteractor.transitionState
+            .map { state ->
+                when (state) {
+                    is ObservableTransitionState.Idle -> false
+                    is ObservableTransitionState.Transition ->
+                        state.isInitiatedByUserInput &&
+                            (state.toScene == sceneKey || state.fromScene == sceneKey)
+                }
+            }
+            .distinctUntilChanged()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index c2863fb..d88fab0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -75,14 +75,14 @@
 import com.android.systemui.statusbar.commandline.CommandRegistry;
 import com.android.systemui.statusbar.policy.CallbackController;
 
+import dagger.Lazy;
+
 import java.io.FileDescriptor;
 import java.io.FileOutputStream;
 import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 
-import dagger.Lazy;
-
 /**
  * This class takes the functions from IStatusBar that come in on
  * binder pool threads and posts messages to get them onto the main
@@ -424,7 +424,7 @@
         default void onTracingStateChanged(boolean enabled) { }
 
         /**
-         * Requests {@link com.android.systemui.accessibility.WindowMagnification} to invoke
+         * Requests {@link com.android.systemui.accessibility.Magnification} to invoke
          * {@code android.view.accessibility.AccessibilityManager#
          * setWindowMagnificationConnection(IWindowMagnificationConnection)}
          *
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
index 3120128..39b7930 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
@@ -18,6 +18,7 @@
 import android.view.View
 import android.view.animation.PathInterpolator
 import com.android.app.animation.Interpolators
+import com.android.keyguard.logging.ScrimLogger
 import com.android.systemui.shade.TouchLogger
 import com.android.systemui.statusbar.LightRevealEffect.Companion.getPercentPastThreshold
 import com.android.systemui.util.getColorWithAlpha
@@ -89,7 +90,7 @@
     }
 }
 
-class LinearLightRevealEffect(private val isVertical: Boolean) : LightRevealEffect {
+data class LinearLightRevealEffect(private val isVertical: Boolean) : LightRevealEffect {
 
     // Interpolator that reveals >80% of the content at 0.5 progress, makes revealing faster
     private val interpolator =
@@ -155,7 +156,7 @@
     }
 }
 
-class CircleReveal(
+data class CircleReveal(
     /** X-value of the circle center of the reveal. */
     val centerX: Int,
     /** Y-value of the circle center of the reveal. */
@@ -181,7 +182,7 @@
     }
 }
 
-class PowerButtonReveal(
+data class PowerButtonReveal(
     /** Approximate Y-value of the center of the power button on the physical device. */
     val powerButtonY: Float
 ) : LightRevealEffect {
@@ -253,7 +254,9 @@
 ) : View(context, attrs) {
 
     /** Listener that is called if the scrim's opaqueness changes */
-    lateinit var isScrimOpaqueChangedListener: Consumer<Boolean>
+    var isScrimOpaqueChangedListener: Consumer<Boolean>? = null
+
+    var scrimLogger: ScrimLogger? = null
 
     /**
      * How much of the underlying views are revealed, in percent. 0 means they will be completely
@@ -263,7 +266,9 @@
         set(value) {
             if (field != value) {
                 field = value
-
+                if (value <= 0.0f || value >= 1.0f) {
+                    scrimLogger?.d(TAG, "revealAmount", "$value on ${logString()}")
+                }
                 revealEffect.setRevealAmountOnScrim(value, this)
                 updateScrimOpaque()
                 Trace.traceCounter(
@@ -285,6 +290,7 @@
                 field = value
 
                 revealEffect.setRevealAmountOnScrim(revealAmount, this)
+                scrimLogger?.d(TAG, "revealEffect", "$value on ${logString()}")
                 invalidate()
             }
         }
@@ -301,6 +307,7 @@
      */
     internal var viewWidth: Int = initialWidth ?: 0
         private set
+
     internal var viewHeight: Int = initialHeight ?: 0
         private set
 
@@ -342,7 +349,8 @@
         private set(value) {
             if (field != value) {
                 field = value
-                isScrimOpaqueChangedListener.accept(field)
+                isScrimOpaqueChangedListener?.accept(field)
+                scrimLogger?.d(TAG, "isScrimOpaque", "$value on ${logString()}")
             }
         }
 
@@ -360,11 +368,13 @@
 
     override fun setAlpha(alpha: Float) {
         super.setAlpha(alpha)
+        scrimLogger?.d(TAG, "alpha", "$alpha on ${logString()}")
         updateScrimOpaque()
     }
 
     override fun setVisibility(visibility: Int) {
         super.setVisibility(visibility)
+        scrimLogger?.d(TAG, "visibility", "$visibility on ${logString()}")
         updateScrimOpaque()
     }
 
@@ -424,11 +434,7 @@
     }
 
     override fun onDraw(canvas: Canvas) {
-        if (
-            revealGradientWidth <= 0 ||
-            revealGradientHeight <= 0 ||
-            revealAmount == 0f
-        ) {
+        if (revealGradientWidth <= 0 || revealGradientHeight <= 0 || revealAmount == 0f) {
             if (revealAmount < 1f) {
                 canvas.drawColor(revealGradientEndColor)
             }
@@ -461,4 +467,8 @@
                 PorterDuff.Mode.MULTIPLY
             )
     }
+
+    private fun logString(): String {
+        return this::class.simpleName!! + "@" + hashCode()
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index 2e3f3f8..ae765e4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -21,7 +21,9 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingObserver
 import com.android.systemui.media.controls.ui.MediaHierarchyManager
+import com.android.systemui.navigationbar.gestural.Utilities.isTrackpadScroll
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction
 import com.android.systemui.plugins.FalsingManager
@@ -78,7 +80,8 @@
     private val shadeRepository: ShadeRepository,
     private val shadeInteractor: ShadeInteractor,
     private val powerInteractor: PowerInteractor,
-    private val splitShadeStateController: SplitShadeStateController
+    private val splitShadeStateController: SplitShadeStateController,
+    private val naturalScrollingSettingObserver: NaturalScrollingSettingObserver,
 ) : Dumpable {
     private var pulseHeight: Float = 0f
 
@@ -157,7 +160,8 @@
     var mUdfpsKeyguardViewControllerLegacy: UdfpsKeyguardViewControllerLegacy? = null
 
     /** The touch helper responsible for the drag down animation. */
-    val touchHelper = DragDownHelper(falsingManager, falsingCollector, this, context)
+    val touchHelper = DragDownHelper(falsingManager, falsingCollector, this,
+            naturalScrollingSettingObserver, context)
 
     private val splitShadeOverScroller: SplitShadeLockScreenOverScroller by lazy {
         splitShadeOverScrollerFactory.create({ qS }, { nsslController })
@@ -751,6 +755,7 @@
     private val falsingManager: FalsingManager,
     private val falsingCollector: FalsingCollector,
     private val dragDownCallback: LockscreenShadeTransitionController,
+    private val naturalScrollingSettingObserver: NaturalScrollingSettingObserver,
     context: Context
 ) : Gefingerpoken {
 
@@ -765,6 +770,7 @@
     private var draggedFarEnough = false
     private var startingChild: ExpandableView? = null
     private var lastHeight = 0f
+    private var isTrackpadReverseScroll = false
     var isDraggingDown = false
         private set
 
@@ -802,9 +808,11 @@
                 startingChild = null
                 initialTouchY = y
                 initialTouchX = x
+                isTrackpadReverseScroll = !naturalScrollingSettingObserver.isNaturalScrollingEnabled
+                        && isTrackpadScroll(true, event)
             }
             MotionEvent.ACTION_MOVE -> {
-                val h = y - initialTouchY
+                val h = (if (isTrackpadReverseScroll) -1 else 1) * (y - initialTouchY)
                 // Adjust the touch slop if another gesture may be being performed.
                 val touchSlop =
                     if (event.classification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE) {
@@ -834,7 +842,7 @@
         val y = event.y
         when (event.actionMasked) {
             MotionEvent.ACTION_MOVE -> {
-                lastHeight = y - initialTouchY
+                lastHeight = (if (isTrackpadReverseScroll) -1 else 1) * (y - initialTouchY)
                 captureStartingChild(initialTouchX, initialTouchY)
                 dragDownCallback.dragDownAmount = lastHeight + dragDownAmountOnStart
                 if (startingChild != null) {
@@ -859,12 +867,14 @@
                         !isFalseTouch &&
                         dragDownCallback.canDragDown()
                 ) {
-                    dragDownCallback.onDraggedDown(startingChild, (y - initialTouchY).toInt())
+                    val dragDown = (if (isTrackpadReverseScroll) -1 else 1) * (y - initialTouchY)
+                    dragDownCallback.onDraggedDown(startingChild, dragDown.toInt())
                     if (startingChild != null) {
                         expandCallback.setUserLockedChild(startingChild, false)
                         startingChild = null
                     }
                     isDraggingDown = false
+                    isTrackpadReverseScroll = false
                 } else {
                     stopDragging()
                     return false
@@ -943,6 +953,7 @@
             startingChild = null
         }
         isDraggingDown = false
+        isTrackpadReverseScroll = false
         dragDownCallback.onDragDownReset()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
index 9ff416a..9f2b0a6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.statusbar.StatusBarState.SHADE
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.MAX_HUN_WHEN_AGE_MS
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.HUN_SUPPRESSED_OLD_WHEN
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.BUBBLE
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PEEK
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE
@@ -141,7 +142,11 @@
 }
 
 class PeekOldWhenSuppressor(private val systemClock: SystemClock) :
-    VisualInterruptionFilter(types = setOf(PEEK), reason = "has old `when`") {
+    VisualInterruptionFilter(
+        types = setOf(PEEK),
+        reason = "has old `when`",
+        uiEventId = HUN_SUPPRESSED_OLD_WHEN
+    ) {
     private fun whenAge(entry: NotificationEntry) =
         systemClock.currentTimeMillis() - entry.sbn.notification.`when`
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/FullScreenIntentDecisionProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/FullScreenIntentDecisionProvider.kt
index b44a367..2707ed8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/FullScreenIntentDecisionProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/FullScreenIntentDecisionProvider.kt
@@ -18,6 +18,7 @@
 
 import android.app.NotificationManager.IMPORTANCE_HIGH
 import android.os.PowerManager
+import com.android.internal.logging.UiEventLogger.UiEventEnum
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.StatusBarState.KEYGUARD
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
@@ -37,6 +38,10 @@
 import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SUPPRESSED_ONLY_BY_DND
 import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SUPPRESSIVE_BUBBLE_METADATA
 import com.android.systemui.statusbar.notification.interruption.FullScreenIntentDecisionProvider.DecisionImpl.NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_BUBBLE_METADATA
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR
+import com.android.systemui.statusbar.notification.interruption.VisualInterruptionSuppressor.EventLogData
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 
@@ -52,6 +57,8 @@
         val logReason: String
         val shouldLog: Boolean
         val isWarning: Boolean
+        val uiEventId: UiEventEnum?
+        val eventLogData: EventLogData?
     }
 
     private enum class DecisionImpl(
@@ -60,7 +67,9 @@
         override val wouldFsiWithoutDnd: Boolean = shouldFsi,
         val supersedesDnd: Boolean = false,
         override val shouldLog: Boolean = true,
-        override val isWarning: Boolean = false
+        override val isWarning: Boolean = false,
+        override val uiEventId: UiEventEnum? = null,
+        override val eventLogData: EventLogData? = null
     ) : Decision {
         NO_FSI_NO_FULL_SCREEN_INTENT(
             false,
@@ -73,9 +82,17 @@
         NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR(
             false,
             "suppressive group alert behavior",
-            isWarning = true
+            isWarning = true,
+            uiEventId = FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR,
+            eventLogData = EventLogData("231322873", "groupAlertBehavior")
         ),
-        NO_FSI_SUPPRESSIVE_BUBBLE_METADATA(false, "suppressive bubble metadata", isWarning = true),
+        NO_FSI_SUPPRESSIVE_BUBBLE_METADATA(
+            false,
+            "suppressive bubble metadata",
+            isWarning = true,
+            uiEventId = FSI_SUPPRESSED_SUPPRESSIVE_BUBBLE_METADATA,
+            eventLogData = EventLogData("274759612", "bubbleMetadata")
+        ),
         NO_FSI_PACKAGE_SUSPENDED(false, "package suspended"),
         FSI_DEVICE_NOT_INTERACTIVE(true, "device is not interactive"),
         FSI_DEVICE_DREAMING(true, "device is dreaming"),
@@ -84,7 +101,13 @@
         FSI_KEYGUARD_OCCLUDED(true, "keyguard is occluded"),
         FSI_LOCKED_SHADE(true, "locked shade"),
         FSI_DEVICE_NOT_PROVISIONED(true, "device not provisioned"),
-        NO_FSI_NO_HUN_OR_KEYGUARD(false, "no HUN or keyguard", isWarning = true),
+        NO_FSI_NO_HUN_OR_KEYGUARD(
+            false,
+            "no HUN or keyguard",
+            isWarning = true,
+            uiEventId = FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD,
+            eventLogData = EventLogData("231322873", "no hun or keyguard")
+        ),
         NO_FSI_SUPPRESSED_BY_DND(false, "suppressed by DND", wouldFsiWithoutDnd = false),
         NO_FSI_SUPPRESSED_ONLY_BY_DND(false, "suppressed only by DND", wouldFsiWithoutDnd = true)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
index f2ade34..4045380 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
@@ -49,6 +49,7 @@
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.EventLog;
 import com.android.systemui.util.settings.GlobalSettings;
 import com.android.systemui.util.time.SystemClock;
 
@@ -81,6 +82,7 @@
     private final DeviceProvisionedController mDeviceProvisionedController;
     private final SystemClock mSystemClock;
     private final GlobalSettings mGlobalSettings;
+    private final EventLog mEventLog;
 
     @VisibleForTesting
     protected boolean mUseHeadsUp = false;
@@ -129,7 +131,8 @@
             UserTracker userTracker,
             DeviceProvisionedController deviceProvisionedController,
             SystemClock systemClock,
-            GlobalSettings globalSettings) {
+            GlobalSettings globalSettings,
+            EventLog eventLog) {
         mPowerManager = powerManager;
         mBatteryController = batteryController;
         mAmbientDisplayConfiguration = ambientDisplayConfiguration;
@@ -144,6 +147,7 @@
         mDeviceProvisionedController = deviceProvisionedController;
         mSystemClock = systemClock;
         mGlobalSettings = globalSettings;
+        mEventLog = eventLog;
         ContentObserver headsUpObserver = new ContentObserver(mainHandler) {
             @Override
             public void onChange(boolean selfChange) {
@@ -369,7 +373,7 @@
                 // explicitly prevent logging for this (frequent) case
                 return;
             case NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR:
-                android.util.EventLog.writeEvent(0x534e4554, "231322873", uid,
+                mEventLog.writeEvent(0x534e4554, "231322873", uid,
                         "groupAlertBehavior");
                 mUiEventLogger.log(FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR, uid,
                         packageName);
@@ -377,7 +381,7 @@
                         decision + ": GroupAlertBehavior will prevent HUN");
                 return;
             case NO_FSI_SUPPRESSIVE_BUBBLE_METADATA:
-                android.util.EventLog.writeEvent(0x534e4554, "274759612", uid,
+                mEventLog.writeEvent(0x534e4554, "274759612", uid,
                         "bubbleMetadata");
                 mUiEventLogger.log(FSI_SUPPRESSED_SUPPRESSIVE_BUBBLE_METADATA, uid,
                         packageName);
@@ -385,7 +389,7 @@
                         decision + ": BubbleMetadata may prevent HUN");
                 return;
             case NO_FSI_NO_HUN_OR_KEYGUARD:
-                android.util.EventLog.writeEvent(0x534e4554, "231322873", uid,
+                mEventLog.writeEvent(0x534e4554, "231322873", uid,
                         "no hun or keyguard");
                 mUiEventLogger.log(FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD, uid, packageName);
                 mLogger.logNoFullscreenWarning(entry,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapper.kt
index d7f0baf..f732e8d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapper.kt
@@ -17,6 +17,7 @@
 
 import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.RefactorFlagUtils
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider.Decision
@@ -62,6 +63,21 @@
         wrapped.removeSuppressor(suppressor)
     }
 
+    override fun addCondition(condition: VisualInterruptionCondition) = notValidInLegacyMode()
+
+    override fun removeCondition(condition: VisualInterruptionCondition) = notValidInLegacyMode()
+
+    override fun addFilter(filter: VisualInterruptionFilter) = notValidInLegacyMode()
+
+    override fun removeFilter(filter: VisualInterruptionFilter) = notValidInLegacyMode()
+
+    private fun notValidInLegacyMode() {
+        RefactorFlagUtils.assertOnEngBuild(
+            "This method is only implemented in VisualInterruptionDecisionProviderImpl, " +
+                "and so should only be called when FLAG_VISUAL_INTERRUPTIONS_REFACTOR is enabled."
+        )
+    }
+
     override fun makeUnloggedHeadsUpDecision(entry: NotificationEntry): Decision =
         wrapped.checkHeadsUp(entry, /* log= */ false).let { DecisionImpl.of(it) }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProvider.kt
index da8474e..de8863c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProvider.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification.interruption
 
+import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 
 /**
@@ -51,33 +52,77 @@
         val wouldInterruptWithoutDnd: Boolean
     }
 
-    /**
-     * Initializes the provider.
-     *
-     * Must be called before any method except [addLegacySuppressor].
-     */
+    /** Initializes the provider. */
     fun start() {}
 
     /**
-     * Adds a [component][suppressor] that can suppress visual interruptions.
+     * Adds a [NotificationInterruptSuppressor] that can suppress visual interruptions.
      *
-     * This class may call suppressors in any order.
+     * This method may be called before [start] has been called.
+     *
+     * This class may call suppressors, conditions, and filters in any order.
      *
      * @param[suppressor] the suppressor to add
      */
     fun addLegacySuppressor(suppressor: NotificationInterruptSuppressor)
 
     /**
-     * Removes a [component][suppressor] that can suppress visual interruptions.
+     * Removes a previously-added suppressor.
+     *
+     * This method may be called before [start] has been called.
      *
      * @param[suppressor] the suppressor to remove
      */
-    fun removeLegacySuppressor(suppressor: NotificationInterruptSuppressor)
+    @VisibleForTesting fun removeLegacySuppressor(suppressor: NotificationInterruptSuppressor)
+
+    /**
+     * Adds a [VisualInterruptionCondition] that can suppress visual interruptions without examining
+     * individual notifications.
+     *
+     * This method may be called before [start] has been called.
+     *
+     * This class may call suppressors, conditions, and filters in any order.
+     *
+     * @param[condition] the condition to add
+     */
+    fun addCondition(condition: VisualInterruptionCondition)
+
+    /**
+     * Removes a previously-added condition.
+     *
+     * This method may be called before [start] has been called.
+     *
+     * @param[condition] the condition to remove
+     */
+    @VisibleForTesting fun removeCondition(condition: VisualInterruptionCondition)
+
+    /**
+     * Adds a [VisualInterruptionFilter] that can suppress visual interruptions based on individual
+     * notifications.
+     *
+     * This method may be called before [start] has been called.
+     *
+     * This class may call suppressors, conditions, and filters in any order.
+     *
+     * @param[filter] the filter to add
+     */
+    fun addFilter(filter: VisualInterruptionFilter)
+
+    /**
+     * Removes a previously-added filter.
+     *
+     * This method may be called before [start] has been called.
+     *
+     * @param[filter] the filter to remove
+     */
+    @VisibleForTesting fun removeFilter(filter: VisualInterruptionFilter)
 
     /**
      * Decides whether a [notification][entry] should display as heads-up or not, but does not log
      * that decision.
      *
+     * [start] must be called before this method can be called.
+     *
      * @param[entry] the notification that this decision is about
      * @return the decision to display that notification as heads-up or not
      */
@@ -93,6 +138,8 @@
      * If the device is dozing, the decision will consider whether the notification should "pulse"
      * (wake the screen up and display the ambient view of the notification).
      *
+     * [start] must be called before this method can be called.
+     *
      * @see[makeUnloggedHeadsUpDecision]
      *
      * @param[entry] the notification that this decision is about
@@ -106,6 +153,8 @@
      *
      * The returned decision can be logged by passing it to [logFullScreenIntentDecision].
      *
+     * [start] must be called before this method can be called.
+     *
      * @see[makeAndLogHeadsUpDecision]
      *
      * @param[entry] the notification that this decision is about
@@ -116,6 +165,8 @@
     /**
      * Logs a previous [decision] to launch a full-screen intent or not.
      *
+     * [start] must be called before this method can be called.
+     *
      * @param[decision] the decision to log
      */
     fun logFullScreenIntentDecision(decision: FullScreenIntentDecision)
@@ -123,6 +174,8 @@
     /**
      * Decides whether a [notification][entry] should display as a bubble or not.
      *
+     * [start] must be called before this method can be called.
+     *
      * @param[entry] the notification that this decision is about
      * @return the decision to display that notification as a bubble or not
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
index c0a1a32..2b6e1a1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
@@ -20,12 +20,15 @@
 import android.os.PowerManager
 import android.util.Log
 import com.android.internal.annotations.VisibleForTesting
+import com.android.internal.logging.UiEventLogger
+import com.android.internal.logging.UiEventLogger.UiEventEnum
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider.Decision
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider.FullScreenIntentDecision
+import com.android.systemui.statusbar.notification.interruption.VisualInterruptionSuppressor.EventLogData
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.BUBBLE
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PEEK
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE
@@ -33,6 +36,7 @@
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.statusbar.policy.HeadsUpManager
 import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.EventLog
 import com.android.systemui.util.settings.GlobalSettings
 import com.android.systemui.util.time.SystemClock
 import javax.inject.Inject
@@ -43,6 +47,7 @@
     private val ambientDisplayConfiguration: AmbientDisplayConfiguration,
     private val batteryController: BatteryController,
     deviceProvisionedController: DeviceProvisionedController,
+    private val eventLog: EventLog,
     private val globalSettings: GlobalSettings,
     private val headsUpManager: HeadsUpManager,
     private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider,
@@ -52,14 +57,25 @@
     private val powerManager: PowerManager,
     private val statusBarStateController: StatusBarStateController,
     private val systemClock: SystemClock,
+    private val uiEventLogger: UiEventLogger,
     private val userTracker: UserTracker,
 ) : VisualInterruptionDecisionProvider {
+    interface Loggable {
+        val uiEventId: UiEventEnum?
+        val eventLogData: EventLogData?
+    }
+
     private class DecisionImpl(
         override val shouldInterrupt: Boolean,
         override val logReason: String
     ) : Decision
 
-    private data class LoggableDecision private constructor(val decision: DecisionImpl) {
+    private data class LoggableDecision
+    private constructor(
+        val decision: DecisionImpl,
+        override val uiEventId: UiEventEnum? = null,
+        override val eventLogData: EventLogData? = null
+    ) : Loggable {
         companion object {
             val unsuppressed =
                 LoggableDecision(DecisionImpl(shouldInterrupt = true, logReason = "not suppressed"))
@@ -74,7 +90,9 @@
 
             fun suppressed(suppressor: VisualInterruptionSuppressor) =
                 LoggableDecision(
-                    DecisionImpl(shouldInterrupt = false, logReason = suppressor.reason)
+                    DecisionImpl(shouldInterrupt = false, logReason = suppressor.reason),
+                    uiEventId = suppressor.uiEventId,
+                    eventLogData = suppressor.eventLogData
                 )
         }
     }
@@ -82,7 +100,7 @@
     private class FullScreenIntentDecisionImpl(
         val entry: NotificationEntry,
         private val fsiDecision: FullScreenIntentDecisionProvider.Decision
-    ) : FullScreenIntentDecision {
+    ) : FullScreenIntentDecision, Loggable {
         var hasBeenLogged = false
 
         override val shouldInterrupt
@@ -99,6 +117,12 @@
 
         val isWarning
             get() = fsiDecision.isWarning
+
+        override val uiEventId
+            get() = fsiDecision.uiEventId
+
+        override val eventLogData
+            get() = fsiDecision.eventLogData
     }
 
     private val fullScreenIntentDecisionProvider =
@@ -147,23 +171,23 @@
         legacySuppressors.remove(suppressor)
     }
 
-    fun addCondition(condition: VisualInterruptionCondition) {
+    override fun addCondition(condition: VisualInterruptionCondition) {
         conditions.add(condition)
         condition.start()
     }
 
     @VisibleForTesting
-    fun removeCondition(condition: VisualInterruptionCondition) {
+    override fun removeCondition(condition: VisualInterruptionCondition) {
         conditions.remove(condition)
     }
 
-    fun addFilter(filter: VisualInterruptionFilter) {
+    override fun addFilter(filter: VisualInterruptionFilter) {
         filters.add(filter)
         filter.start()
     }
 
     @VisibleForTesting
-    fun removeFilter(filter: VisualInterruptionFilter) {
+    override fun removeFilter(filter: VisualInterruptionFilter) {
         filters.remove(filter)
     }
 
@@ -214,9 +238,10 @@
     private fun logDecision(
         type: VisualInterruptionType,
         entry: NotificationEntry,
-        loggable: LoggableDecision
+        loggableDecision: LoggableDecision
     ) {
-        logger.logDecision(type.name, entry, loggable.decision)
+        logger.logDecision(type.name, entry, loggableDecision.decision)
+        logEvents(entry, loggableDecision)
     }
 
     override fun makeUnloggedFullScreenIntentDecision(
@@ -250,6 +275,14 @@
         }
 
         logger.logFullScreenIntentDecision(decision.entry, decision, decision.isWarning)
+        logEvents(decision.entry, decision)
+    }
+
+    private fun logEvents(entry: NotificationEntry, loggable: Loggable) {
+        loggable.uiEventId?.let { uiEventLogger.log(it, entry.sbn.uid, entry.sbn.packageName) }
+        loggable.eventLogData?.let {
+            eventLog.writeEvent(0x534e4554, it.number, entry.sbn.uid, it.description)
+        }
     }
 
     private fun checkSuppressInterruptions(entry: NotificationEntry) =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt
index 39199df..2047c62 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt
@@ -18,6 +18,7 @@
 
 import com.android.internal.logging.UiEventLogger.UiEventEnum
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.interruption.VisualInterruptionSuppressor.EventLogData
 
 /**
  * A reason why visual interruptions might be suppressed.
@@ -43,6 +44,9 @@
  * @see VisualInterruptionFilter
  */
 sealed interface VisualInterruptionSuppressor {
+    /** Data to be logged in the EventLog when an interruption is suppressed. */
+    data class EventLogData(val number: String, val description: String)
+
     /** The type(s) of interruption that this suppresses. */
     val types: Set<VisualInterruptionType>
 
@@ -52,6 +56,9 @@
     /** An optional UiEvent ID to be recorded when this suppresses an interruption. */
     val uiEventId: UiEventEnum?
 
+    /** Optional data to be logged in the EventLog when this suppresses an interruption. */
+    val eventLogData: EventLogData?
+
     /**
      * Called after the suppressor is added to the [VisualInterruptionDecisionProvider] but before
      * any other methods are called on the suppressor.
@@ -63,7 +70,8 @@
 abstract class VisualInterruptionCondition(
     override val types: Set<VisualInterruptionType>,
     override val reason: String,
-    override val uiEventId: UiEventEnum? = null
+    override val uiEventId: UiEventEnum? = null,
+    override val eventLogData: EventLogData? = null
 ) : VisualInterruptionSuppressor {
     /** @return true if these interruptions should be suppressed right now. */
     abstract fun shouldSuppress(): Boolean
@@ -73,7 +81,8 @@
 abstract class VisualInterruptionFilter(
     override val types: Set<VisualInterruptionType>,
     override val reason: String,
-    override val uiEventId: UiEventEnum? = null
+    override val uiEventId: UiEventEnum? = null,
+    override val eventLogData: EventLogData? = null
 ) : VisualInterruptionSuppressor {
     /**
      * @param entry the notification to consider suppressing
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 eb1c17a..c2c5eed 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
@@ -73,6 +73,11 @@
             }
             .distinctUntilChanged()
 
+    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/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index cbe9d4b..4e77801 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -49,7 +49,6 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.keyguard.domain.interactor.BiometricUnlockInteractor;
@@ -186,8 +185,6 @@
     private long mLastFpFailureUptimeMillis;
     private int mNumConsecutiveFpFailures;
 
-    private final FeatureFlags mFeatureFlags;
-
     private static final class PendingAuthenticated {
         public final int userId;
         public final BiometricSourceType biometricSourceType;
@@ -291,7 +288,6 @@
             ScreenOffAnimationController screenOffAnimationController,
             VibratorHelper vibrator,
             SystemClock systemClock,
-            FeatureFlags featureFlags,
             DeviceEntryHapticsInteractor hapticsInteractor,
             Lazy<SelectedUserInteractor> selectedUserInteractor,
             BiometricUnlockInteractor biometricUnlockInteractor
@@ -322,7 +318,6 @@
         mVibratorHelper = vibrator;
         mLogger = biometricUnlockLogger;
         mSystemClock = systemClock;
-        mFeatureFlags = featureFlags;
         mOrderUnlockAndWake = resources.getBoolean(
                 com.android.internal.R.bool.config_orderUnlockAndWake);
         mHapticsInteractor = hapticsInteractor;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
index 22b9298..60a4606 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -16,7 +16,6 @@
 
 package com.android.systemui.statusbar.phone;
 
-import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION;
 import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE;
 import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_WAKING;
 
@@ -42,25 +41,23 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.systemui.res.R;
 import com.android.systemui.assist.AssistManager;
 import com.android.systemui.camera.CameraIntents;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.DisplayId;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.QSPanelController;
 import com.android.systemui.recents.ScreenPinningRequest;
+import com.android.systemui.res.R;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.CameraLauncher;
 import com.android.systemui.shade.QuickSettingsController;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shade.ShadeViewController;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
@@ -97,7 +94,6 @@
     private final NotificationStackScrollLayoutController mNotificationStackScrollLayoutController;
     private final StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
     private final PowerManager mPowerManager;
-    private final VibratorHelper mVibratorHelper;
     private final Optional<Vibrator> mVibratorOptional;
     private final DisableFlagsLogger mDisableFlagsLogger;
     private final int mDisplayId;
@@ -108,8 +104,6 @@
     private final Lazy<CameraLauncher> mCameraLauncherLazy;
     private final QuickSettingsController mQsController;
     private final QSHost mQSHost;
-    private final FeatureFlags mFeatureFlags;
-
     private static final VibrationAttributes HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES =
             VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK);
 
@@ -139,15 +133,13 @@
             NotificationStackScrollLayoutController notificationStackScrollLayoutController,
             StatusBarHideIconsForBouncerManager statusBarHideIconsForBouncerManager,
             PowerManager powerManager,
-            VibratorHelper vibratorHelper,
             Optional<Vibrator> vibratorOptional,
             DisableFlagsLogger disableFlagsLogger,
             @DisplayId int displayId,
             Lazy<CameraLauncher> cameraLauncherLazy,
             UserTracker userTracker,
             QSHost qsHost,
-            ActivityStarter activityStarter,
-            FeatureFlags featureFlags) {
+            ActivityStarter activityStarter) {
         mCentralSurfaces = centralSurfaces;
         mQsController = quickSettingsController;
         mContext = context;
@@ -168,14 +160,12 @@
         mNotificationStackScrollLayoutController = notificationStackScrollLayoutController;
         mStatusBarHideIconsForBouncerManager = statusBarHideIconsForBouncerManager;
         mPowerManager = powerManager;
-        mVibratorHelper = vibratorHelper;
         mVibratorOptional = vibratorOptional;
         mDisableFlagsLogger = disableFlagsLogger;
         mDisplayId = displayId;
         mCameraLauncherLazy = cameraLauncherLazy;
         mUserTracker = userTracker;
         mQSHost = qsHost;
-        mFeatureFlags = featureFlags;
 
         mVibrateOnOpening = resources.getBoolean(R.bool.config_vibrateOnIconAnimation);
         mCameraLaunchGestureVibrationEffect = getCameraGestureVibrationEffect(
@@ -544,12 +534,8 @@
 
     @VisibleForTesting
     void vibrateOnNavigationKeyDown() {
-        if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
-            mShadeViewController.performHapticFeedback(
-                    HapticFeedbackConstants.GESTURE_START
-            );
-        } else {
-            mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
-        }
+        mShadeViewController.performHapticFeedback(
+                HapticFeedbackConstants.GESTURE_START
+        );
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index cd7a9ea..46675c2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -129,6 +129,7 @@
 import com.android.systemui.dagger.qualifiers.UiBackground;
 import com.android.systemui.demomode.DemoMode;
 import com.android.systemui.demomode.DemoModeController;
+import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor;
 import com.android.systemui.emergency.EmergencyGesture;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
@@ -2777,7 +2778,7 @@
         mScrimController.setExpansionAffectsAlpha(!unlocking);
 
         if (mAlternateBouncerInteractor.isVisibleState()) {
-            if (!mFeatureFlags.isEnabled(Flags.ALTERNATE_BOUNCER_VIEW)) {
+            if (!DeviceEntryUdfpsRefactor.isEnabled()) {
                 if ((!mKeyguardStateController.isOccluded() || mShadeSurface.isPanelExpanded())
                         && (mState == StatusBarState.SHADE || mState == StatusBarState.SHADE_LOCKED
                         || mTransitionToFullShadeProgress > 0f)) {
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 267b563..274b50f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -62,6 +62,7 @@
 import com.android.systemui.bouncer.ui.BouncerView;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.flags.FeatureFlags;
@@ -1573,7 +1574,7 @@
      * notification shade's child views.
      */
     public boolean shouldInterceptTouchEvent(MotionEvent event) {
-        if (mFlags.isEnabled(Flags.ALTERNATE_BOUNCER_VIEW)) {
+        if (DeviceEntryUdfpsRefactor.isEnabled()) {
             return false;
         }
         return mAlternateBouncerInteractor.isVisibleState();
@@ -1584,7 +1585,7 @@
      * showing.
      */
     public boolean onTouch(MotionEvent event) {
-        if (mFlags.isEnabled(Flags.ALTERNATE_BOUNCER_VIEW)) {
+        if (DeviceEntryUdfpsRefactor.isEnabled()) {
             return false;
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/SubscriptionManagerProxy.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/SubscriptionManagerProxy.kt
index 22d0483..a2f5701 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/SubscriptionManagerProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/SubscriptionManagerProxy.kt
@@ -16,15 +16,41 @@
 
 package com.android.systemui.statusbar.pipeline.mobile.util
 
+import android.annotation.SuppressLint
+import android.content.Context
+import android.telephony.SubscriptionInfo
 import android.telephony.SubscriptionManager
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
 
 interface SubscriptionManagerProxy {
     fun getDefaultDataSubscriptionId(): Int
+    fun isValidSubscriptionId(subId: Int): Boolean
+    suspend fun getActiveSubscriptionInfo(subId: Int): SubscriptionInfo?
 }
 
 /** Injectable proxy class for [SubscriptionManager]'s static methods */
-class SubscriptionManagerProxyImpl @Inject constructor() : SubscriptionManagerProxy {
+class SubscriptionManagerProxyImpl
+@Inject
+constructor(
+    @Application private val applicationContext: Context,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val subscriptionManager: SubscriptionManager,
+) : SubscriptionManagerProxy {
     /** The system default data subscription id, or INVALID_SUBSCRIPTION_ID on error */
     override fun getDefaultDataSubscriptionId() = SubscriptionManager.getDefaultDataSubscriptionId()
+
+    override fun isValidSubscriptionId(subId: Int): Boolean {
+        return SubscriptionManager.isValidSubscriptionId(subId)
+    }
+
+    @SuppressLint("MissingPermission")
+    override suspend fun getActiveSubscriptionInfo(subId: Int): SubscriptionInfo? {
+        return withContext(backgroundDispatcher) {
+            subscriptionManager.getActiveSubscriptionInfo(subId)
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
index 2afb435..36a1e8a 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
@@ -49,6 +49,7 @@
 import com.android.systemui.unfold.util.ScaleAwareTransitionProgressProvider.Companion.areAnimationsEnabled
 import com.android.systemui.util.concurrency.ThreadFactory
 import com.android.app.tracing.traceSection
+import com.android.keyguard.logging.ScrimLogger
 import com.android.wm.shell.displayareahelper.DisplayAreaHelper
 import java.util.Optional
 import java.util.concurrent.Executor
@@ -69,7 +70,8 @@
     @Main private val executor: Executor,
     private val threadFactory: ThreadFactory,
     private val rotationChangeProvider: RotationChangeProvider,
-    private val displayTracker: DisplayTracker
+    private val displayTracker: DisplayTracker,
+    private val scrimLogger: ScrimLogger,
 ) {
 
     private val transitionListener = TransitionListener()
@@ -179,8 +181,8 @@
                 )
                 .apply {
                     revealEffect = createLightRevealEffect()
-                    isScrimOpaqueChangedListener = Consumer {}
                     revealAmount = calculateRevealAmount()
+                    scrimLogger = this@UnfoldLightRevealOverlayAnimation.scrimLogger
                 }
 
         newRoot.setView(newView, params)
diff --git a/packages/SystemUI/src/com/android/systemui/util/EventLog.kt b/packages/SystemUI/src/com/android/systemui/util/EventLog.kt
new file mode 100644
index 0000000..dc794cf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/EventLog.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util
+
+/**
+ * Testable wrapper around {@link android.util.EventLog}.
+ *
+ * Dagger can inject this wrapper into your classes. The implementation just proxies calls to the
+ * real EventLog.
+ *
+ * In tests, pass an instance of FakeEventLog, which allows you to examine the values passed to the
+ * various methods below.
+ */
+interface EventLog {
+    /** @see android.util.EventLog.writeEvent */
+    fun writeEvent(tag: Int, value: Int): Int
+
+    /** @see android.util.EventLog.writeEvent */
+    fun writeEvent(tag: Int, value: Long): Int
+
+    /** @see android.util.EventLog.writeEvent */
+    fun writeEvent(tag: Int, value: Float): Int
+
+    /** @see android.util.EventLog.writeEvent */
+    fun writeEvent(tag: Int, value: String): Int
+
+    /** @see android.util.EventLog.writeEvent */
+    fun writeEvent(tag: Int, vararg values: Any): Int
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/EventLogImpl.kt b/packages/SystemUI/src/com/android/systemui/util/EventLogImpl.kt
new file mode 100644
index 0000000..6fb1adc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/EventLogImpl.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util
+
+import javax.inject.Inject
+
+/** Default implementation of [com.android.systemui.util.EventLog]. */
+class EventLogImpl @Inject constructor() : EventLog {
+    override fun writeEvent(tag: Int, value: Int): Int =
+        android.util.EventLog.writeEvent(tag, value)
+
+    override fun writeEvent(tag: Int, value: Long): Int =
+        android.util.EventLog.writeEvent(tag, value)
+
+    override fun writeEvent(tag: Int, value: Float): Int =
+        android.util.EventLog.writeEvent(tag, value)
+
+    override fun writeEvent(tag: Int, value: String): Int =
+        android.util.EventLog.writeEvent(tag, value)
+
+    override fun writeEvent(tag: Int, vararg values: Any): Int =
+        android.util.EventLog.writeEvent(tag, *values)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/EventLogModule.kt b/packages/SystemUI/src/com/android/systemui/util/EventLogModule.kt
new file mode 100644
index 0000000..ca0876c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/EventLogModule.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util
+
+import com.android.systemui.dagger.SysUISingleton
+import dagger.Binds
+import dagger.Module
+
+@Module
+interface EventLogModule {
+    @SysUISingleton @Binds fun bindEventLog(eventLogImpl: EventLogImpl?): EventLog?
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/CoroutinesModule.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/CoroutinesModule.kt
index 81737c7..cc9335e 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/CoroutinesModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/CoroutinesModule.kt
@@ -5,8 +5,7 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dagger.qualifiers.Tracing
-import com.android.systemui.flags.FeatureFlagsClassic
-import com.android.systemui.flags.Flags
+import com.android.systemui.Flags.coroutineTracing
 import com.android.app.tracing.TraceUtils.Companion.coroutineTracingIsEnabled
 import com.android.app.tracing.TraceContextElement
 import dagger.Module
@@ -15,32 +14,9 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import javax.inject.Qualifier
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.EmptyCoroutineContext
 
-/** Key associated with a [Boolean] flag that enables or disables the coroutine tracing feature. */
-@Qualifier
-annotation class CoroutineTracingEnabledKey
-
-/**
- * Same as [@Application], but does not make use of flags. This should only be used when early usage
- * of [@Application] would introduce a circular dependency on [FeatureFlagsClassic].
- */
-@Qualifier
-@MustBeDocumented
-@Retention(AnnotationRetention.RUNTIME)
-annotation class UnflaggedApplication
-
-/**
- * Same as [@Background], but does not make use of flags. This should only be used when early usage
- * of [@Application] would introduce a circular dependency on [FeatureFlagsClassic].
- */
-@Qualifier
-@MustBeDocumented
-@Retention(AnnotationRetention.RUNTIME)
-annotation class UnflaggedBackground
-
 /** Providers for various coroutines-related constructs. */
 @Module
 class CoroutinesModule {
@@ -53,11 +29,6 @@
 
     @Provides
     @SysUISingleton
-    @UnflaggedApplication
-    fun unflaggedApplicationScope(): CoroutineScope = CoroutineScope(Dispatchers.Main.immediate)
-
-    @Provides
-    @SysUISingleton
     @Main
     @Deprecated(
         "Use @Main CoroutineContext instead",
@@ -98,28 +69,14 @@
         return Dispatchers.IO + tracingCoroutineContext
     }
 
-    @Provides
-    @UnflaggedBackground
-    @SysUISingleton
-    fun unflaggedBackgroundCoroutineContext(): CoroutineContext {
-        return Dispatchers.IO
-    }
-
     @OptIn(ExperimentalCoroutinesApi::class)
     @Provides
     @Tracing
     @SysUISingleton
-    fun tracingCoroutineContext(
-        @CoroutineTracingEnabledKey enableTracing: Boolean
-    ): CoroutineContext = if (enableTracing) TraceContextElement() else EmptyCoroutineContext
-
-    companion object {
-        @[Provides CoroutineTracingEnabledKey]
-        fun provideIsCoroutineTracingEnabledKey(featureFlags: FeatureFlagsClassic): Boolean {
-            return if (featureFlags.isEnabled(Flags.COROUTINE_TRACING)) {
-                coroutineTracingIsEnabled = true
-                true
-            } else false
-        }
+    fun tracingCoroutineContext(): CoroutineContext {
+        return if (coroutineTracing()) {
+            coroutineTracingIsEnabled = true
+            TraceContextElement()
+        } else EmptyCoroutineContext
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IWindowMagnificationConnectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IWindowMagnificationConnectionTest.java
index 67d6aa8..d8799e1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IWindowMagnificationConnectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IWindowMagnificationConnectionTest.java
@@ -54,7 +54,7 @@
 
 /**
  * Tests for {@link android.view.accessibility.IWindowMagnificationConnection} retrieved from
- * {@link WindowMagnification}
+ * {@link Magnification}
  */
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
@@ -86,7 +86,7 @@
     private AccessibilityLogger mA11yLogger;
 
     private IWindowMagnificationConnection mIWindowMagnificationConnection;
-    private WindowMagnification mWindowMagnification;
+    private Magnification mMagnification;
     private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
 
     @Before
@@ -98,16 +98,16 @@
             return null;
         }).when(mAccessibilityManager).setWindowMagnificationConnection(
                 any(IWindowMagnificationConnection.class));
-        mWindowMagnification = new WindowMagnification(getContext(),
+        mMagnification = new Magnification(getContext(),
                 getContext().getMainThreadHandler(), mCommandQueue,
                 mModeSwitchesController, mSysUiState, mOverviewProxyService, mSecureSettings,
                 mDisplayTracker, getContext().getSystemService(DisplayManager.class), mA11yLogger);
-        mWindowMagnification.mMagnificationControllerSupplier = new FakeControllerSupplier(
+        mMagnification.mMagnificationControllerSupplier = new FakeControllerSupplier(
                 mContext.getSystemService(DisplayManager.class));
-        mWindowMagnification.mMagnificationSettingsSupplier = new FakeSettingsSupplier(
+        mMagnification.mMagnificationSettingsSupplier = new FakeSettingsSupplier(
                 mContext.getSystemService(DisplayManager.class));
 
-        mWindowMagnification.requestWindowMagnificationConnection(true);
+        mMagnification.requestWindowMagnificationConnection(true);
         assertNotNull(mIWindowMagnificationConnection);
         mIWindowMagnificationConnection.setConnectionCallback(mConnectionCallback);
     }
@@ -161,7 +161,7 @@
     @Test
     public void showMagnificationButton() throws RemoteException {
         // magnification settings panel should not be showing
-        assertFalse(mWindowMagnification.isMagnificationSettingsPanelShowing(TEST_DISPLAY));
+        assertFalse(mMagnification.isMagnificationSettingsPanelShowing(TEST_DISPLAY));
 
         mIWindowMagnificationConnection.showMagnificationButton(TEST_DISPLAY,
                 Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
@@ -195,8 +195,8 @@
                 testUserId, TEST_DISPLAY, testScale);
         waitForIdleSync();
 
-        assertTrue(mWindowMagnification.mUsersScales.contains(testUserId));
-        assertEquals(mWindowMagnification.mUsersScales.get(testUserId).get(TEST_DISPLAY),
+        assertTrue(mMagnification.mUsersScales.contains(testUserId));
+        assertEquals(mMagnification.mUsersScales.get(testUserId).get(TEST_DISPLAY),
                 (Float) testScale);
         verify(mMagnificationSettingsController).setMagnificationScale(eq(testScale));
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java
similarity index 88%
rename from packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java
index d7b6602..c972feb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java
@@ -65,7 +65,7 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
-public class WindowMagnificationTest extends SysuiTestCase {
+public class MagnificationTest extends SysuiTestCase {
 
     private static final int TEST_DISPLAY = Display.DEFAULT_DISPLAY;
     @Mock
@@ -82,7 +82,7 @@
     private SecureSettings mSecureSettings;
 
     private CommandQueue mCommandQueue;
-    private WindowMagnification mWindowMagnification;
+    private Magnification mMagnification;
     private OverviewProxyListener mOverviewProxyListener;
     private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
 
@@ -107,12 +107,12 @@
         when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
 
         doAnswer(invocation -> {
-            mWindowMagnification.mMagnificationSettingsControllerCallback
+            mMagnification.mMagnificationSettingsControllerCallback
                     .onSettingsPanelVisibilityChanged(TEST_DISPLAY, /* shown= */ true);
             return null;
         }).when(mMagnificationSettingsController).toggleSettingsPanelVisibility();
         doAnswer(invocation -> {
-            mWindowMagnification.mMagnificationSettingsControllerCallback
+            mMagnification.mMagnificationSettingsControllerCallback
                     .onSettingsPanelVisibilityChanged(TEST_DISPLAY, /* shown= */ false);
             return null;
         }).when(mMagnificationSettingsController).closeMagnificationSettings();
@@ -120,15 +120,15 @@
         when(mWindowMagnificationController.isActivated()).thenReturn(true);
 
         mCommandQueue = new CommandQueue(getContext(), mDisplayTracker);
-        mWindowMagnification = new WindowMagnification(getContext(),
+        mMagnification = new Magnification(getContext(),
                 getContext().getMainThreadHandler(), mCommandQueue, mModeSwitchesController,
                 mSysUiState, mOverviewProxyService, mSecureSettings, mDisplayTracker,
                 getContext().getSystemService(DisplayManager.class), mA11yLogger);
-        mWindowMagnification.mMagnificationControllerSupplier = new FakeControllerSupplier(
+        mMagnification.mMagnificationControllerSupplier = new FakeControllerSupplier(
                 mContext.getSystemService(DisplayManager.class), mWindowMagnificationController);
-        mWindowMagnification.mMagnificationSettingsSupplier = new FakeSettingsSupplier(
+        mMagnification.mMagnificationSettingsSupplier = new FakeSettingsSupplier(
                 mContext.getSystemService(DisplayManager.class), mMagnificationSettingsController);
-        mWindowMagnification.start();
+        mMagnification.start();
 
         final ArgumentCaptor<OverviewProxyListener> listenerArgumentCaptor =
                 ArgumentCaptor.forClass(OverviewProxyListener.class);
@@ -156,7 +156,7 @@
         mCommandQueue.requestWindowMagnificationConnection(true);
         waitForIdleSync();
 
-        mWindowMagnification.mWindowMagnifierCallback
+        mMagnification.mWindowMagnifierCallback
                 .onWindowMagnifierBoundsChanged(TEST_DISPLAY, testBounds);
 
         verify(mConnectionCallback).onWindowMagnifierBoundsChanged(TEST_DISPLAY, testBounds);
@@ -169,7 +169,7 @@
         mCommandQueue.requestWindowMagnificationConnection(true);
         waitForIdleSync();
 
-        mWindowMagnification.mWindowMagnifierCallback
+        mMagnification.mWindowMagnifierCallback
                 .onPerformScaleAction(TEST_DISPLAY, newScale, updatePersistence);
 
         verify(mConnectionCallback).onPerformScaleAction(
@@ -181,7 +181,7 @@
         mCommandQueue.requestWindowMagnificationConnection(true);
         waitForIdleSync();
 
-        mWindowMagnification.mWindowMagnifierCallback
+        mMagnification.mWindowMagnifierCallback
                 .onAccessibilityActionPerformed(TEST_DISPLAY);
 
         verify(mConnectionCallback).onAccessibilityActionPerformed(TEST_DISPLAY);
@@ -192,14 +192,14 @@
         mCommandQueue.requestWindowMagnificationConnection(true);
         waitForIdleSync();
 
-        mWindowMagnification.mWindowMagnifierCallback.onMove(TEST_DISPLAY);
+        mMagnification.mWindowMagnifierCallback.onMove(TEST_DISPLAY);
 
         verify(mConnectionCallback).onMove(TEST_DISPLAY);
     }
 
     @Test
     public void onClickSettingsButton_enabled_showPanelForWindowMode() {
-        mWindowMagnification.mWindowMagnifierCallback.onClickSettingsButton(TEST_DISPLAY);
+        mMagnification.mWindowMagnifierCallback.onClickSettingsButton(TEST_DISPLAY);
         waitForIdleSync();
 
         verify(mMagnificationSettingsController).toggleSettingsPanelVisibility();
@@ -212,7 +212,7 @@
     @Test
     public void onSetMagnifierSize_delegateToMagnifier() {
         final @MagnificationSize int index = MagnificationSize.SMALL;
-        mWindowMagnification.mMagnificationSettingsControllerCallback.onSetMagnifierSize(
+        mMagnification.mMagnificationSettingsControllerCallback.onSetMagnifierSize(
                 TEST_DISPLAY, index);
         waitForIdleSync();
 
@@ -225,7 +225,7 @@
 
     @Test
     public void onSetDiagonalScrolling_delegateToMagnifier() {
-        mWindowMagnification.mMagnificationSettingsControllerCallback.onSetDiagonalScrolling(
+        mMagnification.mMagnificationSettingsControllerCallback.onSetDiagonalScrolling(
                 TEST_DISPLAY, /* enable= */ true);
         waitForIdleSync();
 
@@ -235,7 +235,7 @@
     @Test
     public void onEditMagnifierSizeMode_windowActivated_delegateToMagnifier() {
         when(mWindowMagnificationController.isActivated()).thenReturn(true);
-        mWindowMagnification.mMagnificationSettingsControllerCallback.onEditMagnifierSizeMode(
+        mMagnification.mMagnificationSettingsControllerCallback.onEditMagnifierSizeMode(
                 TEST_DISPLAY, /* enable= */ true);
         waitForIdleSync();
 
@@ -243,7 +243,7 @@
         verify(mA11yLogger).log(
                 eq(MagnificationSettingsEvent.MAGNIFICATION_SETTINGS_SIZE_EDITING_ACTIVATED));
 
-        mWindowMagnification.mMagnificationSettingsControllerCallback.onEditMagnifierSizeMode(
+        mMagnification.mMagnificationSettingsControllerCallback.onEditMagnifierSizeMode(
                 TEST_DISPLAY, /* enable= */ false);
         waitForIdleSync();
         verify(mA11yLogger).log(
@@ -258,7 +258,7 @@
         waitForIdleSync();
         final float scale = 3.0f;
         final boolean updatePersistence = false;
-        mWindowMagnification.mMagnificationSettingsControllerCallback.onMagnifierScale(
+        mMagnification.mMagnificationSettingsControllerCallback.onMagnifierScale(
                 TEST_DISPLAY, scale, updatePersistence);
 
         verify(mConnectionCallback).onPerformScaleAction(
@@ -274,7 +274,7 @@
         mCommandQueue.requestWindowMagnificationConnection(true);
         waitForIdleSync();
 
-        mWindowMagnification.mMagnificationSettingsControllerCallback.onModeSwitch(
+        mMagnification.mMagnificationSettingsControllerCallback.onModeSwitch(
                 TEST_DISPLAY, ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
         waitForIdleSync();
 
@@ -292,7 +292,7 @@
         mCommandQueue.requestWindowMagnificationConnection(true);
         waitForIdleSync();
 
-        mWindowMagnification.mMagnificationSettingsControllerCallback.onModeSwitch(
+        mMagnification.mMagnificationSettingsControllerCallback.onModeSwitch(
                 TEST_DISPLAY, ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW);
         waitForIdleSync();
 
@@ -305,7 +305,7 @@
     public void onSettingsPanelVisibilityChanged_windowActivated_delegateToMagnifier() {
         when(mWindowMagnificationController.isActivated()).thenReturn(true);
         final boolean shown = false;
-        mWindowMagnification.mMagnificationSettingsControllerCallback
+        mMagnification.mMagnificationSettingsControllerCallback
                 .onSettingsPanelVisibilityChanged(TEST_DISPLAY, shown);
         waitForIdleSync();
 
@@ -325,9 +325,9 @@
     @Test
     public void overviewProxyIsConnected_controllerIsAvailable_updateSysUiStateFlag() {
         final WindowMagnificationController mController = mock(WindowMagnificationController.class);
-        mWindowMagnification.mMagnificationControllerSupplier = new FakeControllerSupplier(
+        mMagnification.mMagnificationControllerSupplier = new FakeControllerSupplier(
                 mContext.getSystemService(DisplayManager.class), mController);
-        mWindowMagnification.mMagnificationControllerSupplier.get(TEST_DISPLAY);
+        mMagnification.mMagnificationControllerSupplier.get(TEST_DISPLAY);
 
         mOverviewProxyListener.onConnectionChanged(true);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
index 87ab5b0..64ddbc7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
@@ -29,7 +29,10 @@
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
+import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.scene.SceneTestUtils
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
@@ -51,10 +54,12 @@
 
     @Mock private lateinit var lockPatternUtils: LockPatternUtils
     @Mock private lateinit var getSecurityMode: Function<Int, KeyguardSecurityModel.SecurityMode>
+    @Mock private lateinit var tableLogger: TableLogBuffer
 
     private val testUtils = SceneTestUtils(this)
     private val testScope = testUtils.testScope
     private val userRepository = FakeUserRepository()
+    private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository
 
     private lateinit var underTest: AuthenticationRepository
 
@@ -67,6 +72,8 @@
         userRepository.setUserInfos(USER_INFOS)
         runBlocking { userRepository.setSelectedUserInfo(USER_INFOS[0]) }
         whenever(getSecurityMode.apply(anyInt())).thenAnswer { currentSecurityMode }
+        mobileConnectionsRepository =
+            FakeMobileConnectionsRepository(FakeMobileMappingsProxy(), tableLogger)
 
         underTest =
             AuthenticationRepositoryImpl(
@@ -76,6 +83,7 @@
                 userRepository = userRepository,
                 lockPatternUtils = lockPatternUtils,
                 broadcastDispatcher = fakeBroadcastDispatcher,
+                mobileConnectionsRepository = mobileConnectionsRepository,
             )
     }
 
@@ -97,6 +105,11 @@
             assertThat(authMethod).isEqualTo(AuthenticationMethodModel.None)
             assertThat(underTest.getAuthenticationMethod())
                 .isEqualTo(AuthenticationMethodModel.None)
+
+            currentSecurityMode = KeyguardSecurityModel.SecurityMode.SimPin
+            mobileConnectionsRepository.isAnySimSecure.value = true
+            assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Sim)
+            assertThat(underTest.getAuthenticationMethod()).isEqualTo(AuthenticationMethodModel.Sim)
         }
 
     @Test
@@ -157,8 +170,7 @@
 
             userRepository.setSelectedUserInfo(USER_INFOS[1])
             assertThat(values.last()).isTrue()
-
-    }
+        }
 
     private fun setSecurityModeAndDispatchBroadcast(
         securityMode: KeyguardSecurityModel.SecurityMode,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
index 7439db2..56d3d26 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
@@ -455,4 +455,22 @@
 
             assertThat(hintedPinLength).isNull()
         }
+
+    @Test
+    fun authenticate_withTooShortPassword() =
+        testScope.runTest {
+            utils.authenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Password
+            )
+            assertThat(
+                    underTest.authenticate(
+                        buildList {
+                            repeat(utils.authenticationRepository.minPasswordLength - 1) { time ->
+                                add("$time")
+                            }
+                        }
+                    )
+                )
+                .isEqualTo(AuthenticationResult.SKIPPED)
+        }
 }
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 2d95b09..f4122d5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -50,8 +50,6 @@
 import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
 import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
 import com.android.systemui.display.data.repository.FakeDisplayRepository
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.events.ANIMATING_OUT
@@ -87,8 +85,6 @@
     @JvmField @Rule
     var mockitoRule = MockitoJUnit.rule()
 
-    private val featureFlags = FakeFeatureFlags()
-
     @Mock
     lateinit var callback: AuthDialogCallback
     @Mock
@@ -135,7 +131,6 @@
     @Before
     fun setup() {
         displayRepository = FakeDisplayRepository()
-        featureFlags.set(Flags.ONE_WAY_HAPTICS_API_MIGRATION, false)
 
         displayStateInteractor =
             DisplayStateInteractorImpl(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt
index d2b81e0..00ea78f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt
@@ -17,14 +17,14 @@
 package com.android.systemui.biometrics
 
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FakeFeatureFlagsClassicModule
 import com.android.systemui.flags.Flags
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.shade.data.repository.FakeShadeRepository
 import com.android.systemui.user.domain.UserDomainLayerModule
 import dagger.BindsInstance
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index c5f16aa..5f0d4d4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -43,7 +43,7 @@
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
@@ -68,7 +68,6 @@
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 import org.mockito.junit.MockitoJUnit
-import javax.inject.Provider
 import org.mockito.Mockito.`when` as whenever
 
 private const val REQUEST_ID = 2L
@@ -111,11 +110,10 @@
     @Mock private lateinit var featureFlags: FeatureFlags
     @Mock private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
     @Mock private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
-    @Mock private lateinit var udfpsUtils: UdfpsUtils
     @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor
     @Mock private lateinit var udfpsKeyguardAccessibilityDelegate:
             UdfpsKeyguardAccessibilityDelegate
-    @Mock private lateinit var udfpsKeyguardViewModels: Provider<UdfpsKeyguardViewModels>
+    @Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
     @Captor private lateinit var layoutParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams>
 
     private val onTouch = { _: View, _: MotionEvent, _: Boolean -> true }
@@ -164,7 +162,7 @@
             alternateBouncerInteractor,
             isDebuggable,
             udfpsKeyguardAccessibilityDelegate,
-            udfpsKeyguardViewModels,
+            keyguardTransitionInteractor,
             mSelectedUserInteractor,
         )
         block()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index 675ca63..c8c400d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -86,6 +86,7 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.plugins.FalsingManager;
@@ -237,6 +238,8 @@
     private ViewRootImpl mViewRootImpl;
     @Mock
     private FpsUnlockTracker mFpsUnlockTracker;
+    @Mock
+    private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
 
     @Before
     public void setUp() {
@@ -329,7 +332,8 @@
                 mUdfpsKeyguardAccessibilityDelegate,
                 mUdfpsKeyguardViewModels,
                 mSelectedUserInteractor,
-                mFpsUnlockTracker
+                mFpsUnlockTracker,
+                mKeyguardTransitionInteractor
         );
         verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture());
         mOverlayController = mOverlayCaptor.getValue();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java
index 2c4e136..2ea803c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java
@@ -31,6 +31,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.shade.ShadeExpansionStateManager;
@@ -71,6 +72,7 @@
     protected @Mock AlternateBouncerInteractor mAlternateBouncerInteractor;
     protected @Mock UdfpsKeyguardAccessibilityDelegate mUdfpsKeyguardAccessibilityDelegate;
     protected @Mock SelectedUserInteractor mSelectedUserInteractor;
+    protected @Mock KeyguardTransitionInteractor mKeyguardTransitionInteractor;
 
     protected FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
 
@@ -141,11 +143,11 @@
                 mDialogManager,
                 mUdfpsController,
                 mActivityLaunchAnimator,
-                mFeatureFlags,
                 mPrimaryBouncerInteractor,
                 mAlternateBouncerInteractor,
                 mUdfpsKeyguardAccessibilityDelegate,
-                mSelectedUserInteractor);
+                mSelectedUserInteractor,
+                mKeyguardTransitionInteractor);
         return controller;
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerTest.java
index 21928cd..98d8b05 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerTest.java
@@ -66,17 +66,12 @@
     public void testViewControllerQueriesSBStateOnAttached() {
         mController.onViewAttached();
         verify(mStatusBarStateController).getState();
-        verify(mStatusBarStateController).getDozeAmount();
 
-        final float dozeAmount = .88f;
         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE_LOCKED);
-        when(mStatusBarStateController.getDozeAmount()).thenReturn(dozeAmount);
         captureStatusBarStateListeners();
 
         mController.onViewAttached();
         verify(mView, atLeast(1)).setPauseAuth(true);
-        verify(mView).onDozeAmountChanged(dozeAmount, dozeAmount,
-                UdfpsKeyguardViewLegacy.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN);
     }
 
     @Test
@@ -91,19 +86,6 @@
     }
 
     @Test
-    public void testDozeEventsSentToView() {
-        mController.onViewAttached();
-        captureStatusBarStateListeners();
-
-        final float linear = .55f;
-        final float eased = .65f;
-        mStatusBarStateListener.onDozeAmountChanged(linear, eased);
-
-        verify(mView).onDozeAmountChanged(linear, eased,
-                UdfpsKeyguardViewLegacy.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN);
-    }
-
-    @Test
     public void testShouldPauseAuthUnpausedAlpha0() {
         mController.onViewAttached();
         captureStatusBarStateListeners();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
index 97dada2..a49150f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
@@ -31,7 +31,12 @@
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.keyguard.DismissCallbackRegistry
 import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.FakeTrustRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.StatusBarState
@@ -49,6 +54,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mock
 import org.mockito.Mockito
 import org.mockito.Mockito.mock
@@ -65,6 +71,7 @@
     private val testScope = TestScope(testDispatcher)
 
     private lateinit var keyguardBouncerRepository: KeyguardBouncerRepository
+    private lateinit var transitionRepository: FakeKeyguardTransitionRepository
 
     @Mock private lateinit var bouncerLogger: TableLogBuffer
 
@@ -78,6 +85,7 @@
                 testScope.backgroundScope,
                 bouncerLogger,
             )
+        transitionRepository = FakeKeyguardTransitionRepository()
         super.setUp()
     }
 
@@ -107,6 +115,12 @@
                 mock(SystemClock::class.java),
                 mKeyguardUpdateMonitor,
             )
+        mKeyguardTransitionInteractor =
+            KeyguardTransitionInteractorFactory.create(
+                    scope = testScope.backgroundScope,
+                    repository = transitionRepository,
+                )
+                .keyguardTransitionInteractor
         return createUdfpsKeyguardViewController(/* useModernBouncer */ true)
     }
 
@@ -258,4 +272,145 @@
 
             job.cancel()
         }
+
+    @Test
+    fun aodToLockscreen_dozeAmountChanged() =
+        testScope.runTest {
+            // GIVEN view is attached
+            mController.onViewAttached()
+            Mockito.reset(mView)
+
+            val job = mController.listenForLockscreenAodTransitions(this)
+
+            // WHEN transitioning from lockscreen to aod
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.AOD,
+                    value = .3f,
+                    transitionState = TransitionState.RUNNING
+                )
+            )
+            runCurrent()
+            // THEN doze amount is updated
+            verify(mView)
+                .onDozeAmountChanged(
+                    eq(.3f),
+                    eq(.3f),
+                    eq(UdfpsKeyguardViewLegacy.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN)
+                )
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.AOD,
+                    value = 1f,
+                    transitionState = TransitionState.FINISHED
+                )
+            )
+            runCurrent()
+            // THEN doze amount is updated
+            verify(mView)
+                .onDozeAmountChanged(
+                    eq(1f),
+                    eq(1f),
+                    eq(UdfpsKeyguardViewLegacy.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN)
+                )
+
+            job.cancel()
+        }
+
+    @Test
+    fun lockscreenToAod_dozeAmountChanged() =
+        testScope.runTest {
+            // GIVEN view is attached
+            mController.onViewAttached()
+            Mockito.reset(mView)
+
+            val job = mController.listenForLockscreenAodTransitions(this)
+
+            // WHEN transitioning from lockscreen to aod
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.AOD,
+                    value = .3f,
+                    transitionState = TransitionState.RUNNING
+                )
+            )
+            runCurrent()
+            // THEN doze amount is updated
+            verify(mView)
+                .onDozeAmountChanged(
+                    eq(.3f),
+                    eq(.3f),
+                    eq(UdfpsKeyguardViewLegacy.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN)
+                )
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.AOD,
+                    value = 1f,
+                    transitionState = TransitionState.FINISHED
+                )
+            )
+            runCurrent()
+            // THEN doze amount is updated
+            verify(mView)
+                .onDozeAmountChanged(
+                    eq(1f),
+                    eq(1f),
+                    eq(UdfpsKeyguardViewLegacy.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN)
+                )
+
+            job.cancel()
+        }
+
+    @Test
+    fun goneToAod_dozeAmountChanged() =
+        testScope.runTest {
+            // GIVEN view is attached
+            mController.onViewAttached()
+            Mockito.reset(mView)
+
+            val job = mController.listenForGoneToAodTransition(this)
+
+            // WHEN transitioning from lockscreen to aod
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.GONE,
+                    to = KeyguardState.AOD,
+                    value = .3f,
+                    transitionState = TransitionState.RUNNING
+                )
+            )
+            runCurrent()
+            // THEN doze amount is updated
+            verify(mView)
+                .onDozeAmountChanged(
+                    eq(.3f),
+                    eq(.3f),
+                    eq(UdfpsKeyguardViewLegacy.ANIMATION_UNLOCKED_SCREEN_OFF)
+                )
+
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.GONE,
+                    to = KeyguardState.AOD,
+                    value = 1f,
+                    transitionState = TransitionState.FINISHED
+                )
+            )
+            runCurrent()
+            // THEN doze amount is updated
+            verify(mView)
+                .onDozeAmountChanged(
+                    eq(1f),
+                    eq(1f),
+                    eq(UdfpsKeyguardViewLegacy.ANIMATION_UNLOCKED_SCREEN_OFF)
+                )
+
+            job.cancel()
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/SimBouncerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/SimBouncerRepositoryTest.kt
new file mode 100644
index 0000000..b391b5a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/SimBouncerRepositoryTest.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.data.repository
+
+import android.telephony.TelephonyManager
+import android.telephony.euicc.EuiccManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeSubscriptionManagerProxy
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.test.StandardTestDispatcher
+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
+import org.mockito.Mock
+import org.mockito.Mockito.anyInt
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SimBouncerRepositoryTest : SysuiTestCase() {
+    @Mock lateinit var euiccManager: EuiccManager
+    @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+
+    private val dispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(dispatcher)
+    private val fakeSubscriptionManagerProxy = FakeSubscriptionManagerProxy()
+    private val keyguardUpdateMonitorCallbacks = mutableListOf<KeyguardUpdateMonitorCallback>()
+
+    private lateinit var underTest: SimBouncerRepositoryImpl
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(/* testClass = */ this)
+        whenever(keyguardUpdateMonitor.registerCallback(any())).thenAnswer {
+            val cb = it.arguments[0] as KeyguardUpdateMonitorCallback
+            keyguardUpdateMonitorCallbacks.add(cb)
+        }
+        whenever(keyguardUpdateMonitor.removeCallback(any())).thenAnswer {
+            keyguardUpdateMonitorCallbacks.remove(it.arguments[0])
+        }
+        underTest =
+            SimBouncerRepositoryImpl(
+                applicationScope = testScope.backgroundScope,
+                backgroundDispatcher = dispatcher,
+                resources = context.resources,
+                keyguardUpdateMonitor = keyguardUpdateMonitor,
+                subscriptionManager = fakeSubscriptionManagerProxy,
+                broadcastDispatcher = fakeBroadcastDispatcher,
+                euiccManager = euiccManager,
+            )
+    }
+
+    @Test
+    fun subscriptionId() =
+        testScope.runTest {
+            val subscriptionId =
+                emitSubscriptionIdAndCollectLastValue(underTest.subscriptionId, subId = 2)
+            assertThat(subscriptionId).isEqualTo(2)
+        }
+
+    @Test
+    fun activeSubscriptionInfo() =
+        testScope.runTest {
+            fakeSubscriptionManagerProxy.setActiveSubscriptionInfo(subId = 2)
+            val activeSubscriptionInfo =
+                emitSubscriptionIdAndCollectLastValue(underTest.activeSubscriptionInfo, subId = 2)
+
+            assertThat(activeSubscriptionInfo?.subscriptionId).isEqualTo(2)
+        }
+
+    @Test
+    fun isLockedEsim_initialValue_isNull() =
+        testScope.runTest {
+            val isLockedEsim by collectLastValue(underTest.isLockedEsim)
+            assertThat(isLockedEsim).isNull()
+        }
+
+    @Test
+    fun isLockedEsim() =
+        testScope.runTest {
+            whenever(euiccManager.isEnabled).thenReturn(true)
+            fakeSubscriptionManagerProxy.setActiveSubscriptionInfo(subId = 2, isEmbedded = true)
+            val isLockedEsim =
+                emitSubscriptionIdAndCollectLastValue(underTest.isLockedEsim, subId = 2)
+            assertThat(isLockedEsim).isTrue()
+        }
+
+    @Test
+    fun isLockedEsim_notEmbedded() =
+        testScope.runTest {
+            fakeSubscriptionManagerProxy.setActiveSubscriptionInfo(subId = 2, isEmbedded = false)
+            val isLockedEsim =
+                emitSubscriptionIdAndCollectLastValue(underTest.isLockedEsim, subId = 2)
+            assertThat(isLockedEsim).isFalse()
+        }
+
+    @Test
+    fun isSimPukLocked() =
+        testScope.runTest {
+            val isSimPukLocked =
+                emitSubscriptionIdAndCollectLastValue(
+                    underTest.isSimPukLocked,
+                    subId = 2,
+                    isSimPuk = true
+                )
+            assertThat(isSimPukLocked).isTrue()
+        }
+
+    @Test
+    fun setSimPukUserInput() {
+        val pukCode = "00000000"
+        val pinCode = "1234"
+        underTest.setSimPukUserInput(pukCode, pinCode)
+        assertThat(underTest.simPukInputModel.enteredSimPuk).isEqualTo(pukCode)
+        assertThat(underTest.simPukInputModel.enteredSimPin).isEqualTo(pinCode)
+    }
+
+    @Test
+    fun setSimPukUserInput_nullPuk() {
+        val pukCode = null
+        val pinCode = "1234"
+        underTest.setSimPukUserInput(pukCode, pinCode)
+        assertThat(underTest.simPukInputModel.enteredSimPuk).isNull()
+        assertThat(underTest.simPukInputModel.enteredSimPin).isEqualTo(pinCode)
+    }
+
+    @Test
+    fun setSimPukUserInput_nullPin() {
+        val pukCode = "00000000"
+        val pinCode = null
+        underTest.setSimPukUserInput(pukCode, pinCode)
+        assertThat(underTest.simPukInputModel.enteredSimPuk).isEqualTo(pukCode)
+        assertThat(underTest.simPukInputModel.enteredSimPin).isNull()
+    }
+
+    @Test
+    fun setSimPukUserInput_nullCodes() {
+        underTest.setSimPukUserInput()
+        assertThat(underTest.simPukInputModel.enteredSimPuk).isNull()
+        assertThat(underTest.simPukInputModel.enteredSimPin).isNull()
+    }
+
+    @Test
+    fun setSimPinVerificationErrorMessage() =
+        testScope.runTest {
+            val errorMsg = "error"
+            underTest.setSimVerificationErrorMessage(errorMsg)
+            val msg by collectLastValue(underTest.errorDialogMessage)
+            assertThat(msg).isEqualTo(errorMsg)
+        }
+
+    /** Emits a new sim card state and collects the last value of the flow argument. */
+    @OptIn(ExperimentalCoroutinesApi::class)
+    private fun <T> TestScope.emitSubscriptionIdAndCollectLastValue(
+        flow: Flow<T>,
+        subId: Int = 1,
+        isSimPuk: Boolean = false
+    ): T? {
+        val value by collectLastValue(flow)
+        runCurrent()
+        val simState =
+            if (isSimPuk) {
+                TelephonyManager.SIM_STATE_PUK_REQUIRED
+            } else {
+                TelephonyManager.SIM_STATE_PIN_REQUIRED
+            }
+        whenever(keyguardUpdateMonitor.getNextSubIdForState(anyInt())).thenReturn(-1)
+        whenever(keyguardUpdateMonitor.getNextSubIdForState(simState)).thenReturn(subId)
+        keyguardUpdateMonitorCallbacks.forEach {
+            it.onSimStateChanged(subId, /* slotId= */ 0, simState)
+        }
+        runCurrent()
+        return value
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
index 296f966..50d2fd2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
@@ -88,6 +88,19 @@
         }
 
     @Test
+    fun pinAuthMethod_sim_skipsAuthentication() =
+        testScope.runTest {
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Sim)
+            runCurrent()
+
+            // We rely on TelephonyManager to authenticate the sim card.
+            // Additionally, authenticating the sim card does not unlock the device.
+            // Thus, when auth method is sim, we expect to skip here.
+            assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
+                .isEqualTo(AuthenticationResult.SKIPPED)
+        }
+
+    @Test
     fun pinAuthMethod_tryAutoConfirm_withAutoConfirmPin() =
         testScope.runTest {
             val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled)
@@ -159,6 +172,19 @@
             underTest.resetMessage()
             assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
 
+            // Too short input.
+            assertThat(
+                    underTest.authenticate(
+                        buildList {
+                            repeat(utils.authenticationRepository.minPasswordLength - 1) { time ->
+                                add("$time")
+                            }
+                        }
+                    )
+                )
+                .isEqualTo(AuthenticationResult.SKIPPED)
+            assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD)
+
             // Correct input.
             assertThat(underTest.authenticate("password".toList()))
                 .isEqualTo(AuthenticationResult.SUCCEEDED)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt
new file mode 100644
index 0000000..8c53c0e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.domain.interactor
+
+import android.content.res.Resources
+import android.telephony.PinResult
+import android.telephony.SubscriptionInfo
+import android.telephony.TelephonyManager
+import android.telephony.euicc.EuiccManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.repository.FakeSimBouncerRepository
+import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor.Companion.INVALID_SUBSCRIPTION_ID
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.res.R
+import com.android.systemui.scene.SceneTestUtils
+import com.android.systemui.util.mockito.whenever
+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
+import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class SimBouncerInteractorTest : SysuiTestCase() {
+    @Mock lateinit var telephonyManager: TelephonyManager
+    @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    @Mock lateinit var euiccManager: EuiccManager
+
+    private val utils = SceneTestUtils(this)
+    private val bouncerSimRepository = FakeSimBouncerRepository()
+    private val resources: Resources = context.resources
+    private val testScope = utils.testScope
+
+    private lateinit var underTest: SimBouncerInteractor
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        underTest =
+            SimBouncerInteractor(
+                context,
+                testScope.backgroundScope,
+                utils.testDispatcher,
+                bouncerSimRepository,
+                telephonyManager,
+                resources,
+                keyguardUpdateMonitor,
+                euiccManager,
+                utils.mobileConnectionsRepository,
+            )
+    }
+
+    @Test
+    fun getDefaultMessage() {
+        bouncerSimRepository.setSubscriptionId(1)
+        bouncerSimRepository.setActiveSubscriptionInfo(
+            SubscriptionInfo.Builder().setDisplayName("sim").build()
+        )
+        whenever(telephonyManager.activeModemCount).thenReturn(1)
+
+        assertThat(underTest.getDefaultMessage())
+            .isEqualTo(resources.getString(R.string.kg_sim_pin_instructions))
+    }
+
+    @Test
+    fun getDefaultMessage_isPuk() {
+        bouncerSimRepository.setSimPukLocked(true)
+        bouncerSimRepository.setSubscriptionId(1)
+        bouncerSimRepository.setActiveSubscriptionInfo(
+            SubscriptionInfo.Builder().setDisplayName("sim").build()
+        )
+        whenever(telephonyManager.activeModemCount).thenReturn(1)
+
+        assertThat(underTest.getDefaultMessage())
+            .isEqualTo(resources.getString(R.string.kg_puk_enter_puk_hint))
+    }
+
+    @Test
+    fun getDefaultMessage_isEsimLocked() {
+        bouncerSimRepository.setLockedEsim(true)
+        bouncerSimRepository.setSubscriptionId(1)
+        bouncerSimRepository.setActiveSubscriptionInfo(
+            SubscriptionInfo.Builder().setDisplayName("sim").build()
+        )
+        whenever(telephonyManager.activeModemCount).thenReturn(1)
+
+        val msg = resources.getString(R.string.kg_sim_pin_instructions)
+        assertThat(underTest.getDefaultMessage())
+            .isEqualTo(resources.getString(R.string.kg_sim_lock_esim_instructions, msg))
+    }
+
+    @Test
+    fun getDefaultMessage_multipleSims() {
+        bouncerSimRepository.setSubscriptionId(1)
+        bouncerSimRepository.setActiveSubscriptionInfo(
+            SubscriptionInfo.Builder().setDisplayName("sim").build()
+        )
+        whenever(telephonyManager.activeModemCount).thenReturn(2)
+
+        assertThat(underTest.getDefaultMessage())
+            .isEqualTo(resources.getString(R.string.kg_sim_pin_instructions_multi, "sim"))
+    }
+
+    @Test
+    fun getDefaultMessage_multipleSims_isPuk() {
+        bouncerSimRepository.setSimPukLocked(true)
+        bouncerSimRepository.setSubscriptionId(1)
+        bouncerSimRepository.setActiveSubscriptionInfo(
+            SubscriptionInfo.Builder().setDisplayName("sim").build()
+        )
+        whenever(telephonyManager.activeModemCount).thenReturn(2)
+
+        assertThat(underTest.getDefaultMessage())
+            .isEqualTo(resources.getString(R.string.kg_puk_enter_puk_hint_multi, "sim"))
+    }
+
+    @Test
+    fun getDefaultMessage_multipleSims_emptyDisplayName() {
+        bouncerSimRepository.setSubscriptionId(1)
+        bouncerSimRepository.setActiveSubscriptionInfo(SubscriptionInfo.Builder().build())
+        whenever(telephonyManager.activeModemCount).thenReturn(2)
+
+        assertThat(underTest.getDefaultMessage())
+            .isEqualTo(resources.getString(R.string.kg_sim_pin_instructions))
+    }
+
+    @Test
+    fun getDefaultMessage_multipleSims_emptyDisplayName_isPuk() {
+        bouncerSimRepository.setSimPukLocked(true)
+        bouncerSimRepository.setSubscriptionId(1)
+        bouncerSimRepository.setActiveSubscriptionInfo(SubscriptionInfo.Builder().build())
+        whenever(telephonyManager.activeModemCount).thenReturn(2)
+
+        assertThat(underTest.getDefaultMessage())
+            .isEqualTo(resources.getString(R.string.kg_puk_enter_puk_hint))
+    }
+
+    @Test
+    fun resetSimPukUserInput() {
+        bouncerSimRepository.setSimPukUserInput("00000000", "1234")
+
+        assertThat(bouncerSimRepository.simPukInputModel.enteredSimPuk).isEqualTo("00000000")
+        assertThat(bouncerSimRepository.simPukInputModel.enteredSimPin).isEqualTo("1234")
+
+        underTest.resetSimPukUserInput()
+
+        assertThat(bouncerSimRepository.simPukInputModel.enteredSimPuk).isNull()
+        assertThat(bouncerSimRepository.simPukInputModel.enteredSimPin).isNull()
+    }
+
+    @Test
+    fun disableEsim() =
+        testScope.runTest {
+            val portIndex = 1
+            bouncerSimRepository.setActiveSubscriptionInfo(
+                SubscriptionInfo.Builder().setPortIndex(portIndex).build()
+            )
+
+            underTest.disableEsim()
+            runCurrent()
+
+            verify(euiccManager)
+                .switchToSubscription(
+                    eq(INVALID_SUBSCRIPTION_ID),
+                    eq(portIndex),
+                    ArgumentMatchers.any()
+                )
+        }
+
+    @Test
+    fun verifySimPin() =
+        testScope.runTest {
+            bouncerSimRepository.setSubscriptionId(1)
+            bouncerSimRepository.setSimPukLocked(false)
+            whenever(telephonyManager.createForSubscriptionId(anyInt()))
+                .thenReturn(telephonyManager)
+            whenever(telephonyManager.supplyIccLockPin(anyString()))
+                .thenReturn(PinResult(PinResult.PIN_RESULT_TYPE_SUCCESS, 1))
+
+            val msg: String? = underTest.verifySim(listOf(0, 0, 0, 0))
+            runCurrent()
+            assertThat(msg).isNull()
+
+            verify(keyguardUpdateMonitor).reportSimUnlocked(1)
+        }
+
+    @Test
+    fun verifySimPin_incorrect_oneRemainingAttempt() =
+        testScope.runTest {
+            bouncerSimRepository.setSubscriptionId(1)
+            bouncerSimRepository.setSimPukLocked(false)
+            whenever(telephonyManager.createForSubscriptionId(anyInt()))
+                .thenReturn(telephonyManager)
+            whenever(telephonyManager.supplyIccLockPin(anyString()))
+                .thenReturn(
+                    PinResult(
+                        PinResult.PIN_RESULT_TYPE_INCORRECT,
+                        1,
+                    )
+                )
+
+            val msg: String? = underTest.verifySim(listOf(0, 0, 0, 0))
+            runCurrent()
+
+            assertThat(msg).isNull()
+            val errorDialogMessage by collectLastValue(bouncerSimRepository.errorDialogMessage)
+            assertThat(errorDialogMessage)
+                .isEqualTo(
+                    "Enter SIM PIN. You have 1 remaining attempt before you must contact" +
+                        " your carrier to unlock your device."
+                )
+        }
+
+    @Test
+    fun verifySimPin_incorrect_threeRemainingAttempts() =
+        testScope.runTest {
+            bouncerSimRepository.setSubscriptionId(1)
+            bouncerSimRepository.setSimPukLocked(false)
+            whenever(telephonyManager.createForSubscriptionId(anyInt()))
+                .thenReturn(telephonyManager)
+            whenever(telephonyManager.supplyIccLockPin(anyString()))
+                .thenReturn(
+                    PinResult(
+                        PinResult.PIN_RESULT_TYPE_INCORRECT,
+                        3,
+                    )
+                )
+
+            val msg = underTest.verifySim(listOf(0, 0, 0, 0))
+            runCurrent()
+
+            assertThat(msg).isEqualTo("Enter SIM PIN. You have 3 remaining attempts.")
+        }
+
+    @Test
+    fun verifySimPin_notCorrectLength_tooShort() =
+        testScope.runTest {
+            bouncerSimRepository.setSubscriptionId(1)
+            bouncerSimRepository.setSimPukLocked(false)
+
+            val msg = underTest.verifySim(listOf(0))
+
+            assertThat(msg).isEqualTo(resources.getString(R.string.kg_invalid_sim_pin_hint))
+        }
+
+    @Test
+    fun verifySimPin_notCorrectLength_tooLong() =
+        testScope.runTest {
+            bouncerSimRepository.setSubscriptionId(1)
+            bouncerSimRepository.setSimPukLocked(false)
+
+            val msg = underTest.verifySim(listOf(0, 0, 0, 0, 0, 0, 0, 0, 0))
+
+            assertThat(msg).isEqualTo(resources.getString(R.string.kg_invalid_sim_pin_hint))
+        }
+
+    @Test
+    fun verifySimPuk() =
+        testScope.runTest {
+            whenever(telephonyManager.createForSubscriptionId(anyInt()))
+                .thenReturn(telephonyManager)
+            whenever(telephonyManager.supplyIccLockPuk(anyString(), anyString()))
+                .thenReturn(PinResult(PinResult.PIN_RESULT_TYPE_SUCCESS, 1))
+            bouncerSimRepository.setSubscriptionId(1)
+            bouncerSimRepository.setSimPukLocked(true)
+
+            var msg = underTest.verifySim(listOf(0, 0, 0, 0, 0, 0, 0, 0, 0))
+            assertThat(msg).isEqualTo(resources.getString(R.string.kg_puk_enter_pin_hint))
+
+            msg = underTest.verifySim(listOf(0, 0, 0, 0))
+            assertThat(msg).isEqualTo(resources.getString(R.string.kg_enter_confirm_pin_hint))
+
+            msg = underTest.verifySim(listOf(0, 0, 0, 0))
+            assertThat(msg).isNull()
+
+            runCurrent()
+            verify(keyguardUpdateMonitor).reportSimUnlocked(1)
+        }
+
+    @Test
+    fun verifySimPuk_inputTooShort() =
+        testScope.runTest {
+            bouncerSimRepository.setSubscriptionId(1)
+            bouncerSimRepository.setSimPukLocked(true)
+            val msg = underTest.verifySim(listOf(0, 0, 0, 0))
+            assertThat(msg).isEqualTo(resources.getString(R.string.kg_invalid_sim_puk_hint))
+        }
+
+    @Test
+    fun verifySimPuk_pinNotCorrectLength() =
+        testScope.runTest {
+            bouncerSimRepository.setSubscriptionId(1)
+            bouncerSimRepository.setSimPukLocked(true)
+
+            underTest.verifySim(listOf(0, 0, 0, 0, 0, 0, 0, 0, 0))
+
+            val msg = underTest.verifySim(listOf(0, 0, 0))
+            assertThat(msg).isEqualTo(resources.getString(R.string.kg_invalid_sim_pin_hint))
+        }
+
+    @Test
+    fun verifySimPuk_confirmedPinDoesNotMatch() =
+        testScope.runTest {
+            bouncerSimRepository.setSubscriptionId(1)
+            bouncerSimRepository.setSimPukLocked(true)
+
+            underTest.verifySim(listOf(0, 0, 0, 0, 0, 0, 0, 0, 0))
+            underTest.verifySim(listOf(0, 0, 0, 0))
+
+            val msg = underTest.verifySim(listOf(0, 0, 0, 1))
+            assertThat(msg).isEqualTo(resources.getString(R.string.kg_puk_enter_pin_hint))
+        }
+
+    @Test
+    fun onErrorDialogDismissed_clearsErrorDialogMessageInRepository() {
+        bouncerSimRepository.setSimVerificationErrorMessage("abc")
+        assertThat(bouncerSimRepository.errorDialogMessage.value).isNotNull()
+
+        underTest.onErrorDialogDismissed()
+
+        assertThat(bouncerSimRepository.errorDialogMessage.value).isNull()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
index cfcb545..63c992b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
@@ -48,6 +48,8 @@
             viewModelScope = testScope.backgroundScope,
             interactor = bouncerInteractor,
             isInputEnabled = MutableStateFlow(true),
+            simBouncerInteractor = utils.simBouncerInteractor,
+            authenticationMethod = AuthenticationMethodModel.Pin,
         )
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
index f4346b5..75d6a00 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
@@ -233,6 +233,7 @@
             AuthenticationMethodModel.Pin,
             AuthenticationMethodModel.Password,
             AuthenticationMethodModel.Pattern,
+            AuthenticationMethodModel.Sim,
         )
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
index c498edf..9b1e958 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
@@ -78,12 +78,28 @@
             lockDeviceAndOpenPasswordBouncer()
 
             assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
-            assertThat(password).isEqualTo("")
+            assertThat(password).isEmpty()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Password)
         }
 
     @Test
+    fun onHidden_resetsPasswordInputAndMessage() =
+        testScope.runTest {
+            val message by collectLastValue(bouncerViewModel.message)
+            val password by collectLastValue(underTest.password)
+            lockDeviceAndOpenPasswordBouncer()
+
+            underTest.onPasswordInputChanged("password")
+            assertThat(message?.text).isNotEqualTo(ENTER_YOUR_PASSWORD)
+            assertThat(password).isNotEmpty()
+
+            underTest.onHidden()
+            assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
+            assertThat(password).isEmpty()
+        }
+
+    @Test
     fun onPasswordInputChanged() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.desiredScene)
@@ -121,7 +137,7 @@
             underTest.onPasswordInputChanged("wrong")
             underTest.onAuthenticateKeyPressed()
 
-            assertThat(password).isEqualTo("")
+            assertThat(password).isEmpty()
             assertThat(message?.text).isEqualTo(WRONG_PASSWORD)
         }
 
@@ -134,14 +150,13 @@
                 AuthenticationMethodModel.Password
             )
             utils.deviceEntryRepository.setUnlocked(false)
-            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
-            underTest.onShown()
-            // Enter nothing.
+            switchToScene(SceneKey.Bouncer)
+
+            // No input entered.
 
             underTest.onAuthenticateKeyPressed()
 
-            assertThat(password).isEqualTo("")
+            assertThat(password).isEmpty()
             assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
         }
 
@@ -182,32 +197,33 @@
             assertThat(password).isEqualTo("password")
 
             // The user doesn't confirm the password, but navigates back to the lockscreen instead.
-            sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen), "reason")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Lockscreen), "reason")
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen))
+            switchToScene(SceneKey.Lockscreen)
 
             // The user navigates to the bouncer again.
-            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-
-            underTest.onShown()
+            switchToScene(SceneKey.Bouncer)
 
             // Ensure the previously-entered password is not shown.
             assertThat(password).isEmpty()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
         }
 
+    private fun TestScope.switchToScene(toScene: SceneKey) {
+        val currentScene by collectLastValue(sceneInteractor.desiredScene)
+        val bouncerShown = currentScene?.key != SceneKey.Bouncer && toScene == SceneKey.Bouncer
+        val bouncerHidden = currentScene?.key == SceneKey.Bouncer && toScene != SceneKey.Bouncer
+        sceneInteractor.changeScene(SceneModel(toScene), "reason")
+        sceneInteractor.onSceneChanged(SceneModel(toScene), "reason")
+        if (bouncerShown) underTest.onShown()
+        if (bouncerHidden) underTest.onHidden()
+        runCurrent()
+
+        assertThat(currentScene).isEqualTo(SceneModel(toScene))
+    }
+
     private fun TestScope.lockDeviceAndOpenPasswordBouncer() {
         utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Password)
         utils.deviceEntryRepository.setUnlocked(false)
-        sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
-        sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
-
-        assertThat(collectLastValue(sceneInteractor.desiredScene).invoke())
-            .isEqualTo(SceneModel(SceneKey.Bouncer))
-        underTest.onShown()
-        runCurrent()
+        switchToScene(SceneKey.Bouncer)
     }
 
     companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index 3f5ddba..125fe68 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -373,15 +373,23 @@
         )
     }
 
+    private fun TestScope.switchToScene(toScene: SceneKey) {
+        val currentScene by collectLastValue(sceneInteractor.desiredScene)
+        val bouncerShown = currentScene?.key != SceneKey.Bouncer && toScene == SceneKey.Bouncer
+        val bouncerHidden = currentScene?.key == SceneKey.Bouncer && toScene != SceneKey.Bouncer
+        sceneInteractor.changeScene(SceneModel(toScene), "reason")
+        sceneInteractor.onSceneChanged(SceneModel(toScene), "reason")
+        if (bouncerShown) underTest.onShown()
+        if (bouncerHidden) underTest.onHidden()
+        runCurrent()
+
+        assertThat(currentScene).isEqualTo(SceneModel(toScene))
+    }
+
     private fun TestScope.lockDeviceAndOpenPatternBouncer() {
         utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pattern)
         utils.deviceEntryRepository.setUnlocked(false)
-        sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
-        sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
-        assertThat(collectLastValue(sceneInteractor.desiredScene).invoke())
-            .isEqualTo(SceneModel(SceneKey.Bouncer))
-        underTest.onShown()
-        runCurrent()
+        switchToScene(SceneKey.Bouncer)
     }
 
     companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index 7a9cb6c..c30e405 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -63,6 +63,8 @@
             viewModelScope = testScope.backgroundScope,
             interactor = bouncerInteractor,
             isInputEnabled = MutableStateFlow(true).asStateFlow(),
+            simBouncerInteractor = utils.simBouncerInteractor,
+            authenticationMethod = AuthenticationMethodModel.Pin,
         )
 
     @Before
@@ -74,49 +76,77 @@
     @Test
     fun onShown() =
         testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
-            utils.deviceEntryRepository.setUnlocked(false)
-            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
-
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-
-            underTest.onShown()
+            lockDeviceAndOpenPinBouncer()
 
             assertThat(message?.text).ignoringCase().isEqualTo(ENTER_YOUR_PIN)
             assertThat(pin).isEmpty()
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
             assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Pin)
         }
 
     @Test
+    fun simBouncerViewModel_simAreaIsVisible() =
+        testScope.runTest {
+            val underTest =
+                PinBouncerViewModel(
+                    applicationContext = context,
+                    viewModelScope = testScope.backgroundScope,
+                    interactor = bouncerInteractor,
+                    isInputEnabled = MutableStateFlow(true).asStateFlow(),
+                    simBouncerInteractor = utils.simBouncerInteractor,
+                    authenticationMethod = AuthenticationMethodModel.Sim,
+                )
+
+            assertThat(underTest.isSimAreaVisible).isTrue()
+        }
+
+    @Test
+    fun onErrorDialogDismissed_clearsDialogMessage() =
+        testScope.runTest {
+            val dialogMessage by collectLastValue(underTest.errorDialogMessage)
+            utils.simBouncerRepository.setSimVerificationErrorMessage("abc")
+            assertThat(dialogMessage).isEqualTo("abc")
+
+            underTest.onErrorDialogDismissed()
+
+            assertThat(dialogMessage).isNull()
+        }
+
+    @Test
+    fun simBouncerViewModel_autoConfirmEnabled_hintedPinLengthIsNull() =
+        testScope.runTest {
+            val underTest =
+                PinBouncerViewModel(
+                    applicationContext = context,
+                    viewModelScope = testScope.backgroundScope,
+                    interactor = bouncerInteractor,
+                    isInputEnabled = MutableStateFlow(true).asStateFlow(),
+                    simBouncerInteractor = utils.simBouncerInteractor,
+                    authenticationMethod = AuthenticationMethodModel.Sim,
+                )
+            utils.authenticationRepository.setAutoConfirmFeatureEnabled(true)
+            val hintedPinLength by collectLastValue(underTest.hintedPinLength)
+
+            assertThat(hintedPinLength).isNull()
+        }
+
+    @Test
     fun onPinButtonClicked() =
         testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
-            utils.deviceEntryRepository.setUnlocked(false)
-            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
-
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            underTest.onShown()
-            runCurrent()
+            lockDeviceAndOpenPinBouncer()
 
             underTest.onPinButtonClicked(1)
 
             assertThat(message?.text).isEmpty()
             assertThat(pin).containsExactly(1)
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
         }
 
     @Test
     fun onBackspaceButtonClicked() =
         testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             lockDeviceAndOpenPinBouncer()
@@ -128,7 +158,6 @@
 
             assertThat(message?.text).isEmpty()
             assertThat(pin).isEmpty()
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
         }
 
     @Test
@@ -176,9 +205,7 @@
                 collectLastValue(authenticationInteractor.authenticationChallengeResult)
             lockDeviceAndOpenPinBouncer()
 
-            FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
-                underTest.onPinButtonClicked(digit)
-            }
+            FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked)
 
             underTest.onAuthenticateButtonClicked()
 
@@ -226,9 +253,7 @@
             assertThat(authResult).isFalse()
 
             // Enter the correct PIN:
-            FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
-                underTest.onPinButtonClicked(digit)
-            }
+            FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked)
             assertThat(message?.text).isEmpty()
 
             underTest.onAuthenticateButtonClicked()
@@ -244,9 +269,7 @@
                 collectLastValue(authenticationInteractor.authenticationChallengeResult)
             lockDeviceAndOpenPinBouncer()
 
-            FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
-                underTest.onPinButtonClicked(digit)
-            }
+            FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked)
 
             assertThat(authResult).isTrue()
         }
@@ -275,31 +298,21 @@
     @Test
     fun onShown_againAfterSceneChange_resetsPin() =
         testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             lockDeviceAndOpenPinBouncer()
 
             // The user types a PIN.
-            FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
-                underTest.onPinButtonClicked(digit)
-            }
+            FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked)
             assertThat(pin).isNotEmpty()
 
             // The user doesn't confirm the PIN, but navigates back to the lockscreen instead.
-            sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen), "reason")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Lockscreen), "reason")
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen))
+            switchToScene(SceneKey.Lockscreen)
 
             // The user navigates to the bouncer again.
-            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-
-            underTest.onShown()
+            switchToScene(SceneKey.Bouncer)
 
             // Ensure the previously-entered PIN is not shown.
             assertThat(pin).isEmpty()
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
         }
 
     @Test
@@ -366,16 +379,23 @@
             assertThat(isAnimationEnabled).isTrue()
         }
 
+    private fun TestScope.switchToScene(toScene: SceneKey) {
+        val currentScene by collectLastValue(sceneInteractor.desiredScene)
+        val bouncerShown = currentScene?.key != SceneKey.Bouncer && toScene == SceneKey.Bouncer
+        val bouncerHidden = currentScene?.key == SceneKey.Bouncer && toScene != SceneKey.Bouncer
+        sceneInteractor.changeScene(SceneModel(toScene), "reason")
+        sceneInteractor.onSceneChanged(SceneModel(toScene), "reason")
+        if (bouncerShown) underTest.onShown()
+        if (bouncerHidden) underTest.onHidden()
+        runCurrent()
+
+        assertThat(currentScene).isEqualTo(SceneModel(toScene))
+    }
+
     private fun TestScope.lockDeviceAndOpenPinBouncer() {
         utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
         utils.deviceEntryRepository.setUnlocked(false)
-        sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
-        sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
-
-        assertThat(collectLastValue(sceneInteractor.desiredScene).invoke())
-            .isEqualTo(SceneModel(SceneKey.Bouncer))
-        underTest.onShown()
-        runCurrent()
+        switchToScene(SceneKey.Bouncer)
     }
 
     companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index af4bf36..e0567a4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.communal.domain.model.CommunalContentModel
 import com.android.systemui.communal.shared.model.CommunalSceneKey
 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
+import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
@@ -45,6 +46,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @SmallTest
@@ -59,6 +61,7 @@
     private lateinit var widgetRepository: FakeCommunalWidgetRepository
     private lateinit var smartspaceRepository: FakeSmartspaceRepository
     private lateinit var keyguardRepository: FakeKeyguardRepository
+    private lateinit var editWidgetsActivityStarter: EditWidgetsActivityStarter
 
     private lateinit var underTest: CommunalInteractor
 
@@ -76,6 +79,7 @@
         widgetRepository = withDeps.widgetRepository
         smartspaceRepository = withDeps.smartspaceRepository
         keyguardRepository = withDeps.keyguardRepository
+        editWidgetsActivityStarter = withDeps.editWidgetsActivityStarter
 
         underTest = withDeps.communalInteractor
     }
@@ -322,4 +326,11 @@
             runCurrent()
             assertThat(isCommunalShowing()).isEqualTo(true)
         }
+
+    @Test
+    fun testShowWidgetEditorStartsActivity() =
+        testScope.runTest {
+            underTest.showWidgetEditor()
+            verify(editWidgetsActivityStarter).startActivity()
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
index abd9f28..0004f52 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
@@ -90,6 +90,16 @@
         }
 
     @Test
+    fun isUnlocked_whenAuthMethodIsSimAndUnlocked_isFalse() =
+        testScope.runTest {
+            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Sim)
+            utils.deviceEntryRepository.setUnlocked(true)
+
+            val isUnlocked by collectLastValue(underTest.isUnlocked)
+            assertThat(isUnlocked).isFalse()
+        }
+
+    @Test
     fun isDeviceEntered_onLockscreenWithSwipe_isFalse() =
         testScope.runTest {
             val isDeviceEntered by collectLastValue(underTest.isDeviceEntered)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
index 799bd5a..7242cb2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.power.domain.interactor.PowerInteractorFactory
 import com.android.systemui.statusbar.CircleReveal
 import com.android.systemui.statusbar.LightRevealEffect
+import com.android.systemui.util.mockito.mock
 import junit.framework.Assert.assertEquals
 import junit.framework.Assert.assertFalse
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -60,15 +61,11 @@
         MockitoAnnotations.initMocks(this)
         fakeKeyguardRepository = FakeKeyguardRepository()
         powerRepository = FakePowerRepository()
-        powerInteractor = PowerInteractorFactory.create(
-                repository = powerRepository
-        ).powerInteractor
+        powerInteractor =
+            PowerInteractorFactory.create(repository = powerRepository).powerInteractor
 
-        underTest = LightRevealScrimRepositoryImpl(
-                fakeKeyguardRepository,
-                context,
-                powerInteractor,
-        )
+        underTest =
+            LightRevealScrimRepositoryImpl(fakeKeyguardRepository, context, powerInteractor, mock())
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
index b439fcf..722c11d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
@@ -18,9 +18,9 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.SysUITestModule
-import com.android.TestMocksModule
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractorTest.kt
index 7fb0dd5..49f7565 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractorTest.kt
@@ -18,9 +18,9 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.SysUITestModule
-import com.android.TestMocksModule
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository
@@ -413,6 +413,13 @@
             )
             transitionRepository.sendTransitionStep(
                 TransitionStep(
+                    transitionState = TransitionState.CANCELED,
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.GONE,
+                )
+            )
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
                     transitionState = TransitionState.STARTED,
                     from = KeyguardState.GONE,
                     to = KeyguardState.AOD,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
index 29b546b..4f7d944 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
@@ -26,13 +26,14 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
-import com.android.systemui.keyguard.shared.model.KeyguardState.OFF
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
+import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
 import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
 import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
 import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.google.common.truth.Truth.assertThat
+import junit.framework.Assert.assertEquals
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
@@ -136,7 +137,7 @@
 
     @Test
     fun startedKeyguardStateTests() = testScope.runTest {
-        val finishedSteps by collectValues(underTest.startedKeyguardState)
+        val startedStates by collectValues(underTest.startedKeyguardState)
         runCurrent()
         val steps = mutableListOf<TransitionStep>()
 
@@ -153,7 +154,7 @@
             runCurrent()
         }
 
-        assertThat(finishedSteps).isEqualTo(listOf(OFF, PRIMARY_BOUNCER, AOD, GONE))
+        assertThat(startedStates).isEqualTo(listOf(LOCKSCREEN, PRIMARY_BOUNCER, AOD, GONE))
     }
 
     @Test
@@ -162,12 +163,12 @@
 
         val steps = mutableListOf<TransitionStep>()
 
-        steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
-        steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
-        steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
         steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED))
         steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING))
         steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED))
+        steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
+        steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
+        steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
         steps.add(TransitionStep(AOD, GONE, 1f, STARTED))
 
         steps.forEach {
@@ -175,7 +176,9 @@
             runCurrent()
         }
 
-        assertThat(finishedSteps).isEqualTo(listOf(steps[2], steps[5]))
+        // Ignore the default state.
+        assertThat(finishedSteps.subList(1, finishedSteps.size))
+                .isEqualTo(listOf(steps[2], steps[5]))
     }
 
     @Test
@@ -650,6 +653,81 @@
         ))
     }
 
+    @Test
+    fun finishedKeyguardState_emitsAgainIfCancelledAndReversed() = testScope.runTest {
+        val finishedStates by collectValues(underTest.finishedKeyguardState)
+
+        // We default FINISHED in LOCKSCREEN.
+        assertEquals(listOf(
+                LOCKSCREEN
+        ), finishedStates)
+
+        sendSteps(
+                TransitionStep(LOCKSCREEN, AOD, 0f, STARTED),
+                TransitionStep(LOCKSCREEN, AOD, 0.5f, RUNNING),
+                TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED),
+        )
+
+        // We're FINISHED in AOD.
+        assertEquals(listOf(
+                LOCKSCREEN,
+                AOD,
+        ), finishedStates)
+
+        // Transition back to LOCKSCREEN.
+        sendSteps(
+                TransitionStep(AOD, LOCKSCREEN, 0f, STARTED),
+                TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING),
+                TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED),
+        )
+
+        // We're FINISHED in LOCKSCREEN.
+        assertEquals(listOf(
+                LOCKSCREEN,
+                AOD,
+                LOCKSCREEN,
+        ), finishedStates)
+
+        sendSteps(
+                TransitionStep(LOCKSCREEN, GONE, 0f, STARTED),
+                TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING),
+        )
+
+        // We've STARTED a transition to GONE but not yet finished it so we're still FINISHED in
+        // LOCKSCREEN.
+        assertEquals(listOf(
+                LOCKSCREEN,
+                AOD,
+                LOCKSCREEN,
+        ), finishedStates)
+
+        sendSteps(
+                TransitionStep(LOCKSCREEN, GONE, 0.6f, CANCELED),
+        )
+
+        // We've CANCELED a transition to GONE, we're still FINISHED in LOCKSCREEN.
+        assertEquals(listOf(
+                LOCKSCREEN,
+                AOD,
+                LOCKSCREEN,
+        ), finishedStates)
+
+        sendSteps(
+                TransitionStep(GONE, LOCKSCREEN, 0.6f, STARTED),
+                TransitionStep(GONE, LOCKSCREEN, 0.9f, RUNNING),
+                TransitionStep(GONE, LOCKSCREEN, 1f, FINISHED),
+        )
+
+        // Expect another emission of LOCKSCREEN, as we have FINISHED a second transition to
+        // LOCKSCREEN after the cancellation.
+        assertEquals(listOf(
+                LOCKSCREEN,
+                AOD,
+                LOCKSCREEN,
+                LOCKSCREEN,
+        ), finishedStates)
+    }
+
     private suspend fun sendSteps(vararg steps: TransitionStep) {
         steps.forEach {
             repository.sendTransitionStep(it)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index c292102..bf23bf8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -618,11 +618,26 @@
     @Test
     fun dozingToLockscreenCannotBeInterruptedByDreaming() =
         testScope.runTest {
+            transitionRepository.sendTransitionSteps(
+                KeyguardState.LOCKSCREEN,
+                KeyguardState.DOZING,
+                testScheduler
+            )
             // GIVEN a prior transition has started to LOCKSCREEN
             transitionRepository.sendTransitionStep(
                 TransitionStep(
                     from = KeyguardState.DOZING,
                     to = KeyguardState.LOCKSCREEN,
+                    value = 0f,
+                    transitionState = TransitionState.STARTED,
+                    ownerName = "KeyguardTransitionScenariosTest",
+                )
+            )
+            runCurrent()
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.DOZING,
+                    to = KeyguardState.LOCKSCREEN,
                     value = 0.5f,
                     transitionState = TransitionState.RUNNING,
                     ownerName = "KeyguardTransitionScenariosTest",
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
index c02add1..b483085 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.statusbar.LightRevealEffect
 import com.android.systemui.statusbar.LightRevealScrim
+import com.android.systemui.util.mockito.mock
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -39,6 +40,7 @@
 import org.junit.runner.RunWith
 import org.mockito.Mockito.anyBoolean
 import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 import org.mockito.Spy
@@ -79,7 +81,8 @@
             LightRevealScrimInteractor(
                 keyguardTransitionInteractor,
                 fakeLightRevealScrimRepository,
-                testScope.backgroundScope
+                testScope.backgroundScope,
+                mock()
             )
     }
 
@@ -120,6 +123,9 @@
     @Test
     fun lightRevealEffect_startsAnimationOnlyForDifferentStateTargets() =
         testScope.runTest {
+            runCurrent()
+            reset(fakeLightRevealScrimRepository)
+
             fakeKeyguardTransitionRepository.sendTransitionStep(
                 TransitionStep(
                     transitionState = TransitionState.STARTED,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt
index 2dfc132..16d072e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt
@@ -80,11 +80,7 @@
         MockitoAnnotations.initMocks(this)
         testScope = TestScope()
         configRepository = FakeConfigurationRepository()
-        featureFlags =
-            FakeFeatureFlags().apply {
-                set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, true)
-                set(Flags.FACE_AUTH_REFACTOR, false)
-            }
+        featureFlags = FakeFeatureFlags().apply { set(Flags.FACE_AUTH_REFACTOR, false) }
         KeyguardInteractorFactory.create(featureFlags = featureFlags).let {
             keyguardInteractor = it.keyguardInteractor
             keyguardRepository = it.repository
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/InWindowLauncherUnlockAnimationManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/InWindowLauncherUnlockAnimationManagerTest.kt
index 570dfb3..e9399cc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/InWindowLauncherUnlockAnimationManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/InWindowLauncherUnlockAnimationManagerTest.kt
@@ -18,7 +18,7 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.SysUITestModule
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSectionTest.kt
index 75bdcdd..a010ea9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSectionTest.kt
@@ -67,10 +67,7 @@
         mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
 
         featureFlags =
-            FakeFeatureFlagsClassic().apply {
-                set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, false)
-                set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, false)
-            }
+            FakeFeatureFlagsClassic().apply { set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, false) }
         underTest =
             DefaultDeviceEntryIconSection(
                 keyguardUpdateMonitor,
@@ -98,7 +95,7 @@
     @Test
     fun addViewsConditionally_migrateAndRefactorFlagsOn() {
         mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
-        featureFlags.set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, true)
+        mSetFlagsRule.enableFlags(AConfigFlags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
         val constraintLayout = ConstraintLayout(context, null)
         underTest.addViews(constraintLayout)
         assertThat(constraintLayout.childCount).isGreaterThan(0)
@@ -107,7 +104,7 @@
     @Test
     fun addViewsConditionally_migrateFlagOff() {
         mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
-        featureFlags.set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, false)
+        mSetFlagsRule.disableFlags(AConfigFlags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
         val constraintLayout = ConstraintLayout(context, null)
         underTest.addViews(constraintLayout)
         assertThat(constraintLayout.childCount).isEqualTo(0)
@@ -115,7 +112,7 @@
 
     @Test
     fun applyConstraints_udfps_refactor_off() {
-        featureFlags.set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, false)
+        mSetFlagsRule.disableFlags(AConfigFlags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
         val cs = ConstraintSet()
         underTest.applyConstraints(cs)
 
@@ -127,7 +124,7 @@
 
     @Test
     fun applyConstraints_udfps_refactor_on() {
-        featureFlags.set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, true)
+        mSetFlagsRule.enableFlags(AConfigFlags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
         val cs = ConstraintSet()
         underTest.applyConstraints(cs)
 
@@ -139,7 +136,7 @@
 
     @Test
     fun testCenterIcon_udfps_refactor_off() {
-        featureFlags.set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, false)
+        mSetFlagsRule.disableFlags(AConfigFlags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
         val cs = ConstraintSet()
         underTest.centerIcon(Point(5, 6), 1F, cs)
 
@@ -155,7 +152,7 @@
 
     @Test
     fun testCenterIcon_udfps_refactor_on() {
-        featureFlags.set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, true)
+        mSetFlagsRule.enableFlags(AConfigFlags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
         val cs = ConstraintSet()
         underTest.centerIcon(Point(5, 6), 1F, cs)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index 259c74f..d3019f1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -21,18 +21,16 @@
 
 import android.view.View
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.TestMocksModule
-import com.android.collectLastValue
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.Flags as AConfigFlags
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
+import com.android.systemui.collectLastValue
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
-import com.android.systemui.Flags as AConfigFlags
 import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.android.systemui.flags.FakeFeatureFlagsClassicModule
 import com.android.systemui.flags.Flags
@@ -46,6 +44,8 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.plugins.ClockController
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.statusbar.notification.data.repository.FakeNotificationsKeyguardViewStateRepository
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
@@ -64,7 +64,6 @@
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -109,10 +108,7 @@
 
         mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
 
-        val featureFlags =
-            FakeFeatureFlagsClassic().apply {
-                set(Flags.FACE_AUTH_REFACTOR, true)
-            }
+        val featureFlags = FakeFeatureFlagsClassic().apply { set(Flags.FACE_AUTH_REFACTOR, true) }
 
         val withDeps = KeyguardInteractorFactory.create(featureFlags = featureFlags)
         keyguardInteractor = withDeps.keyguardInteractor
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelTest.kt
index 4074851..c50be04 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelTest.kt
@@ -18,15 +18,13 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.TestMocksModule
-import com.android.collectLastValue
-import com.android.collectValues
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
 import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
+import com.android.systemui.collectLastValue
+import com.android.systemui.collectValues
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
 import com.android.systemui.flags.FakeFeatureFlagsClassicModule
@@ -39,6 +37,8 @@
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.shade.data.repository.FakeShadeRepository
 import com.android.systemui.user.domain.UserDomainLayerModule
 import com.google.common.collect.Range
@@ -77,15 +77,15 @@
                 mocks: TestMocksModule,
             ): TestComponent
         }
+    }
 
-        fun shadeExpanded(expanded: Boolean) {
-            if (expanded) {
-                shadeRepository.setQsExpansion(1f)
-            } else {
-                keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
-                shadeRepository.setQsExpansion(0f)
-                shadeRepository.setLockscreenShadeExpansion(0f)
-            }
+    private fun TestComponent.shadeExpanded(expanded: Boolean) {
+        if (expanded) {
+            shadeRepository.setQsExpansion(1f)
+        } else {
+            keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+            shadeRepository.setQsExpansion(0f)
+            shadeRepository.setLockscreenShadeExpansion(0f)
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
index 5c85357..26704da 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
@@ -18,14 +18,12 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.TestMocksModule
-import com.android.collectLastValue
-import com.android.collectValues
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
+import com.android.systemui.collectLastValue
+import com.android.systemui.collectValues
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FakeFeatureFlagsClassicModule
 import com.android.systemui.flags.Flags
@@ -35,13 +33,14 @@
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.shade.data.repository.FakeShadeRepository
 import com.android.systemui.user.domain.UserDomainLayerModule
 import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import dagger.BindsInstance
 import dagger.Component
-import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -69,15 +68,15 @@
                 mocks: TestMocksModule,
             ): TestComponent
         }
+    }
 
-        fun shadeExpanded(expanded: Boolean) {
-            if (expanded) {
-                shadeRepository.setQsExpansion(1f)
-            } else {
-                keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
-                shadeRepository.setQsExpansion(0f)
-                shadeRepository.setLockscreenShadeExpansion(0f)
-            }
+    private fun TestComponent.shadeExpanded(expanded: Boolean) {
+        if (expanded) {
+            shadeRepository.setQsExpansion(1f)
+        } else {
+            keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+            shadeRepository.setQsExpansion(0f)
+            shadeRepository.setLockscreenShadeExpansion(0f)
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
index 4cbefa3d..ff3135a6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
@@ -18,14 +18,12 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.TestMocksModule
-import com.android.collectLastValue
-import com.android.collectValues
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
+import com.android.systemui.collectLastValue
+import com.android.systemui.collectValues
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FakeFeatureFlagsClassicModule
 import com.android.systemui.flags.Flags
@@ -35,6 +33,8 @@
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.shade.data.repository.FakeShadeRepository
 import com.android.systemui.user.domain.UserDomainLayerModule
 import com.google.common.collect.Range
@@ -68,15 +68,15 @@
                 mocks: TestMocksModule,
             ): TestComponent
         }
+    }
 
-        fun shadeExpanded(expanded: Boolean) {
-            if (expanded) {
-                shadeRepository.setQsExpansion(1f)
-            } else {
-                keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
-                shadeRepository.setQsExpansion(0f)
-                shadeRepository.setLockscreenShadeExpansion(0f)
-            }
+    private fun TestComponent.shadeExpanded(expanded: Boolean) {
+        if (expanded) {
+            shadeRepository.setQsExpansion(1f)
+        } else {
+            keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+            shadeRepository.setQsExpansion(0f)
+            shadeRepository.setLockscreenShadeExpansion(0f)
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
index 4f56435..8afd8e4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
@@ -18,13 +18,11 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.TestMocksModule
-import com.android.collectLastValue
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
+import com.android.systemui.collectLastValue
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FakeFeatureFlagsClassicModule
 import com.android.systemui.flags.Flags
@@ -34,6 +32,8 @@
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.shade.data.repository.FakeShadeRepository
 import com.android.systemui.user.domain.UserDomainLayerModule
 import com.google.common.collect.Range
@@ -69,15 +69,15 @@
                 mocks: TestMocksModule,
             ): TestComponent
         }
+    }
 
-        fun shadeExpanded(expanded: Boolean) {
-            if (expanded) {
-                shadeRepository.setQsExpansion(1f)
-            } else {
-                keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
-                shadeRepository.setQsExpansion(0f)
-                shadeRepository.setLockscreenShadeExpansion(0f)
-            }
+    private fun TestComponent.shadeExpanded(expanded: Boolean) {
+        if (expanded) {
+            shadeRepository.setQsExpansion(1f)
+        } else {
+            keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+            shadeRepository.setQsExpansion(0f)
+            shadeRepository.setLockscreenShadeExpansion(0f)
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModelTest.kt
index 32acefe..5058b16 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModelTest.kt
@@ -67,11 +67,7 @@
         overrideResource(com.android.systemui.res.R.dimen.lock_icon_padding, defaultPadding)
         testScope = TestScope()
         shadeRepository = FakeShadeRepository()
-        featureFlags =
-            FakeFeatureFlags().apply {
-                set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, true)
-                set(Flags.FACE_AUTH_REFACTOR, false)
-            }
+        featureFlags = FakeFeatureFlags().apply { set(Flags.FACE_AUTH_REFACTOR, false) }
         KeyguardInteractorFactory.create(
                 featureFlags = featureFlags,
             )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt
index 4f970d7..f039f53 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt
@@ -75,11 +75,7 @@
         keyguardRepository = FakeKeyguardRepository()
         bouncerRepository = FakeKeyguardBouncerRepository()
         fakeCommandQueue = FakeCommandQueue()
-        featureFlags =
-            FakeFeatureFlags().apply {
-                set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, true)
-                set(Flags.FACE_AUTH_REFACTOR, false)
-            }
+        featureFlags = FakeFeatureFlags().apply { set(Flags.FACE_AUTH_REFACTOR, false) }
         bouncerRepository = FakeKeyguardBouncerRepository()
         transitionRepository = FakeKeyguardTransitionRepository()
         shadeRepository = FakeShadeRepository()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt
index 30e4866..c1805db 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt
@@ -83,11 +83,7 @@
         testScope = TestScope()
         transitionRepository = FakeKeyguardTransitionRepository()
         shadeRepository = FakeShadeRepository()
-        featureFlags =
-            FakeFeatureFlags().apply {
-                set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, true)
-                set(Flags.FACE_AUTH_REFACTOR, false)
-            }
+        featureFlags = FakeFeatureFlags().apply { set(Flags.FACE_AUTH_REFACTOR, false) }
         KeyguardInteractorFactory.create(
                 featureFlags = featureFlags,
             )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
index a2eb5ef..db7c987 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
@@ -35,7 +35,7 @@
 import com.android.systemui.media.dream.MediaDreamComplication
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.res.R
-import com.android.systemui.shade.ShadeExpansionStateManager
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.phone.KeyguardBypassController
@@ -50,6 +50,7 @@
 import com.android.systemui.utils.os.FakeHandler
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
@@ -74,7 +75,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
 class MediaHierarchyManagerTest : SysuiTestCase() {
 
     @Mock private lateinit var lockHost: MediaHost
@@ -91,6 +92,7 @@
     @Mock private lateinit var mediaDataManager: MediaDataManager
     @Mock private lateinit var uniqueObjectHostView: UniqueObjectHostView
     @Mock private lateinit var dreamOverlayStateController: DreamOverlayStateController
+    @Mock private lateinit var shadeInteractor: ShadeInteractor
     @Mock lateinit var logger: MediaViewLogger
     @Captor
     private lateinit var wakefullnessObserver: ArgumentCaptor<(WakefulnessLifecycle.Observer)>
@@ -101,12 +103,12 @@
         ArgumentCaptor<(DreamOverlayStateController.Callback)>
     @JvmField @Rule val mockito = MockitoJUnit.rule()
     private lateinit var mediaHierarchyManager: MediaHierarchyManager
+    private lateinit var isQsBypassingShade: MutableStateFlow<Boolean>
     private lateinit var mediaFrame: ViewGroup
     private val configurationController = FakeConfigurationController()
     private val communalRepository = FakeCommunalRepository(isCommunalEnabled = true)
     private val communalInteractor =
         CommunalInteractorFactory.create(communalRepository = communalRepository).communalInteractor
-    private val notifPanelEvents = ShadeExpansionStateManager()
     private val settings = FakeSettings()
     private lateinit var testableLooper: TestableLooper
     private lateinit var fakeHandler: FakeHandler
@@ -122,6 +124,8 @@
         testableLooper = TestableLooper.get(this)
         fakeHandler = FakeHandler(testableLooper.looper)
         whenever(mediaCarouselController.mediaFrame).thenReturn(mediaFrame)
+        isQsBypassingShade = MutableStateFlow(false)
+        whenever(shadeInteractor.isQsBypassingShade).thenReturn(isQsBypassingShade)
         mediaHierarchyManager =
             MediaHierarchyManager(
                 context,
@@ -135,7 +139,7 @@
                 communalInteractor,
                 configurationController,
                 wakefulnessLifecycle,
-                notifPanelEvents,
+                shadeInteractor,
                 settings,
                 fakeHandler,
                 testScope.backgroundScope,
@@ -430,17 +434,21 @@
         assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isTrue()
     }
 
+    @OptIn(ExperimentalCoroutinesApi::class)
     @Test
-    fun isCurrentlyInGuidedTransformation_hostsVisible_expandImmediateEnabled_returnsFalse() {
-        notifPanelEvents.notifyExpandImmediateChange(true)
-        goToLockscreen()
-        enterGuidedTransformation()
-        whenever(lockHost.visible).thenReturn(true)
-        whenever(qsHost.visible).thenReturn(true)
-        whenever(qqsHost.visible).thenReturn(true)
+    fun isCurrentlyInGuidedTransformation_hostsVisible_expandImmediateEnabled_returnsFalse() =
+        testScope.runTest {
+            runCurrent()
+            isQsBypassingShade.value = true
+            runCurrent()
+            goToLockscreen()
+            enterGuidedTransformation()
+            whenever(lockHost.visible).thenReturn(true)
+            whenever(qsHost.visible).thenReturn(true)
+            whenever(qqsHost.visible).thenReturn(true)
 
-        assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isFalse()
-    }
+            assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isFalse()
+        }
 
     @Test
     fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsFalse_with_active() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
index 3b07913..5245b22 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
@@ -27,7 +27,6 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.isNull;
-import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -41,7 +40,6 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.UserHandle;
-import android.platform.test.flag.junit.SetFlagsRule;
 import android.testing.AndroidTestingRunner;
 import android.util.SparseArray;
 import android.view.View;
@@ -82,7 +80,6 @@
 import com.android.systemui.util.time.FakeSystemClock;
 
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -107,10 +104,6 @@
             ComponentName.unflattenFromString("TEST_PKG/.TEST_CLS");
     private static final String CUSTOM_TILE_SPEC = CustomTile.toSpec(CUSTOM_TILE);
     private static final String SETTING = QSHost.TILES_SETTING;
-
-    @Rule
-    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
-
     @Mock
     private PluginManager mPluginManager;
     @Mock
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
index 6cc52d7..fbd63c6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
@@ -403,6 +403,31 @@
         verify(falseContext).bindServiceAsUser(any(), any(), eq(flags), any());
     }
 
+    @Test
+    public void testNullBindingCallsUnbind() {
+        Context mockContext = mock(Context.class);
+        // Binding has to succeed
+        when(mockContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true);
+        TileLifecycleManager manager = new TileLifecycleManager(mHandler, mockContext,
+                mock(IQSService.class),
+                mMockPackageManagerAdapter,
+                mMockBroadcastDispatcher,
+                mTileServiceIntent,
+                mUser,
+                mActivityManager,
+                mExecutor);
+
+        manager.executeSetBindService(true);
+        mExecutor.runAllReady();
+
+        ArgumentCaptor<ServiceConnection> captor = ArgumentCaptor.forClass(ServiceConnection.class);
+        verify(mockContext).bindServiceAsUser(any(), captor.capture(), anyInt(), any());
+
+        captor.getValue().onNullBinding(mTileServiceComponentName);
+        mExecutor.runAllReady();
+        verify(mockContext).unbindService(captor.getValue());
+    }
+
     private void mockChangeEnabled(long changeId, boolean enabled) {
         doReturn(enabled).when(() -> CompatChanges.isChangeEnabled(eq(changeId), anyString(),
                 any(UserHandle.class)));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
index 355ca81..8c896a6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
@@ -21,7 +21,6 @@
 import android.content.Intent
 import android.content.pm.UserInfo
 import android.os.UserHandle
-import android.platform.test.flag.junit.SetFlagsRule
 import android.service.quicksettings.Tile
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -62,7 +61,6 @@
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.anyString
@@ -77,8 +75,6 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 class CurrentTilesInteractorImplTest : SysuiTestCase() {
 
-    @Rule @JvmField val setFlagsRule = SetFlagsRule()
-
     private val tileSpecRepository: TileSpecRepository = FakeTileSpecRepository()
     private val userRepository = FakeUserRepository()
     private val installedTilesPackageRepository = FakeInstalledTilesComponentRepository()
@@ -109,7 +105,7 @@
     fun setup() {
         MockitoAnnotations.initMocks(this)
 
-        setFlagsRule.enableFlags(FLAG_QS_NEW_PIPELINE)
+        mSetFlagsRule.enableFlags(FLAG_QS_NEW_PIPELINE)
         // TODO(b/299909337): Add test checking the new factory is used when the flag is on
         featureFlags.set(Flags.QS_PIPELINE_NEW_TILES, true)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepositoryTest.kt
index 62ca965..2e63708 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepositoryTest.kt
@@ -1,13 +1,11 @@
 package com.android.systemui.qs.pipeline.shared
 
-import android.platform.test.flag.junit.SetFlagsRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -15,22 +13,20 @@
 @RunWith(AndroidJUnit4::class)
 class QSPipelineFlagsRepositoryTest : SysuiTestCase() {
 
-    @Rule @JvmField val setFlagsRule = SetFlagsRule()
-
     private val fakeFeatureFlagsClassic = FakeFeatureFlagsClassic()
 
     private val underTest = QSPipelineFlagsRepository(fakeFeatureFlagsClassic)
 
     @Test
     fun pipelineFlagDisabled() {
-        setFlagsRule.disableFlags(Flags.FLAG_QS_NEW_PIPELINE)
+        mSetFlagsRule.disableFlags(Flags.FLAG_QS_NEW_PIPELINE)
 
         assertThat(underTest.pipelineEnabled).isFalse()
     }
 
     @Test
     fun pipelineFlagEnabled() {
-        setFlagsRule.enableFlags(Flags.FLAG_QS_NEW_PIPELINE)
+        mSetFlagsRule.enableFlags(Flags.FLAG_QS_NEW_PIPELINE)
 
         assertThat(underTest.pipelineEnabled).isTrue()
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index cef888b..6a054cd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -256,6 +256,8 @@
                 falsingCollector = utils.falsingCollector(),
                 powerInteractor = powerInteractor,
                 bouncerInteractor = bouncerInteractor,
+                simBouncerInteractor = utils.simBouncerInteractor,
+                authenticationInteractor = utils.authenticationInteractor()
             )
         startable.start()
 
@@ -483,6 +485,32 @@
             verify(telecomManager).showInCallScreen(any())
         }
 
+    @Test
+    fun showBouncer_whenLockedSimIntroduced() =
+        testScope.runTest {
+            setAuthMethod(AuthenticationMethodModel.None)
+            introduceLockedSim()
+            assertCurrentScene(SceneKey.Bouncer)
+        }
+
+    @Test
+    fun goesToGone_whenSimUnlocked_whileDeviceUnlocked() =
+        testScope.runTest {
+            introduceLockedSim()
+            emulateUiSceneTransition(expectedVisible = true)
+            enterSimPin(authMethodAfterSimUnlock = AuthenticationMethodModel.None)
+            assertCurrentScene(SceneKey.Gone)
+        }
+
+    @Test
+    fun showLockscreen_whenSimUnlocked_whileDeviceLocked() =
+        testScope.runTest {
+            introduceLockedSim()
+            emulateUiSceneTransition(expectedVisible = true)
+            enterSimPin(authMethodAfterSimUnlock = AuthenticationMethodModel.Pin)
+            assertCurrentScene(SceneKey.Lockscreen)
+        }
+
     /**
      * Asserts that the current scene in the view-model matches what's expected.
      *
@@ -683,6 +711,35 @@
         runCurrent()
     }
 
+    /**
+     * Enters the correct PIN in the sim bouncer UI.
+     *
+     * Asserts that the current scene is [SceneKey.Bouncer] and that the current bouncer UI is a PIN
+     * before proceeding.
+     *
+     * Does not assert that the device is locked or unlocked.
+     */
+    private fun TestScope.enterSimPin(
+        authMethodAfterSimUnlock: AuthenticationMethodModel = AuthenticationMethodModel.None
+    ) {
+        assertWithMessage("Cannot enter PIN when not on the Bouncer scene!")
+            .that(getCurrentSceneInUi())
+            .isEqualTo(SceneKey.Bouncer)
+        val authMethodViewModel by collectLastValue(bouncerViewModel.authMethodViewModel)
+        assertWithMessage("Cannot enter PIN when not using a PIN authentication method!")
+            .that(authMethodViewModel)
+            .isInstanceOf(PinBouncerViewModel::class.java)
+
+        val pinBouncerViewModel = authMethodViewModel as PinBouncerViewModel
+        FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
+            pinBouncerViewModel.onPinButtonClicked(digit)
+        }
+        pinBouncerViewModel.onAuthenticateButtonClicked()
+        setAuthMethod(authMethodAfterSimUnlock)
+        utils.mobileConnectionsRepository.isAnySimSecure.value = false
+        runCurrent()
+    }
+
     /** Changes device wakefulness state from asleep to awake, going through intermediary states. */
     private fun TestScope.wakeUpDevice() {
         val wakefulnessModel = powerInteractor.detailedWakefulness.value
@@ -723,4 +780,10 @@
             runCurrent()
         }
     }
+
+    private fun TestScope.introduceLockedSim() {
+        setAuthMethod(AuthenticationMethodModel.Sim)
+        utils.mobileConnectionsRepository.isAnySimSecure.value = true
+        runCurrent()
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index 2f654e2..c4ec56c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -89,6 +89,8 @@
             falsingCollector = falsingCollector,
             powerInteractor = powerInteractor,
             bouncerInteractor = bouncerInteractor,
+            simBouncerInteractor = utils.simBouncerInteractor,
+            authenticationInteractor = authenticationInteractor,
         )
 
     @Before
@@ -587,6 +589,64 @@
             verify(falsingCollector, times(2)).onBouncerHidden()
         }
 
+    @Test
+    fun switchesToBouncer_whenSimBecomesLocked() =
+        testScope.runTest {
+            val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key })
+
+            prepareState(
+                initialSceneKey = SceneKey.Lockscreen,
+                authenticationMethod = AuthenticationMethodModel.Pin,
+                isDeviceUnlocked = false,
+            )
+            underTest.start()
+            runCurrent()
+
+            utils.mobileConnectionsRepository.isAnySimSecure.value = true
+            runCurrent()
+
+            assertThat(currentSceneKey).isEqualTo(SceneKey.Bouncer)
+        }
+
+    @Test
+    fun switchesToLockscreen_whenSimBecomesUnlocked() =
+        testScope.runTest {
+            utils.mobileConnectionsRepository.isAnySimSecure.value = true
+            val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key })
+
+            prepareState(
+                initialSceneKey = SceneKey.Bouncer,
+                authenticationMethod = AuthenticationMethodModel.Pin,
+                isDeviceUnlocked = false,
+            )
+            underTest.start()
+            runCurrent()
+            utils.mobileConnectionsRepository.isAnySimSecure.value = false
+            runCurrent()
+
+            assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen)
+        }
+
+    @Test
+    fun switchesToGone_whenSimBecomesUnlocked_ifDeviceUnlockedAndLockscreenDisabled() =
+        testScope.runTest {
+            utils.mobileConnectionsRepository.isAnySimSecure.value = true
+            val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key })
+
+            prepareState(
+                initialSceneKey = SceneKey.Lockscreen,
+                authenticationMethod = AuthenticationMethodModel.None,
+                isDeviceUnlocked = true,
+                isLockscreenEnabled = false,
+            )
+            underTest.start()
+            runCurrent()
+            utils.mobileConnectionsRepository.isAnySimSecure.value = false
+            runCurrent()
+
+            assertThat(currentSceneKey).isEqualTo(SceneKey.Gone)
+        }
+
     private fun TestScope.prepareState(
         isDeviceUnlocked: Boolean = false,
         isBypassEnabled: Boolean = false,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index f1429b5..ba8a666 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -100,6 +100,7 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingObserver;
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl;
 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingLockscreenHostedTransitionViewModel;
@@ -123,11 +124,12 @@
 import com.android.systemui.qs.QSFragmentLegacy;
 import com.android.systemui.res.R;
 import com.android.systemui.scene.SceneTestUtils;
-import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags;
 import com.android.systemui.screenrecord.RecordingController;
 import com.android.systemui.shade.data.repository.FakeShadeRepository;
 import com.android.systemui.shade.data.repository.ShadeRepository;
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
+import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl;
+import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl;
 import com.android.systemui.shade.transition.ShadeTransitionController;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.KeyguardIndicationController;
@@ -334,6 +336,7 @@
     @Mock private CastController mCastController;
     @Mock private SharedNotificationContainerInteractor mSharedNotificationContainerInteractor;
     @Mock private KeyguardClockPositionAlgorithm mKeyguardClockPositionAlgorithm;
+    @Mock private NaturalScrollingSettingObserver mNaturalScrollingSettingObserver;
 
     protected final int mMaxUdfpsBurnInOffsetY = 5;
     protected FakeFeatureFlagsClassic mFeatureFlags = new FakeFeatureFlagsClassic();
@@ -389,26 +392,27 @@
         mPowerInteractor = keyguardInteractorDeps.getPowerInteractor();
         when(mKeyguardTransitionInteractor.isInTransitionToStateWhere(any())).thenReturn(
                 StateFlowKt.MutableStateFlow(false));
-        mShadeInteractor = new ShadeInteractor(
+        mShadeInteractor = new ShadeInteractorImpl(
                 mTestScope.getBackgroundScope(),
                 new FakeDeviceProvisioningRepository(),
                 new FakeDisableFlagsRepository(),
                 mDozeParameters,
-                new FakeSceneContainerFlags(),
-                mUtils::sceneInteractor,
                 mFakeKeyguardRepository,
                 mKeyguardTransitionInteractor,
                 mPowerInteractor,
                 new FakeUserSetupRepository(),
                 mock(UserSwitcherInteractor.class),
-                new SharedNotificationContainerInteractor(
-                        new FakeConfigurationRepository(),
-                        mContext,
-                        new ResourcesSplitShadeStateController()
-                ),
-                mShadeRepository
+                new ShadeInteractorLegacyImpl(
+                        mTestScope.getBackgroundScope(),
+                        mFakeKeyguardRepository,
+                        new SharedNotificationContainerInteractor(
+                                new FakeConfigurationRepository(),
+                                mContext,
+                                new ResourcesSplitShadeStateController()
+                        ),
+                        mShadeRepository
+                )
         );
-
         SystemClock systemClock = new FakeSystemClock();
         mStatusBarStateController = new StatusBarStateControllerImpl(mUiEventLogger,
                 mInteractionJankMonitor, mJavaAdapter, () -> mShadeInteractor);
@@ -498,6 +502,7 @@
         when(mScreenOffAnimationController.shouldAnimateClockChange()).thenReturn(true);
         when(mQs.getView()).thenReturn(mView);
         when(mQSFragment.getView()).thenReturn(mView);
+        when(mNaturalScrollingSettingObserver.isNaturalScrollingEnabled()).thenReturn(true);
         doAnswer(invocation -> {
             mFragmentListener = invocation.getArgument(1);
             return null;
@@ -708,7 +713,8 @@
                 mKeyguardFaceAuthInteractor,
                 new ResourcesSplitShadeStateController(),
                 mPowerInteractor,
-                mKeyguardClockPositionAlgorithm);
+                mKeyguardClockPositionAlgorithm,
+                mNaturalScrollingSettingObserver);
         mNotificationPanelViewController.initDependencies(
                 mCentralSurfaces,
                 null,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index 6aaa0a1..8403ac5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -78,6 +78,8 @@
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.data.repository.FakeShadeRepository;
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
+import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl;
+import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
@@ -231,25 +233,26 @@
                 mKeyguardSecurityModel,
                 mSelectedUserInteractor,
                 powerInteractor);
-
-        mShadeInteractor =
-                new ShadeInteractor(
+        mShadeInteractor = new ShadeInteractorImpl(
+                mTestScope.getBackgroundScope(),
+                new FakeDeviceProvisioningRepository(),
+                new FakeDisableFlagsRepository(),
+                mock(DozeParameters.class),
+                keyguardRepository,
+                keyguardTransitionInteractor,
+                powerInteractor,
+                new FakeUserSetupRepository(),
+                mock(UserSwitcherInteractor.class),
+                new ShadeInteractorLegacyImpl(
                         mTestScope.getBackgroundScope(),
-                        new FakeDeviceProvisioningRepository(),
-                        new FakeDisableFlagsRepository(),
-                        mock(DozeParameters.class),
-                        sceneContainerFlags,
-                        () -> sceneInteractor,
                         keyguardRepository,
-                        keyguardTransitionInteractor,
-                        powerInteractor,
-                        new FakeUserSetupRepository(),
-                        mock(UserSwitcherInteractor.class),
                         new SharedNotificationContainerInteractor(
                                 configurationRepository,
                                 mContext,
                                 new ResourcesSplitShadeStateController()),
-                        shadeRepository);
+                        shadeRepository
+                )
+        );
 
         mNotificationShadeWindowController = new NotificationShadeWindowControllerImpl(
                 mContext,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index 2dd0af7..d89491c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -30,6 +30,7 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.LockIconViewController
 import com.android.keyguard.dagger.KeyguardBouncerComponent
+import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.back.domain.interactor.BackActionInteractor
 import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository
@@ -51,7 +52,6 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.dump.logcatLogBuffer
 import com.android.systemui.flags.FakeFeatureFlagsClassic
-import com.android.systemui.flags.Flags.ALTERNATE_BOUNCER_VIEW
 import com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED
 import com.android.systemui.flags.Flags.REVAMPED_BOUNCER_MESSAGES
 import com.android.systemui.flags.Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION
@@ -97,7 +97,6 @@
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
-import java.util.Optional
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.test.TestScope
@@ -112,8 +111,9 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
+import java.util.Optional
+import org.mockito.Mockito.`when` as whenever
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -198,7 +198,7 @@
         featureFlagsClassic.set(SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true)
         featureFlagsClassic.set(REVAMPED_BOUNCER_MESSAGES, true)
         featureFlagsClassic.set(LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false)
-        featureFlagsClassic.set(ALTERNATE_BOUNCER_VIEW, false)
+        mSetFlagsRule.disableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
 
         mCommunalRepository = FakeCommunalRepository()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
index 4b62906..9c8816c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
@@ -29,7 +29,6 @@
 import com.android.keyguard.LockIconViewController
 import com.android.keyguard.dagger.KeyguardBouncerComponent
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.back.domain.interactor.BackActionInteractor
 import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository
 import com.android.systemui.bouncer.data.repository.BouncerMessageRepositoryImpl
 import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
@@ -60,7 +59,6 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
 import com.android.systemui.log.BouncerLogger
-import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.res.R
 import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
 import com.android.systemui.statusbar.DragDownHelper
@@ -114,8 +112,6 @@
     @Mock private lateinit var centralSurfaces: CentralSurfaces
     @Mock private lateinit var dozeServiceHost: DozeServiceHost
     @Mock private lateinit var dozeScrimController: DozeScrimController
-    @Mock private lateinit var backActionInteractor: BackActionInteractor
-    @Mock private lateinit var powerInteractor: PowerInteractor
     @Mock private lateinit var dockManager: DockManager
     @Mock private lateinit var notificationPanelViewController: NotificationPanelViewController
     @Mock private lateinit var notificationStackScrollLayout: NotificationStackScrollLayout
@@ -192,7 +188,7 @@
         featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true)
         featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
         featureFlags.set(Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false)
-        featureFlags.set(Flags.ALTERNATE_BOUNCER_VIEW, false)
+        mSetFlagsRule.disableFlags(com.android.systemui.Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
         testScope = TestScope()
         controller =
             NotificationShadeWindowViewController(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
index ff110c5..26b84e3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
@@ -68,6 +68,8 @@
 import com.android.systemui.screenrecord.RecordingController;
 import com.android.systemui.shade.data.repository.FakeShadeRepository;
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
+import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl;
+import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl;
 import com.android.systemui.shade.transition.ShadeTransitionController;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
@@ -267,25 +269,26 @@
         ResourcesSplitShadeStateController splitShadeStateController =
                 new ResourcesSplitShadeStateController();
 
-        mShadeInteractor =
-                new ShadeInteractor(
+        mShadeInteractor = new ShadeInteractorImpl(
+                mTestScope.getBackgroundScope(),
+                deviceProvisioningRepository,
+                mDisableFlagsRepository,
+                mDozeParameters,
+                mKeyguardRepository,
+                keyguardTransitionInteractor,
+                powerInteractor,
+                new FakeUserSetupRepository(),
+                mUserSwitcherInteractor,
+                new ShadeInteractorLegacyImpl(
                         mTestScope.getBackgroundScope(),
-                        deviceProvisioningRepository,
-                        mDisableFlagsRepository,
-                        mDozeParameters,
-                        sceneContainerFlags,
-                        () -> sceneInteractor,
                         mKeyguardRepository,
-                        keyguardTransitionInteractor,
-                        powerInteractor,
-                        new FakeUserSetupRepository(),
-                        mUserSwitcherInteractor,
                         new SharedNotificationContainerInteractor(
                                 configurationRepository,
                                 mContext,
                                 splitShadeStateController),
                         mShadeRepository
-                );
+                )
+        );
 
         KeyguardStatusView keyguardStatusView = new KeyguardStatusView(mContext);
         keyguardStatusView.setId(R.id.keyguard_status_view);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerTest.java
index 7cb6d93..997e0e2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerTest.java
@@ -285,6 +285,20 @@
     }
 
     @Test
+    public void updateQsState_fullscreenTrue() {
+        mQsController.setExpanded(true);
+        mQsController.updateQsState();
+        assertThat(mShadeRepository.getLegacyQsFullscreen().getValue()).isTrue();
+    }
+
+    @Test
+    public void updateQsState_fullscreenFalse() {
+        mQsController.setExpanded(false);
+        mQsController.updateQsState();
+        assertThat(mShadeRepository.getLegacyQsFullscreen().getValue()).isFalse();
+    }
+
+    @Test
     public void shadeExpanded_onKeyguard() {
         mStatusBarStateController.setState(KEYGUARD);
         // set maxQsExpansion in NPVC
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt
index 20b19fd..5f8777d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt
@@ -207,4 +207,22 @@
             underTest.setLegacyIsQsExpanded(true)
             assertThat(underTest.legacyIsQsExpanded.value).isEqualTo(true)
         }
+
+    @Test
+    fun updateLegacyExpandImmediate() =
+        testScope.runTest {
+            assertThat(underTest.legacyExpandImmediate.value).isEqualTo(false)
+
+            underTest.setLegacyExpandImmediate(true)
+            assertThat(underTest.legacyExpandImmediate.value).isEqualTo(true)
+        }
+
+    @Test
+    fun updateLegacyQsFullscreen() =
+        testScope.runTest {
+            assertThat(underTest.legacyQsFullscreen.value).isEqualTo(false)
+
+            underTest.setLegacyQsFullscreen(true)
+            assertThat(underTest.legacyQsFullscreen.value).isEqualTo(true)
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
similarity index 66%
rename from packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
index ff7443f..61e4370 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.systemui.shade.data.repository
+package com.android.systemui.shade.domain.interactor
 
 import android.app.StatusBarManager.DISABLE2_NONE
 import android.app.StatusBarManager.DISABLE2_NOTIFICATION_SHADE
@@ -22,13 +22,11 @@
 import android.content.pm.UserInfo
 import android.os.UserManager
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.TestMocksModule
-import com.android.collectLastValue
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
+import com.android.systemui.collectLastValue
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FakeFeatureFlagsClassicModule
@@ -45,10 +43,10 @@
 import com.android.systemui.power.shared.model.WakeSleepReason
 import com.android.systemui.power.shared.model.WakefulnessState
 import com.android.systemui.res.R
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.scene.domain.interactor.SceneInteractor
-import com.android.systemui.scene.shared.model.ObservableTransitionState
-import com.android.systemui.scene.shared.model.SceneKey
-import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.data.repository.FakeShadeRepository
 import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
 import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository
 import com.android.systemui.statusbar.phone.DozeParameters
@@ -62,14 +60,12 @@
 import com.google.common.truth.Truth.assertThat
 import dagger.BindsInstance
 import dagger.Component
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.runBlocking
 import org.junit.Before
 import org.junit.Test
 
 @SmallTest
-class ShadeInteractorTest : SysuiTestCase() {
+class ShadeInteractorImplTest : SysuiTestCase() {
 
     @SysUISingleton
     @Component(
@@ -79,7 +75,7 @@
                 UserDomainLayerModule::class,
             ]
     )
-    interface TestComponent : SysUITestComponent<ShadeInteractor> {
+    interface TestComponent : SysUITestComponent<ShadeInteractorImpl> {
 
         val configurationRepository: FakeConfigurationRepository
         val deviceProvisioningRepository: FakeDeviceProvisioningRepository
@@ -105,7 +101,7 @@
     private val dozeParameters: DozeParameters = mock()
 
     private val testComponent: TestComponent =
-        DaggerShadeInteractorTest_TestComponent.factory()
+        DaggerShadeInteractorImplTest_TestComponent.factory()
             .create(
                 test = this,
                 featureFlags =
@@ -446,154 +442,6 @@
         }
 
     @Test
-    fun lockscreenShadeExpansion_idle_onScene() =
-        testComponent.runTest() {
-            // GIVEN an expansion flow based on transitions to and from a scene
-            val key = SceneKey.Shade
-            val expansion = underTest.sceneBasedExpansion(sceneInteractor, key)
-            val expansionAmount by collectLastValue(expansion)
-
-            // WHEN transition state is idle on the scene
-            val transitionState =
-                MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key))
-            sceneInteractor.setTransitionState(transitionState)
-
-            // THEN expansion is 1
-            assertThat(expansionAmount).isEqualTo(1f)
-        }
-
-    @Test
-    fun lockscreenShadeExpansion_idle_onDifferentScene() =
-        testComponent.runTest() {
-            // GIVEN an expansion flow based on transitions to and from a scene
-            val expansion = underTest.sceneBasedExpansion(sceneInteractor, SceneKey.Shade)
-            val expansionAmount by collectLastValue(expansion)
-
-            // WHEN transition state is idle on a different scene
-            val transitionState =
-                MutableStateFlow<ObservableTransitionState>(
-                    ObservableTransitionState.Idle(SceneKey.Lockscreen)
-                )
-            sceneInteractor.setTransitionState(transitionState)
-
-            // THEN expansion is 0
-            assertThat(expansionAmount).isEqualTo(0f)
-        }
-
-    @Test
-    fun lockscreenShadeExpansion_transitioning_toScene() =
-        testComponent.runTest() {
-            // GIVEN an expansion flow based on transitions to and from a scene
-            val key = SceneKey.QuickSettings
-            val expansion = underTest.sceneBasedExpansion(sceneInteractor, key)
-            val expansionAmount by collectLastValue(expansion)
-
-            // WHEN transition state is starting to move to the scene
-            val progress = MutableStateFlow(0f)
-            val transitionState =
-                MutableStateFlow<ObservableTransitionState>(
-                    ObservableTransitionState.Transition(
-                        fromScene = SceneKey.Lockscreen,
-                        toScene = key,
-                        progress = progress,
-                        isInitiatedByUserInput = false,
-                        isUserInputOngoing = flowOf(false),
-                    )
-                )
-            sceneInteractor.setTransitionState(transitionState)
-
-            // THEN expansion is 0
-            assertThat(expansionAmount).isEqualTo(0f)
-
-            // WHEN transition state is partially to the scene
-            progress.value = .4f
-
-            // THEN expansion matches the progress
-            assertThat(expansionAmount).isEqualTo(.4f)
-
-            // WHEN transition completes
-            progress.value = 1f
-
-            // THEN expansion is 1
-            assertThat(expansionAmount).isEqualTo(1f)
-        }
-
-    @Test
-    fun lockscreenShadeExpansion_transitioning_fromScene() =
-        testComponent.runTest() {
-            // GIVEN an expansion flow based on transitions to and from a scene
-            val key = SceneKey.QuickSettings
-            val expansion = underTest.sceneBasedExpansion(sceneInteractor, key)
-            val expansionAmount by collectLastValue(expansion)
-
-            // WHEN transition state is starting to move to the scene
-            val progress = MutableStateFlow(0f)
-            val transitionState =
-                MutableStateFlow<ObservableTransitionState>(
-                    ObservableTransitionState.Transition(
-                        fromScene = key,
-                        toScene = SceneKey.Lockscreen,
-                        progress = progress,
-                        isInitiatedByUserInput = false,
-                        isUserInputOngoing = flowOf(false),
-                    )
-                )
-            sceneInteractor.setTransitionState(transitionState)
-
-            // THEN expansion is 1
-            assertThat(expansionAmount).isEqualTo(1f)
-
-            // WHEN transition state is partially to the scene
-            progress.value = .4f
-
-            // THEN expansion reflects the progress
-            assertThat(expansionAmount).isEqualTo(.6f)
-
-            // WHEN transition completes
-            progress.value = 1f
-
-            // THEN expansion is 0
-            assertThat(expansionAmount).isEqualTo(0f)
-        }
-
-    @Test
-    fun lockscreenShadeExpansion_transitioning_toAndFromDifferentScenes() =
-        testComponent.runTest() {
-            // GIVEN an expansion flow based on transitions to and from a scene
-            val expansion = underTest.sceneBasedExpansion(sceneInteractor, SceneKey.QuickSettings)
-            val expansionAmount by collectLastValue(expansion)
-
-            // WHEN transition state is starting to between different scenes
-            val progress = MutableStateFlow(0f)
-            val transitionState =
-                MutableStateFlow<ObservableTransitionState>(
-                    ObservableTransitionState.Transition(
-                        fromScene = SceneKey.Lockscreen,
-                        toScene = SceneKey.Shade,
-                        progress = progress,
-                        isInitiatedByUserInput = false,
-                        isUserInputOngoing = flowOf(false),
-                    )
-                )
-            sceneInteractor.setTransitionState(transitionState)
-
-            // THEN expansion is 0
-            assertThat(expansionAmount).isEqualTo(0f)
-
-            // WHEN transition state is partially complete
-            progress.value = .4f
-
-            // THEN expansion is still 0
-            assertThat(expansionAmount).isEqualTo(0f)
-
-            // WHEN transition completes
-            progress.value = 1f
-
-            // THEN expansion is still 0
-            assertThat(expansionAmount).isEqualTo(0f)
-        }
-
-    @Test
     fun userInteractingWithShade_shadeDraggedUpAndDown() =
         testComponent.runTest() {
             val actual by collectLastValue(underTest.isUserInteractingWithShade)
@@ -815,199 +663,6 @@
             // THEN user is not interacting
             assertThat(actual).isFalse()
         }
-    @Test
-    fun userInteracting_idle() =
-        testComponent.runTest() {
-            // GIVEN an interacting flow based on transitions to and from a scene
-            val key = SceneKey.Shade
-            val interactingFlow = underTest.sceneBasedInteracting(sceneInteractor, key)
-            val interacting by collectLastValue(interactingFlow)
-
-            // WHEN transition state is idle
-            val transitionState =
-                MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key))
-            sceneInteractor.setTransitionState(transitionState)
-
-            // THEN interacting is false
-            assertThat(interacting).isFalse()
-        }
-
-    @Test
-    fun userInteracting_transitioning_toScene_programmatic() =
-        testComponent.runTest() {
-            // GIVEN an interacting flow based on transitions to and from a scene
-            val key = SceneKey.QuickSettings
-            val interactingFlow = underTest.sceneBasedInteracting(sceneInteractor, key)
-            val interacting by collectLastValue(interactingFlow)
-
-            // WHEN transition state is starting to move to the scene
-            val progress = MutableStateFlow(0f)
-            val transitionState =
-                MutableStateFlow<ObservableTransitionState>(
-                    ObservableTransitionState.Transition(
-                        fromScene = SceneKey.Lockscreen,
-                        toScene = key,
-                        progress = progress,
-                        isInitiatedByUserInput = false,
-                        isUserInputOngoing = flowOf(false),
-                    )
-                )
-            sceneInteractor.setTransitionState(transitionState)
-
-            // THEN interacting is false
-            assertThat(interacting).isFalse()
-
-            // WHEN transition state is partially to the scene
-            progress.value = .4f
-
-            // THEN interacting is false
-            assertThat(interacting).isFalse()
-
-            // WHEN transition completes
-            progress.value = 1f
-
-            // THEN interacting is false
-            assertThat(interacting).isFalse()
-        }
-
-    @Test
-    fun userInteracting_transitioning_toScene_userInputDriven() =
-        testComponent.runTest() {
-            // GIVEN an interacting flow based on transitions to and from a scene
-            val key = SceneKey.QuickSettings
-            val interactingFlow = underTest.sceneBasedInteracting(sceneInteractor, key)
-            val interacting by collectLastValue(interactingFlow)
-
-            // WHEN transition state is starting to move to the scene
-            val progress = MutableStateFlow(0f)
-            val transitionState =
-                MutableStateFlow<ObservableTransitionState>(
-                    ObservableTransitionState.Transition(
-                        fromScene = SceneKey.Lockscreen,
-                        toScene = key,
-                        progress = progress,
-                        isInitiatedByUserInput = true,
-                        isUserInputOngoing = flowOf(false),
-                    )
-                )
-            sceneInteractor.setTransitionState(transitionState)
-
-            // THEN interacting is true
-            assertThat(interacting).isTrue()
-
-            // WHEN transition state is partially to the scene
-            progress.value = .4f
-
-            // THEN interacting is true
-            assertThat(interacting).isTrue()
-
-            // WHEN transition completes
-            progress.value = 1f
-
-            // THEN interacting is true
-            assertThat(interacting).isTrue()
-        }
-
-    @Test
-    fun userInteracting_transitioning_fromScene_programmatic() =
-        testComponent.runTest() {
-            // GIVEN an interacting flow based on transitions to and from a scene
-            val key = SceneKey.QuickSettings
-            val interactingFlow = underTest.sceneBasedInteracting(sceneInteractor, key)
-            val interacting by collectLastValue(interactingFlow)
-
-            // WHEN transition state is starting to move to the scene
-            val progress = MutableStateFlow(0f)
-            val transitionState =
-                MutableStateFlow<ObservableTransitionState>(
-                    ObservableTransitionState.Transition(
-                        fromScene = key,
-                        toScene = SceneKey.Lockscreen,
-                        progress = progress,
-                        isInitiatedByUserInput = false,
-                        isUserInputOngoing = flowOf(false),
-                    )
-                )
-            sceneInteractor.setTransitionState(transitionState)
-
-            // THEN interacting is false
-            assertThat(interacting).isFalse()
-
-            // WHEN transition state is partially to the scene
-            progress.value = .4f
-
-            // THEN interacting is false
-            assertThat(interacting).isFalse()
-
-            // WHEN transition completes
-            progress.value = 1f
-
-            // THEN interacting is false
-            assertThat(interacting).isFalse()
-        }
-
-    @Test
-    fun userInteracting_transitioning_fromScene_userInputDriven() =
-        testComponent.runTest() {
-            // GIVEN an interacting flow based on transitions to and from a scene
-            val key = SceneKey.QuickSettings
-            val interactingFlow = underTest.sceneBasedInteracting(sceneInteractor, key)
-            val interacting by collectLastValue(interactingFlow)
-
-            // WHEN transition state is starting to move to the scene
-            val progress = MutableStateFlow(0f)
-            val transitionState =
-                MutableStateFlow<ObservableTransitionState>(
-                    ObservableTransitionState.Transition(
-                        fromScene = key,
-                        toScene = SceneKey.Lockscreen,
-                        progress = progress,
-                        isInitiatedByUserInput = true,
-                        isUserInputOngoing = flowOf(false),
-                    )
-                )
-            sceneInteractor.setTransitionState(transitionState)
-
-            // THEN interacting is true
-            assertThat(interacting).isTrue()
-
-            // WHEN transition state is partially to the scene
-            progress.value = .4f
-
-            // THEN interacting is true
-            assertThat(interacting).isTrue()
-
-            // WHEN transition completes
-            progress.value = 1f
-
-            // THEN interacting is true
-            assertThat(interacting).isTrue()
-        }
-
-    @Test
-    fun userInteracting_transitioning_toAndFromDifferentScenes() =
-        testComponent.runTest() {
-            // GIVEN an interacting flow based on transitions to and from a scene
-            val interactingFlow = underTest.sceneBasedInteracting(sceneInteractor, SceneKey.Shade)
-            val interacting by collectLastValue(interactingFlow)
-
-            // WHEN transition state is starting to between different scenes
-            val progress = MutableStateFlow(0f)
-            val transitionState =
-                MutableStateFlow<ObservableTransitionState>(
-                    ObservableTransitionState.Transition(
-                        fromScene = SceneKey.Lockscreen,
-                        toScene = SceneKey.QuickSettings,
-                        progress = MutableStateFlow(0f),
-                        isInitiatedByUserInput = true,
-                        isUserInputOngoing = flowOf(false),
-                    )
-                )
-            sceneInteractor.setTransitionState(transitionState)
-
-            // THEN interacting is false
-            assertThat(interacting).isFalse()
-        }
 
     @Test
     fun isShadeTouchable_isFalse_whenFrpIsActive() =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt
new file mode 100644
index 0000000..92eb6ed
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt
@@ -0,0 +1,415 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.shade.domain.interactor
+
+import android.content.pm.UserInfo
+import android.os.UserManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
+import com.android.systemui.collectLastValue
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FakeFeatureFlagsClassicModule
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.power.data.repository.FakePowerRepository
+import com.android.systemui.res.R
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.statusbar.phone.DozeParameters
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.domain.UserDomainLayerModule
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+
+@SmallTest
+class ShadeInteractorLegacyImplTest : SysuiTestCase() {
+
+    @SysUISingleton
+    @Component(
+        modules =
+            [
+                SysUITestModule::class,
+                UserDomainLayerModule::class,
+            ]
+    )
+    interface TestComponent : SysUITestComponent<ShadeInteractorLegacyImpl> {
+
+        val configurationRepository: FakeConfigurationRepository
+        val keyguardRepository: FakeKeyguardRepository
+        val keyguardTransitionRepository: FakeKeyguardTransitionRepository
+        val powerRepository: FakePowerRepository
+        val sceneInteractor: SceneInteractor
+        val shadeRepository: FakeShadeRepository
+        val userRepository: FakeUserRepository
+
+        @Component.Factory
+        interface Factory {
+            fun create(
+                @BindsInstance test: SysuiTestCase,
+                featureFlags: FakeFeatureFlagsClassicModule,
+                mocks: TestMocksModule,
+            ): TestComponent
+        }
+    }
+
+    private val dozeParameters: DozeParameters = mock()
+
+    private val testComponent: TestComponent =
+        DaggerShadeInteractorLegacyImplTest_TestComponent.factory()
+            .create(
+                test = this,
+                featureFlags =
+                    FakeFeatureFlagsClassicModule {
+                        set(Flags.FACE_AUTH_REFACTOR, false)
+                        set(Flags.FULL_SCREEN_USER_SWITCHER, true)
+                    },
+                mocks =
+                    TestMocksModule(
+                        dozeParameters = dozeParameters,
+                    ),
+            )
+
+    @Before
+    fun setUp() {
+        runBlocking {
+            val userInfos =
+                listOf(
+                    UserInfo(
+                        /* id= */ 0,
+                        /* name= */ "zero",
+                        /* iconPath= */ "",
+                        /* flags= */ UserInfo.FLAG_PRIMARY or
+                            UserInfo.FLAG_ADMIN or
+                            UserInfo.FLAG_FULL,
+                        UserManager.USER_TYPE_FULL_SYSTEM,
+                    ),
+                )
+            testComponent.apply {
+                userRepository.setUserInfos(userInfos)
+                userRepository.setSelectedUserInfo(userInfos[0])
+            }
+        }
+    }
+
+    @Test
+    fun fullShadeExpansionWhenShadeLocked() =
+        testComponent.runTest() {
+            val actual by collectLastValue(underTest.shadeExpansion)
+
+            keyguardRepository.setStatusBarState(StatusBarState.SHADE_LOCKED)
+            shadeRepository.setLockscreenShadeExpansion(0.5f)
+
+            assertThat(actual).isEqualTo(1f)
+        }
+
+    @Test
+    fun fullShadeExpansionWhenStatusBarStateIsNotShadeLocked() =
+        testComponent.runTest() {
+            val actual by collectLastValue(underTest.shadeExpansion)
+
+            keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+
+            shadeRepository.setLockscreenShadeExpansion(0.5f)
+            assertThat(actual).isEqualTo(0.5f)
+
+            shadeRepository.setLockscreenShadeExpansion(0.8f)
+            assertThat(actual).isEqualTo(0.8f)
+        }
+
+    @Test
+    fun shadeExpansionWhenInSplitShadeAndQsExpanded() =
+        testComponent.runTest() {
+            val actual by collectLastValue(underTest.shadeExpansion)
+
+            // WHEN split shade is enabled and QS is expanded
+            keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            overrideResource(R.bool.config_use_split_notification_shade, true)
+            configurationRepository.onAnyConfigurationChange()
+            shadeRepository.setQsExpansion(.5f)
+            shadeRepository.setLegacyShadeExpansion(.7f)
+            runCurrent()
+
+            // THEN legacy shade expansion is passed through
+            assertThat(actual).isEqualTo(.7f)
+        }
+
+    @Test
+    fun shadeExpansionWhenNotInSplitShadeAndQsExpanded() =
+        testComponent.runTest() {
+            val actual by collectLastValue(underTest.shadeExpansion)
+
+            // WHEN split shade is not enabled and QS is expanded
+            keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            overrideResource(R.bool.config_use_split_notification_shade, false)
+            shadeRepository.setQsExpansion(.5f)
+            shadeRepository.setLegacyShadeExpansion(1f)
+            runCurrent()
+
+            // THEN shade expansion is zero
+            assertThat(actual).isEqualTo(0f)
+        }
+
+    @Test
+    fun shadeExpansionWhenNotInSplitShadeAndQsCollapsed() =
+        testComponent.runTest() {
+            val actual by collectLastValue(underTest.shadeExpansion)
+
+            // WHEN split shade is not enabled and QS is expanded
+            keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            shadeRepository.setQsExpansion(0f)
+            shadeRepository.setLegacyShadeExpansion(.6f)
+
+            // THEN shade expansion is zero
+            assertThat(actual).isEqualTo(.6f)
+        }
+
+    @Test
+    fun userInteractingWithShade_shadeDraggedUpAndDown() =
+        testComponent.runTest() {
+            val actual by collectLastValue(underTest.isUserInteractingWithShade)
+            // GIVEN shade collapsed and not tracking input
+            shadeRepository.setLegacyShadeExpansion(0f)
+            shadeRepository.setLegacyShadeTracking(false)
+            runCurrent()
+
+            // THEN user is not interacting
+            assertThat(actual).isFalse()
+
+            // WHEN shade tracking starts
+            shadeRepository.setLegacyShadeTracking(true)
+            runCurrent()
+
+            // THEN user is interacting
+            assertThat(actual).isTrue()
+
+            // WHEN shade dragged down halfway
+            shadeRepository.setLegacyShadeExpansion(.5f)
+            runCurrent()
+
+            // THEN user is interacting
+            assertThat(actual).isTrue()
+
+            // WHEN shade fully expanded but tracking is not stopped
+            shadeRepository.setLegacyShadeExpansion(1f)
+            runCurrent()
+
+            // THEN user is interacting
+            assertThat(actual).isTrue()
+
+            // WHEN shade fully collapsed but tracking is not stopped
+            shadeRepository.setLegacyShadeExpansion(0f)
+            runCurrent()
+
+            // THEN user is interacting
+            assertThat(actual).isTrue()
+
+            // WHEN shade dragged halfway and tracking is stopped
+            shadeRepository.setLegacyShadeExpansion(.6f)
+            shadeRepository.setLegacyShadeTracking(false)
+            runCurrent()
+
+            // THEN user is interacting
+            assertThat(actual).isTrue()
+
+            // WHEN shade completes expansion stopped
+            shadeRepository.setLegacyShadeExpansion(1f)
+            runCurrent()
+
+            // THEN user is not interacting
+            assertThat(actual).isFalse()
+        }
+
+    @Test
+    fun userInteractingWithShade_shadeExpanded() =
+        testComponent.runTest() {
+            val actual by collectLastValue(underTest.isUserInteractingWithShade)
+            // GIVEN shade collapsed and not tracking input
+            shadeRepository.setLegacyShadeExpansion(0f)
+            shadeRepository.setLegacyShadeTracking(false)
+            runCurrent()
+
+            // THEN user is not interacting
+            assertThat(actual).isFalse()
+
+            // WHEN shade tracking starts
+            shadeRepository.setLegacyShadeTracking(true)
+            runCurrent()
+
+            // THEN user is interacting
+            assertThat(actual).isTrue()
+
+            // WHEN shade dragged down halfway
+            shadeRepository.setLegacyShadeExpansion(.5f)
+            runCurrent()
+
+            // THEN user is interacting
+            assertThat(actual).isTrue()
+
+            // WHEN shade fully expanded and tracking is stopped
+            shadeRepository.setLegacyShadeExpansion(1f)
+            shadeRepository.setLegacyShadeTracking(false)
+            runCurrent()
+
+            // THEN user is not interacting
+            assertThat(actual).isFalse()
+        }
+
+    @Test
+    fun userInteractingWithShade_shadePartiallyExpanded() =
+        testComponent.runTest() {
+            val actual by collectLastValue(underTest.isUserInteractingWithShade)
+            // GIVEN shade collapsed and not tracking input
+            shadeRepository.setLegacyShadeExpansion(0f)
+            shadeRepository.setLegacyShadeTracking(false)
+            runCurrent()
+
+            // THEN user is not interacting
+            assertThat(actual).isFalse()
+
+            // WHEN shade tracking starts
+            shadeRepository.setLegacyShadeTracking(true)
+            runCurrent()
+
+            // THEN user is interacting
+            assertThat(actual).isTrue()
+
+            // WHEN shade partially expanded
+            shadeRepository.setLegacyShadeExpansion(.4f)
+            runCurrent()
+
+            // THEN user is interacting
+            assertThat(actual).isTrue()
+
+            // WHEN tracking is stopped
+            shadeRepository.setLegacyShadeTracking(false)
+            runCurrent()
+
+            // THEN user is interacting
+            assertThat(actual).isTrue()
+
+            // WHEN shade goes back to collapsed
+            shadeRepository.setLegacyShadeExpansion(0f)
+            runCurrent()
+
+            // THEN user is not interacting
+            assertThat(actual).isFalse()
+        }
+
+    @Test
+    fun userInteractingWithShade_shadeCollapsed() =
+        testComponent.runTest() {
+            val actual by collectLastValue(underTest.isUserInteractingWithShade)
+            // GIVEN shade expanded and not tracking input
+            shadeRepository.setLegacyShadeExpansion(1f)
+            shadeRepository.setLegacyShadeTracking(false)
+            runCurrent()
+
+            // THEN user is not interacting
+            assertThat(actual).isFalse()
+
+            // WHEN shade tracking starts
+            shadeRepository.setLegacyShadeTracking(true)
+            runCurrent()
+
+            // THEN user is interacting
+            assertThat(actual).isTrue()
+
+            // WHEN shade dragged up halfway
+            shadeRepository.setLegacyShadeExpansion(.5f)
+            runCurrent()
+
+            // THEN user is interacting
+            assertThat(actual).isTrue()
+
+            // WHEN shade fully collapsed and tracking is stopped
+            shadeRepository.setLegacyShadeExpansion(0f)
+            shadeRepository.setLegacyShadeTracking(false)
+            runCurrent()
+
+            // THEN user is not interacting
+            assertThat(actual).isFalse()
+        }
+
+    @Test
+    fun userInteractingWithQs_qsDraggedUpAndDown() =
+        testComponent.runTest() {
+            val actual by collectLastValue(underTest.isUserInteractingWithQs)
+            // GIVEN qs collapsed and not tracking input
+            shadeRepository.setQsExpansion(0f)
+            shadeRepository.setLegacyQsTracking(false)
+            runCurrent()
+
+            // THEN user is not interacting
+            assertThat(actual).isFalse()
+
+            // WHEN qs tracking starts
+            shadeRepository.setLegacyQsTracking(true)
+            runCurrent()
+
+            // THEN user is interacting
+            assertThat(actual).isTrue()
+
+            // WHEN qs dragged down halfway
+            shadeRepository.setQsExpansion(.5f)
+            runCurrent()
+
+            // THEN user is interacting
+            assertThat(actual).isTrue()
+
+            // WHEN qs fully expanded but tracking is not stopped
+            shadeRepository.setQsExpansion(1f)
+            runCurrent()
+
+            // THEN user is interacting
+            assertThat(actual).isTrue()
+
+            // WHEN qs fully collapsed but tracking is not stopped
+            shadeRepository.setQsExpansion(0f)
+            runCurrent()
+
+            // THEN user is interacting
+            assertThat(actual).isTrue()
+
+            // WHEN qs dragged halfway and tracking is stopped
+            shadeRepository.setQsExpansion(.6f)
+            shadeRepository.setLegacyQsTracking(false)
+            runCurrent()
+
+            // THEN user is interacting
+            assertThat(actual).isTrue()
+
+            // WHEN qs completes expansion stopped
+            shadeRepository.setQsExpansion(1f)
+            runCurrent()
+
+            // THEN user is not interacting
+            assertThat(actual).isFalse()
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
new file mode 100644
index 0000000..729f3f8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
@@ -0,0 +1,583 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.domain.interactor
+
+import android.content.pm.UserInfo
+import android.os.UserManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
+import com.android.systemui.collectLastValue
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FakeFeatureFlagsClassicModule
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.power.data.repository.FakePowerRepository
+import com.android.systemui.res.R
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.ObservableTransitionState
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.statusbar.phone.DozeParameters
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.domain.UserDomainLayerModule
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth
+import dagger.BindsInstance
+import dagger.Component
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+
+@SmallTest
+class ShadeInteractorSceneContainerImplTest : SysuiTestCase() {
+
+    @SysUISingleton
+    @Component(
+        modules =
+            [
+                SysUITestModule::class,
+                UserDomainLayerModule::class,
+            ]
+    )
+    interface TestComponent : SysUITestComponent<ShadeInteractorSceneContainerImpl> {
+
+        val configurationRepository: FakeConfigurationRepository
+        val keyguardRepository: FakeKeyguardRepository
+        val keyguardTransitionRepository: FakeKeyguardTransitionRepository
+        val powerRepository: FakePowerRepository
+        val sceneInteractor: SceneInteractor
+        val shadeRepository: FakeShadeRepository
+        val userRepository: FakeUserRepository
+
+        @Component.Factory
+        interface Factory {
+            fun create(
+                @BindsInstance test: SysuiTestCase,
+                featureFlags: FakeFeatureFlagsClassicModule,
+                mocks: TestMocksModule,
+            ): TestComponent
+        }
+    }
+
+    private val dozeParameters: DozeParameters = mock()
+
+    private val testComponent: TestComponent =
+        DaggerShadeInteractorSceneContainerImplTest_TestComponent.factory()
+            .create(
+                test = this,
+                featureFlags =
+                    FakeFeatureFlagsClassicModule {
+                        set(Flags.FACE_AUTH_REFACTOR, false)
+                        set(Flags.FULL_SCREEN_USER_SWITCHER, true)
+                    },
+                mocks =
+                    TestMocksModule(
+                        dozeParameters = dozeParameters,
+                    ),
+            )
+
+    @Before
+    fun setUp() {
+        runBlocking {
+            val userInfos =
+                listOf(
+                    UserInfo(
+                        /* id= */ 0,
+                        /* name= */ "zero",
+                        /* iconPath= */ "",
+                        /* flags= */ UserInfo.FLAG_PRIMARY or
+                            UserInfo.FLAG_ADMIN or
+                            UserInfo.FLAG_FULL,
+                        UserManager.USER_TYPE_FULL_SYSTEM,
+                    ),
+                )
+            testComponent.apply {
+                userRepository.setUserInfos(userInfos)
+                userRepository.setSelectedUserInfo(userInfos[0])
+            }
+        }
+    }
+
+    @Ignore("b/309825977")
+    @Test
+    fun qsExpansionWhenInSplitShadeAndQsExpanded() =
+        testComponent.runTest() {
+            val actual by collectLastValue(underTest.qsExpansion)
+
+            // WHEN split shade is enabled and QS is expanded
+            keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            overrideResource(R.bool.config_use_split_notification_shade, true)
+            configurationRepository.onAnyConfigurationChange()
+            val progress = MutableStateFlow(.3f)
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Transition(
+                        fromScene = SceneKey.QuickSettings,
+                        toScene = SceneKey.Shade,
+                        progress = progress,
+                        isInitiatedByUserInput = false,
+                        isUserInputOngoing = flowOf(false),
+                    )
+                )
+            sceneInteractor.setTransitionState(transitionState)
+            runCurrent()
+
+            // THEN legacy shade expansion is passed through
+            Truth.assertThat(actual).isEqualTo(.3f)
+        }
+
+    @Ignore("b/309825977")
+    @Test
+    fun qsExpansionWhenNotInSplitShadeAndQsExpanded() =
+        testComponent.runTest() {
+            val actual by collectLastValue(underTest.qsExpansion)
+
+            // WHEN split shade is not enabled and QS is expanded
+            keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            overrideResource(R.bool.config_use_split_notification_shade, false)
+            val progress = MutableStateFlow(.3f)
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Transition(
+                        fromScene = SceneKey.QuickSettings,
+                        toScene = SceneKey.Shade,
+                        progress = progress,
+                        isInitiatedByUserInput = false,
+                        isUserInputOngoing = flowOf(false),
+                    )
+                )
+            sceneInteractor.setTransitionState(transitionState)
+            runCurrent()
+
+            // THEN shade expansion is zero
+            Truth.assertThat(actual).isEqualTo(.7f)
+        }
+
+    @Test
+    fun qsFullscreen_falseWhenTransitioning() =
+        testComponent.runTest() {
+            val actual by collectLastValue(underTest.isQsFullscreen)
+
+            // WHEN scene transition active
+            keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            val progress = MutableStateFlow(.3f)
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Transition(
+                        fromScene = SceneKey.QuickSettings,
+                        toScene = SceneKey.Shade,
+                        progress = progress,
+                        isInitiatedByUserInput = false,
+                        isUserInputOngoing = flowOf(false),
+                    )
+                )
+            sceneInteractor.setTransitionState(transitionState)
+            runCurrent()
+
+            // THEN QS is not fullscreen
+            Truth.assertThat(actual).isFalse()
+        }
+
+    @Test
+    fun qsFullscreen_falseWhenIdleNotQS() =
+        testComponent.runTest() {
+            val actual by collectLastValue(underTest.isQsFullscreen)
+
+            // WHEN Idle but not on QuickSettings scene
+            keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Idle(SceneKey.Shade)
+                )
+            sceneInteractor.setTransitionState(transitionState)
+            runCurrent()
+
+            // THEN QS is not fullscreen
+            Truth.assertThat(actual).isFalse()
+        }
+
+    @Test
+    fun qsFullscreen_trueWhenIdleQS() =
+        testComponent.runTest() {
+            val actual by collectLastValue(underTest.isQsFullscreen)
+
+            // WHEN Idle on QuickSettings scene
+            keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Idle(SceneKey.QuickSettings)
+                )
+            sceneInteractor.setTransitionState(transitionState)
+            runCurrent()
+
+            // THEN QS is fullscreen
+            Truth.assertThat(actual).isTrue()
+        }
+
+    @Test
+    fun lockscreenShadeExpansion_idle_onScene() =
+        testComponent.runTest() {
+            // GIVEN an expansion flow based on transitions to and from a scene
+            val key = SceneKey.Shade
+            val expansion = underTest.sceneBasedExpansion(sceneInteractor, key)
+            val expansionAmount by collectLastValue(expansion)
+
+            // WHEN transition state is idle on the scene
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key))
+            sceneInteractor.setTransitionState(transitionState)
+
+            // THEN expansion is 1
+            Truth.assertThat(expansionAmount).isEqualTo(1f)
+        }
+
+    @Test
+    fun lockscreenShadeExpansion_idle_onDifferentScene() =
+        testComponent.runTest() {
+            // GIVEN an expansion flow based on transitions to and from a scene
+            val expansion = underTest.sceneBasedExpansion(sceneInteractor, SceneKey.Shade)
+            val expansionAmount by collectLastValue(expansion)
+
+            // WHEN transition state is idle on a different scene
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Idle(SceneKey.Lockscreen)
+                )
+            sceneInteractor.setTransitionState(transitionState)
+
+            // THEN expansion is 0
+            Truth.assertThat(expansionAmount).isEqualTo(0f)
+        }
+
+    @Test
+    fun lockscreenShadeExpansion_transitioning_toScene() =
+        testComponent.runTest() {
+            // GIVEN an expansion flow based on transitions to and from a scene
+            val key = SceneKey.QuickSettings
+            val expansion = underTest.sceneBasedExpansion(sceneInteractor, key)
+            val expansionAmount by collectLastValue(expansion)
+
+            // WHEN transition state is starting to move to the scene
+            val progress = MutableStateFlow(0f)
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Transition(
+                        fromScene = SceneKey.Lockscreen,
+                        toScene = key,
+                        progress = progress,
+                        isInitiatedByUserInput = false,
+                        isUserInputOngoing = flowOf(false),
+                    )
+                )
+            sceneInteractor.setTransitionState(transitionState)
+
+            // THEN expansion is 0
+            Truth.assertThat(expansionAmount).isEqualTo(0f)
+
+            // WHEN transition state is partially to the scene
+            progress.value = .4f
+
+            // THEN expansion matches the progress
+            Truth.assertThat(expansionAmount).isEqualTo(.4f)
+
+            // WHEN transition completes
+            progress.value = 1f
+
+            // THEN expansion is 1
+            Truth.assertThat(expansionAmount).isEqualTo(1f)
+        }
+
+    @Test
+    fun lockscreenShadeExpansion_transitioning_fromScene() =
+        testComponent.runTest() {
+            // GIVEN an expansion flow based on transitions to and from a scene
+            val key = SceneKey.QuickSettings
+            val expansion = underTest.sceneBasedExpansion(sceneInteractor, key)
+            val expansionAmount by collectLastValue(expansion)
+
+            // WHEN transition state is starting to move to the scene
+            val progress = MutableStateFlow(0f)
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Transition(
+                        fromScene = key,
+                        toScene = SceneKey.Lockscreen,
+                        progress = progress,
+                        isInitiatedByUserInput = false,
+                        isUserInputOngoing = flowOf(false),
+                    )
+                )
+            sceneInteractor.setTransitionState(transitionState)
+
+            // THEN expansion is 1
+            Truth.assertThat(expansionAmount).isEqualTo(1f)
+
+            // WHEN transition state is partially to the scene
+            progress.value = .4f
+
+            // THEN expansion reflects the progress
+            Truth.assertThat(expansionAmount).isEqualTo(.6f)
+
+            // WHEN transition completes
+            progress.value = 1f
+
+            // THEN expansion is 0
+            Truth.assertThat(expansionAmount).isEqualTo(0f)
+        }
+
+    @Test
+    fun lockscreenShadeExpansion_transitioning_toAndFromDifferentScenes() =
+        testComponent.runTest() {
+            // GIVEN an expansion flow based on transitions to and from a scene
+            val expansion = underTest.sceneBasedExpansion(sceneInteractor, SceneKey.QuickSettings)
+            val expansionAmount by collectLastValue(expansion)
+
+            // WHEN transition state is starting to between different scenes
+            val progress = MutableStateFlow(0f)
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Transition(
+                        fromScene = SceneKey.Lockscreen,
+                        toScene = SceneKey.Shade,
+                        progress = progress,
+                        isInitiatedByUserInput = false,
+                        isUserInputOngoing = flowOf(false),
+                    )
+                )
+            sceneInteractor.setTransitionState(transitionState)
+
+            // THEN expansion is 0
+            Truth.assertThat(expansionAmount).isEqualTo(0f)
+
+            // WHEN transition state is partially complete
+            progress.value = .4f
+
+            // THEN expansion is still 0
+            Truth.assertThat(expansionAmount).isEqualTo(0f)
+
+            // WHEN transition completes
+            progress.value = 1f
+
+            // THEN expansion is still 0
+            Truth.assertThat(expansionAmount).isEqualTo(0f)
+        }
+
+    @Test
+    fun userInteracting_idle() =
+        testComponent.runTest() {
+            // GIVEN an interacting flow based on transitions to and from a scene
+            val key = SceneKey.Shade
+            val interactingFlow = underTest.sceneBasedInteracting(sceneInteractor, key)
+            val interacting by collectLastValue(interactingFlow)
+
+            // WHEN transition state is idle
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key))
+            sceneInteractor.setTransitionState(transitionState)
+
+            // THEN interacting is false
+            Truth.assertThat(interacting).isFalse()
+        }
+
+    @Test
+    fun userInteracting_transitioning_toScene_programmatic() =
+        testComponent.runTest() {
+            // GIVEN an interacting flow based on transitions to and from a scene
+            val key = SceneKey.QuickSettings
+            val interactingFlow = underTest.sceneBasedInteracting(sceneInteractor, key)
+            val interacting by collectLastValue(interactingFlow)
+
+            // WHEN transition state is starting to move to the scene
+            val progress = MutableStateFlow(0f)
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Transition(
+                        fromScene = SceneKey.Lockscreen,
+                        toScene = key,
+                        progress = progress,
+                        isInitiatedByUserInput = false,
+                        isUserInputOngoing = flowOf(false),
+                    )
+                )
+            sceneInteractor.setTransitionState(transitionState)
+
+            // THEN interacting is false
+            Truth.assertThat(interacting).isFalse()
+
+            // WHEN transition state is partially to the scene
+            progress.value = .4f
+
+            // THEN interacting is false
+            Truth.assertThat(interacting).isFalse()
+
+            // WHEN transition completes
+            progress.value = 1f
+
+            // THEN interacting is false
+            Truth.assertThat(interacting).isFalse()
+        }
+
+    @Test
+    fun userInteracting_transitioning_toScene_userInputDriven() =
+        testComponent.runTest() {
+            // GIVEN an interacting flow based on transitions to and from a scene
+            val key = SceneKey.QuickSettings
+            val interactingFlow = underTest.sceneBasedInteracting(sceneInteractor, key)
+            val interacting by collectLastValue(interactingFlow)
+
+            // WHEN transition state is starting to move to the scene
+            val progress = MutableStateFlow(0f)
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Transition(
+                        fromScene = SceneKey.Lockscreen,
+                        toScene = key,
+                        progress = progress,
+                        isInitiatedByUserInput = true,
+                        isUserInputOngoing = flowOf(false),
+                    )
+                )
+            sceneInteractor.setTransitionState(transitionState)
+
+            // THEN interacting is true
+            Truth.assertThat(interacting).isTrue()
+
+            // WHEN transition state is partially to the scene
+            progress.value = .4f
+
+            // THEN interacting is true
+            Truth.assertThat(interacting).isTrue()
+
+            // WHEN transition completes
+            progress.value = 1f
+
+            // THEN interacting is true
+            Truth.assertThat(interacting).isTrue()
+        }
+
+    @Test
+    fun userInteracting_transitioning_fromScene_programmatic() =
+        testComponent.runTest() {
+            // GIVEN an interacting flow based on transitions to and from a scene
+            val key = SceneKey.QuickSettings
+            val interactingFlow = underTest.sceneBasedInteracting(sceneInteractor, key)
+            val interacting by collectLastValue(interactingFlow)
+
+            // WHEN transition state is starting to move to the scene
+            val progress = MutableStateFlow(0f)
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Transition(
+                        fromScene = key,
+                        toScene = SceneKey.Lockscreen,
+                        progress = progress,
+                        isInitiatedByUserInput = false,
+                        isUserInputOngoing = flowOf(false),
+                    )
+                )
+            sceneInteractor.setTransitionState(transitionState)
+
+            // THEN interacting is false
+            Truth.assertThat(interacting).isFalse()
+
+            // WHEN transition state is partially to the scene
+            progress.value = .4f
+
+            // THEN interacting is false
+            Truth.assertThat(interacting).isFalse()
+
+            // WHEN transition completes
+            progress.value = 1f
+
+            // THEN interacting is false
+            Truth.assertThat(interacting).isFalse()
+        }
+
+    @Test
+    fun userInteracting_transitioning_fromScene_userInputDriven() =
+        testComponent.runTest() {
+            // GIVEN an interacting flow based on transitions to and from a scene
+            val key = SceneKey.QuickSettings
+            val interactingFlow = underTest.sceneBasedInteracting(sceneInteractor, key)
+            val interacting by collectLastValue(interactingFlow)
+
+            // WHEN transition state is starting to move to the scene
+            val progress = MutableStateFlow(0f)
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Transition(
+                        fromScene = key,
+                        toScene = SceneKey.Lockscreen,
+                        progress = progress,
+                        isInitiatedByUserInput = true,
+                        isUserInputOngoing = flowOf(false),
+                    )
+                )
+            sceneInteractor.setTransitionState(transitionState)
+
+            // THEN interacting is true
+            Truth.assertThat(interacting).isTrue()
+
+            // WHEN transition state is partially to the scene
+            progress.value = .4f
+
+            // THEN interacting is true
+            Truth.assertThat(interacting).isTrue()
+
+            // WHEN transition completes
+            progress.value = 1f
+
+            // THEN interacting is true
+            Truth.assertThat(interacting).isTrue()
+        }
+
+    @Test
+    fun userInteracting_transitioning_toAndFromDifferentScenes() =
+        testComponent.runTest() {
+            // GIVEN an interacting flow based on transitions to and from a scene
+            val interactingFlow = underTest.sceneBasedInteracting(sceneInteractor, SceneKey.Shade)
+            val interacting by collectLastValue(interactingFlow)
+
+            // WHEN transition state is starting to between different scenes
+            val progress = MutableStateFlow(0f)
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Transition(
+                        fromScene = SceneKey.Lockscreen,
+                        toScene = SceneKey.QuickSettings,
+                        progress = MutableStateFlow(0f),
+                        isInitiatedByUserInput = true,
+                        isUserInputOngoing = flowOf(false),
+                    )
+                )
+            sceneInteractor.setTransitionState(transitionState)
+
+            // THEN interacting is false
+            Truth.assertThat(interacting).isFalse()
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/DragDownHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/DragDownHelperTest.kt
index d925d0a..ea7c068 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/DragDownHelperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/DragDownHelperTest.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.ExpandHelper
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingObserver
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.notification.row.ExpandableView
 import com.android.systemui.util.mockito.mock
@@ -48,16 +49,19 @@
     private val dragDownloadCallback: LockscreenShadeTransitionController = mock()
     private val expandableView: ExpandableView = mock()
     private val expandCallback: ExpandHelper.Callback = mock()
+    private val naturalScrollingSettingObserver: NaturalScrollingSettingObserver = mock()
 
     @Before
     fun setUp() {
         whenever(expandableView.collapsedHeight).thenReturn(collapsedHeight)
+        whenever(naturalScrollingSettingObserver.isNaturalScrollingEnabled).thenReturn(true)
 
         dragDownHelper = DragDownHelper(
                 falsingManager,
                 falsingCollector,
                 dragDownloadCallback,
-                mContext
+                naturalScrollingSettingObserver,
+                mContext,
         ).also {
             it.expandCallback = expandCallback
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
index 970a0f7..3efcf7b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
@@ -5,8 +5,8 @@
 import android.testing.TestableLooper
 import android.testing.TestableLooper.RunWithLooper
 import androidx.test.filters.SmallTest
-import com.android.SysUITestModule
-import com.android.TestMocksModule
+import com.android.systemui.SysUITestModule
+import com.android.systemui.TestMocksModule
 import com.android.systemui.ExpandHelper
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollectorFake
@@ -14,6 +14,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FakeFeatureFlagsClassicModule
 import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingObserver
 import com.android.systemui.media.controls.ui.MediaHierarchyManager
 import com.android.systemui.plugins.qs.QS
 import com.android.systemui.power.domain.interactor.PowerInteractor
@@ -99,6 +100,7 @@
     @Mock lateinit var stackscroller: NotificationStackScrollLayout
     @Mock lateinit var statusbarStateController: SysuiStatusBarStateController
     @Mock lateinit var transitionControllerCallback: LockscreenShadeTransitionController.Callback
+    @Mock lateinit var naturalScrollingSettingObserver: NaturalScrollingSettingObserver
 
     @JvmField @Rule val mockito = MockitoJUnit.rule()
 
@@ -123,6 +125,7 @@
         whenever(lockScreenUserManager.shouldShowLockscreenNotifications()).thenReturn(true)
         whenever(lockScreenUserManager.isLockscreenPublicMode(anyInt())).thenReturn(true)
         whenever(keyguardBypassController.bypassEnabled).thenReturn(false)
+        whenever(naturalScrollingSettingObserver.isNaturalScrollingEnabled).thenReturn(true)
 
         testComponent =
             DaggerLockscreenShadeTransitionControllerTest_TestComponent.factory()
@@ -185,6 +188,7 @@
                 shadeInteractor = testComponent.shadeInteractor,
                 powerInteractor = testComponent.powerInteractor,
                 splitShadeStateController = ResourcesSplitShadeStateController(),
+                naturalScrollingSettingObserver = naturalScrollingSettingObserver,
             )
 
         transitionController.addCallback(transitionControllerCallback)
@@ -607,9 +611,9 @@
         @Component.Factory
         interface Factory {
             fun create(
-                @BindsInstance test: SysuiTestCase,
-                featureFlags: FakeFeatureFlagsClassicModule,
-                mocks: TestMocksModule,
+                    @BindsInstance test: SysuiTestCase,
+                    featureFlags: FakeFeatureFlagsClassicModule,
+                    mocks: TestMocksModule,
             ): TestComponent
         }
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index 4b79a49..8fa7cd2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -44,6 +44,8 @@
 import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags
 import com.android.systemui.shade.data.repository.FakeShadeRepository
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl
+import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl
 import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository
 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository
@@ -153,23 +155,25 @@
             mock(),
             mock(),
             powerInteractor)
-        shadeInteractor = ShadeInteractor(
+        shadeInteractor = ShadeInteractorImpl(
             testScope.backgroundScope,
             FakeDeviceProvisioningRepository(),
             FakeDisableFlagsRepository(),
             mock(),
-            sceneContainerFlags,
-            utils::sceneInteractor,
             keyguardRepository,
             keyguardTransitionInteractor,
             powerInteractor,
             FakeUserSetupRepository(),
             mock(),
-            SharedNotificationContainerInteractor(
-                configurationRepository,
-                mContext,
-                ResourcesSplitShadeStateController()),
-            shadeRepository,
+            ShadeInteractorLegacyImpl(
+                testScope.backgroundScope,
+                keyguardRepository,
+                SharedNotificationContainerInteractor(
+                    configurationRepository,
+                    mContext,
+                    ResourcesSplitShadeStateController()),
+                shadeRepository,
+            )
         )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt
index d479937..f3094cd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt
@@ -17,13 +17,13 @@
 package com.android.systemui.statusbar.notification.data.repository
 
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.collectLastValue
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.collectLastValue
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.mockito.withArgCaptor
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractorTest.kt
index 707026e..b775079 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractorTest.kt
@@ -16,8 +16,8 @@
 
 import android.app.StatusBarManager
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt
index bb6f1b6..bb3113a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt
@@ -14,20 +14,17 @@
 package com.android.systemui.statusbar.notification.domain.interactor
 
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.collectLastValue
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.collectLastValue
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.statusbar.notification.data.repository.FakeNotificationsKeyguardViewStateRepository
 import com.google.common.truth.Truth.assertThat
 import dagger.BindsInstance
 import dagger.Component
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
 import org.junit.Test
 
 @SmallTest
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
index 05deb1c..0341035 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
@@ -17,14 +17,14 @@
 
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.TestMocksModule
-import com.android.collectLastValue
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
+import com.android.systemui.collectLastValue
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
+import com.android.systemui.runTest
 import com.android.systemui.statusbar.data.repository.NotificationListenerSettingsRepository
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt
index c2c33de..68761ef 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt
@@ -18,14 +18,12 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.TestMocksModule
-import com.android.collectLastValue
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
 import com.android.systemui.biometrics.domain.BiometricsDomainLayerModule
+import com.android.systemui.collectLastValue
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FakeFeatureFlagsClassicModule
 import com.android.systemui.flags.Flags
@@ -39,6 +37,8 @@
 import com.android.systemui.power.data.repository.FakePowerRepository
 import com.android.systemui.power.shared.model.WakeSleepReason
 import com.android.systemui.power.shared.model.WakefulnessState
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
 import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
index 87e9735..c2a1519 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
@@ -20,14 +20,12 @@
 import android.graphics.drawable.Icon
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.TestMocksModule
-import com.android.collectLastValue
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
 import com.android.systemui.biometrics.domain.BiometricsDomainLayerModule
+import com.android.systemui.collectLastValue
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FakeFeatureFlagsClassicModule
 import com.android.systemui.flags.Flags
@@ -42,6 +40,8 @@
 import com.android.systemui.power.data.repository.FakePowerRepository
 import com.android.systemui.power.shared.model.WakeSleepReason
 import com.android.systemui.power.shared.model.WakefulnessState
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.shade.data.repository.FakeShadeRepository
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
index 1c62161..3e331a6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
@@ -76,6 +76,7 @@
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.FakeEventLog;
 import com.android.systemui.util.settings.FakeGlobalSettings;
 import com.android.systemui.util.time.FakeSystemClock;
 
@@ -126,6 +127,7 @@
     DeviceProvisionedController mDeviceProvisionedController;
     FakeSystemClock mSystemClock;
     FakeGlobalSettings mGlobalSettings;
+    FakeEventLog mEventLog;
 
     private NotificationInterruptStateProviderImpl mNotifInterruptionStateProvider;
 
@@ -138,6 +140,7 @@
         mSystemClock = new FakeSystemClock();
         mGlobalSettings = new FakeGlobalSettings();
         mGlobalSettings.putInt(HEADS_UP_NOTIFICATIONS_ENABLED, HEADS_UP_ON);
+        mEventLog = new FakeEventLog();
 
         mNotifInterruptionStateProvider =
                 new NotificationInterruptStateProviderImpl(
@@ -155,7 +158,8 @@
                         mUserTracker,
                         mDeviceProvisionedController,
                         mSystemClock,
-                        mGlobalSettings);
+                        mGlobalSettings,
+                        mEventLog);
         mNotifInterruptionStateProvider.mUseHeadsUp = true;
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt
index e1581ea..acc5cea 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt
@@ -53,6 +53,7 @@
                 deviceProvisionedController,
                 systemClock,
                 globalSettings,
+                eventLog
             )
         )
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
index 1064475..9e7df5f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
@@ -33,6 +33,7 @@
             ambientDisplayConfiguration,
             batteryController,
             deviceProvisionedController,
+            eventLog,
             globalSettings,
             headsUpManager,
             keyguardNotificationVisibilityProvider,
@@ -42,6 +43,7 @@
             powerManager,
             statusBarStateController,
             systemClock,
+            uiEventLogger,
             userTracker,
         )
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
index 5e81156..5dcb6c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
@@ -47,6 +47,7 @@
 import android.provider.Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED
 import android.provider.Settings.Global.HEADS_UP_OFF
 import android.provider.Settings.Global.HEADS_UP_ON
+import com.android.internal.logging.UiEventLogger.UiEventEnum
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.log.LogBuffer
@@ -63,8 +64,13 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.MAX_HUN_WHEN_AGE_MS
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_BUBBLE_METADATA
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.HUN_SUPPRESSED_OLD_WHEN
 import com.android.systemui.statusbar.policy.FakeDeviceProvisionedController
 import com.android.systemui.statusbar.policy.HeadsUpManager
+import com.android.systemui.util.FakeEventLog
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.settings.FakeGlobalSettings
@@ -100,6 +106,7 @@
     protected val ambientDisplayConfiguration = FakeAmbientDisplayConfiguration(context)
     protected val batteryController = FakeBatteryController(leakCheck)
     protected val deviceProvisionedController = FakeDeviceProvisionedController()
+    protected val eventLog = FakeEventLog()
     protected val flags: NotifPipelineFlags = mock()
     protected val globalSettings =
         FakeGlobalSettings().also { it.putInt(HEADS_UP_NOTIFICATIONS_ENABLED, HEADS_UP_ON) }
@@ -147,18 +154,21 @@
     fun testShouldPeek() {
         ensurePeekState()
         assertShouldHeadsUp(buildPeekEntry())
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldNotPeek_settingDisabled() {
         ensurePeekState { hunSettingEnabled = false }
         assertShouldNotHeadsUp(buildPeekEntry())
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldNotPeek_packageSnoozed_withoutFsi() {
         ensurePeekState { hunSnoozed = true }
         assertShouldNotHeadsUp(buildPeekEntry())
+        assertNoEventsLogged()
     }
 
     @Test
@@ -167,6 +177,13 @@
         forEachPeekableFsiState {
             ensurePeekState { hunSnoozed = true }
             assertShouldHeadsUp(entry)
+
+            // The old code logs a UiEvent when a HUN snooze is bypassed because the notification
+            // has an FSI, but that doesn't fit into the new code's suppressor-based logic, so we're
+            // not reimplementing it.
+            if (provider !is NotificationInterruptStateProviderWrapper) {
+                assertNoEventsLogged()
+            }
         }
     }
 
@@ -174,42 +191,49 @@
     fun testShouldNotPeek_alreadyBubbled() {
         ensurePeekState { statusBarState = SHADE }
         assertShouldNotHeadsUp(buildPeekEntry { isBubble = true })
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldPeek_isBubble_shadeLocked() {
         ensurePeekState { statusBarState = SHADE_LOCKED }
         assertShouldHeadsUp(buildPeekEntry { isBubble = true })
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldPeek_isBubble_keyguard() {
         ensurePeekState { statusBarState = KEYGUARD }
         assertShouldHeadsUp(buildPeekEntry { isBubble = true })
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldNotPeek_dnd() {
         ensurePeekState()
         assertShouldNotHeadsUp(buildPeekEntry { suppressedVisualEffects = SUPPRESSED_EFFECT_PEEK })
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldNotPeek_notImportant() {
         ensurePeekState()
         assertShouldNotHeadsUp(buildPeekEntry { importance = IMPORTANCE_DEFAULT })
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldNotPeek_screenOff() {
         ensurePeekState { isScreenOn = false }
         assertShouldNotHeadsUp(buildPeekEntry())
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldNotPeek_dreaming() {
         ensurePeekState { isDreaming = true }
         assertShouldNotHeadsUp(buildPeekEntry())
+        assertNoEventsLogged()
     }
 
     @Test
@@ -219,33 +243,56 @@
     }
 
     @Test
+    fun testLogsHunOldWhen() {
+        assertNoEventsLogged()
+
+        ensurePeekState()
+        val entry = buildPeekEntry { whenMs = whenAgo(MAX_HUN_WHEN_AGE_MS) }
+
+        // The old code logs the "old when" UiEvent unconditionally, so don't expect that it hasn't.
+        if (provider !is NotificationInterruptStateProviderWrapper) {
+            provider.makeUnloggedHeadsUpDecision(entry)
+            assertNoEventsLogged()
+        }
+
+        provider.makeAndLogHeadsUpDecision(entry)
+        assertUiEventLogged(HUN_SUPPRESSED_OLD_WHEN, entry.sbn.uid, entry.sbn.packageName)
+        assertNoSystemEventLogged()
+    }
+
+    @Test
     fun testShouldPeek_oldWhen_now() {
         ensurePeekState()
         assertShouldHeadsUp(buildPeekEntry { whenMs = whenAgo(0) })
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldPeek_oldWhen_notOldEnough() {
         ensurePeekState()
         assertShouldHeadsUp(buildPeekEntry { whenMs = whenAgo(MAX_HUN_WHEN_AGE_MS - 1) })
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldPeek_oldWhen_zeroWhen() {
         ensurePeekState()
         assertShouldHeadsUp(buildPeekEntry { whenMs = 0L })
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldPeek_oldWhen_negativeWhen() {
         ensurePeekState()
         assertShouldHeadsUp(buildPeekEntry { whenMs = -1L })
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldPeek_oldWhen_fullScreenIntent() {
         ensurePeekState()
         assertShouldHeadsUp(buildFsiEntry { whenMs = whenAgo(MAX_HUN_WHEN_AGE_MS) })
+        assertNoEventsLogged()
     }
 
     @Test
@@ -257,6 +304,7 @@
                 isForegroundService = true
             }
         )
+        assertNoEventsLogged()
     }
 
     @Test
@@ -268,18 +316,21 @@
                 isUserInitiatedJob = true
             }
         )
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldNotPeek_hiddenOnKeyguard() {
         ensurePeekState({ keyguardShouldHideNotification = true })
         assertShouldNotHeadsUp(buildPeekEntry())
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldPeek_defaultLegacySuppressor() {
         ensurePeekState()
         withLegacySuppressor(neverSuppresses) { assertShouldHeadsUp(buildPeekEntry()) }
+        assertNoEventsLogged()
     }
 
     @Test
@@ -288,6 +339,7 @@
         withLegacySuppressor(alwaysSuppressesInterruptions) {
             assertShouldNotHeadsUp(buildPeekEntry())
         }
+        assertNoEventsLogged()
     }
 
     @Test
@@ -296,6 +348,7 @@
         withLegacySuppressor(alwaysSuppressesAwakeInterruptions) {
             assertShouldNotHeadsUp(buildPeekEntry())
         }
+        assertNoEventsLogged()
     }
 
     @Test
@@ -304,24 +357,28 @@
         withLegacySuppressor(alwaysSuppressesAwakeHeadsUp) {
             assertShouldNotHeadsUp(buildPeekEntry())
         }
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldPulse() {
         ensurePulseState()
         assertShouldHeadsUp(buildPulseEntry())
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldNotPulse_disabled() {
         ensurePulseState { pulseOnNotificationsEnabled = false }
         assertShouldNotHeadsUp(buildPulseEntry())
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldNotPulse_batterySaver() {
         ensurePulseState { isAodPowerSave = true }
         assertShouldNotHeadsUp(buildPulseEntry())
+        assertNoEventsLogged()
     }
 
     @Test
@@ -330,30 +387,35 @@
         assertShouldNotHeadsUp(
             buildPulseEntry { suppressedVisualEffects = SUPPRESSED_EFFECT_AMBIENT }
         )
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldNotPulse_visibilityOverridePrivate() {
         ensurePulseState()
         assertShouldNotHeadsUp(buildPulseEntry { visibilityOverride = VISIBILITY_PRIVATE })
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldNotPulse_importanceLow() {
         ensurePulseState()
         assertShouldNotHeadsUp(buildPulseEntry { importance = IMPORTANCE_LOW })
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldNotPulse_hiddenOnKeyguard() {
         ensurePulseState({ keyguardShouldHideNotification = true })
         assertShouldNotHeadsUp(buildPulseEntry())
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldPulse_defaultLegacySuppressor() {
         ensurePulseState()
         withLegacySuppressor(neverSuppresses) { assertShouldHeadsUp(buildPulseEntry()) }
+        assertNoEventsLogged()
     }
 
     @Test
@@ -362,6 +424,7 @@
         withLegacySuppressor(alwaysSuppressesInterruptions) {
             assertShouldNotHeadsUp(buildPulseEntry())
         }
+        assertNoEventsLogged()
     }
 
     @Test
@@ -370,6 +433,7 @@
         withLegacySuppressor(alwaysSuppressesAwakeInterruptions) {
             assertShouldHeadsUp(buildPulseEntry())
         }
+        assertNoEventsLogged()
     }
 
     @Test
@@ -378,6 +442,7 @@
         withLegacySuppressor(alwaysSuppressesAwakeHeadsUp) {
             assertShouldHeadsUp(buildPulseEntry())
         }
+        assertNoEventsLogged()
     }
 
     private fun withPeekAndPulseEntry(
@@ -399,6 +464,7 @@
             groupAlertBehavior = GROUP_ALERT_SUMMARY
         }) {
             assertShouldNotHeadsUp(it)
+            assertNoEventsLogged()
         }
     }
 
@@ -410,6 +476,7 @@
             groupAlertBehavior = GROUP_ALERT_CHILDREN
         }) {
             assertShouldHeadsUp(it)
+            assertNoEventsLogged()
         }
     }
 
@@ -421,24 +488,30 @@
             groupAlertBehavior = GROUP_ALERT_SUMMARY
         }) {
             assertShouldHeadsUp(it)
+            assertNoEventsLogged()
         }
     }
 
     @Test
     fun testShouldNotHeadsUp_justLaunchedFsi() {
-        withPeekAndPulseEntry({ hasJustLaunchedFsi = true }) { assertShouldNotHeadsUp(it) }
+        withPeekAndPulseEntry({ hasJustLaunchedFsi = true }) {
+            assertShouldNotHeadsUp(it)
+            assertNoEventsLogged()
+        }
     }
 
     @Test
     fun testShouldBubble_withIntentAndIcon() {
         ensureBubbleState()
         assertShouldBubble(buildBubbleEntry { bubbleIsShortcut = false })
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldBubble_withShortcut() {
         ensureBubbleState()
         assertShouldBubble(buildBubbleEntry { bubbleIsShortcut = true })
+        assertNoEventsLogged()
     }
 
     @Test
@@ -451,6 +524,7 @@
                 groupAlertBehavior = GROUP_ALERT_SUMMARY
             }
         )
+        assertNoEventsLogged()
     }
 
     @Test
@@ -462,24 +536,28 @@
                 hasBubbleMetadata = false
             }
         )
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldNotBubble_missingBubbleMetadata() {
         ensureBubbleState()
         assertShouldNotBubble(buildBubbleEntry { hasBubbleMetadata = false })
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldNotBubble_notAllowedToBubble() {
         ensureBubbleState()
         assertShouldNotBubble(buildBubbleEntry { canBubble = false })
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldBubble_defaultLegacySuppressor() {
         ensureBubbleState()
         withLegacySuppressor(neverSuppresses) { assertShouldBubble(buildBubbleEntry()) }
+        assertNoEventsLogged()
     }
 
     @Test
@@ -488,6 +566,7 @@
         withLegacySuppressor(alwaysSuppressesInterruptions) {
             assertShouldNotBubble(buildBubbleEntry())
         }
+        assertNoEventsLogged()
     }
 
     @Test
@@ -496,6 +575,7 @@
         withLegacySuppressor(alwaysSuppressesAwakeInterruptions) {
             assertShouldNotBubble(buildBubbleEntry())
         }
+        assertNoEventsLogged()
     }
 
     @Test
@@ -504,17 +584,22 @@
         withLegacySuppressor(alwaysSuppressesAwakeHeadsUp) {
             assertShouldBubble(buildBubbleEntry())
         }
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldNotBubble_hiddenOnKeyguard() {
         ensureBubbleState({ keyguardShouldHideNotification = true })
         assertShouldNotBubble(buildBubbleEntry())
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldNotFsi_noFullScreenIntent() {
-        forEachFsiState { assertShouldNotFsi(buildFsiEntry { hasFsi = false }) }
+        forEachFsiState {
+            assertShouldNotFsi(buildFsiEntry { hasFsi = false })
+            assertNoEventsLogged()
+        }
     }
 
     @Test
@@ -526,6 +611,7 @@
                     isStickyAndNotDemoted = true
                 }
             )
+            assertNoEventsLogged()
         }
     }
 
@@ -536,12 +622,16 @@
                 buildFsiEntry { suppressedVisualEffects = SUPPRESSED_EFFECT_FULL_SCREEN_INTENT },
                 expectWouldInterruptWithoutDnd = true
             )
+            assertNoEventsLogged()
         }
     }
 
     @Test
     fun testShouldNotFsi_notImportantEnough() {
-        forEachFsiState { assertShouldNotFsi(buildFsiEntry { importance = IMPORTANCE_DEFAULT }) }
+        forEachFsiState {
+            assertShouldNotFsi(buildFsiEntry { importance = IMPORTANCE_DEFAULT })
+            assertNoEventsLogged()
+        }
     }
 
     @Test
@@ -554,6 +644,7 @@
                 },
                 expectWouldInterruptWithoutDnd = false
             )
+            assertNoEventsLogged()
         }
     }
 
@@ -571,6 +662,27 @@
     }
 
     @Test
+    fun testLogsFsiSuppressiveGroupAlertBehavior() {
+        ensureNotInteractiveFsiState()
+        val entry = buildFsiEntry {
+            isGrouped = true
+            isGroupSummary = true
+            groupAlertBehavior = GROUP_ALERT_CHILDREN
+        }
+
+        val decision = provider.makeUnloggedFullScreenIntentDecision(entry)
+        assertNoEventsLogged()
+
+        provider.logFullScreenIntentDecision(decision)
+        assertUiEventLogged(
+            FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR,
+            entry.sbn.uid,
+            entry.sbn.packageName
+        )
+        assertSystemEventLogged("231322873", entry.sbn.uid, "groupAlertBehavior")
+    }
+
+    @Test
     fun testShouldFsi_suppressiveGroupAlertBehavior_notGrouped() {
         forEachFsiState {
             assertShouldFsi(
@@ -580,6 +692,7 @@
                     groupAlertBehavior = GROUP_ALERT_CHILDREN
                 }
             )
+            assertNoEventsLogged()
         }
     }
 
@@ -609,26 +722,52 @@
     }
 
     @Test
+    fun testLogsFsiSuppressiveBubbleMetadata() {
+        ensureNotInteractiveFsiState()
+        val entry = buildFsiEntry {
+            hasBubbleMetadata = true
+            bubbleSuppressesNotification = true
+        }
+
+        val decision = provider.makeUnloggedFullScreenIntentDecision(entry)
+        assertNoEventsLogged()
+
+        provider.logFullScreenIntentDecision(decision)
+        assertUiEventLogged(
+            FSI_SUPPRESSED_SUPPRESSIVE_BUBBLE_METADATA,
+            entry.sbn.uid,
+            entry.sbn.packageName
+        )
+        assertSystemEventLogged("274759612", entry.sbn.uid, "bubbleMetadata")
+    }
+
+    @Test
     fun testShouldNotFsi_packageSuspended() {
-        forEachFsiState { assertShouldNotFsi(buildFsiEntry { packageSuspended = true }) }
+        forEachFsiState {
+            assertShouldNotFsi(buildFsiEntry { packageSuspended = true })
+            assertNoEventsLogged()
+        }
     }
 
     @Test
     fun testShouldFsi_notInteractive() {
         ensureNotInteractiveFsiState()
         assertShouldFsi(buildFsiEntry())
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldFsi_dreaming() {
         ensureDreamingFsiState()
         assertShouldFsi(buildFsiEntry())
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldFsi_keyguard() {
         ensureKeyguardFsiState()
         assertShouldFsi(buildFsiEntry())
+        assertNoEventsLogged()
     }
 
     @Test
@@ -636,6 +775,7 @@
         forEachPeekableFsiState {
             ensurePeekState()
             assertShouldNotFsi(buildFsiEntry())
+            assertNoEventsLogged()
         }
     }
 
@@ -644,6 +784,7 @@
         forEachPeekableFsiState {
             ensurePeekState { hunSnoozed = true }
             assertShouldNotFsi(buildFsiEntry())
+            assertNoEventsLogged()
         }
     }
 
@@ -651,18 +792,21 @@
     fun testShouldFsi_lockedShade() {
         ensureLockedShadeFsiState()
         assertShouldFsi(buildFsiEntry())
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldFsi_keyguardOccluded() {
         ensureKeyguardOccludedFsiState()
         assertShouldFsi(buildFsiEntry())
+        assertNoEventsLogged()
     }
 
     @Test
     fun testShouldFsi_deviceNotProvisioned() {
         ensureDeviceNotProvisionedFsiState()
         assertShouldFsi(buildFsiEntry())
+        assertNoEventsLogged()
     }
 
     @Test
@@ -672,9 +816,23 @@
     }
 
     @Test
+    fun testLogsFsiNoHunOrKeyguard() {
+        ensureNoHunOrKeyguardFsiState()
+        val entry = buildFsiEntry()
+
+        val decision = provider.makeUnloggedFullScreenIntentDecision(entry)
+        assertNoEventsLogged()
+
+        provider.logFullScreenIntentDecision(decision)
+        assertUiEventLogged(FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD, entry.sbn.uid, entry.sbn.packageName)
+        assertSystemEventLogged("231322873", entry.sbn.uid, "no hun or keyguard")
+    }
+
+    @Test
     fun testShouldFsi_defaultLegacySuppressor() {
         forEachFsiState {
             withLegacySuppressor(neverSuppresses) { assertShouldFsi(buildFsiEntry()) }
+            assertNoEventsLogged()
         }
     }
 
@@ -682,6 +840,7 @@
     fun testShouldFsi_suppressInterruptions() {
         forEachFsiState {
             withLegacySuppressor(alwaysSuppressesInterruptions) { assertShouldFsi(buildFsiEntry()) }
+            assertNoEventsLogged()
         }
     }
 
@@ -691,6 +850,7 @@
             withLegacySuppressor(alwaysSuppressesAwakeInterruptions) {
                 assertShouldFsi(buildFsiEntry())
             }
+            assertNoEventsLogged()
         }
     }
 
@@ -698,6 +858,7 @@
     fun testShouldFsi_suppressAwakeHeadsUp() {
         forEachFsiState {
             withLegacySuppressor(alwaysSuppressesAwakeHeadsUp) { assertShouldFsi(buildFsiEntry()) }
+            assertNoEventsLogged()
         }
     }
 
@@ -1080,6 +1241,45 @@
         run(block)
     }
 
+    private fun assertNoEventsLogged() {
+        assertNoUiEventLogged()
+        assertNoSystemEventLogged()
+    }
+
+    private fun assertNoUiEventLogged() {
+        assertEquals(0, uiEventLogger.numLogs())
+    }
+
+    private fun assertUiEventLogged(uiEventId: UiEventEnum, uid: Int, packageName: String) {
+        assertEquals(1, uiEventLogger.numLogs())
+
+        val event = uiEventLogger.get(0)
+        assertEquals(uiEventId.id, event.eventId)
+        assertEquals(uid, event.uid)
+        assertEquals(packageName, event.packageName)
+    }
+
+    private fun assertNoSystemEventLogged() {
+        assertEquals(0, eventLog.events.size)
+    }
+
+    private fun assertSystemEventLogged(number: String, uid: Int, description: String) {
+        assertEquals(1, eventLog.events.size)
+
+        val event = eventLog.events[0]
+        assertEquals(0x534e4554, event.tag)
+
+        val value = event.value
+        assertTrue(value is Array<*>)
+
+        if (value is Array<*>) {
+            assertEquals(3, value.size)
+            assertEquals(number, value[0])
+            assertEquals(uid, value[1])
+            assertEquals(description, value[2])
+        }
+    }
+
     private fun whenAgo(whenAgeMs: Long) = systemClock.currentTimeMillis() - whenAgeMs
 }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
index 7423c2d..917569c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
@@ -19,16 +19,16 @@
 import android.os.PowerManager
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.TestMocksModule
-import com.android.collectLastValue
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
+import com.android.systemui.collectLastValue
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.power.data.repository.FakePowerRepository
+import com.android.systemui.runTest
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModelModule
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index db8f217..9c70c82 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -19,13 +19,11 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.TestMocksModule
-import com.android.collectLastValue
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
+import com.android.systemui.collectLastValue
 import com.android.systemui.common.shared.model.SharedNotificationContainerPosition
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
 import com.android.systemui.dagger.SysUISingleton
@@ -39,6 +37,8 @@
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.res.R
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.shade.data.repository.FakeShadeRepository
 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
 import com.android.systemui.user.domain.UserDomainLayerModule
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java
index 1d8a346..84cd518 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.phone;
 
+import static com.android.systemui.Flags.FLAG_QS_NEW_PIPELINE;
 import static com.android.systemui.qs.dagger.QSFlagsModule.RBC_AVAILABLE;
 import static com.android.systemui.statusbar.phone.AutoTileManager.DEVICE_CONTROLS;
 
@@ -135,6 +136,8 @@
         MockitoAnnotations.initMocks(this);
         mSecureSettings = new FakeSettings();
 
+        mSetFlagsRule.disableFlags(FLAG_QS_NEW_PIPELINE);
+
         mContext.getOrCreateTestableResources().addOverride(
                 R.array.config_quickSettingsAutoAdd,
                 new String[] {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
index 164325a..e61b4f8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
@@ -16,7 +16,6 @@
 
 package com.android.systemui.statusbar.phone;
 
-import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION;
 import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -50,7 +49,6 @@
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.keyguard.domain.interactor.BiometricUnlockInteractor;
@@ -130,14 +128,11 @@
     @Mock
     private BiometricUnlockInteractor mBiometricUnlockInteractor;
     private final FakeSystemClock mSystemClock = new FakeSystemClock();
-    private FakeFeatureFlags mFeatureFlags;
     private BiometricUnlockController mBiometricUnlockController;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mFeatureFlags = new FakeFeatureFlags();
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false);
         when(mKeyguardStateController.isShowing()).thenReturn(true);
         when(mUpdateMonitor.isDeviceInteractive()).thenReturn(true);
         when(mKeyguardStateController.isFaceEnrolled()).thenReturn(true);
@@ -165,7 +160,6 @@
                 mAuthController, mStatusBarStateController,
                 mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper,
                 mSystemClock,
-                mFeatureFlags,
                 mDeviceEntryHapticsInteractor,
                 () -> mSelectedUserInteractor,
                 mBiometricUnlockInteractor
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
index e7dad6a..912c27d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
@@ -18,8 +18,6 @@
 
 import static android.view.Display.DEFAULT_DISPLAY;
 
-import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION;
-
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -30,7 +28,6 @@
 import android.app.StatusBarManager;
 import android.os.PowerManager;
 import android.os.UserHandle;
-import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.testing.AndroidTestingRunner;
 import android.view.HapticFeedbackConstants;
@@ -42,7 +39,6 @@
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.assist.AssistManager;
-import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.qs.QSHost;
@@ -53,7 +49,6 @@
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shade.ShadeViewController;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
@@ -94,14 +89,12 @@
     @Mock private DozeServiceHost mDozeServiceHost;
     @Mock private NotificationStackScrollLayoutController mNotificationStackScrollLayoutController;
     @Mock private PowerManager mPowerManager;
-    @Mock private VibratorHelper mVibratorHelper;
     @Mock private Vibrator mVibrator;
     @Mock private StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
     @Mock private Lazy<CameraLauncher> mCameraLauncherLazy;
     @Mock private UserTracker mUserTracker;
     @Mock private QSHost mQSHost;
     @Mock private ActivityStarter mActivityStarter;
-    private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
 
     CentralSurfacesCommandQueueCallbacks mSbcqCallbacks;
 
@@ -131,15 +124,13 @@
                 mNotificationStackScrollLayoutController,
                 mStatusBarHideIconsForBouncerManager,
                 mPowerManager,
-                mVibratorHelper,
                 Optional.of(mVibrator),
                 new DisableFlagsLogger(),
                 DEFAULT_DISPLAY,
                 mCameraLauncherLazy,
                 mUserTracker,
                 mQSHost,
-                mActivityStarter,
-                mFeatureFlags);
+                mActivityStarter);
 
         when(mUserTracker.getUserHandle()).thenReturn(
                 UserHandle.of(ActivityManager.getCurrentUser()));
@@ -192,18 +183,7 @@
     }
 
     @Test
-    public void vibrateOnNavigationKeyDown_oneWayHapticsDisabled_usesVibrate() {
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false);
-
-        mSbcqCallbacks.vibrateOnNavigationKeyDown();
-
-        verify(mVibratorHelper).vibrate(VibrationEffect.EFFECT_TICK);
-    }
-
-    @Test
-    public void vibrateOnNavigationKeyDown_oneWayHapticsEnabled_usesPerformHapticFeedback() {
-        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
-
+    public void vibrateOnNavigationKeyDown_usesPerformHapticFeedback() {
         mSbcqCallbacks.vibrateOnNavigationKeyDown();
 
         verify(mShadeViewController).performHapticFeedback(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 027c11c..6570724 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -178,6 +178,8 @@
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 import com.android.systemui.statusbar.window.StatusBarWindowController;
 import com.android.systemui.statusbar.window.StatusBarWindowStateController;
+import com.android.systemui.util.EventLog;
+import com.android.systemui.util.FakeEventLog;
 import com.android.systemui.util.WallpaperController;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.concurrency.MessageRouterImpl;
@@ -324,6 +326,7 @@
     private ShadeController mShadeController;
     private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
     private final FakeGlobalSettings mFakeGlobalSettings = new FakeGlobalSettings();
+    private final FakeEventLog mFakeEventLog = new FakeEventLog();
     private final FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
     private final FakeExecutor mUiBgExecutor = new FakeExecutor(mFakeSystemClock);
     private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
@@ -349,8 +352,8 @@
         mFeatureFlags.set(Flags.LIGHT_REVEAL_MIGRATION, true);
         // Turn AOD on and toggle feature flag for jank fixes
         mFeatureFlags.set(Flags.ZJ_285570694_LOCKSCREEN_TRANSITION_FROM_AOD, true);
-        mFeatureFlags.set(Flags.ALTERNATE_BOUNCER_VIEW, false);
         when(mDozeParameters.getAlwaysOn()).thenReturn(true);
+        mSetFlagsRule.disableFlags(com.android.systemui.Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR);
 
         IThermalService thermalService = mock(IThermalService.class);
         mPowerManager = new PowerManager(mContext, mPowerManagerService, thermalService,
@@ -374,7 +377,8 @@
                         mUserTracker,
                         mDeviceProvisionedController,
                         mFakeSystemClock,
-                        mFakeGlobalSettings);
+                        mFakeGlobalSettings,
+                        mFakeEventLog);
 
         mContext.addMockSystemService(TrustManager.class, mock(TrustManager.class));
         mContext.addMockSystemService(FingerprintManager.class, mock(FingerprintManager.class));
@@ -1187,7 +1191,8 @@
                 UserTracker userTracker,
                 DeviceProvisionedController deviceProvisionedController,
                 SystemClock systemClock,
-                GlobalSettings globalSettings) {
+                GlobalSettings globalSettings,
+                EventLog eventLog) {
             super(
                     powerManager,
                     ambientDisplayConfiguration,
@@ -1203,7 +1208,8 @@
                     userTracker,
                     deviceProvisionedController,
                     systemClock,
-                    globalSettings
+                    globalSettings,
+                    eventLog
             );
             mUseHeadsUp = true;
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 46b3996..225ddb6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -178,7 +178,7 @@
         mFeatureFlags.set(Flags.WM_ENABLE_PREDICTIVE_BACK_BOUNCER_ANIM, true);
         mFeatureFlags.set(Flags.REFACTOR_KEYGUARD_DISMISS_INTENT, false);
         mFeatureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false);
-        mFeatureFlags.set(Flags.ALTERNATE_BOUNCER_VIEW, false);
+        mSetFlagsRule.disableFlags(com.android.systemui.Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR);
 
         when(mNotificationShadeWindowController.getWindowRootView())
                 .thenReturn(mNotificationShadeWindowView);
@@ -771,7 +771,7 @@
         mStatusBarKeyguardViewManager.addCallback(mCallback);
 
         // GIVEN alternate bouncer view flag enabled & the alternate bouncer is visible
-        mFeatureFlags.set(Flags.ALTERNATE_BOUNCER_VIEW, true);
+        mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR);
         when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true);
 
         // THEN the touch is not acted upon
@@ -781,7 +781,7 @@
     @Test
     public void onInterceptTouch_alternateBouncerViewFlagEnabled() {
         // GIVEN alternate bouncer view flag enabled & the alternate bouncer is visible
-        mFeatureFlags.set(Flags.ALTERNATE_BOUNCER_VIEW, true);
+        mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR);
         when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true);
 
         // THEN the touch is not intercepted
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeSubscriptionManagerProxy.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeSubscriptionManagerProxy.kt
index 3dc7de6..a802381 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeSubscriptionManagerProxy.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeSubscriptionManagerProxy.kt
@@ -16,12 +16,28 @@
 
 package com.android.systemui.statusbar.pipeline.mobile.util
 
+import android.telephony.SubscriptionInfo
 import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
 
 /** Fake of [SubscriptionManagerProxy] for easy testing */
 class FakeSubscriptionManagerProxy(
     /** Set the default data subId to be returned in [getDefaultDataSubscriptionId] */
-    var defaultDataSubId: Int = INVALID_SUBSCRIPTION_ID
+    var defaultDataSubId: Int = INVALID_SUBSCRIPTION_ID,
+    var activeSubscriptionInfo: SubscriptionInfo? = null
 ) : SubscriptionManagerProxy {
     override fun getDefaultDataSubscriptionId(): Int = defaultDataSubId
+
+    override fun isValidSubscriptionId(subId: Int): Boolean {
+        return subId > -1
+    }
+
+    override suspend fun getActiveSubscriptionInfo(subId: Int): SubscriptionInfo? {
+        return activeSubscriptionInfo
+    }
+
+    /** Sets the active subscription info. */
+    fun setActiveSubscriptionInfo(subId: Int, isEmbedded: Boolean = false) {
+        activeSubscriptionInfo =
+            SubscriptionInfo.Builder().setId(subId).setEmbedded(isEmbedded).build()
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
index 78e7971..99e62ee 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
@@ -19,13 +19,13 @@
 import android.app.NotificationManager.Policy
 import android.provider.Settings
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.collectLastValue
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.collectLastValue
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.statusbar.policy.data.repository.FakeZenModeRepository
 import com.android.systemui.user.domain.UserDomainLayerModule
 import com.google.common.truth.Truth.assertThat
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 102c3fc..aa5f987 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -126,6 +126,8 @@
 import com.android.systemui.shade.ShadeWindowLogger;
 import com.android.systemui.shade.data.repository.FakeShadeRepository;
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
+import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl;
+import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.NotificationEntryHelper;
@@ -160,6 +162,7 @@
 import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository;
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
 import com.android.systemui.user.domain.interactor.UserSwitcherInteractor;
+import com.android.systemui.util.FakeEventLog;
 import com.android.systemui.util.settings.FakeGlobalSettings;
 import com.android.systemui.util.time.SystemClock;
 import com.android.wm.shell.ShellTaskOrganizer;
@@ -454,23 +457,24 @@
                 new ResourcesSplitShadeStateController();
 
         mShadeInteractor =
-                new ShadeInteractor(
+                new ShadeInteractorImpl(
                         mTestScope.getBackgroundScope(),
                         deviceProvisioningRepository,
                         new FakeDisableFlagsRepository(),
                         mDozeParameters,
-                        sceneContainerFlags,
-                        () -> sceneInteractor,
                         keyguardRepository,
                         keyguardTransitionInteractor,
                         powerInteractor,
                         new FakeUserSetupRepository(),
                         mock(UserSwitcherInteractor.class),
-                        new SharedNotificationContainerInteractor(
-                                configurationRepository,
-                                mContext,
-                                splitShadeStateController),
-                        new FakeShadeRepository()
+                        new ShadeInteractorLegacyImpl(
+                                mTestScope.getBackgroundScope(), keyguardRepository,
+                                new SharedNotificationContainerInteractor(
+                                        configurationRepository,
+                                        mContext,
+                                        splitShadeStateController),
+                                shadeRepository
+                        )
                 );
 
         mNotificationShadeWindowController = new NotificationShadeWindowControllerImpl(
@@ -540,7 +544,8 @@
                         mock(UserTracker.class),
                         mock(DeviceProvisionedController.class),
                         mock(SystemClock.class),
-                        fakeGlobalSettings
+                        fakeGlobalSettings,
+                        new FakeEventLog()
                 );
 
         mShellTaskOrganizer = new ShellTaskOrganizer(mock(ShellInit.class),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
index 975555c..c9964c2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
@@ -31,6 +31,7 @@
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.EventLog;
 import com.android.systemui.util.settings.GlobalSettings;
 import com.android.systemui.util.time.SystemClock;
 
@@ -52,7 +53,8 @@
             UserTracker userTracker,
             DeviceProvisionedController deviceProvisionedController,
             SystemClock systemClock,
-            GlobalSettings globalSettings) {
+            GlobalSettings globalSettings,
+            EventLog eventLog) {
         super(
                 powerManager,
                 ambientDisplayConfiguration,
@@ -68,7 +70,8 @@
                 userTracker,
                 deviceProvisionedController,
                 systemClock,
-                globalSettings);
+                globalSettings,
+                eventLog);
         mUseHeadsUp = true;
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/CoroutineTestScopeModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/CoroutineTestScopeModule.kt
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/CoroutineTestScopeModule.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/CoroutineTestScopeModule.kt
index 360aa0f..de310b4 100644
--- a/packages/SystemUI/tests/src/com/android/CoroutineTestScopeModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/CoroutineTestScopeModule.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android
+package com.android.systemui
 
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
diff --git a/packages/SystemUI/tests/src/com/android/SysUITestModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt
similarity index 77%
rename from packages/SystemUI/tests/src/com/android/SysUITestModule.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt
index 97e43ad..d0c1267 100644
--- a/packages/SystemUI/tests/src/com/android/SysUITestModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt
@@ -13,24 +13,29 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android
+package com.android.systemui
 
 import android.content.Context
 import android.content.res.Resources
 import android.testing.TestableContext
 import android.testing.TestableResources
-import com.android.systemui.FakeSystemUiModule
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.SysuiTestableContext
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.broadcast.FakeBroadcastDispatcher
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.scene.shared.flag.SceneContainerFlags
+import com.android.systemui.shade.domain.interactor.BaseShadeInteractor
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl
+import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl
+import com.android.systemui.shade.domain.interactor.ShadeInteractorSceneContainerImpl
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
+import javax.inject.Provider
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.EmptyCoroutineContext
 import kotlinx.coroutines.CoroutineStart
@@ -56,6 +61,7 @@
     @Binds @Application fun bindAppResources(resources: Resources): Resources
     @Binds @Main fun bindMainResources(resources: Resources): Resources
     @Binds fun bindBroadcastDispatcher(fake: FakeBroadcastDispatcher): BroadcastDispatcher
+    @Binds @SysUISingleton fun bindsShadeInteractor(sii: ShadeInteractorImpl): ShadeInteractor
 
     companion object {
         @Provides
@@ -72,6 +78,19 @@
         @Provides
         fun provideFakeBroadcastDispatcher(test: SysuiTestCase): FakeBroadcastDispatcher =
             test.fakeBroadcastDispatcher
+
+        @Provides
+        fun provideBaseShadeInteractor(
+            sceneContainerFlags: SceneContainerFlags,
+            sceneContainerOn: Provider<ShadeInteractorSceneContainerImpl>,
+            sceneContainerOff: Provider<ShadeInteractorLegacyImpl>
+        ): BaseShadeInteractor {
+            return if (sceneContainerFlags.isEnabled()) {
+                sceneContainerOn.get()
+            } else {
+                sceneContainerOff.get()
+            }
+        }
     }
 }
 
diff --git a/packages/SystemUI/tests/src/com/android/TestMocksModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/TestMocksModule.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
index fd50f15..37a4f61 100644
--- a/packages/SystemUI/tests/src/com/android/TestMocksModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android
+package com.android.systemui
 
 import android.app.ActivityManager
 import android.app.admin.DevicePolicyManager
@@ -24,7 +24,6 @@
 import com.android.keyguard.KeyguardSecurityModel
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardViewController
-import com.android.systemui.GuestResumeSessionReceiver
 import com.android.systemui.animation.DialogLaunchAnimator
 import com.android.systemui.demomode.DemoModeController
 import com.android.systemui.dump.DumpManager
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
index af1930e..45ded7f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
@@ -60,6 +60,8 @@
 
     override val minPatternLength: Int = 4
 
+    override val minPasswordLength: Int = 4
+
     private val _isPinEnhancedPrivacyEnabled = MutableStateFlow(false)
     override val isPinEnhancedPrivacyEnabled: StateFlow<Boolean> =
         _isPinEnhancedPrivacyEnabled.asStateFlow()
@@ -178,6 +180,7 @@
                 is AuthenticationMethodModel.Password -> SecurityMode.Password
                 is AuthenticationMethodModel.Pattern -> SecurityMode.Pattern
                 is AuthenticationMethodModel.None -> SecurityMode.None
+                is AuthenticationMethodModel.Sim -> SecurityMode.SimPin
             }
         }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeSimBouncerRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeSimBouncerRepository.kt
new file mode 100644
index 0000000..890e69d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeSimBouncerRepository.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.data.repository
+
+import android.telephony.SubscriptionInfo
+import com.android.systemui.bouncer.data.model.SimPukInputModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+/** Fakes the SimBouncerRepository. */
+class FakeSimBouncerRepository : SimBouncerRepository {
+    private val _subscriptionId: MutableStateFlow<Int> = MutableStateFlow(-1)
+    override val subscriptionId: StateFlow<Int> = _subscriptionId
+    private val _activeSubscriptionInfo: MutableStateFlow<SubscriptionInfo?> =
+        MutableStateFlow(null)
+    override val activeSubscriptionInfo: StateFlow<SubscriptionInfo?> = _activeSubscriptionInfo
+    private val _isLockedEsim: MutableStateFlow<Boolean?> = MutableStateFlow(null)
+    override val isLockedEsim: StateFlow<Boolean?> = _isLockedEsim
+    private val _isSimPukLocked: MutableStateFlow<Boolean> = MutableStateFlow(false)
+    override val isSimPukLocked: StateFlow<Boolean> = _isSimPukLocked
+    private val _errorDialogMessage: MutableStateFlow<String?> = MutableStateFlow(null)
+    override val errorDialogMessage: StateFlow<String?> = _errorDialogMessage
+    private var _simPukInputModel = SimPukInputModel()
+    override val simPukInputModel: SimPukInputModel
+        get() = _simPukInputModel
+
+    fun setSubscriptionId(subId: Int) {
+        _subscriptionId.value = subId
+    }
+
+    fun setActiveSubscriptionInfo(subscriptioninfo: SubscriptionInfo) {
+        _activeSubscriptionInfo.value = subscriptioninfo
+    }
+
+    fun setLockedEsim(isLockedEsim: Boolean) {
+        _isLockedEsim.value = isLockedEsim
+    }
+
+    fun setSimPukLocked(isSimPukLocked: Boolean) {
+        _isSimPukLocked.value = isSimPukLocked
+    }
+
+    fun setErrorDialogMessage(msg: String?) {
+        _errorDialogMessage.value = msg
+    }
+
+    override fun setSimPukUserInput(enteredSimPuk: String?, enteredSimPin: String?) {
+        _simPukInputModel = SimPukInputModel(enteredSimPuk, enteredSimPin)
+    }
+
+    override fun setSimVerificationErrorMessage(msg: String?) {
+        _errorDialogMessage.value = msg
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt
index 0c821ea..3aee889 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.communal.data.repository.FakeCommunalRepository
 import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository
 import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository
+import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
@@ -40,6 +41,7 @@
         smartspaceRepository: FakeSmartspaceRepository = FakeSmartspaceRepository(),
         tutorialRepository: FakeCommunalTutorialRepository = FakeCommunalTutorialRepository(),
         appWidgetHost: AppWidgetHost = mock(),
+        editWidgetsActivityStarter: EditWidgetsActivityStarter = mock(),
     ): WithDependencies {
         val withDeps =
             CommunalTutorialInteractorFactory.create(
@@ -57,6 +59,7 @@
             withDeps.keyguardInteractor,
             withDeps.communalTutorialInteractor,
             appWidgetHost,
+            editWidgetsActivityStarter,
             CommunalInteractor(
                 communalRepository,
                 widgetRepository,
@@ -64,6 +67,7 @@
                 smartspaceRepository,
                 withDeps.communalTutorialInteractor,
                 appWidgetHost,
+                editWidgetsActivityStarter,
             ),
         )
     }
@@ -78,6 +82,7 @@
         val keyguardInteractor: KeyguardInteractor,
         val tutorialInteractor: CommunalTutorialInteractor,
         val appWidgetHost: AppWidgetHost,
+        val editWidgetsActivityStarter: EditWidgetsActivityStarter,
         val communalInteractor: CommunalInteractor,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index 3674244..a94ca29 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -40,7 +40,7 @@
 class FakeKeyguardTransitionRepository @Inject constructor() : KeyguardTransitionRepository {
 
     private val _transitions =
-        MutableSharedFlow<TransitionStep>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
+        MutableSharedFlow<TransitionStep>(replay = 2, onBufferOverflow = BufferOverflow.DROP_OLDEST)
     override val transitions: SharedFlow<TransitionStep> = _transitions
 
     init {
@@ -130,7 +130,7 @@
      * only a FINISHED step, override [validateStep].
      */
     suspend fun sendTransitionStep(step: TransitionStep, validateStep: Boolean = true) {
-        _transitions.replayCache.getOrNull(0)?.let { lastStep ->
+        _transitions.replayCache.last().let { lastStep ->
             if (
                 validateStep &&
                     step.transitionState == TransitionState.FINISHED &&
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
index c8869aa..29e73b5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
@@ -23,6 +23,10 @@
 import android.graphics.Bitmap
 import android.graphics.drawable.BitmapDrawable
 import android.telecom.TelecomManager
+import android.telephony.PinResult
+import android.telephony.PinResult.PIN_RESULT_TYPE_SUCCESS
+import android.telephony.TelephonyManager
+import android.telephony.euicc.EuiccManager
 import com.android.internal.logging.MetricsLogger
 import com.android.internal.util.EmergencyAffordanceManager
 import com.android.systemui.SysuiTestCase
@@ -32,9 +36,11 @@
 import com.android.systemui.bouncer.data.repository.BouncerRepository
 import com.android.systemui.bouncer.data.repository.EmergencyServicesRepository
 import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.data.repository.FakeSimBouncerRepository
 import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
 import com.android.systemui.bouncer.domain.interactor.EmergencyDialerIntentFactory
+import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.classifier.FalsingCollectorFake
@@ -73,6 +79,7 @@
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.shade.data.repository.FakeShadeRepository
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
 import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
 import com.android.systemui.telephony.data.repository.TelephonyRepository
@@ -89,6 +96,9 @@
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.currentTime
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito
 
 /**
  * Utilities for creating scene container framework related repositories, interactors, and
@@ -127,9 +137,33 @@
     }
     val telephonyRepository: FakeTelephonyRepository by lazy { FakeTelephonyRepository() }
 
+    val bouncerRepository = BouncerRepository(featureFlags)
     val communalRepository: FakeCommunalRepository by lazy { FakeCommunalRepository() }
     val keyguardRepository: FakeKeyguardRepository by lazy { FakeKeyguardRepository() }
     val powerRepository: FakePowerRepository by lazy { FakePowerRepository() }
+    val simBouncerRepository: FakeSimBouncerRepository by lazy { FakeSimBouncerRepository() }
+    val telephonyManager: TelephonyManager =
+        Mockito.mock(TelephonyManager::class.java).apply {
+            whenever(createForSubscriptionId(anyInt())).thenReturn(this)
+            whenever(supplyIccLockPin(anyString()))
+                .thenReturn(PinResult(PIN_RESULT_TYPE_SUCCESS, 3))
+        }
+    val mobileConnectionsRepository: FakeMobileConnectionsRepository by lazy {
+        FakeMobileConnectionsRepository(mock(), mock())
+    }
+
+    val simBouncerInteractor =
+        SimBouncerInteractor(
+            applicationContext = context,
+            backgroundDispatcher = testDispatcher,
+            applicationScope = applicationScope(),
+            repository = simBouncerRepository,
+            telephonyManager = telephonyManager,
+            resources = context.resources,
+            keyguardUpdateMonitor = mock(),
+            euiccManager = context.getSystemService(Context.EUICC_SERVICE) as EuiccManager,
+            mobileConnectionsRepository = mobileConnectionsRepository,
+        )
 
     val userRepository: UserRepository by lazy {
         FakeUserRepository().apply {
@@ -228,11 +262,12 @@
         return BouncerInteractor(
             applicationScope = applicationScope(),
             applicationContext = context,
-            repository = BouncerRepository(featureFlags),
+            repository = bouncerRepository,
             authenticationInteractor = authenticationInteractor,
             flags = sceneContainerFlags,
             falsingInteractor = falsingInteractor(),
-            powerInteractor = powerInteractor()
+            powerInteractor = powerInteractor(),
+            simBouncerInteractor = simBouncerInteractor,
         )
     }
 
@@ -253,6 +288,7 @@
             users = flowOf(users),
             userSwitcherMenu = flowOf(createMenuActions()),
             actionButtonInteractor = actionButtonInteractor,
+            simBouncerInteractor = simBouncerInteractor,
         )
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
index 02318ab..92ec4f2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
@@ -66,6 +66,15 @@
         _legacyIsQsExpanded.value = legacyIsQsExpanded
     }
 
+    private val _legacyExpandImmediate = MutableStateFlow(false)
+    @Deprecated("Use ShadeInteractor instead")
+    override val legacyExpandImmediate = _legacyExpandImmediate
+
+    @Deprecated("Use ShadeInteractor instead")
+    override fun setLegacyExpandImmediate(legacyExpandImmediate: Boolean) {
+        _legacyExpandImmediate.value = legacyExpandImmediate
+    }
+
     @Deprecated("Use ShadeInteractor instead")
     override fun setLegacyExpandedOrAwaitingInputTransfer(
         legacyExpandedOrAwaitingInputTransfer: Boolean
@@ -88,6 +97,14 @@
         legacyLockscreenShadeTracking.value = tracking
     }
 
+    private val _legacyQsFullscreen = MutableStateFlow(false)
+    @Deprecated("Use ShadeInteractor instead") override val legacyQsFullscreen = _legacyQsFullscreen
+
+    @Deprecated("Use ShadeInteractor instead")
+    override fun setLegacyQsFullscreen(legacyQsFullscreen: Boolean) {
+        _legacyQsFullscreen.value = legacyQsFullscreen
+    }
+
     fun setShadeModel(model: ShadeModel) {
         _shadeModel.value = model
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeEventLog.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeEventLog.kt
new file mode 100644
index 0000000..ea2eeabf
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeEventLog.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util
+
+/** A fake [com.android.systemui.util.EventLog] for tests. */
+class FakeEventLog : EventLog {
+    data class Event(val tag: Int, val value: Any)
+
+    private val _events: MutableList<Event> = mutableListOf()
+    val events: List<Event>
+        get() = _events
+
+    fun clear() {
+        _events.clear()
+    }
+
+    override fun writeEvent(tag: Int, value: Int): Int {
+        _events.add(Event(tag, value))
+        return 1
+    }
+
+    override fun writeEvent(tag: Int, value: Long): Int {
+        _events.add(Event(tag, value))
+        return 1
+    }
+
+    override fun writeEvent(tag: Int, value: Float): Int {
+        _events.add(Event(tag, value))
+        return 1
+    }
+
+    override fun writeEvent(tag: Int, value: String): Int {
+        _events.add(Event(tag, value))
+        return 1
+    }
+
+    override fun writeEvent(tag: Int, vararg values: Any): Int {
+        _events.add(Event(tag, values))
+        return 1
+    }
+}
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index fc4ed1d..b9e34ee 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -33,3 +33,10 @@
     ],
     visibility: ["//visibility:public"],
 }
+
+java_host_for_device {
+    name: "core-xml-for-device",
+    libs: [
+        "core-xml-for-host",
+    ],
+}
diff --git a/ravenwood/framework-minus-apex-ravenwood-policies.txt b/ravenwood/framework-minus-apex-ravenwood-policies.txt
index 692d598..aa2d470 100644
--- a/ravenwood/framework-minus-apex-ravenwood-policies.txt
+++ b/ravenwood/framework-minus-apex-ravenwood-policies.txt
@@ -103,7 +103,33 @@
 # Containers
 class android.os.BaseBundle stubclass
 class android.os.Bundle stubclass
+class android.os.PersistableBundle stubclass
 
 # Misc
 class android.os.PatternMatcher stubclass
 class android.os.ParcelUuid stubclass
+
+# XML
+class com.android.internal.util.XmlPullParserWrapper stubclass
+class com.android.internal.util.XmlSerializerWrapper stubclass
+class com.android.internal.util.XmlUtils stubclass
+
+class com.android.modules.utils.BinaryXmlPullParser stubclass
+class com.android.modules.utils.BinaryXmlSerializer stubclass
+class com.android.modules.utils.FastDataInput stubclass
+class com.android.modules.utils.FastDataOutput stubclass
+class com.android.modules.utils.ModifiedUtf8 stubclass
+class com.android.modules.utils.TypedXmlPullParser stubclass
+class com.android.modules.utils.TypedXmlSerializer stubclass
+
+# Uri
+class android.net.Uri stubclass
+class android.net.UriCodec stubclass
+
+# Context: just enough to support wrapper, no further functionality
+class android.content.Context stub
+    method <init> ()V stub
+
+# Text
+class android.text.TextUtils stub
+    method isEmpty (Ljava/lang/CharSequence;)Z stub
diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt
index 776a19a..0290bbe 100644
--- a/ravenwood/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/ravenwood-annotation-allowed-classes.txt
@@ -2,8 +2,21 @@
 
 com.android.internal.util.ArrayUtils
 
+android.util.Xml
+
 android.os.Binder
 android.os.Binder$IdentitySupplier
 android.os.IBinder
 android.os.Process
 android.os.SystemClock
+android.os.UserHandle
+
+android.content.ClipData
+android.content.ClipData$Item
+android.content.ClipDescription
+android.content.ComponentName
+android.content.ContentUris
+android.content.ContentValues
+android.content.Intent
+android.content.IntentFilter
+android.content.UriMatcher
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapper.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionWrapper.java
similarity index 98%
rename from services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapper.java
rename to services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionWrapper.java
index 6c6394f..f0c44d6 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapper.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionWrapper.java
@@ -35,15 +35,15 @@
 /**
  * A wrapper of {@link IWindowMagnificationConnection}.
  */
-class WindowMagnificationConnectionWrapper {
+class MagnificationConnectionWrapper {
 
     private static final boolean DBG = false;
-    private static final String TAG = "WindowMagnificationConnectionWrapper";
+    private static final String TAG = "MagnificationConnectionWrapper";
 
     private final @NonNull IWindowMagnificationConnection mConnection;
     private final @NonNull AccessibilityTraceManager mTrace;
 
-    WindowMagnificationConnectionWrapper(@NonNull IWindowMagnificationConnection connection,
+    MagnificationConnectionWrapper(@NonNull IWindowMagnificationConnection connection,
             @NonNull AccessibilityTraceManager trace) {
         mConnection = connection;
         mTrace = trace;
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java
index 816f22f..3ea805b 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java
@@ -60,7 +60,7 @@
 import java.util.concurrent.atomic.AtomicLongFieldUpdater;
 
 /**
- * A class to manipulate window magnification through {@link WindowMagnificationConnectionWrapper}
+ * A class to manipulate window magnification through {@link MagnificationConnectionWrapper}
  * create by {@link #setConnection(IWindowMagnificationConnection)}. To set the connection with
  * SysUI, call {@code StatusBarManagerInternal#requestWindowMagnificationConnection(boolean)}.
  * The applied magnification scale is constrained by
@@ -133,7 +133,7 @@
     @VisibleForTesting
     @GuardedBy("mLock")
     @Nullable
-    WindowMagnificationConnectionWrapper mConnectionWrapper;
+    MagnificationConnectionWrapper mConnectionWrapper;
     @GuardedBy("mLock")
     private ConnectionCallback mConnectionCallback;
     @GuardedBy("mLock")
@@ -245,7 +245,7 @@
                 }
             }
             if (connection != null) {
-                mConnectionWrapper = new WindowMagnificationConnectionWrapper(connection, mTrace);
+                mConnectionWrapper = new MagnificationConnectionWrapper(connection, mTrace);
             }
 
             if (mConnectionWrapper != null) {
diff --git a/services/backup/Android.bp b/services/backup/Android.bp
index b086406..acb5911 100644
--- a/services/backup/Android.bp
+++ b/services/backup/Android.bp
@@ -19,5 +19,16 @@
     defaults: ["platform_service_defaults"],
     srcs: [":services.backup-sources"],
     libs: ["services.core"],
-    static_libs: ["app-compat-annotations"],
+    static_libs: ["app-compat-annotations", "backup_flags_lib"],
+}
+
+aconfig_declarations {
+    name: "backup_flags",
+    package: "com.android.server.backup",
+    srcs: ["flags.aconfig"],
+}
+
+java_aconfig_library {
+    name: "backup_flags_lib",
+    aconfig_declarations: "backup_flags",
 }
diff --git a/services/backup/flags.aconfig b/services/backup/flags.aconfig
new file mode 100644
index 0000000..d695d36
--- /dev/null
+++ b/services/backup/flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.backup"
+
+flag {
+    name: "enable_skipping_restore_launched_apps"
+    namespace: "onboarding"
+    description: "Enforce behavior determined by BackupTransport implementation on whether to skip "
+            "restore for apps that have been launched."
+    bug: "308401499"
+    is_fixed_read_only: true
+}
\ No newline at end of file
diff --git a/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java b/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java
index 70d7fac..9f7b627 100644
--- a/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java
+++ b/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java
@@ -24,6 +24,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.backup.BackupAgent;
 import android.app.backup.BackupAnnotations.BackupDestination;
 import android.app.backup.IBackupManagerMonitor;
 import android.app.backup.IRestoreObserver;
@@ -32,11 +33,15 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.PackageManagerInternal;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.Message;
 import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.LocalServices;
+import com.android.server.backup.Flags;
 import com.android.server.backup.TransportManager;
 import com.android.server.backup.UserBackupManagerService;
 import com.android.server.backup.internal.OnTaskFinishedListener;
@@ -296,12 +301,26 @@
         return -1;
     }
 
-    private BackupEligibilityRules getBackupEligibilityRules(RestoreSet restoreSet) {
+    @VisibleForTesting
+    BackupEligibilityRules getBackupEligibilityRules(RestoreSet restoreSet) {
         // TODO(b/182986784): Remove device name comparison once a designated field for operation
         //  type is added to RestoreSet object.
         int backupDestination = DEVICE_NAME_FOR_D2D_SET.equals(restoreSet.device)
                 ? BackupDestination.DEVICE_TRANSFER : BackupDestination.CLOUD;
-        return mBackupManagerService.getEligibilityRulesForOperation(backupDestination);
+
+        if (!Flags.enableSkippingRestoreLaunchedApps()) {
+            return mBackupManagerService.getEligibilityRulesForOperation(backupDestination);
+        }
+
+        boolean skipRestoreForLaunchedApps = (restoreSet.backupTransportFlags
+                & BackupAgent.FLAG_SKIP_RESTORE_FOR_LAUNCHED_APPS) != 0;
+
+        return new BackupEligibilityRules(mBackupManagerService.getPackageManager(),
+                LocalServices.getService(PackageManagerInternal.class),
+                mUserId,
+                mBackupManagerService.getContext(),
+                backupDestination,
+                skipRestoreForLaunchedApps);
     }
 
     public synchronized int restorePackage(String packageName, IRestoreObserver observer,
diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
index bbec79d..96a873e 100644
--- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
+++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
@@ -63,6 +63,7 @@
 import com.android.server.backup.BackupAndRestoreFeatureFlags;
 import com.android.server.backup.BackupRestoreTask;
 import com.android.server.backup.BackupUtils;
+import com.android.server.backup.Flags;
 import com.android.server.backup.OperationStorage;
 import com.android.server.backup.OperationStorage.OpType;
 import com.android.server.backup.PackageManagerBackupAgent;
@@ -263,7 +264,14 @@
                         continue;
                     }
 
-                    if (backupEligibilityRules.appIsEligibleForBackup(info.applicationInfo)) {
+
+                    ApplicationInfo applicationInfo = info.applicationInfo;
+                    if (backupEligibilityRules.appIsEligibleForBackup(applicationInfo)) {
+                        if (Flags.enableSkippingRestoreLaunchedApps()
+                            && !backupEligibilityRules.isAppEligibleForRestore(applicationInfo)) {
+                            continue;
+                        }
+
                         mAcceptSet.add(info);
                     }
                 } catch (NameNotFoundException e) {
diff --git a/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java b/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java
index 7c47f1e..f24a3c1 100644
--- a/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java
+++ b/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java
@@ -80,6 +80,7 @@
     private final int mUserId;
     private boolean mIsProfileUser = false;
     @BackupDestination  private final int mBackupDestination;
+    private final boolean mSkipRestoreForLaunchedApps;
 
     /**
      * When  this change is enabled, {@code adb backup}  is automatically turned on for apps
@@ -112,12 +113,23 @@
             int userId,
             Context context,
             @BackupDestination int backupDestination) {
+        this(packageManager, packageManagerInternal, userId, context, backupDestination,
+                /* skipRestoreForLaunchedApps */ false);
+    }
+
+    public BackupEligibilityRules(PackageManager packageManager,
+            PackageManagerInternal packageManagerInternal,
+            int userId,
+            Context context,
+            @BackupDestination int backupDestination,
+            boolean skipRestoreForLaunchedApps) {
         mPackageManager = packageManager;
         mPackageManagerInternal = packageManagerInternal;
         mUserId = userId;
         mBackupDestination = backupDestination;
         UserManager userManager = context.getSystemService(UserManager.class);
         mIsProfileUser = userManager.isProfile();
+        mSkipRestoreForLaunchedApps = skipRestoreForLaunchedApps;
     }
 
     /**
@@ -132,6 +144,9 @@
      *     <li>it is the special shared-storage backup package used for 'adb backup'
      * </ol>
      *
+     * These eligibility conditions are also checked before restore, in case the backup happened on
+     * a device / from the version of the app where these rules were not enforced.
+     *
      * However, the above eligibility rules are ignored for non-system apps in in case of
      * device-to-device migration, see {@link BackupDestination}.
      */
@@ -283,6 +298,27 @@
         }
     }
 
+    /**
+     * Determine if data restore should be run for the given package.
+     *
+     * <p>This is used in combination with {@link #appIsEligibleForBackup(ApplicationInfo)} that
+     * checks whether the backup being restored should have happened in the first place.</p>
+     */
+    public boolean isAppEligibleForRestore(ApplicationInfo app) {
+        if (!mSkipRestoreForLaunchedApps) {
+            return true;
+        }
+
+        // If an app implemented a BackupAgent, they are expected to handle being restored even
+        // after first launch and avoid conflicts between existing app data and restored data.
+        if (app.backupAgentName != null) {
+            return true;
+        }
+
+        // Otherwise only restore an app if it hasn't been launched before.
+        return !mPackageManagerInternal.wasPackageEverLaunched(app.packageName, mUserId);
+    }
+
     /** Avoid backups of 'disabled' apps. */
     @VisibleForTesting
     boolean appIsDisabled(
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 22693ab..49457fb 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -199,7 +199,7 @@
         "biometrics_flags_lib",
         "am_flags_lib",
         "com_android_wm_shell_flags_lib",
-        "android.app.flags-aconfig-java"
+        "service-jobscheduler-deviceidle.flags-aconfig-java",
     ],
     javac_shard_size: 50,
     javacflags: [
diff --git a/services/core/java/android/content/pm/PackageManagerInternal.java b/services/core/java/android/content/pm/PackageManagerInternal.java
index 4b00434..7e4cf4f 100644
--- a/services/core/java/android/content/pm/PackageManagerInternal.java
+++ b/services/core/java/android/content/pm/PackageManagerInternal.java
@@ -1421,6 +1421,11 @@
             @UserIdInt int userId);
 
     /**
+     * Checks if package is stopped for a specific user.
+     */
+    public abstract boolean isPackageStopped(@NonNull String packageName, @UserIdInt int userId);
+
+    /**
      * Sends the PACKAGE_RESTARTED broadcast.
      */
     public abstract void sendPackageRestartedBroadcast(@NonNull String packageName,
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index f92af67..b2a7948 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -9367,6 +9367,8 @@
      */
     void appendDropBoxProcessHeaders(ProcessRecord process, String processName,
             final VolatileDropboxEntryStates volatileStates, final StringBuilder sb) {
+        sb.append("SystemUptimeMs: ").append(SystemClock.uptimeMillis()).append("\n");
+
         // Watchdog thread ends up invoking this function (with
         // a null ProcessRecord) to add the stack file to dropbox.
         // Do not acquire a lock on this (am) in such cases, as it
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index f9fc4d4..36356bd 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -111,6 +111,7 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.ParseUtils;
 import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.NetworkCapabilitiesUtils;
 import com.android.server.LocalServices;
 import com.android.server.Watchdog;
@@ -475,7 +476,12 @@
                 ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE));
         final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
         try {
-            nms.registerObserver(mActivityChangeObserver);
+            if (!SdkLevel.isAtLeastV()) {
+                // On V+ devices, ConnectivityService calls BatteryStats API to update
+                // RadioPowerState change. So BatteryStatsService registers the callback only on
+                // pre V devices.
+                nms.registerObserver(mActivityChangeObserver);
+            }
             cm.registerDefaultNetworkCallback(mNetworkCallback);
         } catch (RemoteException e) {
             Slog.e(TAG, "Could not register INetworkManagement event observer " + e);
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 599d998..028be88 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -144,6 +144,7 @@
         "haptics",
         "hardware_backed_security_mainline",
         "input",
+        "lse_desktop_experience",
         "machine_learning",
         "mainline_modularization",
         "mainline_sdk",
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 7292ea6..14aab13 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -531,19 +531,6 @@
             }
         }
 
-        // Functions for uid mode access and manipulation.
-        public SparseIntArray getNonDefaultUidModes() {
-            return mAppOpsCheckingService.getNonDefaultUidModes(uid);
-        }
-
-        public int getUidMode(int op) {
-            return mAppOpsCheckingService.getUidMode(uid, op);
-        }
-
-        public boolean setUidMode(int op, int mode) {
-            return mAppOpsCheckingService.setUidMode(uid, op, mode);
-        }
-
         @SuppressWarnings("GuardedBy")
         int evalMode(int op, int mode) {
             return getUidStateTracker().evalMode(uid, op, mode);
@@ -613,15 +600,6 @@
             this.packageName = packageName;
         }
 
-        @Mode int getMode() {
-            return mAppOpsCheckingService.getPackageMode(packageName, this.op,
-                    UserHandle.getUserId(this.uid));
-        }
-        void setMode(@Mode int mode) {
-            mAppOpsCheckingService.setPackageMode(packageName, this.op, mode,
-                    UserHandle.getUserId(this.uid));
-        }
-
         void removeAttributionsWithNoTime() {
             for (int i = mAttributions.size() - 1; i >= 0; i--) {
                 if (!mAttributions.valueAt(i).hasAnyTime()) {
@@ -653,7 +631,11 @@
                         mAttributions.valueAt(i).createAttributedOpEntryLocked());
             }
 
-            return new OpEntry(op, getMode(), attributionEntries);
+            return new OpEntry(
+                    op,
+                    mAppOpsCheckingService.getPackageMode(
+                            this.packageName, this.op, UserHandle.getUserId(this.uid)),
+                    attributionEntries);
         }
 
         @NonNull OpEntry createSingleAttributionEntryLocked(@Nullable String attributionTag) {
@@ -668,7 +650,11 @@
                 }
             }
 
-            return new OpEntry(op, getMode(), attributionEntries);
+            return new OpEntry(
+                    op,
+                    mAppOpsCheckingService.getPackageMode(
+                            this.packageName, this.op, UserHandle.getUserId(this.uid)),
+                    attributionEntries);
         }
 
         boolean isRunning() {
@@ -1384,8 +1370,10 @@
                     }
                     final int code = foregroundOps.keyAt(fgi);
 
-                    if (uidState.getUidMode(code) != AppOpsManager.opToDefaultMode(code)
-                            && uidState.getUidMode(code) == AppOpsManager.MODE_FOREGROUND) {
+                    if (mAppOpsCheckingService.getUidMode(uidState.uid, code)
+                                    != AppOpsManager.opToDefaultMode(code)
+                            && mAppOpsCheckingService.getUidMode(uidState.uid, code)
+                                    == AppOpsManager.MODE_FOREGROUND) {
                         mHandler.sendMessage(PooledLambda.obtainMessage(
                                 AppOpsService::notifyOpChangedForAllPkgsInUid,
                                 this, code, uidState.uid, true));
@@ -1405,7 +1393,11 @@
                                     if (op == null) {
                                         continue;
                                     }
-                                    if (op.getMode() == AppOpsManager.MODE_FOREGROUND) {
+                                    if (mAppOpsCheckingService.getPackageMode(
+                                                    op.packageName,
+                                                    op.op,
+                                                    UserHandle.getUserId(op.uid))
+                                            == AppOpsManager.MODE_FOREGROUND) {
                                         mHandler.sendMessage(PooledLambda.obtainMessage(
                                                 AppOpsService::notifyOpChanged,
                                                 this, listenerSet.valueAt(cbi), code, uidState.uid,
@@ -1497,7 +1489,7 @@
     @Nullable
     private ArrayList<AppOpsManager.OpEntry> collectUidOps(@NonNull UidState uidState,
             @Nullable int[] ops) {
-        final SparseIntArray opModes = uidState.getNonDefaultUidModes();
+        final SparseIntArray opModes = mAppOpsCheckingService.getNonDefaultUidModes(uidState.uid);
         if (opModes == null) {
             return null;
         }
@@ -1778,7 +1770,11 @@
             Ops ops = getOpsLocked(uid, packageName, null, false, null, /* edit */ false);
             if (ops != null) {
                 ops.remove(op.op);
-                op.setMode(AppOpsManager.opToDefaultMode(op.op));
+                mAppOpsCheckingService.setPackageMode(
+                        packageName,
+                        op.op,
+                        AppOpsManager.opToDefaultMode(op.op),
+                        UserHandle.getUserId(op.uid));
                 if (ops.size() <= 0) {
                     UidState uidState = ops.uidState;
                     ArrayMap<String, Ops> pkgOps = uidState.pkgOps;
@@ -1848,15 +1844,16 @@
                 uidState = new UidState(uid);
                 mUidStates.put(uid, uidState);
             }
-            if (uidState.getUidMode(code) != AppOpsManager.opToDefaultMode(code)) {
-                previousMode = uidState.getUidMode(code);
+            if (mAppOpsCheckingService.getUidMode(uidState.uid, code)
+                    != AppOpsManager.opToDefaultMode(code)) {
+                previousMode = mAppOpsCheckingService.getUidMode(uidState.uid, code);
             } else {
                 // doesn't look right but is legacy behavior.
                 previousMode = MODE_DEFAULT;
             }
 
             mIgnoredCallback = permissionPolicyCallback;
-            if (!uidState.setUidMode(code, mode)) {
+            if (!mAppOpsCheckingService.setUidMode(uidState.uid, code, mode)) {
                 return;
             }
             if (mode != MODE_ERRORED && mode != previousMode) {
@@ -2133,10 +2130,15 @@
         synchronized (this) {
             Op op = getOpLocked(code, uid, packageName, null, false, pvr.bypass, /* edit */ true);
             if (op != null) {
-                if (op.getMode() != mode) {
-                    previousMode = op.getMode();
+                if (mAppOpsCheckingService.getPackageMode(
+                                op.packageName, op.op, UserHandle.getUserId(op.uid))
+                        != mode) {
+                    previousMode =
+                            mAppOpsCheckingService.getPackageMode(
+                                    op.packageName, op.op, UserHandle.getUserId(op.uid));
                     mIgnoredCallback = permissionPolicyCallback;
-                    op.setMode(mode);
+                    mAppOpsCheckingService.setPackageMode(op.packageName, op.op, mode,
+                            UserHandle.getUserId(op.uid));
                 }
             }
         }
@@ -2274,7 +2276,7 @@
             for (int i = mUidStates.size() - 1; i >= 0; i--) {
                 UidState uidState = mUidStates.valueAt(i);
 
-                SparseIntArray opModes = uidState.getNonDefaultUidModes();
+                SparseIntArray opModes = mAppOpsCheckingService.getNonDefaultUidModes(uidState.uid);
                 if (opModes != null && (uidState.uid == reqUid || reqUid == -1)) {
                     final int uidOpCount = opModes.size();
                     for (int j = uidOpCount - 1; j >= 0; j--) {
@@ -2283,7 +2285,7 @@
                             int previousMode = opModes.valueAt(j);
                             int newMode = isUidOpGrantedByRole(uidState.uid, code) ? MODE_ALLOWED :
                                     AppOpsManager.opToDefaultMode(code);
-                            uidState.setUidMode(code, newMode);
+                            mAppOpsCheckingService.setUidMode(uidState.uid, code, newMode);
                             for (String packageName : getPackagesForUid(uidState.uid)) {
                                 callbacks = addCallbacks(callbacks, code, uidState.uid, packageName,
                                         previousMode, mOpModeWatchers.get(code));
@@ -2325,14 +2327,22 @@
                             continue;
                         }
                         if (AppOpsManager.opAllowsReset(curOp.op)) {
-                            int previousMode = curOp.getMode();
+                            int previousMode =
+                                    mAppOpsCheckingService.getPackageMode(
+                                            curOp.packageName,
+                                            curOp.op,
+                                            UserHandle.getUserId(curOp.uid));
                             int newMode = isPackageOpGrantedByRole(packageName, uidState.uid,
                                     curOp.op) ? MODE_ALLOWED : AppOpsManager.opToDefaultMode(
                                     curOp.op);
                             if (previousMode == newMode) {
                                 continue;
                             }
-                            curOp.setMode(newMode);
+                            mAppOpsCheckingService.setPackageMode(
+                                    curOp.packageName,
+                                    curOp.op,
+                                    newMode,
+                                    UserHandle.getUserId(curOp.uid));
                             changed = true;
                             uidChanged = true;
                             final int uid = curOp.uidState.uid;
@@ -2592,15 +2602,22 @@
             code = AppOpsManager.opToSwitch(code);
             UidState uidState = getUidStateLocked(uid, false);
             if (uidState != null
-                    && uidState.getUidMode(code) != AppOpsManager.opToDefaultMode(code)) {
-                final int rawMode = uidState.getUidMode(code);
+                    && mAppOpsCheckingService.getUidMode(uidState.uid, code)
+                            != AppOpsManager.opToDefaultMode(code)) {
+                final int rawMode = mAppOpsCheckingService.getUidMode(uidState.uid, code);
                 return raw ? rawMode : uidState.evalMode(code, rawMode);
             }
             Op op = getOpLocked(code, uid, packageName, null, false, pvr.bypass, /* edit */ false);
             if (op == null) {
                 return AppOpsManager.opToDefaultMode(code);
             }
-            return raw ? op.getMode() : op.uidState.evalMode(op.op, op.getMode());
+            return raw
+                    ? mAppOpsCheckingService.getPackageMode(
+                            op.packageName, op.op, UserHandle.getUserId(op.uid))
+                    : op.uidState.evalMode(
+                            op.op,
+                            mAppOpsCheckingService.getPackageMode(
+                                    op.packageName, op.op, UserHandle.getUserId(op.uid)));
         }
     }
 
@@ -2836,8 +2853,11 @@
             }
             // If there is a non-default per UID policy (we set UID op mode only if
             // non-default) it takes over, otherwise use the per package policy.
-            if (uidState.getUidMode(switchCode) != AppOpsManager.opToDefaultMode(switchCode)) {
-                final int uidMode = uidState.evalMode(code, uidState.getUidMode(switchCode));
+            if (mAppOpsCheckingService.getUidMode(uidState.uid, switchCode)
+                    != AppOpsManager.opToDefaultMode(switchCode)) {
+                final int uidMode =
+                        uidState.evalMode(
+                                code, mAppOpsCheckingService.getUidMode(uidState.uid, switchCode));
                 if (uidMode != AppOpsManager.MODE_ALLOWED) {
                     if (DEBUG) Slog.d(TAG, "noteOperation: uid reject #" + uidMode + " for code "
                             + switchCode + " (" + code + ") uid " + uid + " package "
@@ -2850,7 +2870,13 @@
             } else {
                 final Op switchOp = switchCode != code ? getOpLocked(ops, switchCode, uid, true)
                         : op;
-                final int mode = switchOp.uidState.evalMode(switchOp.op, switchOp.getMode());
+                final int mode =
+                        switchOp.uidState.evalMode(
+                                switchOp.op,
+                                mAppOpsCheckingService.getPackageMode(
+                                        switchOp.packageName,
+                                        switchOp.op,
+                                        UserHandle.getUserId(switchOp.uid)));
                 if (mode != AppOpsManager.MODE_ALLOWED) {
                     if (DEBUG) Slog.d(TAG, "noteOperation: reject #" + mode + " for code "
                             + switchCode + " (" + code + ") uid " + uid + " package "
@@ -3372,8 +3398,11 @@
             final int switchCode = AppOpsManager.opToSwitch(code);
             // If there is a non-default per UID policy (we set UID op mode only if
             // non-default) it takes over, otherwise use the per package policy.
-            if (uidState.getUidMode(switchCode) != AppOpsManager.opToDefaultMode(switchCode)) {
-                final int uidMode = uidState.evalMode(code, uidState.getUidMode(switchCode));
+            if (mAppOpsCheckingService.getUidMode(uidState.uid, switchCode)
+                    != AppOpsManager.opToDefaultMode(switchCode)) {
+                final int uidMode =
+                        uidState.evalMode(
+                                code, mAppOpsCheckingService.getUidMode(uidState.uid, switchCode));
                 if (!shouldStartForMode(uidMode, startIfModeDefault)) {
                     if (DEBUG) {
                         Slog.d(TAG, "startOperation: uid reject #" + uidMode + " for code "
@@ -3388,7 +3417,13 @@
             } else {
                 final Op switchOp = switchCode != code ? getOpLocked(ops, switchCode, uid, true)
                         : op;
-                final int mode = switchOp.uidState.evalMode(switchOp.op, switchOp.getMode());
+                final int mode =
+                        switchOp.uidState.evalMode(
+                                switchOp.op,
+                                mAppOpsCheckingService.getPackageMode(
+                                        switchOp.packageName,
+                                        switchOp.op,
+                                        UserHandle.getUserId(switchOp.uid)));
                 if (mode != AppOpsManager.MODE_ALLOWED
                         && (!startIfModeDefault || mode != MODE_DEFAULT)) {
                     if (DEBUG) Slog.d(TAG, "startOperation: reject #" + mode + " for code "
@@ -3478,8 +3513,11 @@
             final int switchCode = AppOpsManager.opToSwitch(code);
             // If there is a non-default mode per UID policy (we set UID op mode only if
             // non-default) it takes over, otherwise use the per package policy.
-            if (uidState.getUidMode(switchCode) != AppOpsManager.opToDefaultMode(switchCode)) {
-                final int uidMode = uidState.evalMode(code, uidState.getUidMode(switchCode));
+            if (mAppOpsCheckingService.getUidMode(uidState.uid, switchCode)
+                    != AppOpsManager.opToDefaultMode(switchCode)) {
+                final int uidMode =
+                        uidState.evalMode(
+                                code, mAppOpsCheckingService.getUidMode(uidState.uid, switchCode));
                 if (!shouldStartForMode(uidMode, startIfModeDefault)) {
                     if (DEBUG) {
                         Slog.d(TAG, "startOperation: uid reject #" + uidMode + " for code "
@@ -3491,7 +3529,13 @@
             } else {
                 final Op switchOp = switchCode != code ? getOpLocked(ops, switchCode, uid, true)
                         : op;
-                final int mode = switchOp.uidState.evalMode(switchOp.op, switchOp.getMode());
+                final int mode =
+                        switchOp.uidState.evalMode(
+                                switchOp.op,
+                                mAppOpsCheckingService.getPackageMode(
+                                        switchOp.packageName,
+                                        switchOp.op,
+                                        UserHandle.getUserId(switchOp.uid)));
                 if (mode != AppOpsManager.MODE_ALLOWED
                         && (!startIfModeDefault || mode != MODE_DEFAULT)) {
                     if (DEBUG) {
@@ -5620,7 +5664,8 @@
             }
             for (int i=0; i<mUidStates.size(); i++) {
                 UidState uidState = mUidStates.valueAt(i);
-                final SparseIntArray opModes = uidState.getNonDefaultUidModes();
+                final SparseIntArray opModes =
+                        mAppOpsCheckingService.getNonDefaultUidModes(uidState.uid);
                 final ArrayMap<String, Ops> pkgOps = uidState.pkgOps;
 
                 if (dumpWatchers || dumpHistory) {
@@ -5648,7 +5693,12 @@
                             }
                             if (!hasMode) {
                                 for (int opi = 0; !hasMode && opi < ops.size(); opi++) {
-                                    if (ops.valueAt(opi).getMode() == dumpMode) {
+                                    final Op op = ops.valueAt(opi);
+                                    if (mAppOpsCheckingService.getPackageMode(
+                                                    op.packageName,
+                                                    op.op,
+                                                    UserHandle.getUserId(op.uid))
+                                            == dumpMode) {
                                         hasMode = true;
                                     }
                                 }
@@ -5699,7 +5749,12 @@
                         if (dumpOp >= 0 && dumpOp != opCode) {
                             continue;
                         }
-                        if (dumpMode >= 0 && dumpMode != op.getMode()) {
+                        if (dumpMode >= 0
+                                && dumpMode
+                                        != mAppOpsCheckingService.getPackageMode(
+                                                op.packageName,
+                                                op.op,
+                                                UserHandle.getUserId(op.uid))) {
                             continue;
                         }
                         if (!printedPackage) {
@@ -5707,14 +5762,25 @@
                             printedPackage = true;
                         }
                         pw.print("      "); pw.print(AppOpsManager.opToName(opCode));
-                        pw.print(" ("); pw.print(AppOpsManager.modeToName(op.getMode()));
+                        pw.print(" (");
+                        pw.print(
+                                AppOpsManager.modeToName(
+                                        mAppOpsCheckingService.getPackageMode(
+                                                op.packageName,
+                                                op.op,
+                                                UserHandle.getUserId(op.uid))));
                         final int switchOp = AppOpsManager.opToSwitch(opCode);
                         if (switchOp != opCode) {
                             pw.print(" / switch ");
                             pw.print(AppOpsManager.opToName(switchOp));
                             final Op switchObj = ops.get(switchOp);
-                            int mode = switchObj == null
-                                    ? AppOpsManager.opToDefaultMode(switchOp) : switchObj.getMode();
+                            int mode =
+                                    switchObj == null
+                                            ? AppOpsManager.opToDefaultMode(switchOp)
+                                            : mAppOpsCheckingService.getPackageMode(
+                                                    switchObj.packageName,
+                                                    switchObj.op,
+                                                    UserHandle.getUserId(switchObj.uid));
                             pw.print("="); pw.print(AppOpsManager.modeToName(mode));
                         }
                         pw.println("): ");
@@ -5848,7 +5914,13 @@
         for (int pkgNum = 0; pkgNum < numPkgOps; pkgNum++) {
             Ops ops = uidState.pkgOps.valueAt(pkgNum);
             Op op = ops != null ? ops.get(code) : null;
-            if (op == null || (op.getMode() != MODE_ALLOWED && op.getMode() != MODE_FOREGROUND)) {
+            if (op == null) {
+                continue;
+            }
+            final int mode =
+                    mAppOpsCheckingService.getPackageMode(
+                            op.packageName, op.op, UserHandle.getUserId(op.uid));
+            if (mode != MODE_ALLOWED && mode != MODE_FOREGROUND) {
                 continue;
             }
             int numAttrTags = op.mAttributions.size();
diff --git a/services/core/java/com/android/server/content/ContentService.java b/services/core/java/com/android/server/content/ContentService.java
index 1b48e3c..9f4b3d2 100644
--- a/services/core/java/com/android/server/content/ContentService.java
+++ b/services/core/java/com/android/server/content/ContentService.java
@@ -1058,7 +1058,8 @@
 
         final long identityToken = clearCallingIdentity();
         try {
-            return getSyncManager().computeSyncable(account, userId, providerName, false);
+            return getSyncManager().computeSyncable(account, userId, providerName, false,
+                    /*checkStoppedState=*/ false);
         } finally {
             restoreCallingIdentity(identityToken);
         }
diff --git a/services/core/java/com/android/server/content/SyncJobService.java b/services/core/java/com/android/server/content/SyncJobService.java
index 1da7f0c..cd3f0f0 100644
--- a/services/core/java/com/android/server/content/SyncJobService.java
+++ b/services/core/java/com/android/server/content/SyncJobService.java
@@ -19,6 +19,7 @@
 import android.annotation.Nullable;
 import android.app.job.JobParameters;
 import android.app.job.JobService;
+import android.content.pm.PackageManagerInternal;
 import android.os.Message;
 import android.os.SystemClock;
 import android.util.Log;
@@ -28,6 +29,7 @@
 import android.util.SparseLongArray;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.server.LocalServices;
 
 public class SyncJobService extends JobService {
     private static final String TAG = "SyncManager";
@@ -97,6 +99,20 @@
             return true;
         }
 
+        // TODO(b/209852664): remove this logic from here once it's added within JobScheduler.
+        // JobScheduler should not call onStartJob for syncs whose source packages are stopped.
+        // Until JS adds the relevant logic, this is a temporary solution to keep deferring syncs
+        // for packages in the stopped state.
+        if (android.content.pm.Flags.stayStopped()) {
+            if (LocalServices.getService(PackageManagerInternal.class)
+                    .isPackageStopped(op.owningPackage, op.target.userId)) {
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Slog.d(TAG, "Skipping sync for force-stopped package: " + op.owningPackage);
+                }
+                return false;
+            }
+        }
+
         boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE);
         synchronized (sLock) {
             final int jobId = params.getJobId();
diff --git a/services/core/java/com/android/server/content/SyncManager.java b/services/core/java/com/android/server/content/SyncManager.java
index ac7d9c1..575b309 100644
--- a/services/core/java/com/android/server/content/SyncManager.java
+++ b/services/core/java/com/android/server/content/SyncManager.java
@@ -438,6 +438,23 @@
         }
     };
 
+    private final BroadcastReceiver mForceStoppedReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final boolean isLoggable = Log.isLoggable(TAG, Log.DEBUG);
+            // For now, just log when packages were force-stopped and unstopped for debugging.
+            if (isLoggable) {
+                if (Intent.ACTION_PACKAGE_RESTARTED.equals(intent.getAction())) {
+                    Log.d(TAG, "Package force-stopped: "
+                            + intent.getData().getSchemeSpecificPart());
+                } else if (Intent.ACTION_PACKAGE_UNSTOPPED.equals(intent.getAction())) {
+                    Log.d(TAG, "Package unstopped: "
+                            + intent.getData().getSchemeSpecificPart());
+                }
+            }
+        }
+    };
+
     private final HandlerThread mThread;
     private final SyncHandler mSyncHandler;
     private final SyncManagerConstants mConstants;
@@ -701,6 +718,12 @@
         mContext.registerReceiverAsUser(
                 mUserIntentReceiver, UserHandle.ALL, intentFilter, null, null);
 
+        intentFilter = new IntentFilter();
+        intentFilter.addAction(Intent.ACTION_PACKAGE_RESTARTED);
+        intentFilter.addAction(Intent.ACTION_PACKAGE_UNSTOPPED);
+        intentFilter.addDataScheme("package");
+        context.registerReceiver(mForceStoppedReceiver, intentFilter);
+
         intentFilter = new IntentFilter(Intent.ACTION_TIME_CHANGED);
         context.registerReceiver(mOtherIntentsReceiver, intentFilter);
 
@@ -1108,7 +1131,7 @@
 
             for (String authority : syncableAuthorities) {
                 int isSyncable = computeSyncable(account.account, account.userId, authority,
-                        !checkIfAccountReady);
+                        !checkIfAccountReady, /*checkStoppedState=*/ true);
 
                 if (isSyncable == AuthorityInfo.NOT_SYNCABLE) {
                     continue;
@@ -1228,7 +1251,7 @@
     }
 
     public int computeSyncable(Account account, int userId, String authority,
-            boolean checkAccountAccess) {
+            boolean checkAccountAccess, boolean checkStoppedState) {
         final int status = getIsSyncable(account, userId, authority);
         if (status == AuthorityInfo.NOT_SYNCABLE) {
             return AuthorityInfo.NOT_SYNCABLE;
@@ -1241,6 +1264,9 @@
         }
         final int owningUid = syncAdapterInfo.uid;
         final String owningPackage = syncAdapterInfo.componentName.getPackageName();
+        if (checkStoppedState && isPackageStopped(owningPackage, userId)) {
+            return AuthorityInfo.NOT_SYNCABLE;
+        }
         if (mAmi.isAppStartModeDisabled(owningUid, owningPackage)) {
             Slog.w(TAG, "Not scheduling job " + syncAdapterInfo.uid + ":"
                     + syncAdapterInfo.componentName
@@ -1256,6 +1282,17 @@
         return status;
     }
 
+    /**
+     * Returns whether the package is in a stopped state or not.
+     * Always returns {@code false} if the {@code android.content.pm.stay_stopped} flag is not set.
+     */
+    private boolean isPackageStopped(String packageName, int userId) {
+        if (android.content.pm.Flags.stayStopped()) {
+            return mPackageManagerInternal.isPackageStopped(packageName, userId);
+        }
+        return false;
+    }
+
     private boolean canAccessAccount(Account account, String packageName, int uid) {
         if (mAccountManager.hasAccountAccess(account, packageName,
                 UserHandle.getUserHandleForUid(uid))) {
@@ -3496,6 +3533,9 @@
             for (SyncOperation op: ops) {
                 if (op.isPeriodic && op.target.matchesSpec(target)
                         && op.areExtrasEqual(extras, /*includeSyncSettings=*/ true)) {
+                    if (isPackageStopped(op.owningPackage, target.userId)) {
+                        continue; // skip stopped package
+                    }
                     maybeUpdateSyncPeriodH(op, pollFrequencyMillis, flexMillis);
                     return;
                 }
@@ -3627,7 +3667,8 @@
                 }
             }
             // Drop this sync request if it isn't syncable.
-            state = computeSyncable(target.account, target.userId, target.provider, true);
+            state = computeSyncable(target.account, target.userId, target.provider, true,
+                    /*checkStoppedState=*/ true);
             if (state == AuthorityInfo.SYNCABLE_NO_ACCOUNT_ACCESS) {
                 if (isLoggable) {
                     Slog.v(TAG, "    Dropping sync operation: "
diff --git a/services/core/java/com/android/server/display/ColorFade.java b/services/core/java/com/android/server/display/ColorFade.java
index 3de188f..93d9b8d 100644
--- a/services/core/java/com/android/server/display/ColorFade.java
+++ b/services/core/java/com/android/server/display/ColorFade.java
@@ -411,6 +411,33 @@
     }
 
     /**
+     * Destroys ColorFade animation and its resources
+     *
+     * This method should be called when the ColorFade is no longer in use; i.e. when
+     * the {@link #mDisplayId display} has been removed.
+     */
+    public void destroy() {
+        if (DEBUG) {
+            Slog.d(TAG, "destroy");
+        }
+        if (mPrepared) {
+            if (mCreatedResources) {
+                attachEglContext();
+                try {
+                    destroyScreenshotTexture();
+                    destroyGLShaders();
+                    destroyGLBuffers();
+                    destroyEglSurface();
+                } finally {
+                    detachEglContext();
+                }
+            }
+            destroyEglContext();
+            destroySurface();
+        }
+    }
+
+    /**
      * Draws an animation frame showing the color fade activated at the
      * specified level.
      *
@@ -793,6 +820,12 @@
         }
     }
 
+    private void destroyEglContext() {
+        if (mEglDisplay != null && mEglContext != null) {
+            EGL14.eglDestroyContext(mEglDisplay, mEglContext);
+        }
+    }
+
     private static FloatBuffer createNativeFloatBuffer(int size) {
         ByteBuffer bb = ByteBuffer.allocateDirect(size * 4);
         bb.order(ByteOrder.nativeOrder());
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index a0beedb..b99de5c 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -71,7 +71,7 @@
 import com.android.server.display.config.RefreshRateZone;
 import com.android.server.display.config.SdrHdrRatioMap;
 import com.android.server.display.config.SdrHdrRatioPoint;
-import com.android.server.display.config.SensorDetails;
+import com.android.server.display.config.SensorData;
 import com.android.server.display.config.ThermalStatus;
 import com.android.server.display.config.ThermalThrottling;
 import com.android.server.display.config.ThresholdPoint;
@@ -349,6 +349,20 @@
  *      <proxSensor>
  *        <type>android.sensor.proximity</type>
  *        <name>1234 Proximity Sensor</name>
+ *        <refreshRate>
+ *             <minimum>60</minimum>
+ *             <maximum>60</maximum>
+ *         </refreshRate>
+ *         <supportedModes>
+ *             <point>
+ *                 <first>60</first>   // refreshRate
+ *                 <second>60</second> //vsyncRate
+ *             </point>
+ *             <point>
+ *                 <first>120</first>   // refreshRate
+ *                 <second>120</second> //vsyncRate
+ *             </point>
+ *          </supportedModes>
  *      </proxSensor>
  *
  *      <ambientLightHorizonLong>10001</ambientLightHorizonLong>
@@ -581,15 +595,15 @@
     private final Context mContext;
 
     // The details of the ambient light sensor associated with this display.
-    private final SensorData mAmbientLightSensor = new SensorData();
+    private SensorData mAmbientLightSensor;
 
     // The details of the doze brightness sensor associated with this display.
-    private final SensorData mScreenOffBrightnessSensor = new SensorData();
+    private SensorData mScreenOffBrightnessSensor;
 
     // The details of the proximity sensor associated with this display.
     // Is null when no sensor should be used for that display
     @Nullable
-    private SensorData mProximitySensor = new SensorData();
+    private SensorData mProximitySensor;
 
     private final List<RefreshRateLimitation> mRefreshRateLimitations =
             new ArrayList<>(2 /*initialCapacity*/);
@@ -1913,9 +1927,10 @@
                 loadLuxThrottling(config);
                 loadQuirks(config);
                 loadBrightnessRamps(config);
-                loadAmbientLightSensorFromDdc(config);
-                loadScreenOffBrightnessSensorFromDdc(config);
-                loadProxSensorFromDdc(config);
+                mAmbientLightSensor = SensorData.loadAmbientLightSensorConfig(config,
+                        mContext.getResources());
+                mScreenOffBrightnessSensor = SensorData.loadScreenOffBrightnessSensorConfig(config);
+                mProximitySensor = SensorData.loadProxSensorConfig(config);
                 loadAmbientHorizonFromDdc(config);
                 loadBrightnessChangeThresholds(config);
                 loadAutoBrightnessConfigValues(config);
@@ -1940,9 +1955,9 @@
         loadBrightnessConstraintsFromConfigXml();
         loadBrightnessMapFromConfigXml();
         loadBrightnessRampsFromConfigXml();
-        loadAmbientLightSensorFromConfigXml();
+        mAmbientLightSensor = SensorData.loadAmbientLightSensorConfig(mContext.getResources());
+        mProximitySensor = SensorData.loadSensorUnspecifiedConfig();
         loadBrightnessChangeThresholdsFromXml();
-        setProxSensorUnspecified();
         loadAutoBrightnessConfigsFromConfigXml();
         loadAutoBrightnessAvailableFromConfigXml();
         loadRefreshRateSetting(null);
@@ -1966,8 +1981,8 @@
         mBrightnessRampDecreaseMaxIdleMillis = 0;
         mBrightnessRampIncreaseMaxIdleMillis = 0;
         setSimpleMappingStrategyValues();
-        loadAmbientLightSensorFromConfigXml();
-        setProxSensorUnspecified();
+        mAmbientLightSensor = SensorData.loadAmbientLightSensorConfig(mContext.getResources());
+        mProximitySensor = SensorData.loadSensorUnspecifiedConfig();
         loadAutoBrightnessAvailableFromConfigXml();
     }
 
@@ -2919,64 +2934,10 @@
         mBrightnessRampSlowDecrease = mBrightnessRampSlowIncrease;
     }
 
-    private void loadAmbientLightSensorFromConfigXml() {
-        mAmbientLightSensor.name = "";
-        mAmbientLightSensor.type = mContext.getResources().getString(
-                com.android.internal.R.string.config_displayLightSensorType);
-    }
-
     private void loadAutoBrightnessConfigsFromConfigXml() {
         loadAutoBrightnessDisplayBrightnessMapping(null /*AutoBrightnessConfig*/);
     }
 
-    private void loadAmbientLightSensorFromDdc(DisplayConfiguration config) {
-        final SensorDetails sensorDetails = config.getLightSensor();
-        if (sensorDetails != null) {
-            loadSensorData(sensorDetails, mAmbientLightSensor);
-        } else {
-            loadAmbientLightSensorFromConfigXml();
-        }
-    }
-
-    private void setProxSensorUnspecified() {
-        mProximitySensor = new SensorData();
-    }
-
-    private void loadScreenOffBrightnessSensorFromDdc(DisplayConfiguration config) {
-        final SensorDetails sensorDetails = config.getScreenOffBrightnessSensor();
-        if (sensorDetails != null) {
-            loadSensorData(sensorDetails, mScreenOffBrightnessSensor);
-        }
-    }
-
-    private void loadProxSensorFromDdc(DisplayConfiguration config) {
-        SensorDetails sensorDetails = config.getProxSensor();
-        if (sensorDetails != null) {
-            String name = sensorDetails.getName();
-            String type = sensorDetails.getType();
-            if ("".equals(name) && "".equals(type)) {
-                // <proxSensor> with empty values to the config means no sensor should be used
-                mProximitySensor = null;
-            } else {
-                mProximitySensor = new SensorData();
-                loadSensorData(sensorDetails, mProximitySensor);
-            }
-        } else {
-            setProxSensorUnspecified();
-        }
-    }
-
-    private void loadSensorData(@NonNull SensorDetails sensorDetails,
-            @NonNull SensorData sensorData) {
-        sensorData.name = sensorDetails.getName();
-        sensorData.type = sensorDetails.getType();
-        final RefreshRateRange rr = sensorDetails.getRefreshRate();
-        if (rr != null) {
-            sensorData.minRefreshRate = rr.getMinimum().floatValue();
-            sensorData.maxRefreshRate = rr.getMaximum().floatValue();
-        }
-    }
-
     private void loadBrightnessChangeThresholdsFromXml() {
         loadBrightnessChangeThresholds(/* config= */ null);
     }
@@ -3390,37 +3351,6 @@
     }
 
     /**
-     * Uniquely identifies a Sensor, with the combination of Type and Name.
-     */
-    public static class SensorData {
-        public String type;
-        public String name;
-        public float minRefreshRate = 0.0f;
-        public float maxRefreshRate = Float.POSITIVE_INFINITY;
-
-        @Override
-        public String toString() {
-            return "Sensor{"
-                    + "type: " + type
-                    + ", name: " + name
-                    + ", refreshRateRange: [" + minRefreshRate + ", " + maxRefreshRate + "]"
-                    + "} ";
-        }
-
-        /**
-         * @return True if the sensor matches both the specified name and type, or one if only one
-         * is specified (not-empty). Always returns false if both parameters are null or empty.
-         */
-        public boolean matches(String sensorName, String sensorType) {
-            final boolean isNameSpecified = !TextUtils.isEmpty(sensorName);
-            final boolean isTypeSpecified = !TextUtils.isEmpty(sensorType);
-            return (isNameSpecified || isTypeSpecified)
-                    && (!isNameSpecified || sensorName.equals(name))
-                    && (!isTypeSpecified || sensorType.equals(type));
-        }
-    }
-
-    /**
      * Container for high brightness mode configuration data.
      */
     static class HighBrightnessModeData {
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index eae153c..11f4e5f 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -158,7 +158,7 @@
 import com.android.server.SystemService;
 import com.android.server.UiThread;
 import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
-import com.android.server.display.DisplayDeviceConfig.SensorData;
+import com.android.server.display.config.SensorData;
 import com.android.server.display.feature.DeviceConfigParameterProvider;
 import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.layout.Layout;
@@ -1607,6 +1607,19 @@
         final long secondToken = Binder.clearCallingIdentity();
         try {
             final int displayId;
+            final String displayUniqueId = VirtualDisplayAdapter.generateDisplayUniqueId(
+                    packageName, callingUid, virtualDisplayConfig);
+
+            if (virtualDisplayConfig.isHomeSupported()) {
+                if ((flags & VIRTUAL_DISPLAY_FLAG_TRUSTED) == 0) {
+                    Slog.w(TAG, "Display created with home support but lacks "
+                            + "VIRTUAL_DISPLAY_FLAG_TRUSTED, ignoring the home support request.");
+                } else {
+                    mWindowManagerInternal.setHomeSupportedOnDisplay(displayUniqueId,
+                            Display.TYPE_VIRTUAL, true);
+                }
+            }
+
             synchronized (mSyncRoot) {
                 displayId =
                         createVirtualDisplayLocked(
@@ -1614,6 +1627,7 @@
                                 projection,
                                 callingUid,
                                 packageName,
+                                displayUniqueId,
                                 virtualDevice,
                                 surface,
                                 flags,
@@ -1625,6 +1639,13 @@
                 }
             }
 
+            if (displayId == Display.INVALID_DISPLAY && virtualDisplayConfig.isHomeSupported()
+                    && (flags & VIRTUAL_DISPLAY_FLAG_TRUSTED) != 0) {
+                // Failed to create the virtual display, so we should clean up the WM settings
+                // because it won't receive the onDisplayRemoved callback.
+                mWindowManagerInternal.clearDisplaySettings(displayUniqueId, Display.TYPE_VIRTUAL);
+            }
+
             // Build a session describing the MediaProjection instance, if there is one. A session
             // for a VirtualDisplay or physical display mirroring is handled in DisplayContent.
             ContentRecordingSession session = null;
@@ -1698,6 +1719,7 @@
             IMediaProjection projection,
             int callingUid,
             String packageName,
+            String uniqueId,
             IVirtualDevice virtualDevice,
             Surface surface,
             int flags,
@@ -1710,10 +1732,9 @@
             return -1;
         }
 
-
         Slog.d(TAG, "Virtual Display: creating DisplayDevice with VirtualDisplayAdapter");
         DisplayDevice device = mVirtualDisplayAdapter.createVirtualDisplayLocked(
-                callback, projection, callingUid, packageName, surface, flags,
+                callback, projection, callingUid, packageName, uniqueId, surface, flags,
                 virtualDisplayConfig);
         if (device == null) {
             Slog.w(TAG, "Virtual Display: VirtualDisplayAdapter failed to create DisplayDevice");
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index d8ac52e..5761c31 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -1540,12 +1540,13 @@
                 performScreenOffTransition = true;
                 break;
             case DisplayPowerRequest.POLICY_DOZE:
-                if (mPowerRequest.dozeScreenState != Display.STATE_UNKNOWN) {
+                if (mDozeStateOverride != Display.STATE_UNKNOWN) {
+                    state = mDozeStateOverride;
+                } else if (mPowerRequest.dozeScreenState != Display.STATE_UNKNOWN) {
                     state = mPowerRequest.dozeScreenState;
                 } else {
                     state = Display.STATE_DOZE;
                 }
-                state = mDozeStateOverride == Display.STATE_UNKNOWN ? state : mDozeStateOverride;
                 if (!mAllowAutoBrightnessWhileDozingConfig) {
                     brightnessState = mPowerRequest.dozeScreenBrightness;
                     mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE);
diff --git a/services/core/java/com/android/server/display/DisplayPowerState.java b/services/core/java/com/android/server/display/DisplayPowerState.java
index be03a80..f994c05 100644
--- a/services/core/java/com/android/server/display/DisplayPowerState.java
+++ b/services/core/java/com/android/server/display/DisplayPowerState.java
@@ -320,7 +320,9 @@
     public void stop() {
         mStopped = true;
         mPhotonicModulator.interrupt();
-        dismissColorFade();
+        if (mColorFade != null) {
+            mColorFade.destroy();
+        }
         mCleanListener = null;
         mHandler.removeCallbacksAndMessages(null);
     }
diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
index b002587..90e32a6 100644
--- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
@@ -64,7 +64,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.io.PrintWriter;
-import java.util.Iterator;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * A display adapter that provides virtual displays on behalf of applications.
@@ -72,15 +72,17 @@
  * Display adapters are guarded by the {@link DisplayManagerService.SyncRoot} lock.
  * </p>
  */
-@VisibleForTesting
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
 public class VirtualDisplayAdapter extends DisplayAdapter {
     static final String TAG = "VirtualDisplayAdapter";
-    static final boolean DEBUG = false;
 
     // Unique id prefix for virtual displays
     @VisibleForTesting
     static final String UNIQUE_ID_PREFIX = "virtual:";
 
+    // Unique id suffix for virtual displays
+    private static final AtomicInteger sNextUniqueIndex = new AtomicInteger(0);
+
     private final ArrayMap<IBinder, VirtualDisplayDevice> mVirtualDisplayDevices = new ArrayMap<>();
     private final Handler mHandler;
     private final SurfaceControlDisplayFactory mSurfaceControlDisplayFactory;
@@ -111,8 +113,8 @@
     }
 
     public DisplayDevice createVirtualDisplayLocked(IVirtualDisplayCallback callback,
-            IMediaProjection projection, int ownerUid, String ownerPackageName, Surface surface,
-            int flags, VirtualDisplayConfig virtualDisplayConfig) {
+            IMediaProjection projection, int ownerUid, String ownerPackageName, String uniqueId,
+            Surface surface, int flags, VirtualDisplayConfig virtualDisplayConfig) {
         IBinder appToken = callback.asBinder();
         if (mVirtualDisplayDevices.containsKey(appToken)) {
             Slog.wtfStack(TAG,
@@ -125,23 +127,13 @@
 
         IBinder displayToken = mSurfaceControlDisplayFactory.createDisplay(name, secure,
                 virtualDisplayConfig.getRequestedRefreshRate());
-        final String baseUniqueId =
-                UNIQUE_ID_PREFIX + ownerPackageName + "," + ownerUid + "," + name + ",";
-        final int uniqueIndex = getNextUniqueIndex(baseUniqueId);
-        String uniqueId = virtualDisplayConfig.getUniqueId();
-        if (uniqueId == null) {
-            uniqueId = baseUniqueId + uniqueIndex;
-        } else {
-            uniqueId = UNIQUE_ID_PREFIX + ownerPackageName + ":" + uniqueId;
-        }
         MediaProjectionCallback mediaProjectionCallback =  null;
         if (projection != null) {
             mediaProjectionCallback = new MediaProjectionCallback(appToken);
         }
         VirtualDisplayDevice device = new VirtualDisplayDevice(displayToken, appToken,
-                ownerUid, ownerPackageName, surface, flags,
-                new Callback(callback, mHandler), projection, mediaProjectionCallback,
-                uniqueId, uniqueIndex, virtualDisplayConfig);
+                ownerUid, ownerPackageName, surface, flags, new Callback(callback, mHandler),
+                projection, mediaProjectionCallback, uniqueId, virtualDisplayConfig);
 
         mVirtualDisplayDevices.put(appToken, device);
 
@@ -219,26 +211,20 @@
     }
 
     /**
-     * Returns the next unique index for the uniqueIdPrefix
+     * Generates a virtual display's unique identifier.
+     *
+     * <p>It is always prefixed with "virtual:package-name". If the provided config explicitly
+     * specifies a unique ID, then it's simply appended. Otherwise, the UID, display name and a
+     * unique index are appended.</p>
+     *
+     * <p>The unique index is incremented for every virtual display unique ID generation and serves
+     * for differentiating between displays with the same name created by the same owner.</p>
      */
-    private int getNextUniqueIndex(String uniqueIdPrefix) {
-        if (mVirtualDisplayDevices.isEmpty()) {
-            return 0;
-        }
-
-        int nextUniqueIndex = 0;
-        Iterator<VirtualDisplayDevice> it = mVirtualDisplayDevices.values().iterator();
-        while (it.hasNext()) {
-            VirtualDisplayDevice device = it.next();
-            if (device.getUniqueId().startsWith(uniqueIdPrefix)
-                    && device.mUniqueIndex >= nextUniqueIndex) {
-                // Increment the next unique index to be greater than ones we have already ran
-                // across for displays that have the same unique Id prefix.
-                nextUniqueIndex = device.mUniqueIndex + 1;
-            }
-        }
-
-        return nextUniqueIndex;
+    static String generateDisplayUniqueId(String packageName, int uid,
+            VirtualDisplayConfig config) {
+        return UNIQUE_ID_PREFIX + packageName + ((config.getUniqueId() != null)
+                ? (":" + config.getUniqueId())
+                : ("," + uid + "," + config.getName() + "," + sNextUniqueIndex.getAndIncrement()));
     }
 
     private void handleBinderDiedLocked(IBinder appToken) {
@@ -278,7 +264,6 @@
         private int mDisplayState;
         private boolean mStopped;
         private int mPendingChanges;
-        private int mUniqueIndex;
         private Display.Mode mMode;
         private boolean mIsDisplayOn;
         private int mDisplayIdToMirror;
@@ -287,7 +272,7 @@
         public VirtualDisplayDevice(IBinder displayToken, IBinder appToken,
                 int ownerUid, String ownerPackageName, Surface surface, int flags,
                 Callback callback, IMediaProjection projection,
-                IMediaProjectionCallback mediaProjectionCallback, String uniqueId, int uniqueIndex,
+                IMediaProjectionCallback mediaProjectionCallback, String uniqueId,
                 VirtualDisplayConfig virtualDisplayConfig) {
             super(VirtualDisplayAdapter.this, displayToken, uniqueId, getContext());
             mAppToken = appToken;
@@ -306,7 +291,6 @@
             mMediaProjectionCallback = mediaProjectionCallback;
             mDisplayState = Display.STATE_UNKNOWN;
             mPendingChanges |= PENDING_SURFACE_CHANGE;
-            mUniqueIndex = uniqueIndex;
             mIsDisplayOn = surface != null;
             mDisplayIdToMirror = virtualDisplayConfig.getDisplayIdToMirror();
             mIsWindowManagerMirroring = virtualDisplayConfig.isWindowManagerMirroringEnabled();
diff --git a/services/core/java/com/android/server/display/config/SensorData.java b/services/core/java/com/android/server/display/config/SensorData.java
new file mode 100644
index 0000000..3bb35bf
--- /dev/null
+++ b/services/core/java/com/android/server/display/config/SensorData.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.config;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.res.Resources;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Uniquely identifies a Sensor, with the combination of Type and Name.
+ */
+public class SensorData {
+
+    @Nullable
+    public final String type;
+    @Nullable
+    public final String name;
+    public final float minRefreshRate;
+    public final float maxRefreshRate;
+    public final List<SupportedMode> supportedModes;
+
+    @VisibleForTesting
+    public SensorData() {
+        this(/* type= */ null, /* name= */ null);
+    }
+
+    @VisibleForTesting
+    public SensorData(String type, String name) {
+        this(type, name, /* minRefreshRate= */ 0f, /* maxRefreshRate= */ Float.POSITIVE_INFINITY);
+    }
+
+    @VisibleForTesting
+    public SensorData(String type, String name, float minRefreshRate, float maxRefreshRate) {
+        this(type, name, minRefreshRate, maxRefreshRate, /* supportedModes= */ List.of());
+    }
+
+    @VisibleForTesting
+    public SensorData(String type, String name, float minRefreshRate, float maxRefreshRate,
+            List<SupportedMode> supportedModes) {
+        this.type = type;
+        this.name = name;
+        this.minRefreshRate = minRefreshRate;
+        this.maxRefreshRate = maxRefreshRate;
+        this.supportedModes = Collections.unmodifiableList(supportedModes);
+    }
+
+    /**
+     * @return True if the sensor matches both the specified name and type, or one if only one
+     * is specified (not-empty). Always returns false if both parameters are null or empty.
+     */
+    public boolean matches(String sensorName, String sensorType) {
+        final boolean isNameSpecified = !TextUtils.isEmpty(sensorName);
+        final boolean isTypeSpecified = !TextUtils.isEmpty(sensorType);
+        return (isNameSpecified || isTypeSpecified)
+                && (!isNameSpecified || sensorName.equals(name))
+                && (!isTypeSpecified || sensorType.equals(type));
+    }
+
+    @Override
+    public String toString() {
+        return "SensorData{"
+                + "type= " + type
+                + ", name= " + name
+                + ", refreshRateRange: [" + minRefreshRate + ", " + maxRefreshRate + "]"
+                + ", supportedModes=" + supportedModes
+                + '}';
+    }
+
+    /**
+     * Loads ambient light sensor data from DisplayConfiguration and if missing from resources xml
+     */
+    public static SensorData loadAmbientLightSensorConfig(DisplayConfiguration config,
+            Resources resources) {
+        SensorDetails sensorDetails = config.getLightSensor();
+        if (sensorDetails != null) {
+            return loadSensorData(sensorDetails);
+        } else {
+            return loadAmbientLightSensorConfig(resources);
+        }
+    }
+
+    /**
+     * Loads ambient light sensor data from resources xml
+     */
+    public static SensorData loadAmbientLightSensorConfig(Resources resources) {
+        return new SensorData(
+                resources.getString(com.android.internal.R.string.config_displayLightSensorType),
+                /* name= */ "");
+    }
+
+    /**
+     * Loads screen off brightness sensor data from DisplayConfiguration
+     */
+    public static SensorData loadScreenOffBrightnessSensorConfig(DisplayConfiguration config) {
+        SensorDetails sensorDetails = config.getScreenOffBrightnessSensor();
+        if (sensorDetails != null) {
+            return loadSensorData(sensorDetails);
+        } else {
+            return new SensorData();
+        }
+    }
+
+    /**
+     * Loads proximity sensor data from DisplayConfiguration
+     */
+    @Nullable
+    public static SensorData loadProxSensorConfig(DisplayConfiguration config) {
+        SensorDetails sensorDetails = config.getProxSensor();
+        if (sensorDetails != null) {
+            String name = sensorDetails.getName();
+            String type = sensorDetails.getType();
+            if ("".equals(name) && "".equals(type)) {
+                // <proxSensor> with empty values to the config means no sensor should be used.
+                // See also {@link com.android.server.display.utils.SensorUtils}
+                return null;
+            } else {
+                return loadSensorData(sensorDetails);
+            }
+        } else {
+            return new SensorData();
+        }
+    }
+
+    /**
+     * Loads sensor unspecified config, this means system should use default sensor.
+     * See also {@link com.android.server.display.utils.SensorUtils}
+     */
+    @NonNull
+    public static SensorData loadSensorUnspecifiedConfig() {
+        return new SensorData();
+    }
+
+    private static SensorData loadSensorData(@NonNull SensorDetails sensorDetails) {
+        float minRefreshRate = 0f;
+        float maxRefreshRate = Float.POSITIVE_INFINITY;
+        RefreshRateRange rr = sensorDetails.getRefreshRate();
+        if (rr != null) {
+            minRefreshRate = rr.getMinimum().floatValue();
+            maxRefreshRate = rr.getMaximum().floatValue();
+        }
+        ArrayList<SupportedMode> supportedModes = new ArrayList<>();
+        NonNegativeFloatToFloatMap configSupportedModes = sensorDetails.getSupportedModes();
+        if (configSupportedModes != null) {
+            for (NonNegativeFloatToFloatPoint supportedMode : configSupportedModes.getPoint()) {
+                supportedModes.add(new SupportedMode(supportedMode.getFirst().floatValue(),
+                        supportedMode.getSecond().floatValue()));
+            }
+        }
+
+        return new SensorData(sensorDetails.getType(), sensorDetails.getName(), minRefreshRate,
+                maxRefreshRate, supportedModes);
+    }
+
+    public static class SupportedMode {
+        public final float refreshRate;
+        public final float vsyncRate;
+
+        public SupportedMode(float refreshRate, float vsyncRate) {
+            this.refreshRate = refreshRate;
+            this.vsyncRate = vsyncRate;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/display/state/DisplayStateController.java b/services/core/java/com/android/server/display/state/DisplayStateController.java
index 5d6e650..5f28934 100644
--- a/services/core/java/com/android/server/display/state/DisplayStateController.java
+++ b/services/core/java/com/android/server/display/state/DisplayStateController.java
@@ -61,12 +61,13 @@
                 mPerformScreenOffTransition = true;
                 break;
             case DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE:
-                if (displayPowerRequest.dozeScreenState != Display.STATE_UNKNOWN) {
+                if (mDozeStateOverride != Display.STATE_UNKNOWN) {
+                    state = mDozeStateOverride;
+                } else if (displayPowerRequest.dozeScreenState != Display.STATE_UNKNOWN) {
                     state = displayPowerRequest.dozeScreenState;
                 } else {
                     state = Display.STATE_DOZE;
                 }
-                state = mDozeStateOverride == Display.STATE_UNKNOWN ? state : mDozeStateOverride;
                 break;
             case DisplayManagerInternal.DisplayPowerRequest.POLICY_DIM:
             case DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT:
diff --git a/services/core/java/com/android/server/display/utils/SensorUtils.java b/services/core/java/com/android/server/display/utils/SensorUtils.java
index 56321cd..8b9fe108 100644
--- a/services/core/java/com/android/server/display/utils/SensorUtils.java
+++ b/services/core/java/com/android/server/display/utils/SensorUtils.java
@@ -21,7 +21,7 @@
 import android.hardware.SensorManager;
 import android.text.TextUtils;
 
-import com.android.server.display.DisplayDeviceConfig;
+import com.android.server.display.config.SensorData;
 
 import java.util.List;
 
@@ -36,7 +36,7 @@
      */
     @Nullable
     public static Sensor findSensor(@Nullable SensorManager sensorManager,
-            @Nullable DisplayDeviceConfig.SensorData sensorData, int fallbackType) {
+            @Nullable SensorData sensorData, int fallbackType) {
         if (sensorData == null) {
             return null;
         } else {
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 568618e..57e424d 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -243,6 +243,10 @@
     private static final String MIGRATED_FRP2 = "migrated_frp2";
     private static final String MIGRATED_KEYSTORE_NS = "migrated_keystore_namespace";
     private static final String MIGRATED_SP_CE_ONLY = "migrated_all_users_to_sp_and_bound_ce";
+    private static final String MIGRATED_SP_FULL = "migrated_all_users_to_sp_and_bound_keys";
+
+    private static final boolean FIX_UNLOCKED_DEVICE_REQUIRED_KEYS =
+            android.security.Flags.fixUnlockedDeviceRequiredKeys();
 
     // Duration that LockSettingsService will store the gatekeeper password for. This allows
     // multiple biometric enrollments without prompting the user to enter their password via
@@ -856,9 +860,11 @@
         @Override
         public void onReceive(Context context, Intent intent) {
             if (Intent.ACTION_USER_ADDED.equals(intent.getAction())) {
-                // Notify keystore that a new user was added.
-                final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
-                AndroidKeyStoreMaintenance.onUserAdded(userHandle);
+                if (!FIX_UNLOCKED_DEVICE_REQUIRED_KEYS) {
+                    // Notify keystore that a new user was added.
+                    final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
+                    AndroidKeyStoreMaintenance.onUserAdded(userHandle);
+                }
             } else if (Intent.ACTION_USER_STARTING.equals(intent.getAction())) {
                 final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
                 mStorage.prefetchUser(userHandle);
@@ -1022,24 +1028,53 @@
             }
             mEarlyCreatedUsers = null; // no longer needed
 
-            // Also do a one-time migration of all users to SP-based credentials with the CE key
-            // encrypted by the SP.  This is needed for the system user on the first boot of a
-            // device, as the system user is special and never goes through the user creation flow
-            // that other users do.  It is also needed for existing users on a device upgraded from
-            // Android 13 or earlier, where users with no LSKF didn't necessarily have an SP, and if
-            // they did have an SP then their CE key wasn't encrypted by it.
+            // Do a one-time migration for any unsecured users: create the user's synthetic password
+            // if not already done, encrypt the user's CE key with the synthetic password if not
+            // already done, and create the user's Keystore super keys if not already done.
             //
-            // If this gets interrupted (e.g. by the device powering off), there shouldn't be a
-            // problem since this will run again on the next boot, and setCeStorageProtection() is
-            // okay with the CE key being already protected by the given secret.
-            if (getString(MIGRATED_SP_CE_ONLY, null, 0) == null) {
-                for (UserInfo user : mUserManager.getAliveUsers()) {
-                    removeStateForReusedUserIdIfNecessary(user.id, user.serialNumber);
-                    synchronized (mSpManager) {
-                        migrateUserToSpWithBoundCeKeyLocked(user.id);
+            // This is needed for the following cases:
+            //
+            // - Finalizing the creation of the system user on the first boot of a device, as the
+            //   system user is special and doesn't go through the normal user creation flow.
+            //
+            // - Upgrading from Android 13 or earlier, where unsecured users didn't necessarily have
+            //   a synthetic password, and if they did have a synthetic password their CE key wasn't
+            //   encrypted by it.  Also, unsecured users didn't have Keystore super keys.
+            //
+            // - Upgrading from Android 14, where unsecured users didn't have Keystore super keys.
+            //
+            // The end result is that all users, regardless of whether they are secured or not, have
+            // a synthetic password with all keys initialized and protected by it.
+            //
+            // Note: if this migration gets interrupted (e.g. by the device powering off), there
+            // shouldn't be a problem since this will run again on the next boot, and
+            // setCeStorageProtection() and initKeystoreSuperKeys(..., true) are idempotent.
+            if (FIX_UNLOCKED_DEVICE_REQUIRED_KEYS) {
+                if (!getBoolean(MIGRATED_SP_FULL, false, 0)) {
+                    for (UserInfo user : mUserManager.getAliveUsers()) {
+                        removeStateForReusedUserIdIfNecessary(user.id, user.serialNumber);
+                        synchronized (mSpManager) {
+                            migrateUserToSpWithBoundKeysLocked(user.id);
+                        }
                     }
+                    setBoolean(MIGRATED_SP_FULL, true, 0);
                 }
-                setString(MIGRATED_SP_CE_ONLY, "true", 0);
+            } else {
+                if (getString(MIGRATED_SP_CE_ONLY, null, 0) == null) {
+                    for (UserInfo user : mUserManager.getAliveUsers()) {
+                        removeStateForReusedUserIdIfNecessary(user.id, user.serialNumber);
+                        synchronized (mSpManager) {
+                            migrateUserToSpWithBoundCeKeyLocked(user.id);
+                        }
+                    }
+                    setString(MIGRATED_SP_CE_ONLY, "true", 0);
+                }
+
+                if (getBoolean(MIGRATED_SP_FULL, false, 0)) {
+                    // The FIX_UNLOCKED_DEVICE_REQUIRED_KEYS flag was enabled but then got disabled.
+                    // Ensure the full migration runs again the next time the flag is enabled...
+                    setBoolean(MIGRATED_SP_FULL, false, 0);
+                }
             }
 
             mThirdPartyAppsStarted = true;
@@ -1070,6 +1105,37 @@
         }
     }
 
+    @GuardedBy("mSpManager")
+    private void migrateUserToSpWithBoundKeysLocked(@UserIdInt int userId) {
+        if (isUserSecure(userId)) {
+            Slogf.d(TAG, "User %d is secured; no migration needed", userId);
+            return;
+        }
+        long protectorId = getCurrentLskfBasedProtectorId(userId);
+        if (protectorId == SyntheticPasswordManager.NULL_PROTECTOR_ID) {
+            Slogf.i(TAG, "Migrating unsecured user %d to SP-based credential", userId);
+            initializeSyntheticPassword(userId);
+            return;
+        }
+        Slogf.i(TAG, "Existing unsecured user %d has a synthetic password", userId);
+        AuthenticationResult result = mSpManager.unlockLskfBasedProtector(
+                getGateKeeperService(), protectorId, LockscreenCredential.createNone(), userId,
+                null);
+        SyntheticPassword sp = result.syntheticPassword;
+        if (sp == null) {
+            Slogf.wtf(TAG, "Failed to unwrap synthetic password for unsecured user %d", userId);
+            return;
+        }
+        // While setCeStorageProtection() is idempotent, it does log some error messages when called
+        // again.  Skip it if we know it was already handled by an earlier upgrade to Android 14.
+        if (getString(MIGRATED_SP_CE_ONLY, null, 0) == null) {
+            Slogf.i(TAG, "Encrypting CE key of user %d with synthetic password", userId);
+            setCeStorageProtection(userId, sp);
+        }
+        Slogf.i(TAG, "Initializing Keystore super keys for user %d", userId);
+        initKeystoreSuperKeys(userId, sp, /* allowExisting= */ true);
+    }
+
     /**
      * Returns the lowest password quality that still presents the same UI for entering it.
      *
@@ -1351,6 +1417,20 @@
         AndroidKeyStoreMaintenance.onUserPasswordChanged(userHandle, password);
     }
 
+    @VisibleForTesting /** Note: this method is overridden in unit tests */
+    void initKeystoreSuperKeys(@UserIdInt int userId, SyntheticPassword sp, boolean allowExisting) {
+        final byte[] password = sp.deriveKeyStorePassword();
+        try {
+            int res = AndroidKeyStoreMaintenance.initUserSuperKeys(userId, password, allowExisting);
+            if (res != 0) {
+                throw new IllegalStateException("Failed to initialize Keystore super keys for user "
+                        + userId);
+            }
+        } finally {
+            Arrays.fill(password, (byte) 0);
+        }
+    }
+
     private void unlockKeystore(int userId, SyntheticPassword sp) {
         Authorization.onLockScreenEvent(false, userId, sp.deriveKeyStorePassword(), null);
     }
@@ -2074,6 +2154,9 @@
                 return;
             }
             onSyntheticPasswordUnlocked(userId, result.syntheticPassword);
+            if (FIX_UNLOCKED_DEVICE_REQUIRED_KEYS) {
+                unlockKeystore(userId, result.syntheticPassword);
+            }
             unlockCeStorage(userId, result.syntheticPassword);
         }
     }
@@ -2353,6 +2436,16 @@
     }
 
     private void createNewUser(@UserIdInt int userId, int userSerialNumber) {
+
+        // Delete all Keystore keys for userId, just in case any were left around from a removed
+        // user with the same userId.  This should be unnecessary, but we've been doing this for a
+        // long time, so for now we keep doing it just in case it's ever important.  Don't wait
+        // until initKeystoreSuperKeys() to do this; that can be delayed if the user is being
+        // created during early boot, and maybe something will use Keystore before then.
+        if (FIX_UNLOCKED_DEVICE_REQUIRED_KEYS) {
+            AndroidKeyStoreMaintenance.onUserAdded(userId);
+        }
+
         synchronized (mUserCreationAndRemovalLock) {
             // During early boot, don't actually create the synthetic password yet, but rather
             // automatically delay it to later.  We do this because protecting the synthetic
@@ -2759,7 +2852,7 @@
 
     /**
      * Creates the synthetic password (SP) for the given user, protects it with an empty LSKF, and
-     * protects the user's CE key with a key derived from the SP.
+     * protects the user's CE storage key and Keystore super keys with keys derived from the SP.
      *
      * <p>This is called just once in the lifetime of the user: at user creation time (possibly
      * delayed until the time when Weaver is guaranteed to be available), or when upgrading from
@@ -2778,6 +2871,9 @@
                     LockscreenCredential.createNone(), sp, userId);
             setCurrentLskfBasedProtectorId(protectorId, userId);
             setCeStorageProtection(userId, sp);
+            if (FIX_UNLOCKED_DEVICE_REQUIRED_KEYS) {
+                initKeystoreSuperKeys(userId, sp, /* allowExisting= */ false);
+            }
             onSyntheticPasswordCreated(userId, sp);
             Slogf.i(TAG, "Successfully initialized synthetic password for user %d", userId);
             return sp;
@@ -2870,11 +2966,10 @@
     /**
      * Changes the user's LSKF by creating an LSKF-based protector that uses the new LSKF (which may
      * be empty) and replacing the old LSKF-based protector with it.  The SP itself is not changed.
-     *
-     * Also maintains the invariants described in {@link SyntheticPasswordManager} by
-     * setting/clearing the protection (by the SP) on the user's auth-bound Keystore keys when the
-     * LSKF is added/removed, respectively.  If an LSKF is being added, then the Gatekeeper auth
-     * token is also refreshed.
+     * <p>
+     * Also maintains the invariants described in {@link SyntheticPasswordManager} by enrolling /
+     * deleting the synthetic password into Gatekeeper as the LSKF is set / cleared, and asking
+     * Keystore to delete the user's auth-bound keys when the LSKF is cleared.
      */
     @GuardedBy("mSpManager")
     private long setLockCredentialWithSpLocked(LockscreenCredential credential,
@@ -2893,7 +2988,9 @@
             if (!mSpManager.hasSidForUser(userId)) {
                 mSpManager.newSidForUser(getGateKeeperService(), sp, userId);
                 mSpManager.verifyChallenge(getGateKeeperService(), sp, 0L, userId);
-                setKeystorePassword(sp.deriveKeyStorePassword(), userId);
+                if (!FIX_UNLOCKED_DEVICE_REQUIRED_KEYS) {
+                    setKeystorePassword(sp.deriveKeyStorePassword(), userId);
+                }
             }
         } else {
             // Cache all profile password if they use unified work challenge. This will later be
@@ -2904,7 +3001,11 @@
             gateKeeperClearSecureUserId(userId);
             unlockCeStorage(userId, sp);
             unlockKeystore(userId, sp);
-            setKeystorePassword(null, userId);
+            if (FIX_UNLOCKED_DEVICE_REQUIRED_KEYS) {
+                AndroidKeyStoreMaintenance.onUserLskfRemoved(userId);
+            } else {
+                setKeystorePassword(null, userId);
+            }
             removeBiometricsForUser(userId);
         }
         setCurrentLskfBasedProtectorId(newProtectorId, userId);
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
index 8e9c21f..cc205d4 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -90,10 +90,15 @@
  *
  *  - The user's credential-encrypted storage is always protected by the SP.
  *
- *  - The user's auth-bound Keystore keys are protected by the SP, but only while an LSKF is set.
- *    This works by setting the user's Keystore and Gatekeeper passwords to SP-derived secrets, but
- *    only while an LSKF is set.  When the LSKF is removed, these passwords are cleared,
- *    invalidating the user's auth-bound keys.
+ *  - The user's Keystore superencryption keys are always protected by the SP.  These in turn
+ *    protect the Keystore keys that require user authentication, an unlocked device, or both.
+ *
+ *  - A secret derived from the synthetic password is enrolled in Gatekeeper for the user, but only
+ *    while the user has a (nonempty) LSKF.  This enrollment has an associated ID called the Secure
+ *    user ID or SID.  This use of Gatekeeper, which is separate from the use of GateKeeper that may
+ *    be used in the LSKF-based protector, makes it so that unlocking the synthetic password
+ *    generates a HardwareAuthToken (but only when the user has LSKF).  That HardwareAuthToken can
+ *    be provided to KeyMint to authorize the use of the user's authentication-bound Keystore keys.
  *
  * Files stored on disk for each user:
  *   For the SP itself, stored under NULL_PROTECTOR_ID:
diff --git a/services/core/java/com/android/server/net/NetworkManagementService.java b/services/core/java/com/android/server/net/NetworkManagementService.java
index 550ad5d..681d1a0 100644
--- a/services/core/java/com/android/server/net/NetworkManagementService.java
+++ b/services/core/java/com/android/server/net/NetworkManagementService.java
@@ -74,7 +74,6 @@
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.HexDump;
 import com.android.modules.utils.build.SdkLevel;
-import com.android.net.flags.Flags;
 import com.android.net.module.util.NetdUtils;
 import com.android.net.module.util.PermissionUtils;
 import com.android.server.FgThread;
@@ -328,10 +327,10 @@
     /**
      * Notify our observers of a change in the data activity state of the interface
      */
-    private void notifyInterfaceClassActivity(int type, boolean isActive, long tsNanos,
+    private void notifyInterfaceClassActivity(int label, boolean isActive, long tsNanos,
             int uid) {
         invokeForAllObservers(o -> o.interfaceClassDataActivityChanged(
-                type, isActive, tsNanos, uid));
+                label, isActive, tsNanos, uid));
     }
 
     // Sync the state of the given chain with the native daemon.
@@ -1062,7 +1061,7 @@
             }
             Trace.traceBegin(Trace.TRACE_TAG_NETWORK, "setDataSaverModeEnabled");
             try {
-                if (Flags.setDataSaverViaCm()) {
+                if (SdkLevel.isAtLeastV()) {
                     // setDataSaverEnabled throws if it fails to set data saver.
                     mContext.getSystemService(ConnectivityManager.class)
                             .setDataSaverEnabled(enable);
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 8c75367..7e51526 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -706,6 +706,7 @@
     private boolean mNotificationEffectsEnabledForAutomotive;
     private DeviceConfig.OnPropertiesChangedListener mDeviceConfigChangedListener;
     protected NotificationAttentionHelper mAttentionHelper;
+    private boolean mFlagRefactorAttentionHelper;
 
     private int mWarnRemoteViewsSizeBytes;
     private int mStripRemoteViewsSizeBytes;
@@ -1189,7 +1190,7 @@
         @Override
         public void onSetDisabled(int status) {
             synchronized (mNotificationLock) {
-                if (Flags.refactorAttentionHelper()) {
+                if (mFlagRefactorAttentionHelper) {
                     mAttentionHelper.updateDisableNotificationEffectsLocked(status);
                 } else {
                     mDisableNotificationEffects =
@@ -1335,7 +1336,7 @@
         public void clearEffects() {
             synchronized (mNotificationLock) {
                 if (DBG) Slog.d(TAG, "clearEffects");
-                if (Flags.refactorAttentionHelper()) {
+                if (mFlagRefactorAttentionHelper) {
                     mAttentionHelper.clearAttentionEffects();
                 } else {
                     clearSoundLocked();
@@ -1564,7 +1565,7 @@
                         int changedFlags = data.getFlags() ^ flags;
                         if ((changedFlags & FLAG_SUPPRESS_NOTIFICATION) != 0) {
                             // Suppress notification flag changed, clear any effects
-                            if (Flags.refactorAttentionHelper()) {
+                            if (mFlagRefactorAttentionHelper) {
                                 mAttentionHelper.clearEffectsLocked(key);
                             } else {
                                 clearEffectsLocked(key);
@@ -1913,7 +1914,7 @@
         public void onReceive(Context context, Intent intent) {
             String action = intent.getAction();
 
-            if (!Flags.refactorAttentionHelper()) {
+            if (!mFlagRefactorAttentionHelper) {
                 if (action.equals(Intent.ACTION_SCREEN_ON)) {
                     // Keep track of screen on/off state, but do not turn off the notification light
                     // until user passes through the lock screen or views the notification.
@@ -2029,7 +2030,7 @@
             ContentResolver resolver = getContext().getContentResolver();
             resolver.registerContentObserver(NOTIFICATION_BADGING_URI,
                     false, this, UserHandle.USER_ALL);
-            if (!Flags.refactorAttentionHelper()) {
+            if (!mFlagRefactorAttentionHelper) {
                 resolver.registerContentObserver(NOTIFICATION_LIGHT_PULSE_URI,
                     false, this, UserHandle.USER_ALL);
             }
@@ -2059,7 +2060,7 @@
 
         public void update(Uri uri) {
             ContentResolver resolver = getContext().getContentResolver();
-            if (!Flags.refactorAttentionHelper()) {
+            if (!mFlagRefactorAttentionHelper) {
                 if (uri == null || NOTIFICATION_LIGHT_PULSE_URI.equals(uri)) {
                     boolean pulseEnabled = Settings.System.getIntForUser(resolver,
                         Settings.System.NOTIFICATION_LIGHT_PULSE, 0, UserHandle.USER_CURRENT)
@@ -2560,7 +2561,9 @@
 
         mToastRateLimiter = toastRateLimiter;
 
-        if (Flags.refactorAttentionHelper()) {
+        //Cache aconfig flag value
+        mFlagRefactorAttentionHelper = Flags.refactorAttentionHelper();
+        if (mFlagRefactorAttentionHelper) {
             mAttentionHelper = new NotificationAttentionHelper(getContext(), lightsManager,
                 mAccessibilityManager, mPackageManagerClient, userManager, usageStats,
                 mNotificationManagerPrivate, mZenModeHelper, flagResolver);
@@ -2570,7 +2573,7 @@
         // If this is called within a test, make sure to unregister the intent receivers by
         // calling onDestroy()
         IntentFilter filter = new IntentFilter();
-        if (!Flags.refactorAttentionHelper()) {
+        if (!mFlagRefactorAttentionHelper) {
             filter.addAction(Intent.ACTION_SCREEN_ON);
             filter.addAction(Intent.ACTION_SCREEN_OFF);
             filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
@@ -2898,7 +2901,7 @@
             }
             registerNotificationPreferencesPullers();
             new LockPatternUtils(getContext()).registerStrongAuthTracker(mStrongAuthTracker);
-            if (Flags.refactorAttentionHelper()) {
+            if (mFlagRefactorAttentionHelper) {
                 mAttentionHelper.onSystemReady();
             }
         } else if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) {
@@ -6571,7 +6574,7 @@
                     pw.println("  mMaxPackageEnqueueRate=" + mMaxPackageEnqueueRate);
                     pw.println("  hideSilentStatusBar="
                             + mPreferencesHelper.shouldHideSilentStatusIcons());
-                    if (Flags.refactorAttentionHelper()) {
+                    if (mFlagRefactorAttentionHelper) {
                         mAttentionHelper.dump(pw, "    ", filter);
                     }
                 }
@@ -7042,9 +7045,8 @@
             channelId = (new Notification.TvExtender(notification)).getChannelId();
         }
         String shortcutId = n.getShortcutId();
-        final NotificationChannel channel = mPreferencesHelper.getConversationNotificationChannel(
-                pkg, notificationUid, channelId, shortcutId,
-                true /* parent ok */, false /* includeDeleted */);
+        final NotificationChannel channel = getNotificationChannelRestoreDeleted(pkg,
+                callingUid, notificationUid, channelId, shortcutId);
         if (channel == null) {
             final String noChannelStr = "No Channel found for "
                     + "pkg=" + pkg
@@ -7162,6 +7164,35 @@
         return true;
     }
 
+    /**
+     * Returns a channel, if exists, and restores deleted conversation channels.
+     */
+    @Nullable
+    private NotificationChannel getNotificationChannelRestoreDeleted(String pkg,
+            int callingUid, int notificationUid, String channelId, String conversationId) {
+        // Restore a deleted conversation channel, if exists. Otherwise use the parent channel.
+        NotificationChannel channel = mPreferencesHelper.getConversationNotificationChannel(
+                pkg, notificationUid, channelId, conversationId,
+                true /* parent ok */, !TextUtils.isEmpty(conversationId) /* includeDeleted */);
+        // Restore deleted conversation channel
+        if (channel != null && channel.isDeleted()) {
+            if (Objects.equals(conversationId, channel.getConversationId())) {
+                boolean needsPolicyFileChange = mPreferencesHelper.createNotificationChannel(
+                        pkg, notificationUid, channel, true /* fromTargetApp */,
+                        mConditionProviders.isPackageOrComponentAllowed(pkg,
+                        UserHandle.getUserId(notificationUid)), callingUid, true);
+                // Update policy file if the conversation channel was restored
+                if (needsPolicyFileChange) {
+                    handleSavePolicyFile();
+                }
+            } else {
+                // Do not restore parent channel
+                channel = null;
+            }
+        }
+        return channel;
+    }
+
     private void onConversationRemovedInternal(String pkg, int uid, Set<String> shortcuts) {
         checkCallerIsSystem();
         Preconditions.checkStringNotEmpty(pkg);
@@ -7844,7 +7875,7 @@
             boolean wasPosted = removeFromNotificationListsLocked(r);
             cancelNotificationLocked(r, false, REASON_SNOOZED, wasPosted, null,
                     SystemClock.elapsedRealtime());
-            if (Flags.refactorAttentionHelper()) {
+            if (mFlagRefactorAttentionHelper) {
                 mAttentionHelper.updateLightsLocked();
             } else {
                 updateLightsLocked();
@@ -7984,7 +8015,7 @@
                     cancelGroupChildrenLocked(r, mCallingUid, mCallingPid, listenerName,
                             mSendDelete, childrenFlagChecker, mReason,
                             mCancellationElapsedTimeMs);
-                    if (Flags.refactorAttentionHelper()) {
+                    if (mFlagRefactorAttentionHelper) {
                         mAttentionHelper.updateLightsLocked();
                     } else {
                         updateLightsLocked();
@@ -8281,7 +8312,7 @@
 
                     int buzzBeepBlinkLoggingCode = 0;
                     if (!r.isHidden()) {
-                        if (Flags.refactorAttentionHelper()) {
+                        if (mFlagRefactorAttentionHelper) {
                             buzzBeepBlinkLoggingCode = mAttentionHelper.buzzBeepBlinkLocked(r,
                                 new NotificationAttentionHelper.Signals(
                                     mUserProfiles.isCurrentProfile(r.getUserId()),
@@ -9268,7 +9299,7 @@
                     || interruptiveChanged;
             if (interceptBefore && !record.isIntercepted()
                     && record.isNewEnoughForAlerting(System.currentTimeMillis())) {
-                if (Flags.refactorAttentionHelper()) {
+                if (mFlagRefactorAttentionHelper) {
                     mAttentionHelper.buzzBeepBlinkLocked(record,
                         new NotificationAttentionHelper.Signals(
                             mUserProfiles.isCurrentProfile(record.getUserId()), mListenerHints));
@@ -9648,7 +9679,7 @@
                 });
             }
 
-            if (Flags.refactorAttentionHelper()) {
+            if (mFlagRefactorAttentionHelper) {
                 mAttentionHelper.clearEffectsLocked(canceledKey);
             } else {
                 // sound
@@ -10012,7 +10043,7 @@
                             cancellationElapsedTimeMs);
                 }
             }
-            if (Flags.refactorAttentionHelper()) {
+            if (mFlagRefactorAttentionHelper) {
                 mAttentionHelper.updateLightsLocked();
             } else {
                 updateLightsLocked();
diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java
index 8bf903a..f45571a 100644
--- a/services/core/java/com/android/server/pm/DeletePackageHelper.java
+++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java
@@ -578,6 +578,12 @@
                             ? null
                             : ps.getUserStateOrDefault(nextUserId).getArchiveState();
 
+            // Preserve firstInstallTime in case of DELETE_KEEP_DATA
+            // For full uninstalls, reset firstInstallTime to 0 as if it has never been installed
+            final long firstInstallTime = (flags & DELETE_KEEP_DATA) == 0
+                    ? 0
+                    : ps.getUserStateOrDefault(nextUserId).getFirstInstallTimeMillis();
+
             ps.setUserState(nextUserId,
                     ps.getCeDataInode(nextUserId),
                     ps.getDeDataInode(nextUserId),
@@ -597,7 +603,7 @@
                     PackageManager.UNINSTALL_REASON_UNKNOWN,
                     null /*harmfulAppWarning*/,
                     null /*splashScreenTheme*/,
-                    0 /*firstInstallTime*/,
+                    firstInstallTime,
                     PackageManager.USER_MIN_ASPECT_RATIO_UNSET,
                     archiveState);
         }
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 4b466be..52fdfd1 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -1616,6 +1616,13 @@
             }
         }
 
+        for (Checksum checksum : checksums) {
+            if (checksum.getValue() == null
+                    || checksum.getValue().length > Checksum.MAX_CHECKSUM_SIZE_BYTES) {
+                throw new IllegalArgumentException("Invalid checksum.");
+            }
+        }
+
         assertCallerIsOwnerOrRoot();
         synchronized (mLock) {
             assertPreparedAndNotCommittedOrDestroyedLocked("addChecksums");
diff --git a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
index ea783b8..b281808 100644
--- a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
+++ b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
@@ -758,6 +758,11 @@
         return snapshot().isPackageQuarantinedForUser(packageName, userId);
     }
 
+    @Override
+    public boolean isPackageStopped(@NonNull String packageName, @UserIdInt int userId) {
+        return snapshot().isPackageStoppedForUser(packageName, userId);
+    }
+
     @NonNull
     @Override
     @Deprecated
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 81a570f..4e14c90 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1388,10 +1388,32 @@
 
         final long identity = Binder.clearCallingIdentity();
         try {
+            // QUIET_MODE_DISABLE_DONT_ASK_CREDENTIAL is only allowed for managed-profiles
+            if (dontAskCredential) {
+                UserInfo userInfo;
+                synchronized (mUsersLock) {
+                    userInfo = getUserInfo(userId);
+                }
+                if (!userInfo.isManagedProfile()) {
+                    throw new IllegalArgumentException("Invalid flags: " + flags
+                            + ". Can't skip credential check for the user");
+                }
+            }
             if (enableQuietMode) {
                 setQuietModeEnabled(userId, true /* enableQuietMode */, target, callingPackage);
                 return true;
             }
+            if (android.os.Flags.allowPrivateProfile()) {
+                final UserProperties userProperties = getUserPropertiesInternal(userId);
+                if (userProperties != null
+                        && userProperties.isAuthAlwaysRequiredToDisableQuietMode()) {
+                    if (onlyIfCredentialNotRequired) {
+                        return false;
+                    }
+                    showConfirmCredentialToDisableQuietMode(userId, target);
+                    return false;
+                }
+            }
             final boolean hasUnifiedChallenge =
                     mLockPatternUtils.isManagedProfileWithUnifiedChallenge(userId);
             if (hasUnifiedChallenge) {
diff --git a/services/core/java/com/android/server/pm/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java
index 29e0c35..7da76c1 100644
--- a/services/core/java/com/android/server/pm/UserTypeFactory.java
+++ b/services/core/java/com/android/server/pm/UserTypeFactory.java
@@ -193,6 +193,7 @@
                         .setStartWithParent(true)
                         .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_SEPARATE)
                         .setShowInSettings(UserProperties.SHOW_IN_SETTINGS_SEPARATE)
+                        .setAuthAlwaysRequiredToDisableQuietMode(false)
                         .setCredentialShareableWithParent(true));
     }
 
@@ -292,7 +293,8 @@
                 .setDefaultSecureSettings(getDefaultNonManagedProfileSecureSettings())
                 .setDefaultUserProperties(new UserProperties.Builder()
                         .setStartWithParent(true)
-                        .setCredentialShareableWithParent(false)
+                        .setCredentialShareableWithParent(true)
+                        .setAuthAlwaysRequiredToDisableQuietMode(true)
                         .setMediaSharedWithParent(false)
                         .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_SEPARATE)
                         .setShowInSettings(UserProperties.SHOW_IN_SETTINGS_SEPARATE)
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index cf1036c..72c10cc 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -32,7 +32,6 @@
 import static android.os.Build.VERSION_CODES.O;
 import static android.os.IInputConstants.INVALID_INPUT_DEVICE_ID;
 import static android.provider.Settings.Secure.VOLUME_HUSH_OFF;
-import static android.view.contentprotection.flags.Flags.createAccessibilityOverlayAppOpEnabled;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 import static android.view.Display.STATE_OFF;
@@ -69,6 +68,7 @@
 import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;
 import static android.view.WindowManagerGlobal.ADD_OKAY;
 import static android.view.WindowManagerGlobal.ADD_PERMISSION_DENIED;
+import static android.view.contentprotection.flags.Flags.createAccessibilityOverlayAppOpEnabled;
 
 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY;
 import static com.android.internal.util.FrameworkStatsLog.ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_WEAR_TRIPLE_PRESS_GESTURE;
@@ -101,6 +101,7 @@
 import android.app.ActivityManagerInternal;
 import android.app.ActivityTaskManager;
 import android.app.AppOpsManager;
+import android.app.IActivityManager;
 import android.app.IUiModeManager;
 import android.app.NotificationManager;
 import android.app.ProgressDialog;
@@ -427,6 +428,7 @@
     WindowManagerInternal mWindowManagerInternal;
     PowerManager mPowerManager;
     ActivityManagerInternal mActivityManagerInternal;
+    IActivityManager mActivityManagerService;
     ActivityTaskManagerInternal mActivityTaskManagerInternal;
     AutofillManagerInternal mAutofillManagerInternal;
     InputManager mInputManager;
@@ -549,7 +551,7 @@
     int mLidNavigationAccessibility;
     int mShortPressOnPowerBehavior;
     private boolean mShouldEarlyShortPressOnPower;
-    private boolean mShouldEarlyShortPressOnStemPrimary;
+    boolean mShouldEarlyShortPressOnStemPrimary;
     int mLongPressOnPowerBehavior;
     long mLongPressOnPowerAssistantTimeoutMs;
     int mVeryLongPressOnPowerBehavior;
@@ -578,6 +580,7 @@
     private int mDoublePressOnStemPrimaryBehavior;
     private int mTriplePressOnStemPrimaryBehavior;
     private int mLongPressOnStemPrimaryBehavior;
+    private RecentTaskInfo mBackgroundRecentTaskInfoOnStemPrimarySingleKeyUp;
 
     private boolean mHandleVolumeKeysInWM;
 
@@ -1563,7 +1566,7 @@
                         ? false
                         : mKeyguardDelegate.isShowing();
                 if (!keyguardActive) {
-                    switchRecentTask();
+                    performStemPrimaryDoublePressSwitchToRecentTask();
                 }
                 break;
         }
@@ -1672,11 +1675,11 @@
     /**
      * Load most recent task (expect current task) and bring it to the front.
      */
-    private void switchRecentTask() {
-        RecentTaskInfo targetTask = mActivityTaskManagerInternal.getMostRecentTaskFromBackground();
+    void performStemPrimaryDoublePressSwitchToRecentTask() {
+        RecentTaskInfo targetTask = mBackgroundRecentTaskInfoOnStemPrimarySingleKeyUp;
         if (targetTask == null) {
             if (DEBUG_INPUT) {
-                Slog.w(TAG, "No recent task available! Show watch face.");
+                Slog.w(TAG, "No recent task available! Show wallpaper.");
             }
             goHome();
             return;
@@ -1695,7 +1698,7 @@
                             + targetTask.baseIntent);
         }
         try {
-            ActivityManager.getService().startActivityFromRecents(targetTask.persistentId, null);
+            mActivityManagerService.startActivityFromRecents(targetTask.persistentId, null);
         } catch (RemoteException | IllegalArgumentException e) {
             Slog.e(TAG, "Failed to start task " + targetTask.persistentId + " from recents", e);
         }
@@ -2219,6 +2222,10 @@
                         }
                     });
         }
+
+        IActivityManager getActivityManagerService() {
+            return ActivityManager.getService();
+        }
     }
 
     /** {@inheritDoc} */
@@ -2233,6 +2240,7 @@
         mWindowManagerFuncs = injector.getWindowManagerFuncs();
         mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
         mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
+        mActivityManagerService = injector.getActivityManagerService();
         mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
         mInputManager = mContext.getSystemService(InputManager.class);
         mInputManagerInternal = LocalServices.getService(InputManagerInternal.class);
@@ -2767,8 +2775,17 @@
 
         @Override
         void onKeyUp(long eventTime, int count) {
-            if (mShouldEarlyShortPressOnStemPrimary && count == 1) {
-                stemPrimaryPress(1 /*pressCount*/);
+            if (count == 1) {
+                // Save info about the most recent task on the first press of the stem key. This
+                // may be used later to switch to the most recent app using double press gesture.
+                // It is possible that we may navigate away from this task before the double
+                // press is detected, as a result of the first press, so we save the  current
+                // most recent task before that happens.
+                mBackgroundRecentTaskInfoOnStemPrimarySingleKeyUp =
+                        mActivityTaskManagerInternal.getMostRecentTaskFromBackground();
+                if (mShouldEarlyShortPressOnStemPrimary) {
+                    stemPrimaryPress(1 /*pressCount*/);
+                }
             }
         }
     }
diff --git a/services/core/java/com/android/server/power/ThermalManagerService.java b/services/core/java/com/android/server/power/ThermalManagerService.java
index 99064bc..d17207b 100644
--- a/services/core/java/com/android/server/power/ThermalManagerService.java
+++ b/services/core/java/com/android/server/power/ThermalManagerService.java
@@ -28,6 +28,7 @@
 import android.hardware.thermal.V1_1.IThermalCallback;
 import android.os.Binder;
 import android.os.CoolingDevice;
+import android.os.Flags;
 import android.os.Handler;
 import android.os.HwBinder;
 import android.os.IBinder;
@@ -181,7 +182,7 @@
                 onTemperatureChanged(temperatures.get(i), false);
             }
             onTemperatureMapChangedLocked();
-            mTemperatureWatcher.updateSevereThresholds();
+            mTemperatureWatcher.updateThresholds();
             mHalReady.set(true);
         }
     }
@@ -506,6 +507,20 @@
         }
 
         @Override
+        public float[] getThermalHeadroomThresholds() {
+            if (!mHalReady.get()) {
+                throw new IllegalStateException("Thermal HAL connection is not initialized");
+            }
+            if (!Flags.allowThermalHeadroomThresholds()) {
+                throw new UnsupportedOperationException("Thermal headroom thresholds not enabled");
+            }
+            synchronized (mTemperatureWatcher.mSamples) {
+                return Arrays.copyOf(mTemperatureWatcher.mHeadroomThresholds,
+                        mTemperatureWatcher.mHeadroomThresholds.length);
+            }
+        }
+
+        @Override
         protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
             dumpInternal(fd, pw, args);
         }
@@ -580,6 +595,12 @@
                             mHalWrapper.getTemperatureThresholds(false, 0));
                 }
             }
+            if (Flags.allowThermalHeadroomThresholds()) {
+                synchronized (mTemperatureWatcher.mSamples) {
+                    pw.println("Temperature headroom thresholds:");
+                    pw.println(Arrays.toString(mTemperatureWatcher.mHeadroomThresholds));
+                }
+            }
         } finally {
             Binder.restoreCallingIdentity(token);
         }
@@ -964,7 +985,14 @@
                         connectToHal();
                     }
                     if (mInstance != null) {
-                        Slog.i(TAG, "Thermal HAL AIDL service connected.");
+                        try {
+                            Slog.i(TAG, "Thermal HAL AIDL service connected with version "
+                                    + mInstance.getInterfaceVersion());
+                        } catch (RemoteException e) {
+                            Slog.e(TAG, "Unable to read interface version from Thermal HAL", e);
+                            connectToHal();
+                            return;
+                        }
                         registerThermalChangedCallback();
                     }
                 }
@@ -1440,26 +1468,55 @@
         ArrayMap<String, Float> mSevereThresholds = new ArrayMap<>();
 
         @GuardedBy("mSamples")
+        float[] mHeadroomThresholds = new float[ThrottlingSeverity.SHUTDOWN + 1];
+        @GuardedBy("mSamples")
         private long mLastForecastCallTimeMillis = 0;
 
         private static final int INACTIVITY_THRESHOLD_MILLIS = 10000;
         @VisibleForTesting
         long mInactivityThresholdMillis = INACTIVITY_THRESHOLD_MILLIS;
 
-        void updateSevereThresholds() {
+        void updateThresholds() {
             synchronized (mSamples) {
                 List<TemperatureThreshold> thresholds =
                         mHalWrapper.getTemperatureThresholds(true, Temperature.TYPE_SKIN);
+                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 temperature =
+                    float severeThreshold =
                             threshold.hotThrottlingThresholds[ThrottlingSeverity.SEVERE];
-                    if (!Float.isNaN(temperature)) {
-                        mSevereThresholds.put(threshold.name,
-                                threshold.hotThrottlingThresholds[ThrottlingSeverity.SEVERE]);
+                    if (!Float.isNaN(severeThreshold)) {
+                        mSevereThresholds.put(threshold.name, severeThreshold);
+                        for (int severity = ThrottlingSeverity.LIGHT;
+                                severity <= ThrottlingSeverity.SHUTDOWN; severity++) {
+                            if (Flags.allowThermalHeadroomThresholds()
+                                    && threshold.hotThrottlingThresholds.length > severity) {
+                                updateHeadroomThreshold(severity,
+                                        threshold.hotThrottlingThresholds[severity],
+                                        severeThreshold);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        // For a 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) {
+                    float headroom = normalizeTemperature(threshold, severeThreshold);
+                    if (Float.isNaN(mHeadroomThresholds[severity])) {
+                        mHeadroomThresholds[severity] = headroom;
+                    } else {
+                        float lastHeadroom = mHeadroomThresholds[severity];
+                        mHeadroomThresholds[severity] = Math.min(lastHeadroom, headroom);
                     }
                 }
             }
@@ -1541,15 +1598,13 @@
         private static final float DEGREES_BETWEEN_ZERO_AND_ONE = 30.0f;
 
         @VisibleForTesting
-        float normalizeTemperature(float temperature, float severeThreshold) {
-            synchronized (mSamples) {
-                float zeroNormalized = severeThreshold - DEGREES_BETWEEN_ZERO_AND_ONE;
-                if (temperature <= zeroNormalized) {
-                    return 0.0f;
-                }
-                float delta = temperature - zeroNormalized;
-                return delta / DEGREES_BETWEEN_ZERO_AND_ONE;
+        static float normalizeTemperature(float temperature, float severeThreshold) {
+            float zeroNormalized = severeThreshold - DEGREES_BETWEEN_ZERO_AND_ONE;
+            if (temperature <= zeroNormalized) {
+                return 0.0f;
             }
+            float delta = temperature - zeroNormalized;
+            return delta / DEGREES_BETWEEN_ZERO_AND_ONE;
         }
 
         private static final int MINIMUM_SAMPLE_COUNT = 3;
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java
index 3f02266..f48178c 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java
@@ -124,7 +124,7 @@
 
         final long ident = Binder.clearCallingIdentity();
         try {
-            return mWindowManagerInternal.shouldShowSystemDecorOnDisplay(displayId);
+            return mWindowManagerInternal.isHomeSupportedOnDisplay(displayId);
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index b3ae2ee..b1abe2a 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -558,7 +558,7 @@
         }
         if (newTarget != null) {
             int displayId = newTarget.getDisplayId();
-            IBinder clientBinder = newTarget.getIWindow().asBinder();
+            IBinder clientBinder = newTarget.getWindowToken();
             mFocusedWindow.put(displayId, clientBinder);
         }
     }
diff --git a/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java b/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java
index cdd1a269..3cf19dd 100644
--- a/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java
+++ b/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java
@@ -34,7 +34,6 @@
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.view.IWindow;
 import android.view.InputWindowHandle;
 import android.view.MagnificationSpec;
 import android.view.WindowInfo;
@@ -197,14 +196,14 @@
             final HashMap<IBinder, Matrix> windowsTransformMatrixMap = new HashMap<>();
 
             for (InputWindowHandle inputWindowHandle : windows) {
-                final IWindow iWindow = inputWindowHandle.getWindow();
-                final WindowState windowState = iWindow != null ? mService.mWindowMap.get(
-                        iWindow.asBinder()) : null;
+                final IBinder iWindow = inputWindowHandle.getWindowToken();
+                final WindowState windowState = iWindow != null ? mService.mWindowMap.get(iWindow)
+                        : null;
 
                 if (windowState != null && windowState.shouldMagnify()) {
                     final Matrix transformMatrix = new Matrix();
                     windowState.getTransformationMatrix(sTempFloats, transformMatrix);
-                    windowsTransformMatrixMap.put(iWindow.asBinder(), transformMatrix);
+                    windowsTransformMatrixMap.put(iWindow, transformMatrix);
                 }
             }
 
@@ -330,8 +329,8 @@
         // the old and new windows at the same index should be the
         // same, otherwise something changed.
         for (int i = 0; i < windowsCount; i++) {
-            final IWindow newWindowToken = newWindows.get(i).getWindow();
-            final IWindow oldWindowToken = oldWindows.get(i).getWindow();
+            final IBinder newWindowToken = newWindows.get(i).getWindowToken();
+            final IBinder oldWindowToken = oldWindows.get(i).getWindowToken();
             final boolean hasNewWindowToken = newWindowToken != null;
             final boolean hasOldWindowToken = oldWindowToken != null;
 
@@ -342,8 +341,7 @@
 
             // If both old and new windows had window tokens, but those tokens differ,
             // then the windows have changed.
-            if (hasNewWindowToken && hasOldWindowToken
-                    && !newWindowToken.asBinder().equals(oldWindowToken.asBinder())) {
+            if (hasNewWindowToken && hasOldWindowToken && !newWindowToken.equals(oldWindowToken)) {
                 return true;
             }
         }
@@ -393,9 +391,7 @@
         for (int index = inputWindowHandles.size() - 1; index >= 0; index--) {
             final Matrix windowTransformMatrix = mTempMatrix2;
             final InputWindowHandle windowHandle = inputWindowHandles.get(index);
-            final IBinder iBinder =
-                    windowHandle.getWindow() != null ? windowHandle.getWindow().asBinder() : null;
-
+            final IBinder iBinder = windowHandle.getWindowToken();
             if (getWindowTransformMatrix(iBinder, windowTransformMatrix)) {
                 generateMagnificationSpecInverseMatrix(windowHandle, currentMagnificationSpec,
                         previousMagnificationSpec, windowTransformMatrix);
@@ -645,7 +641,7 @@
      */
     public static class AccessibilityWindow {
         // Data
-        private IWindow mWindow;
+        private IBinder mWindow;
         private int mDisplayId;
         @WindowManager.LayoutParams.WindowType
         private int mType;
@@ -670,9 +666,8 @@
         public static AccessibilityWindow initializeData(WindowManagerService service,
                 InputWindowHandle inputWindowHandle, Matrix magnificationInverseMatrix,
                 IBinder pipIBinder, Matrix displayMatrix) {
-            final IWindow window = inputWindowHandle.getWindow();
-            final WindowState windowState = window != null ? service.mWindowMap.get(
-                    window.asBinder()) : null;
+            final IBinder window = inputWindowHandle.getWindowToken();
+            final WindowState windowState = window != null ? service.mWindowMap.get(window) : null;
 
             final AccessibilityWindow instance = new AccessibilityWindow();
 
@@ -680,7 +675,7 @@
             instance.mDisplayId = inputWindowHandle.displayId;
             instance.mInputConfig = inputWindowHandle.inputConfig;
             instance.mType = inputWindowHandle.layoutParamsType;
-            instance.mIsPIPMenu = window != null && window.asBinder().equals(pipIBinder);
+            instance.mIsPIPMenu = window != null && window.equals(pipIBinder);
 
             // TODO (b/199357848): gets the private flag of the window from other way.
             instance.mPrivateFlags = windowState != null ? windowState.mAttrs.privateFlags : 0;
@@ -867,7 +862,7 @@
             WindowInfo windowInfo = WindowInfo.obtain();
             windowInfo.displayId = window.mDisplayId;
             windowInfo.type = window.mType;
-            windowInfo.token = window.mWindow != null ? window.mWindow.asBinder() : null;
+            windowInfo.token = window.mWindow;
             windowInfo.hasFlagWatchOutsideTouch = (window.mInputConfig
                     & InputConfig.WATCH_OUTSIDE_TOUCH) != 0;
             // Set it to true to be consistent with the legacy implementation.
@@ -878,7 +873,7 @@
         @Override
         public String toString() {
             String windowToken =
-                    mWindow != null ? mWindow.asBinder().toString() : "(no window token)";
+                    mWindow != null ? mWindow.toString() : "(no window token)";
             return "A11yWindow=[" + windowToken
                     + ", displayId=" + mDisplayId
                     + ", inputConfig=0x" + Integer.toHexString(mInputConfig)
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 87ae045..43f3209 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -39,7 +39,6 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.Bundle;
-import android.os.IBinder;
 import android.os.RemoteCallback;
 import android.os.RemoteException;
 import android.os.SystemProperties;
@@ -60,7 +59,6 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.policy.TransitionAnimation;
 import com.android.internal.protolog.common.ProtoLog;
-import com.android.server.LocalServices;
 import com.android.server.wm.utils.InsetUtils;
 
 import java.io.PrintWriter;
@@ -151,97 +149,77 @@
                 // Don't start any animation for it.
                 return null;
             }
-            WindowManagerInternal windowManagerInternal =
-                    LocalServices.getService(WindowManagerInternal.class);
-            IBinder focusedWindowToken = windowManagerInternal.getFocusedWindowToken();
 
             window = wmService.getFocusedWindowLocked();
 
             if (window == null) {
-                EmbeddedWindowController.EmbeddedWindow embeddedWindow =
-                        wmService.mEmbeddedWindowController.getByInputTransferToken(
-                                focusedWindowToken);
-                if (embeddedWindow != null) {
-                    ProtoLog.d(WM_DEBUG_BACK_PREVIEW,
-                            "Current focused window is embeddedWindow. Dispatch KEYCODE_BACK.");
-                    return null;
-                }
-            }
-
-            // Lets first gather the states of things
-            //  - What is our current window ?
-            //  - Does it has an Activity and a Task ?
-            // TODO Temp workaround for Sysui until b/221071505 is fixed
-            if (window != null) {
-                ProtoLog.d(WM_DEBUG_BACK_PREVIEW,
-                        "Focused window found using getFocusedWindowToken");
-            }
-
-            if (window != null) {
-                // This is needed to bridge the old and new back behavior with recents.  While in
-                // Overview with live tile enabled, the previous app is technically focused but we
-                // add an input consumer to capture all input that would otherwise go to the apps
-                // being controlled by the animation. This means that the window resolved is not
-                // the right window to consume back while in overview, so we need to route it to
-                // launcher and use the legacy behavior of injecting KEYCODE_BACK since the existing
-                // compat callback in VRI only works when the window is focused.
-                // This symptom also happen while shell transition enabled, we can check that by
-                // isTransientLaunch to know whether the focus window is point to live tile.
-                final RecentsAnimationController recentsAnimationController =
-                        wmService.getRecentsAnimationController();
-                final ActivityRecord ar = window.mActivityRecord;
-                if ((ar != null && ar.isActivityTypeHomeOrRecents()
-                        && ar.mTransitionController.isTransientLaunch(ar))
-                        || (recentsAnimationController != null
-                        && recentsAnimationController.shouldApplyInputConsumer(ar))) {
-                    ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "Current focused window being animated by "
-                            + "recents. Overriding back callback to recents controller callback.");
-                    return null;
-                }
-
-                if (!window.isDrawn()) {
-                    ProtoLog.d(WM_DEBUG_BACK_PREVIEW,
-                            "Focused window didn't have a valid surface drawn.");
-                    return null;
-                }
-            }
-
-            if (window == null) {
                 // We don't have any focused window, fallback ont the top currentTask of the focused
                 // display.
                 ProtoLog.w(WM_DEBUG_BACK_PREVIEW,
                         "No focused window, defaulting to top current task's window");
                 currentTask = wmService.mAtmService.getTopDisplayFocusedRootTask();
-                window = currentTask.getWindow(WindowState::isFocused);
+                window = currentTask != null
+                        ? currentTask.getWindow(WindowState::isFocused) : null;
             }
 
-            // Now let's find if this window has a callback from the client side.
-            OnBackInvokedCallbackInfo callbackInfo = null;
-            if (window != null) {
-                currentActivity = window.mActivityRecord;
-                currentTask = window.getTask();
-                callbackInfo = window.getOnBackInvokedCallbackInfo();
-                if (callbackInfo == null) {
-                    Slog.e(TAG, "No callback registered, returning null.");
-                    return null;
-                }
-                if (!callbackInfo.isSystemCallback()) {
-                    backType = BackNavigationInfo.TYPE_CALLBACK;
-                }
-                infoBuilder.setOnBackInvokedCallback(callbackInfo.getCallback());
-                infoBuilder.setAnimationCallback(callbackInfo.isAnimationCallback());
-                mNavigationMonitor.startMonitor(window, navigationObserver);
-            }
-
-            ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "startBackNavigation currentTask=%s, "
-                            + "topRunningActivity=%s, callbackInfo=%s, currentFocus=%s",
-                    currentTask, currentActivity, callbackInfo, window);
-
             if (window == null) {
                 Slog.e(TAG, "Window is null, returning null.");
                 return null;
             }
 
+            // This is needed to bridge the old and new back behavior with recents.  While in
+            // Overview with live tile enabled, the previous app is technically focused but we
+            // add an input consumer to capture all input that would otherwise go to the apps
+            // being controlled by the animation. This means that the window resolved is not
+            // the right window to consume back while in overview, so we need to route it to
+            // launcher and use the legacy behavior of injecting KEYCODE_BACK since the existing
+            // compat callback in VRI only works when the window is focused.
+            // This symptom also happen while shell transition enabled, we can check that by
+            // isTransientLaunch to know whether the focus window is point to live tile.
+            final RecentsAnimationController recentsAnimationController =
+                    wmService.getRecentsAnimationController();
+            final ActivityRecord tmpAR = window.mActivityRecord;
+            if ((tmpAR != null && tmpAR.isActivityTypeHomeOrRecents()
+                    && tmpAR.mTransitionController.isTransientLaunch(tmpAR))
+                    || (recentsAnimationController != null
+                    && recentsAnimationController.shouldApplyInputConsumer(tmpAR))) {
+                ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "Current focused window being animated by "
+                        + "recents. Overriding back callback to recents controller callback.");
+                return null;
+            }
+
+            if (!window.isDrawn()) {
+                ProtoLog.d(WM_DEBUG_BACK_PREVIEW,
+                        "Focused window didn't have a valid surface drawn.");
+                return null;
+            }
+
+            currentActivity = window.mActivityRecord;
+            currentTask = window.getTask();
+            if ((currentTask != null && !currentTask.isVisibleRequested())
+                    || (currentActivity != null && !currentActivity.isVisibleRequested())) {
+                // Closing transition is happening on focus window and should be update soon,
+                // don't drive back navigation with it.
+                ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "Focus window is closing.");
+                return null;
+            }
+            // Now let's find if this window has a callback from the client side.
+            final OnBackInvokedCallbackInfo callbackInfo = window.getOnBackInvokedCallbackInfo();
+            if (callbackInfo == null) {
+                Slog.e(TAG, "No callback registered, returning null.");
+                return null;
+            }
+            if (!callbackInfo.isSystemCallback()) {
+                backType = BackNavigationInfo.TYPE_CALLBACK;
+            }
+            infoBuilder.setOnBackInvokedCallback(callbackInfo.getCallback());
+            infoBuilder.setAnimationCallback(callbackInfo.isAnimationCallback());
+            mNavigationMonitor.startMonitor(window, navigationObserver);
+
+            ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "startBackNavigation currentTask=%s, "
+                            + "topRunningActivity=%s, callbackInfo=%s, currentFocus=%s",
+                    currentTask, currentActivity, callbackInfo, window);
+
             // If we don't need to set up the animation, we return early. This is the case when
             // - We have an application callback.
             // - We don't have any ActivityRecord or Task to animate.
@@ -322,12 +300,13 @@
                     }
                     return false;
                 }, currentTask, false /*includeBoundary*/, true /*traverseTopToBottom*/);
-                final ActivityRecord tmpPre = prevTask.getTopNonFinishingActivity();
+                final ActivityRecord tmpPre = prevTask != null
+                        ? prevTask.getTopNonFinishingActivity() : null;
                 if (tmpPre != null) {
                     prevActivities.add(tmpPre);
                     findAdjacentActivityIfExist(tmpPre, prevActivities);
                 }
-                if (prevActivities.isEmpty()
+                if (prevTask == null || prevActivities.isEmpty()
                         || (isOccluded && !prevActivities.get(0).canShowWhenLocked())) {
                     backType = BackNavigationInfo.TYPE_CALLBACK;
                 } else if (prevTask.isActivityTypeHome()) {
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 7f80807..4924810 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -184,6 +184,7 @@
 import android.graphics.Region.Op;
 import android.hardware.HardwareBuffer;
 import android.hardware.display.DisplayManagerInternal;
+import android.hardware.display.VirtualDisplayConfig;
 import android.metrics.LogMaker;
 import android.os.Bundle;
 import android.os.Debug;
@@ -2191,7 +2192,7 @@
      * @see DisplayWindowPolicyController#getCustomHomeComponent() ()
      */
     @Nullable ComponentName getCustomHomeComponent() {
-        if (!supportsSystemDecorations() || mDwpcHelper == null) {
+        if (!isHomeSupported() || mDwpcHelper == null) {
             return null;
         }
         return mDwpcHelper.getCustomHomeComponent();
@@ -5772,6 +5773,17 @@
                 && isTrusted();
     }
 
+    /**
+     * Checks if this display is configured and allowed to show home activity and wallpaper.
+     *
+     * <p>This is implied for displays that have {@link Display#FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS}
+     * and can also be set via {@link VirtualDisplayConfig.Builder#setHomeSupported}.</p>
+     */
+    boolean isHomeSupported() {
+        return (mWmService.mDisplayWindowSettings.isHomeSupportedLocked(this) && isTrusted())
+                || supportsSystemDecorations();
+    }
+
     SurfaceControl getWindowingLayer() {
         return mWindowingLayer;
     }
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 8a7cc67..b34f912 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -1877,15 +1877,12 @@
                 final InsetsState insetsState = df.mInsetsState;
                 final Rect displayFrame = insetsState.getDisplayFrame();
                 final Insets decor = insetsState.calculateInsets(displayFrame,
-                        dc.mWmService.mDecorTypes,
-                        true /* ignoreVisibility */);
-                final Insets statusBar = insetsState.calculateInsets(displayFrame,
-                        Type.statusBars(), true /* ignoreVisibility */);
+                        dc.mWmService.mDecorTypes, true /* ignoreVisibility */);
+                final Insets configInsets = insetsState.calculateInsets(displayFrame,
+                        dc.mWmService.mConfigTypes, true /* ignoreVisibility */);
                 mNonDecorInsets.set(decor.left, decor.top, decor.right, decor.bottom);
-                mConfigInsets.set(Math.max(statusBar.left, decor.left),
-                        Math.max(statusBar.top, decor.top),
-                        Math.max(statusBar.right, decor.right),
-                        Math.max(statusBar.bottom, decor.bottom));
+                mConfigInsets.set(configInsets.left, configInsets.top, configInsets.right,
+                        configInsets.bottom);
                 mNonDecorFrame.set(displayFrame);
                 mNonDecorFrame.inset(mNonDecorInsets);
                 mConfigFrame.set(displayFrame);
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettings.java b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
index e1753d7..7a95c2d 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowSettings.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
@@ -237,6 +237,37 @@
         mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
     }
 
+    boolean isHomeSupportedLocked(@NonNull DisplayContent dc) {
+        if (dc.getDisplayId() == Display.DEFAULT_DISPLAY) {
+            // Default display should show home.
+            return true;
+        }
+
+        final DisplayInfo displayInfo = dc.getDisplayInfo();
+        final SettingsProvider.SettingsEntry settings = mSettingsProvider.getSettings(displayInfo);
+        return settings.mIsHomeSupported != null
+                ? settings.mIsHomeSupported
+                : shouldShowSystemDecorsLocked(dc);
+    }
+
+    void setHomeSupportedOnDisplayLocked(@NonNull String displayUniqueId, int displayType,
+            boolean supported) {
+        final DisplayInfo displayInfo = new DisplayInfo();
+        displayInfo.uniqueId = displayUniqueId;
+        displayInfo.type = displayType;
+        final SettingsProvider.SettingsEntry overrideSettings =
+                mSettingsProvider.getOverrideSettings(displayInfo);
+        overrideSettings.mIsHomeSupported = supported;
+        mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
+    }
+
+    void clearDisplaySettings(@NonNull String displayUniqueId, int displayType) {
+        final DisplayInfo displayInfo = new DisplayInfo();
+        displayInfo.uniqueId = displayUniqueId;
+        displayInfo.type = displayType;
+        mSettingsProvider.clearDisplaySettings(displayInfo);
+    }
+
     @DisplayImePolicy
     int getImePolicyLocked(@NonNull DisplayContent dc) {
         if (dc.getDisplayId() == Display.DEFAULT_DISPLAY) {
@@ -382,11 +413,18 @@
         void updateOverrideSettings(@NonNull DisplayInfo info, @NonNull SettingsEntry overrides);
 
         /**
-         * Called when a display is removed to cleanup.
+         * Called when a display is removed to cleanup. Note that for non-virtual displays the
+         * relevant settings entry will be kept, if non-empty.
          */
         void onDisplayRemoved(@NonNull DisplayInfo info);
 
         /**
+         * Explicitly removes all settings entory for the given {@link DisplayInfo}, even if it is
+         * not empty.
+         */
+        void clearDisplaySettings(@NonNull DisplayInfo info);
+
+        /**
          * Settings for a display.
          */
         class SettingsEntry {
@@ -411,6 +449,8 @@
             @Nullable
             Boolean mShouldShowSystemDecors;
             @Nullable
+            Boolean mIsHomeSupported;
+            @Nullable
             Integer mImePolicy;
             @Nullable
             Integer mFixedToUserRotation;
@@ -479,6 +519,10 @@
                     mShouldShowSystemDecors = other.mShouldShowSystemDecors;
                     changed = true;
                 }
+                if (!Objects.equals(other.mIsHomeSupported, mIsHomeSupported)) {
+                    mIsHomeSupported = other.mIsHomeSupported;
+                    changed = true;
+                }
                 if (!Objects.equals(other.mImePolicy, mImePolicy)) {
                     mImePolicy = other.mImePolicy;
                     changed = true;
@@ -561,6 +605,11 @@
                     mShouldShowSystemDecors = delta.mShouldShowSystemDecors;
                     changed = true;
                 }
+                if (delta.mIsHomeSupported != null && !Objects.equals(
+                        delta.mIsHomeSupported, mIsHomeSupported)) {
+                    mIsHomeSupported = delta.mIsHomeSupported;
+                    changed = true;
+                }
                 if (delta.mImePolicy != null
                         && !Objects.equals(delta.mImePolicy, mImePolicy)) {
                     mImePolicy = delta.mImePolicy;
@@ -599,6 +648,7 @@
                         && mRemoveContentMode == REMOVE_CONTENT_MODE_UNDEFINED
                         && mShouldShowWithInsecureKeyguard == null
                         && mShouldShowSystemDecors == null
+                        && mIsHomeSupported == null
                         && mImePolicy == null
                         && mFixedToUserRotation == null
                         && mIgnoreOrientationRequest == null
@@ -622,6 +672,7 @@
                         && Objects.equals(mShouldShowWithInsecureKeyguard,
                                 that.mShouldShowWithInsecureKeyguard)
                         && Objects.equals(mShouldShowSystemDecors, that.mShouldShowSystemDecors)
+                        && Objects.equals(mIsHomeSupported, that.mIsHomeSupported)
                         && Objects.equals(mImePolicy, that.mImePolicy)
                         && Objects.equals(mFixedToUserRotation, that.mFixedToUserRotation)
                         && Objects.equals(mIgnoreOrientationRequest, that.mIgnoreOrientationRequest)
@@ -633,9 +684,9 @@
             public int hashCode() {
                 return Objects.hash(mWindowingMode, mUserRotationMode, mUserRotation, mForcedWidth,
                         mForcedHeight, mForcedDensity, mForcedScalingMode, mRemoveContentMode,
-                        mShouldShowWithInsecureKeyguard, mShouldShowSystemDecors, mImePolicy,
-                        mFixedToUserRotation, mIgnoreOrientationRequest, mIgnoreDisplayCutout,
-                        mDontMoveToTop);
+                        mShouldShowWithInsecureKeyguard, mShouldShowSystemDecors, mIsHomeSupported,
+                        mImePolicy, mFixedToUserRotation, mIgnoreOrientationRequest,
+                        mIgnoreDisplayCutout, mDontMoveToTop);
             }
 
             @Override
@@ -651,6 +702,7 @@
                         + ", mRemoveContentMode=" + mRemoveContentMode
                         + ", mShouldShowWithInsecureKeyguard=" + mShouldShowWithInsecureKeyguard
                         + ", mShouldShowSystemDecors=" + mShouldShowSystemDecors
+                        + ", mIsHomeSupported=" + mIsHomeSupported
                         + ", mShouldShowIme=" + mImePolicy
                         + ", mFixedToUserRotation=" + mFixedToUserRotation
                         + ", mIgnoreOrientationRequest=" + mIgnoreOrientationRequest
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java b/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
index ea668fa..c79565a 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
@@ -164,6 +164,11 @@
         mOverrideSettings.onDisplayRemoved(info);
     }
 
+    @Override
+    public void clearDisplaySettings(@NonNull DisplayInfo info) {
+        mOverrideSettings.clearDisplaySettings(info);
+    }
+
     @VisibleForTesting
     int getOverrideSettingsSize() {
         return mOverrideSettings.mSettings.size();
@@ -291,6 +296,12 @@
             }
         }
 
+        void clearDisplaySettings(@NonNull DisplayInfo info) {
+            final String identifier = getIdentifier(info);
+            mSettings.remove(identifier);
+            mVirtualDisplayIdentifiers.remove(identifier);
+        }
+
         private void writeSettings() {
             final FileData fileData = new FileData();
             fileData.mIdentifierType = mIdentifierType;
diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java
index 1462878..1670b36e 100644
--- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java
+++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java
@@ -71,7 +71,7 @@
             mWindowsByInputTransferToken.put(inputTransferToken, window);
             mWindowsByWindowToken.put(window.getWindowToken(), window);
             updateProcessController(window);
-            window.mClient.asBinder().linkToDeath(()-> {
+            window.mClient.linkToDeath(()-> {
                 synchronized (mGlobalLock) {
                     mWindows.remove(inputToken);
                     mWindowsByInputTransferToken.remove(inputTransferToken);
@@ -103,7 +103,7 @@
     void remove(IWindow client) {
         for (int i = mWindows.size() - 1; i >= 0; i--) {
             EmbeddedWindow ew = mWindows.valueAt(i);
-            if (ew.mClient.asBinder() == client.asBinder()) {
+            if (ew.mClient == client.asBinder()) {
                 mWindows.removeAt(i).onRemoved();
                 mWindowsByInputTransferToken.remove(ew.getInputTransferToken());
                 mWindowsByWindowToken.remove(ew.getWindowToken());
@@ -136,7 +136,7 @@
     }
 
     static class EmbeddedWindow implements InputTarget {
-        final IWindow mClient;
+        final IBinder mClient;
         @Nullable final WindowState mHostWindowState;
         @Nullable final ActivityRecord mHostActivityRecord;
         final String mName;
@@ -169,7 +169,7 @@
          * @param windowType to forward to input
          * @param displayId used for focus requests
          */
-        EmbeddedWindow(Session session, WindowManagerService service, IWindow clientToken,
+        EmbeddedWindow(Session session, WindowManagerService service, IBinder clientToken,
                        WindowState hostWindowState, int ownerUid, int ownerPid, int windowType,
                        int displayId, IBinder inputTransferToken, String inputHandleName,
                        boolean isFocusable) {
@@ -241,13 +241,8 @@
             return mWmService.mRoot.getDisplayContent(getDisplayId());
         }
 
-        @Override
-        public IWindow getIWindow() {
-            return mClient;
-        }
-
         public IBinder getWindowToken() {
-            return mClient.asBinder();
+            return mClient;
         }
 
         @Override
diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java
index 61fea4d..997b608 100644
--- a/services/core/java/com/android/server/wm/InputMonitor.java
+++ b/services/core/java/com/android/server/wm/InputMonitor.java
@@ -57,7 +57,6 @@
 import android.os.SystemClock;
 import android.os.Trace;
 import android.os.UserHandle;
-import android.util.ArrayMap;
 import android.util.EventLog;
 import android.util.Slog;
 import android.view.InputChannel;
@@ -74,7 +73,6 @@
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
-import java.util.Set;
 import java.util.function.Consumer;
 
 final class InputMonitor {
@@ -258,7 +256,7 @@
         inputWindowHandle.setDispatchingTimeoutMillis(w.getInputDispatchingTimeoutMillis());
         inputWindowHandle.setTouchOcclusionMode(w.getTouchOcclusionMode());
         inputWindowHandle.setPaused(w.mActivityRecord != null && w.mActivityRecord.paused);
-        inputWindowHandle.setWindowToken(w.mClient);
+        inputWindowHandle.setWindowToken(w.mClient.asBinder());
 
         inputWindowHandle.setName(w.getName());
 
@@ -395,8 +393,12 @@
      */
     void setActiveRecents(@Nullable ActivityRecord activity, @Nullable ActivityRecord layer) {
         final boolean clear = activity == null;
+        final boolean wasActive = mActiveRecentsActivity != null && mActiveRecentsLayerRef != null;
         mActiveRecentsActivity = clear ? null : new WeakReference<>(activity);
         mActiveRecentsLayerRef = clear ? null : new WeakReference<>(layer);
+        if (clear && wasActive) {
+            setUpdateInputWindowsNeededLw();
+        }
     }
 
     private static <T> T getWeak(WeakReference<T> ref) {
diff --git a/services/core/java/com/android/server/wm/InputTarget.java b/services/core/java/com/android/server/wm/InputTarget.java
index 653f5f5..baf0db2 100644
--- a/services/core/java/com/android/server/wm/InputTarget.java
+++ b/services/core/java/com/android/server/wm/InputTarget.java
@@ -16,8 +16,8 @@
 
 package com.android.server.wm;
 
+import android.os.IBinder;
 import android.util.proto.ProtoOutputStream;
-import android.view.IWindow;
 
 /**
  * Common interface between focusable objects.
@@ -33,7 +33,7 @@
     int getDisplayId();
 
     /* Client IWindow for the target. */
-    IWindow getIWindow();
+    IBinder getWindowToken();
 
     /* Owning pid of the target. */
     int getPid();
diff --git a/services/core/java/com/android/server/wm/InputWindowHandleWrapper.java b/services/core/java/com/android/server/wm/InputWindowHandleWrapper.java
index 90d81bd..b748053 100644
--- a/services/core/java/com/android/server/wm/InputWindowHandleWrapper.java
+++ b/services/core/java/com/android/server/wm/InputWindowHandleWrapper.java
@@ -21,7 +21,6 @@
 import android.graphics.Region;
 import android.os.IBinder;
 import android.os.InputConfig;
-import android.view.IWindow;
 import android.view.InputApplicationHandle;
 import android.view.InputWindowHandle;
 import android.view.InputWindowHandle.InputConfigFlags;
@@ -264,8 +263,8 @@
         mChanged = true;
     }
 
-    void setWindowToken(IWindow windowToken) {
-        if (mHandle.getWindow() == windowToken) {
+    void setWindowToken(IBinder windowToken) {
+        if (mHandle.getWindowToken() == windowToken) {
             return;
         }
         mHandle.setWindowToken(windowToken);
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 5227a52..fe2c250 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -1696,8 +1696,8 @@
         }
 
         final DisplayContent display = taskDisplayArea.getDisplayContent();
-        if (display == null || display.isRemoved() || !display.supportsSystemDecorations()) {
-            // Can't launch home on display that doesn't support system decorations.
+        if (display == null || display.isRemoved() || !display.isHomeSupported()) {
+            // Can't launch home on display that doesn't support home.
             return false;
         }
 
@@ -3126,10 +3126,11 @@
         if (preferredFocusableRootTask != null) {
             return preferredFocusableRootTask;
         }
-        if (preferredDisplayArea.mDisplayContent.supportsSystemDecorations()) {
+
+        if (preferredDisplayArea.mDisplayContent.isHomeSupported()) {
             // Stop looking for focusable root task on other displays because the preferred display
-            // supports system decorations. Home activity would be launched on the same display if
-            // no focusable root task found.
+            // supports home. Home activity would be launched on the same display if no focusable
+            // root task found.
             return null;
         }
 
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index 0c55d8a..18d64d7 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -894,7 +894,7 @@
 
     @Override
     public void grantInputChannel(int displayId, SurfaceControl surface,
-            IWindow window, IBinder hostInputToken, int flags, int privateFlags, int type,
+            IBinder clientToken, IBinder hostInputToken, int flags, int privateFlags, int type,
             int inputFeatures, IBinder windowToken, IBinder inputTransferToken,
             String inputHandleName, InputChannel outInputChannel) {
         if (hostInputToken == null && !mCanAddInternalSystemWindow) {
@@ -905,8 +905,8 @@
 
         final long identity = Binder.clearCallingIdentity();
         try {
-            mService.grantInputChannel(this, mUid, mPid, displayId, surface, window, hostInputToken,
-                    flags, mCanAddInternalSystemWindow ? privateFlags : 0,
+            mService.grantInputChannel(this, mUid, mPid, displayId, surface, clientToken,
+                    hostInputToken, flags, mCanAddInternalSystemWindow ? privateFlags : 0,
                     type, inputFeatures, windowToken, inputTransferToken, inputHandleName,
                     outInputChannel);
         } finally {
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index f0a6654..c57983c 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -1767,7 +1767,7 @@
      * Exposes the home task capability of the TaskDisplayArea
      */
     boolean canHostHomeTask() {
-        return mDisplayContent.supportsSystemDecorations() && mCanHostHomeTask;
+        return mDisplayContent.isHomeSupported() && mCanHostHomeTask;
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index f700944..caa57bb 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1344,6 +1344,7 @@
             final DisplayContent dc =
                     mController.mAtm.mRootWindowContainer.getDisplayContent(mRecentsDisplayId);
             dc.getInputMonitor().setActiveRecents(null /* activity */, null /* layer */);
+            dc.getInputMonitor().updateInputWindowsLw(false /* force */);
         }
         if (mTransientLaunches != null) {
             for (int i = mTransientLaunches.size() - 1; i >= 0; --i) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index 9f1bccb..92bd00e 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -28,6 +28,7 @@
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.hardware.display.DisplayManagerInternal;
+import android.hardware.display.VirtualDisplayConfig;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.Message;
@@ -752,9 +753,31 @@
     public abstract Context getTopFocusedDisplayUiContext();
 
     /**
-     * Checks if this display is configured and allowed to show system decorations.
+     * Sets whether the relevant display content can host the relevant home activity and wallpaper.
+     *
+     * @param displayUniqueId The unique ID of the display. Note that the display may not yet be
+     *   created, but whenever it is, this property will be applied.
+     * @param displayType The type of the display, e.g. {@link Display#TYPE_VIRTUAL}.
+     * @param supported Whether home and wallpaper are supported on this display.
      */
-    public abstract boolean shouldShowSystemDecorOnDisplay(int displayId);
+    public abstract void setHomeSupportedOnDisplay(
+            @NonNull String displayUniqueId, int displayType, boolean supported);
+
+    /**
+     * Checks if this display is configured and allowed to show home activity and wallpaper.
+     *
+     * <p>This is implied for displays that have {@link Display#FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS}
+     * and can also be set via {@link VirtualDisplayConfig.Builder#setHomeSupported}.</p>
+     */
+    public abstract boolean isHomeSupportedOnDisplay(int displayId);
+
+    /**
+     * Removes any settings relevant to the given display.
+     *
+     * <p>This may be used when a property is set for a display unique ID before the display
+     * creation but the actual display creation failed for some reason.</p>
+     */
+    public abstract void clearDisplaySettings(@NonNull String displayUniqueId, int displayType);
 
     /**
      * Indicates the policy for how the display should show IME.
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 0e13914..809e2d0 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -1191,7 +1191,8 @@
                 && mFlags.mAllowsScreenSizeDecoupledFromStatusBarAndCutout;
         if (!isScreenSizeDecoupledFromStatusBarAndCutout) {
             mDecorTypes = WindowInsets.Type.displayCutout() | WindowInsets.Type.navigationBars();
-            mConfigTypes = WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars();
+            mConfigTypes = WindowInsets.Type.displayCutout() | WindowInsets.Type.statusBars()
+                    | WindowInsets.Type.navigationBars();
         } else {
             mDecorTypes = WindowInsets.Type.navigationBars();
             mConfigTypes = WindowInsets.Type.navigationBars();
@@ -8268,9 +8269,41 @@
         }
 
         @Override
-        public boolean shouldShowSystemDecorOnDisplay(int displayId) {
+        public void setHomeSupportedOnDisplay(String displayUniqueId, int displayType,
+                boolean supported) {
+            final long origId = Binder.clearCallingIdentity();
+            try {
+                synchronized (mGlobalLock) {
+                    mDisplayWindowSettings.setHomeSupportedOnDisplayLocked(
+                            displayUniqueId, displayType, supported);
+                }
+            } finally {
+                Binder.restoreCallingIdentity(origId);
+            }
+        }
+
+        @Override
+        public boolean isHomeSupportedOnDisplay(int displayId) {
             synchronized (mGlobalLock) {
-                return WindowManagerService.this.shouldShowSystemDecors(displayId);
+                final DisplayContent displayContent = mRoot.getDisplayContent(displayId);
+                if (displayContent == null) {
+                    ProtoLog.w(WM_ERROR, "Attempted to get home support flag of a display that "
+                            + "does not exist: %d", displayId);
+                    return false;
+                }
+                return displayContent.isHomeSupported();
+            }
+        }
+
+        @Override
+        public void clearDisplaySettings(String displayUniqueId, int displayType) {
+            final long origId = Binder.clearCallingIdentity();
+            try {
+                synchronized (mGlobalLock) {
+                    mDisplayWindowSettings.clearDisplaySettings(displayUniqueId, displayType);
+                }
+            } finally {
+                Binder.restoreCallingIdentity(origId);
             }
         }
 
@@ -8899,7 +8932,7 @@
      * views.
      */
     void grantInputChannel(Session session, int callingUid, int callingPid, int displayId,
-            SurfaceControl surface, IWindow window, IBinder hostInputToken,
+            SurfaceControl surface, IBinder clientToken, IBinder hostInputToken,
             int flags, int privateFlags, int inputFeatures, int type, IBinder windowToken,
             IBinder inputTransferToken, String inputHandleName, InputChannel outInputChannel) {
         final int sanitizedType = sanitizeWindowType(session, displayId, windowToken, type);
@@ -8908,7 +8941,7 @@
         Objects.requireNonNull(outInputChannel);
         synchronized (mGlobalLock) {
             EmbeddedWindowController.EmbeddedWindow win =
-                    new EmbeddedWindowController.EmbeddedWindow(session, this, window,
+                    new EmbeddedWindowController.EmbeddedWindow(session, this, clientToken,
                             mInputToWindowMap.get(hostInputToken), callingUid, callingPid,
                             sanitizedType, displayId, inputTransferToken, inputHandleName,
                             (flags & FLAG_NOT_FOCUSABLE) == 0);
@@ -8920,7 +8953,7 @@
 
         updateInputChannel(outInputChannel.getToken(), callingUid, callingPid, displayId, surface,
                 name, applicationHandle, flags, privateFlags, inputFeatures, sanitizedType,
-                null /* region */, window);
+                null /* region */, clientToken);
     }
 
     boolean transferEmbeddedTouchFocusToHost(IWindow embeddedWindow) {
@@ -8995,10 +9028,10 @@
     private void updateInputChannel(IBinder channelToken, int callingUid, int callingPid,
             int displayId, SurfaceControl surface, String name,
             InputApplicationHandle applicationHandle, int flags,
-            int privateFlags, int inputFeatures, int type, Region region, IWindow window) {
+            int privateFlags, int inputFeatures, int type, Region region, IBinder clientToken) {
         final InputWindowHandle h = new InputWindowHandle(applicationHandle, displayId);
         h.token = channelToken;
-        h.setWindowToken(window);
+        h.setWindowToken(clientToken);
         h.name = name;
 
         flags = sanitizeFlagSlippery(flags, name, callingUid, callingPid);
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 8ded38b..5293292 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -1693,8 +1693,8 @@
     }
 
     @Override
-    public IWindow getIWindow() {
-        return mClient;
+    public IBinder getWindowToken() {
+        return mClient.asBinder();
     }
 
     @Override
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 2182093..f1cddc6 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -2557,11 +2557,10 @@
 static void nativeSetKeyRepeatConfiguration(JNIEnv* env, jobject nativeImplObj, jint timeoutMs,
                                             jint delayMs) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
-    im->getInputManager()->getDispatcher().setKeyRepeatConfiguration(static_cast<nsecs_t>(
-                                                                             timeoutMs) *
-                                                                             1000000,
-                                                                     static_cast<nsecs_t>(delayMs) *
-                                                                             1000000);
+    im->getInputManager()->getDispatcher().setKeyRepeatConfiguration(std::chrono::milliseconds(
+                                                                             timeoutMs),
+                                                                     std::chrono::milliseconds(
+                                                                             delayMs));
 }
 
 static jobject createInputSensorInfo(JNIEnv* env, jstring name, jstring vendor, jint version,
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 215934f..cca4261 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -455,6 +455,20 @@
                 <xs:annotation name="nullable"/>
                 <xs:annotation name="final"/>
             </xs:element>
+            <!-- list of supported modes when sensor is ON. Each point corresponds to one mode.
+            Mode format is : first = refreshRate, second = vsyncRate. E.g. :
+            <supportedModes>
+                <point>
+                    <first>60</first>   // refreshRate
+                    <second>60</second> //vsyncRate
+                </point>
+                ....
+            </supportedModes>
+             -->
+            <xs:element type="nonNegativeFloatToFloatMap" name="supportedModes" minOccurs="0">
+                <xs:annotation name="nullable"/>
+                <xs:annotation name="final"/>
+            </xs:element>
         </xs:sequence>
     </xs:complexType>
 
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index f7e0043..f767291 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -349,9 +349,11 @@
     ctor public SensorDetails();
     method @Nullable public final String getName();
     method @Nullable public final com.android.server.display.config.RefreshRateRange getRefreshRate();
+    method @Nullable public final com.android.server.display.config.NonNegativeFloatToFloatMap getSupportedModes();
     method @Nullable public final String getType();
     method public final void setName(@Nullable String);
     method public final void setRefreshRate(@Nullable com.android.server.display.config.RefreshRateRange);
+    method public final void setSupportedModes(@Nullable com.android.server.display.config.NonNegativeFloatToFloatMap);
     method public final void setType(@Nullable String);
   }
 
diff --git a/services/proguard.flags b/services/proguard.flags
index 407505d..88561b4 100644
--- a/services/proguard.flags
+++ b/services/proguard.flags
@@ -45,10 +45,6 @@
    public static void write(...);
 }
 
-# Binder interfaces
--keep,allowoptimization,allowaccessmodification class * extends android.os.IInterface
--keep,allowoptimization,allowaccessmodification class * extends android.os.IHwInterface
-
 # Various classes subclassed in or referenced via JNI in ethernet-service
 -keep public class android.net.** { *; }
 -keep,allowoptimization,allowaccessmodification class com.android.net.module.util.* { *; }
diff --git a/services/tests/displayservicetests/Android.bp b/services/tests/displayservicetests/Android.bp
index 6e4069f..3bafe72 100644
--- a/services/tests/displayservicetests/Android.bp
+++ b/services/tests/displayservicetests/Android.bp
@@ -7,19 +7,12 @@
     default_applicable_licenses: ["frameworks_base_license"],
 }
 
-// Include all test java files.
-filegroup {
-    name: "displayservicetests-sources",
-    srcs: [
-        "src/**/*.java",
-    ],
-}
-
 android_test {
     name: "DisplayServiceTests",
 
     srcs: [
         "src/**/*.java",
+        "src/**/*.kt",
     ],
 
     libs: [
@@ -33,6 +26,8 @@
         "frameworks-base-testutils",
         "junit",
         "junit-params",
+        "kotlin-test",
+        "mockito-kotlin2",
         "mockingservicestests-utils-mockito",
         "platform-compat-test-rules",
         "platform-test-annotations",
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 179a9d5..0bcbeb9 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -17,9 +17,12 @@
 package com.android.server.display;
 
 
+import static com.android.server.display.config.SensorData.SupportedMode;
 import static com.android.server.display.utils.DeviceConfigParsingUtils.ambientBrightnessThresholdsIntToFloat;
 import static com.android.server.display.utils.DeviceConfigParsingUtils.displayBrightnessThresholdsIntToFloat;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -526,6 +529,26 @@
     }
 
     @Test
+    public void testProximitySensorWithRefreshRatesFromDisplayConfig() throws IOException {
+        setupDisplayDeviceConfigFromDisplayConfigFile(
+                getContent(getValidLuxThrottling(), getValidProxSensorWithRefreshRateAndVsyncRate(),
+                        /* includeIdleMode= */ true));
+        assertEquals("test_proximity_sensor",
+                mDisplayDeviceConfig.getProximitySensor().type);
+        assertEquals("Test Proximity Sensor",
+                mDisplayDeviceConfig.getProximitySensor().name);
+        assertEquals(mDisplayDeviceConfig.getProximitySensor().minRefreshRate, 60, SMALL_DELTA);
+        assertEquals(mDisplayDeviceConfig.getProximitySensor().maxRefreshRate, 90, SMALL_DELTA);
+        assertThat(mDisplayDeviceConfig.getProximitySensor().supportedModes).hasSize(2);
+        SupportedMode mode = mDisplayDeviceConfig.getProximitySensor().supportedModes.get(0);
+        assertEquals(mode.refreshRate, 60, SMALL_DELTA);
+        assertEquals(mode.vsyncRate, 65, SMALL_DELTA);
+        mode = mDisplayDeviceConfig.getProximitySensor().supportedModes.get(1);
+        assertEquals(mode.refreshRate, 120, SMALL_DELTA);
+        assertEquals(mode.vsyncRate, 125, SMALL_DELTA);
+    }
+
+    @Test
     public void testBlockingZoneThresholdsFromDisplayConfig() throws IOException {
         setupDisplayDeviceConfigFromDisplayConfigFile();
 
@@ -821,6 +844,27 @@
                 + "</proxSensor>\n";
     }
 
+    private String getValidProxSensorWithRefreshRateAndVsyncRate() {
+        return "<proxSensor>\n"
+                +   "<type>test_proximity_sensor</type>\n"
+                +   "<name>Test Proximity Sensor</name>\n"
+                +   "<refreshRate>\n"
+                +       "<minimum>60</minimum>\n"
+                +       "<maximum>90</maximum>\n"
+                +   "</refreshRate>\n"
+                +   "<supportedModes>\n"
+                +       "<point>\n"
+                +           "<first>60</first>\n"   // refreshRate
+                +           "<second>65</second>\n" //vsyncRate
+                +       "</point>\n"
+                +       "<point>\n"
+                +           "<first>120</first>\n"   // refreshRate
+                +           "<second>125</second>\n" //vsyncRate
+                +       "</point>\n"
+                +   "</supportedModes>"
+                + "</proxSensor>\n";
+    }
+
     private String getProxSensorWithEmptyValues() {
         return "<proxSensor>\n"
                 +   "<type></type>\n"
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index 9684f42..0bf4654 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -123,6 +123,7 @@
 import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
 import com.android.server.display.DisplayManagerService.DeviceStateListener;
 import com.android.server.display.DisplayManagerService.SyncRoot;
+import com.android.server.display.config.SensorData;
 import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.notifications.DisplayNotificationManager;
 import com.android.server.input.InputManagerInternal;
@@ -195,7 +196,8 @@
     @Rule(order = 1)
     public Expect expect = Expect.create();
     @Rule
-    public SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+    public SetFlagsRule mSetFlagsRule =
+            new SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT);
 
     private Context mContext;
 
@@ -2317,11 +2319,8 @@
         String testSensorType = "testType";
         Sensor testSensor = TestUtils.createSensor(testSensorType, testSensorName);
 
-        DisplayDeviceConfig.SensorData sensorData = new DisplayDeviceConfig.SensorData();
-        sensorData.type = testSensorType;
-        sensorData.name = testSensorName;
-        sensorData.minRefreshRate = 10f;
-        sensorData.maxRefreshRate = 100f;
+        SensorData sensorData = new SensorData(testSensorType, testSensorName,
+                /* minRefreshRate= */ 10f, /* maxRefreshRate= */ 100f);
 
         when(mMockDisplayDeviceConfig.getProximitySensor()).thenReturn(sensorData);
         when(mSensorManager.getSensorList(Sensor.TYPE_ALL)).thenReturn(Collections.singletonList(
@@ -2352,12 +2351,6 @@
         String testSensorType = "testType";
         Sensor testSensor = TestUtils.createSensor(testSensorType, testSensorName);
 
-        DisplayDeviceConfig.SensorData sensorData = new DisplayDeviceConfig.SensorData();
-        sensorData.type = testSensorType;
-        sensorData.name = testSensorName;
-        sensorData.minRefreshRate = 10f;
-        sensorData.maxRefreshRate = 100f;
-
         when(mMockDisplayDeviceConfig.getProximitySensor()).thenReturn(null);
         when(mSensorManager.getSensorList(Sensor.TYPE_ALL)).thenReturn(Collections.singletonList(
                 testSensor));
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
index 47521d1..57f392a 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
@@ -77,6 +77,7 @@
 import com.android.server.display.brightness.clamper.BrightnessClamperController;
 import com.android.server.display.brightness.clamper.HdrClamper;
 import com.android.server.display.color.ColorDisplayService;
+import com.android.server.display.config.SensorData;
 import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.feature.flags.Flags;
 import com.android.server.display.layout.Layout;
@@ -1618,23 +1619,13 @@
         when(displayDeviceMock.getUniqueId()).thenReturn(uniqueId);
         when(displayDeviceMock.getDisplayDeviceConfig()).thenReturn(displayDeviceConfigMock);
         when(displayDeviceConfigMock.getProximitySensor()).thenReturn(
-                new DisplayDeviceConfig.SensorData() {
-                    {
-                        type = Sensor.STRING_TYPE_PROXIMITY;
-                        name = null;
-                    }
-                });
+                new SensorData(Sensor.STRING_TYPE_PROXIMITY, null));
         when(displayDeviceConfigMock.getNits()).thenReturn(new float[]{2, 500});
         when(displayDeviceConfigMock.isAutoBrightnessAvailable()).thenReturn(true);
         when(displayDeviceConfigMock.getAmbientLightSensor()).thenReturn(
-                new DisplayDeviceConfig.SensorData());
+                new SensorData());
         when(displayDeviceConfigMock.getScreenOffBrightnessSensor()).thenReturn(
-                new DisplayDeviceConfig.SensorData() {
-                    {
-                        type = Sensor.STRING_TYPE_LIGHT;
-                        name = null;
-                    }
-                });
+                new SensorData(Sensor.STRING_TYPE_LIGHT, null));
         when(displayDeviceConfigMock.getScreenOffBrightnessSensorValueToLux())
                 .thenReturn(new int[0]);
 
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 37ee23f..9617bd0 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -76,6 +76,7 @@
 import com.android.server.display.RampAnimator.DualRampAnimator;
 import com.android.server.display.brightness.BrightnessEvent;
 import com.android.server.display.color.ColorDisplayService;
+import com.android.server.display.config.SensorData;
 import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.feature.flags.Flags;
 import com.android.server.display.layout.Layout;
@@ -1515,23 +1516,13 @@
         when(displayDeviceMock.getUniqueId()).thenReturn(uniqueId);
         when(displayDeviceMock.getDisplayDeviceConfig()).thenReturn(displayDeviceConfigMock);
         when(displayDeviceConfigMock.getProximitySensor()).thenReturn(
-                new DisplayDeviceConfig.SensorData() {
-                    {
-                        type = Sensor.STRING_TYPE_PROXIMITY;
-                        name = null;
-                    }
-                });
+                new SensorData(Sensor.STRING_TYPE_PROXIMITY, null));
         when(displayDeviceConfigMock.getNits()).thenReturn(new float[]{2, 500});
         when(displayDeviceConfigMock.isAutoBrightnessAvailable()).thenReturn(true);
         when(displayDeviceConfigMock.getAmbientLightSensor()).thenReturn(
-                new DisplayDeviceConfig.SensorData());
+                new SensorData());
         when(displayDeviceConfigMock.getScreenOffBrightnessSensor()).thenReturn(
-                new DisplayDeviceConfig.SensorData() {
-                    {
-                        type = Sensor.STRING_TYPE_LIGHT;
-                        name = null;
-                    }
-                });
+                new SensorData(Sensor.STRING_TYPE_LIGHT, null));
         when(displayDeviceConfigMock.getScreenOffBrightnessSensorValueToLux())
                 .thenReturn(new int[0]);
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java
index 534a708..ebd6614 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java
@@ -37,6 +37,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.server.display.config.SensorData;
 import com.android.server.testutils.OffsettableClock;
 
 import org.junit.Before;
@@ -74,14 +75,7 @@
         mClock = new OffsettableClock.Stopped();
         mTestLooper = new TestLooper(mClock::now);
         when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(
-                new DisplayDeviceConfig.SensorData() {
-                    {
-                        type = Sensor.STRING_TYPE_PROXIMITY;
-                        // This is kept null because currently there is no way to define a sensor
-                        // name in TestUtils
-                        name = null;
-                    }
-                });
+                new SensorData(Sensor.STRING_TYPE_PROXIMITY, null));
         setUpProxSensor();
         DisplayPowerProximityStateController.Injector injector =
                 new DisplayPowerProximityStateController.Injector() {
@@ -171,13 +165,7 @@
 
     @Test
     public void isProximitySensorAvailableReturnsFalseWhenNotAvailableAndNoDefault() {
-        when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(
-                new DisplayDeviceConfig.SensorData() {
-                    {
-                        type = null;
-                        name = null;
-                    }
-                });
+        when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(new SensorData());
         mDisplayPowerProximityStateController = new DisplayPowerProximityStateController(
                 mWakelockController, mDisplayDeviceConfig, mTestLooper.getLooper(),
                 mNudgeUpdatePowerState, Display.DEFAULT_DISPLAY,
@@ -188,13 +176,7 @@
     @Test
     public void isProximitySensorAvailableReturnsTrueWhenNotAvailableAndHasDefault()
             throws Exception {
-        when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(
-                new DisplayDeviceConfig.SensorData() {
-                    {
-                        type = null;
-                        name = null;
-                    }
-                });
+        when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(new SensorData());
         when(mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)).thenReturn(
                 TestUtils.createSensor(Sensor.TYPE_PROXIMITY, "proximity"));
         mDisplayPowerProximityStateController = new DisplayPowerProximityStateController(
@@ -207,13 +189,7 @@
     @Test
     public void isProximitySensorAvailableReturnsFalseWhenNotAvailableHasDefaultNonDefaultDisplay()
             throws Exception {
-        when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(
-                new DisplayDeviceConfig.SensorData() {
-                    {
-                        type = null;
-                        name = null;
-                    }
-                });
+        when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(new SensorData());
         when(mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)).thenReturn(
                 TestUtils.createSensor(Sensor.TYPE_PROXIMITY, "proximity"));
         mDisplayPowerProximityStateController = new DisplayPowerProximityStateController(
@@ -240,12 +216,7 @@
     public void notifyDisplayDeviceChangedReloadsTheProximitySensor() throws Exception {
         DisplayDeviceConfig updatedDisplayDeviceConfig = mock(DisplayDeviceConfig.class);
         when(updatedDisplayDeviceConfig.getProximitySensor()).thenReturn(
-                new DisplayDeviceConfig.SensorData() {
-                    {
-                        type = Sensor.STRING_TYPE_PROXIMITY;
-                        name = null;
-                    }
-                });
+                new SensorData(Sensor.STRING_TYPE_PROXIMITY, null));
         Sensor newProxSensor = TestUtils.createSensor(
                 Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_PROXIMITY, 4.0f);
         when(mSensorManager.getSensorList(eq(Sensor.TYPE_ALL)))
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.kt b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.kt
new file mode 100644
index 0000000..49fa254
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display
+
+import android.view.Display
+import androidx.test.filters.SmallTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.junit.MockitoJUnit
+
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+@SmallTest
+class DisplayPowerStateTest {
+
+    private lateinit var displayPowerState: DisplayPowerState
+
+    @get:Rule
+    val mockitoRule = MockitoJUnit.rule()
+
+    private val mockBlanker = mock<DisplayBlanker>()
+    private val mockColorFade = mock<ColorFade>()
+
+    @Before
+    fun setUp() {
+        displayPowerState = DisplayPowerState(mockBlanker, mockColorFade, 123, Display.STATE_ON)
+    }
+
+    @Test
+    fun `destroys ColorFade on stop`() {
+        displayPowerState.stop()
+
+        verify(mockColorFade).destroy()
+    }
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java
index 8bbacc4..73e7ba0 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java
@@ -77,7 +77,7 @@
 
         DisplayDevice result = mVirtualDisplayAdapter.createVirtualDisplayLocked(mMockCallback,
                 /* projection= */ null, /* ownerUid= */ 10, /* packageName= */ "testpackage",
-                /* surface= */ null, /* flags= */ 0, config);
+                /* uniqueId= */ "uniqueId", /* surface= */ null, /* flags= */ 0, config);
 
         assertNotNull(result);
     }
@@ -89,12 +89,12 @@
         VirtualDisplayConfig config2 = new VirtualDisplayConfig.Builder("test2", /* width= */ 1,
                 /* height= */ 1, /* densityDpi= */ 1).build();
         mVirtualDisplayAdapter.createVirtualDisplayLocked(mMockCallback, /* projection= */ null,
-                /* ownerUid= */ 10, /* packageName= */ "testpackage", /* surface= */ null,
-                /* flags= */ 0, config1);
+                /* ownerUid= */ 10, /* packageName= */ "testpackage", /* uniqueId= */ "uniqueId1",
+                /* surface= */ null, /* flags= */ 0, config1);
 
         DisplayDevice result = mVirtualDisplayAdapter.createVirtualDisplayLocked(mMockCallback,
                 /* projection= */ null, /* ownerUid= */ 10, /* packageName= */ "testpackage",
-                /* surface= */ null, /* flags= */ 0, config2);
+                /* uniqueId= */ "uniqueId2", /* surface= */ null, /* flags= */ 0, config2);
 
         assertNull(result);
     }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/utils/SensorUtilsTest.java b/services/tests/displayservicetests/src/com/android/server/display/utils/SensorUtilsTest.java
index 4494b0c..6e2d954 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/utils/SensorUtilsTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/utils/SensorUtilsTest.java
@@ -28,7 +28,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.annotations.Keep;
-import com.android.server.display.DisplayDeviceConfig.SensorData;
+import com.android.server.display.config.SensorData;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -123,9 +123,7 @@
         when(mSensorManager.getSensorList(Sensor.TYPE_ALL)).thenReturn(allSensors);
         when(mSensorManager.getDefaultSensor(fallbackType)).thenReturn(defaultSensor);
 
-        SensorData sensorData = new SensorData();
-        sensorData.name = sensorName;
-        sensorData.type = sensorType;
+        SensorData sensorData = new SensorData(sensorType, sensorName);
 
         Sensor result = SensorUtils.findSensor(mSensorManager, sensorData, fallbackType);
 
diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp
index 063af57..113511e 100644
--- a/services/tests/mockingservicestests/Android.bp
+++ b/services/tests/mockingservicestests/Android.bp
@@ -46,6 +46,7 @@
         "androidx.test.espresso.core",
         "androidx.test.espresso.contrib",
         "androidx.test.ext.truth",
+        "backup_flags_lib",
         "flag-junit",
         "frameworks-base-testutils",
         "hamcrest-library",
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index 37fe8d1..f45dd39 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -2625,7 +2625,7 @@
         doReturn(PROCESS_STATE_TOP).when(sService.mAtmInternal).getTopProcessState();
         doReturn(app).when(sService).getTopApp();
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
-        sService.mOomAdjuster.updateOomAdjLocked(app, OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
 
         assertEquals(FOREGROUND_APP_ADJ, app.mState.getSetAdj());
 
@@ -2637,7 +2637,7 @@
         // Since sr.app is null, this service cannot be in the same process as the
         // client so we expect the BIND_ABOVE_CLIENT adjustment to take effect.
         app.mServices.updateHasAboveClientLocked();
-        sService.mOomAdjuster.updateOomAdjLocked(app, OOM_ADJ_REASON_NONE);
+        updateOomAdj(app);
         assertTrue(app.mServices.hasAboveClient());
         assertNotEquals(FOREGROUND_APP_ADJ, app.mState.getSetAdj());
     }
diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java
new file mode 100644
index 0000000..0006d0a
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.backup.restore;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.app.backup.BackupAgent;
+import android.app.backup.RestoreSet;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManagerInternal;
+import android.os.UserManager;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.modules.utils.testing.ExtendedMockitoRule;
+import com.android.server.LocalServices;
+import com.android.server.backup.Flags;
+import com.android.server.backup.TransportManager;
+import com.android.server.backup.UserBackupManagerService;
+import com.android.server.backup.utils.BackupEligibilityRules;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class ActiveRestoreSessionTest {
+    private static final String TEST_APP_NAME = "test_app";
+
+    private ActiveRestoreSession mRestoreSession;
+    private ApplicationInfo mTestApp;
+
+    @Mock
+    private UserBackupManagerService mBackupManagerService;
+    @Mock
+    private BackupEligibilityRules mBackupEligibilityRules;
+    @Mock
+    private Context mContext;
+    @Mock
+    private PackageManagerInternal mPackageManagerInternal;
+    @Mock
+    private TransportManager mTransportManager;
+    @Mock private UserManager mUserManager;
+
+    @Rule
+    public final SetFlagsRule mFlagsRule = new SetFlagsRule();
+    @Rule
+    public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule
+            .Builder(/* testClassInstance */ this)
+            .mockStatic(LocalServices.class)
+            .afterSessionFinished(
+                    () -> LocalServices.removeServiceForTest(PackageManagerInternal.class)
+            ).build();
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(/* testClass */ this);
+        when(mBackupEligibilityRules.isAppEligibleForRestore(any())).thenReturn(true);
+        when(mBackupManagerService.getEligibilityRulesForOperation(anyInt())).thenReturn(
+                mBackupEligibilityRules);
+        when(mBackupManagerService.getTransportManager()).thenReturn(mTransportManager);
+        when(mBackupManagerService.getContext()).thenReturn(mContext);
+        when(mContext.getSystemService(eq(UserManager.class))).thenReturn(mUserManager);
+        when(LocalServices.getService(PackageManagerInternal.class)).thenReturn(
+                mPackageManagerInternal);
+
+        mRestoreSession = new ActiveRestoreSession(mBackupManagerService,
+                /* packageName */ null,
+                /* transportName */ "",
+                mBackupEligibilityRules);
+        mTestApp = new ApplicationInfo();
+        mTestApp.packageName = TEST_APP_NAME;
+    }
+
+    @Test
+    public void testGetBackupEligibilityRules_skipRestoreFlagOn_skipsLaunchedAppRestore() {
+        mFlagsRule.enableFlags(Flags.FLAG_ENABLE_SKIPPING_RESTORE_LAUNCHED_APPS);
+        RestoreSet restoreSet = new RestoreSet(
+                /* name */ null,
+                /* device */ null,
+                /* token */ 0,
+                /* backupTransportFlags */ BackupAgent.FLAG_SKIP_RESTORE_FOR_LAUNCHED_APPS);
+        when(mPackageManagerInternal.wasPackageEverLaunched(eq(TEST_APP_NAME), anyInt()))
+                .thenReturn(true);
+
+        BackupEligibilityRules eligibilityRules = mRestoreSession.getBackupEligibilityRules(
+                restoreSet);
+
+        assertThat(eligibilityRules.isAppEligibleForRestore(mTestApp)).isFalse();
+    }
+
+    @Test
+    public void testGetBackupEligibilityRules_skipRestoreFlagOff_allowsAppRestore() {
+        mFlagsRule.disableFlags(Flags.FLAG_ENABLE_SKIPPING_RESTORE_LAUNCHED_APPS);
+        RestoreSet restoreSet = new RestoreSet(
+                /* name */ null,
+                /* device */ null,
+                /* token */ 0,
+                /* backupTransportFlags */ BackupAgent.FLAG_SKIP_RESTORE_FOR_LAUNCHED_APPS);
+        when(mPackageManagerInternal.wasPackageEverLaunched(eq(TEST_APP_NAME), anyInt()))
+                .thenReturn(true);
+
+        BackupEligibilityRules eligibilityRules = mRestoreSession.getBackupEligibilityRules(
+                restoreSet);
+
+        assertThat(eligibilityRules.isAppEligibleForRestore(mTestApp)).isTrue();
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupEligibilityRulesTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupEligibilityRulesTest.java
index 0306655..290b260 100644
--- a/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupEligibilityRulesTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupEligibilityRulesTest.java
@@ -860,6 +860,66 @@
         assertThat(result).isFalse();
     }
 
+    @Test
+    public void isAppEligibleForRestore_hasBeenLaunched_returnsFalse() {
+        when(mMockPackageManagerInternal.wasPackageEverLaunched(eq(TEST_PACKAGE_NAME), eq(mUserId)))
+                .thenReturn(true);
+        ApplicationInfo applicationInfo = getApplicationInfo(/* appUid */ 0, /* flags */ 0,
+                /* backupAgentName */ null);
+        BackupEligibilityRules backupEligibilityRules = new BackupEligibilityRules(mPackageManager,
+                mMockPackageManagerInternal, mUserId, mContext, BackupDestination.CLOUD,
+                /* skipRestoreForLaunchedApps */ true);
+
+        boolean isEligible = backupEligibilityRules.isAppEligibleForRestore(applicationInfo);
+
+        assertThat(isEligible).isFalse();
+    }
+
+    @Test
+    public void isAppEligibleForRestore_hasNotBeenLaunched_returnsTrue() {
+        when(mMockPackageManagerInternal.wasPackageEverLaunched(eq(TEST_PACKAGE_NAME), eq(mUserId)))
+                .thenReturn(false);
+        ApplicationInfo applicationInfo = getApplicationInfo(/* appUid */ 0, /* flags */ 0,
+                /* backupAgentName */ null);
+        BackupEligibilityRules backupEligibilityRules = new BackupEligibilityRules(mPackageManager,
+                mMockPackageManagerInternal, mUserId, mContext, BackupDestination.CLOUD,
+                /* skipRestoreForLaunchedApps */ false);
+
+        boolean isEligible = backupEligibilityRules.isAppEligibleForRestore(applicationInfo);
+
+        assertThat(isEligible).isTrue();
+    }
+
+    @Test
+    public void isAppEligibleForRestore_launchedButHasBackupAgent_returnsTrue() {
+        when(mMockPackageManagerInternal.wasPackageEverLaunched(eq(TEST_PACKAGE_NAME), eq(mUserId)))
+                .thenReturn(true);
+        ApplicationInfo applicationInfo = getApplicationInfo(/* appUid */ 0, /* flags */ 0,
+                /* backupAgentName */ "BackupAgent");
+        BackupEligibilityRules backupEligibilityRules = new BackupEligibilityRules(mPackageManager,
+                mMockPackageManagerInternal, mUserId, mContext, BackupDestination.CLOUD,
+                /* skipRestoreForLaunchedApps */ false);
+
+        boolean isEligible = backupEligibilityRules.isAppEligibleForRestore(applicationInfo);
+
+        assertThat(isEligible).isTrue();
+    }
+
+    @Test
+    public void isAppEligibleForRestore_doNotSkipRestoreForLaunched_returnsTrue() {
+        when(mMockPackageManagerInternal.wasPackageEverLaunched(eq(TEST_PACKAGE_NAME), eq(mUserId)))
+                .thenReturn(true);
+        ApplicationInfo applicationInfo = getApplicationInfo(/* appUid */ 0, /* flags */ 0,
+                /* backupAgentName */ null);
+        BackupEligibilityRules backupEligibilityRules = new BackupEligibilityRules(mPackageManager,
+                mMockPackageManagerInternal, mUserId, mContext, BackupDestination.CLOUD,
+                /* skipRestoreForLaunchedApps */ false);
+
+        boolean isEligible = backupEligibilityRules.isAppEligibleForRestore(applicationInfo);
+
+        assertThat(isEligible).isTrue();
+    }
+
     private BackupEligibilityRules getBackupEligibilityRules(
             @BackupDestination int backupDestination) {
         return new BackupEligibilityRules(mPackageManager, mMockPackageManagerInternal, mUserId,
diff --git a/services/tests/servicestests/res/xml/usertypes_test_profile.xml b/services/tests/servicestests/res/xml/usertypes_test_profile.xml
index ef19ba1..e89199d 100644
--- a/services/tests/servicestests/res/xml/usertypes_test_profile.xml
+++ b/services/tests/servicestests/res/xml/usertypes_test_profile.xml
@@ -39,6 +39,7 @@
             crossProfileIntentResolutionStrategy='0'
             mediaSharedWithParent='true'
             credentialShareableWithParent='false'
+            authAlwaysRequiredToDisableQuietMode='true'
             showInSettings='23'
             hideInSettingsInQuietMode='true'
             inheritDevicePolicy='450'
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapperTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionWrapperTest.java
similarity index 92%
rename from services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapperTest.java
rename to services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionWrapperTest.java
index 8608199..cfd0289 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapperTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionWrapperTest.java
@@ -37,11 +37,11 @@
 import org.mockito.MockitoAnnotations;
 
 /**
- * Tests for WindowMagnificationConnectionWrapper. We don't test {@code
- * WindowMagnificationConnectionWrapper#linkToDeath(IBinder.DeathRecipient)} since it's tested in
+ * Tests for MagnificationConnectionWrapper. We don't test {@code
+ * MagnificationConnectionWrapper#linkToDeath(IBinder.DeathRecipient)} since it's tested in
  * {@link WindowMagnificationManagerTest}.
  */
-public class WindowMagnificationConnectionWrapperTest {
+public class MagnificationConnectionWrapperTest {
 
     private static final int TEST_DISPLAY = Display.DEFAULT_DISPLAY;
 
@@ -54,14 +54,14 @@
     private MagnificationAnimationCallback mAnimationCallback;
 
     private MockWindowMagnificationConnection mMockWindowMagnificationConnection;
-    private WindowMagnificationConnectionWrapper mConnectionWrapper;
+    private MagnificationConnectionWrapper mConnectionWrapper;
 
     @Before
     public void setUp() throws RemoteException {
         MockitoAnnotations.initMocks(this);
         mMockWindowMagnificationConnection = new MockWindowMagnificationConnection();
         mConnection = mMockWindowMagnificationConnection.getConnection();
-        mConnectionWrapper = new WindowMagnificationConnectionWrapper(mConnection, mTrace);
+        mConnectionWrapper = new MagnificationConnectionWrapper(mConnection, mTrace);
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
index 1c33d0d..18961c0 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
@@ -34,6 +34,7 @@
 
 import com.android.internal.widget.LockscreenCredential;
 import com.android.server.ServiceThread;
+import com.android.server.locksettings.SyntheticPasswordManager.SyntheticPassword;
 import com.android.server.locksettings.recoverablekeystore.RecoverableKeyStoreManager;
 import com.android.server.pm.UserManagerInternal;
 
@@ -203,6 +204,10 @@
     }
 
     @Override
+    void initKeystoreSuperKeys(int userId, SyntheticPassword sp, boolean allowExisting) {
+    }
+
+    @Override
     protected boolean isCredentialSharableWithParent(int userId) {
         UserInfo userInfo = mUserManager.getUserInfo(userId);
         return userInfo.isCloneProfile() || userInfo.isManagedProfile();
diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkManagementServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkManagementServiceTest.java
index 2cdfbff..13dc120 100644
--- a/services/tests/servicestests/src/com/android/server/net/NetworkManagementServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/net/NetworkManagementServiceTest.java
@@ -57,7 +57,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.app.IBatteryStats;
-import com.android.net.flags.Flags;
+import com.android.modules.utils.build.SdkLevel;
 
 import org.junit.After;
 import org.junit.Before;
@@ -264,7 +264,7 @@
         verify(mCm).addUidToMeteredNetworkDenyList(TEST_UID);
 
         mNMService.setDataSaverModeEnabled(true);
-        if (Flags.setDataSaverViaCm()) {
+        if (SdkLevel.isAtLeastV()) {
             verify(mCm).setDataSaverEnabled(true);
         } else {
             verify(mNetdService).bandwidthEnableDataSaver(true);
@@ -284,7 +284,7 @@
         mNMService.setUidOnMeteredNetworkAllowlist(TEST_UID, false);
         verify(mCm).removeUidFromMeteredNetworkAllowList(TEST_UID);
         mNMService.setDataSaverModeEnabled(false);
-        if (Flags.setDataSaverViaCm()) {
+        if (SdkLevel.isAtLeastV()) {
             verify(mCm).setDataSaverEnabled(false);
         } else {
             verify(mNetdService).bandwidthEnableDataSaver(false);
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
index c684a7b..57b1225 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
@@ -67,6 +67,7 @@
                 .setCrossProfileIntentResolutionStrategy(0)
                 .setMediaSharedWithParent(false)
                 .setCredentialShareableWithParent(true)
+                .setAuthAlwaysRequiredToDisableQuietMode(false)
                 .setDeleteAppWithParent(false)
                 .setAlwaysVisible(false)
                 .build();
@@ -80,6 +81,7 @@
         actualProps.setCrossProfileIntentResolutionStrategy(1);
         actualProps.setMediaSharedWithParent(true);
         actualProps.setCredentialShareableWithParent(false);
+        actualProps.setAuthAlwaysRequiredToDisableQuietMode(true);
         actualProps.setDeleteAppWithParent(true);
         actualProps.setAlwaysVisible(true);
 
@@ -123,6 +125,7 @@
                 .setInheritDevicePolicy(1732)
                 .setMediaSharedWithParent(true)
                 .setDeleteAppWithParent(true)
+                .setAuthAlwaysRequiredToDisableQuietMode(false)
                 .setAlwaysVisible(true)
                 .build();
         final UserProperties orig = new UserProperties(defaultProps);
@@ -131,6 +134,7 @@
         orig.setShowInSettings(1437);
         orig.setInheritDevicePolicy(9456);
         orig.setDeleteAppWithParent(false);
+        orig.setAuthAlwaysRequiredToDisableQuietMode(true);
         orig.setAlwaysVisible(false);
 
         // Test every permission level. (Currently, it's linear so it's easy.)
@@ -182,6 +186,8 @@
                 hasManagePermission);
         assertEqualGetterOrThrows(orig::getUseParentsContacts,
                 copy::getUseParentsContacts, hasManagePermission);
+        assertEqualGetterOrThrows(orig::isAuthAlwaysRequiredToDisableQuietMode,
+                copy::isAuthAlwaysRequiredToDisableQuietMode, hasManagePermission);
 
         // Items requiring hasQueryPermission - put them here using hasQueryPermission.
 
@@ -242,6 +248,8 @@
                 .isEqualTo(actual.isMediaSharedWithParent());
         assertThat(expected.isCredentialShareableWithParent())
                 .isEqualTo(actual.isCredentialShareableWithParent());
+        assertThat(expected.isAuthAlwaysRequiredToDisableQuietMode())
+                .isEqualTo(actual.isAuthAlwaysRequiredToDisableQuietMode());
         assertThat(expected.getDeleteAppWithParent()).isEqualTo(actual.getDeleteAppWithParent());
         assertThat(expected.getAlwaysVisible()).isEqualTo(actual.getAlwaysVisible());
     }
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
index 20270a8..48eb5c6 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
@@ -89,6 +89,7 @@
                 .setCrossProfileIntentResolutionStrategy(1)
                 .setMediaSharedWithParent(true)
                 .setCredentialShareableWithParent(false)
+                .setAuthAlwaysRequiredToDisableQuietMode(true)
                 .setShowInSettings(900)
                 .setHideInSettingsInQuietMode(true)
                 .setInheritDevicePolicy(340)
@@ -160,6 +161,8 @@
                 .getCrossProfileIntentResolutionStrategy());
         assertTrue(type.getDefaultUserPropertiesReference().isMediaSharedWithParent());
         assertFalse(type.getDefaultUserPropertiesReference().isCredentialShareableWithParent());
+        assertTrue(type.getDefaultUserPropertiesReference()
+                .isAuthAlwaysRequiredToDisableQuietMode());
         assertEquals(900, type.getDefaultUserPropertiesReference().getShowInSettings());
         assertTrue(type.getDefaultUserPropertiesReference().getHideInSettingsInQuietMode());
         assertEquals(340, type.getDefaultUserPropertiesReference()
@@ -306,6 +309,7 @@
                 .setCrossProfileIntentResolutionStrategy(1)
                 .setMediaSharedWithParent(false)
                 .setCredentialShareableWithParent(true)
+                .setAuthAlwaysRequiredToDisableQuietMode(false)
                 .setShowInSettings(20)
                 .setHideInSettingsInQuietMode(false)
                 .setInheritDevicePolicy(21)
@@ -347,6 +351,8 @@
         assertFalse(aospType.getDefaultUserPropertiesReference().isMediaSharedWithParent());
         assertTrue(aospType.getDefaultUserPropertiesReference()
                 .isCredentialShareableWithParent());
+        assertFalse(aospType.getDefaultUserPropertiesReference()
+                .isAuthAlwaysRequiredToDisableQuietMode());
         assertEquals(20, aospType.getDefaultUserPropertiesReference().getShowInSettings());
         assertFalse(aospType.getDefaultUserPropertiesReference().getHideInSettingsInQuietMode());
         assertEquals(21, aospType.getDefaultUserPropertiesReference()
@@ -394,6 +400,8 @@
         assertTrue(aospType.getDefaultUserPropertiesReference().isMediaSharedWithParent());
         assertFalse(aospType.getDefaultUserPropertiesReference()
                 .isCredentialShareableWithParent());
+        assertTrue(aospType.getDefaultUserPropertiesReference()
+                .isAuthAlwaysRequiredToDisableQuietMode());
         assertEquals(23, aospType.getDefaultUserPropertiesReference().getShowInSettings());
         assertTrue(aospType.getDefaultUserPropertiesReference().getHideInSettingsInQuietMode());
         assertEquals(450, aospType.getDefaultUserPropertiesReference()
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index 775d42a..2b6d8ed 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -331,6 +331,9 @@
                 .isEqualTo(privateProfileUserProperties.isMediaSharedWithParent());
         assertThat(typeProps.isCredentialShareableWithParent())
                 .isEqualTo(privateProfileUserProperties.isCredentialShareableWithParent());
+        assertThat(typeProps.isAuthAlwaysRequiredToDisableQuietMode())
+                .isEqualTo(privateProfileUserProperties
+                        .isAuthAlwaysRequiredToDisableQuietMode());
         assertThrows(SecurityException.class, privateProfileUserProperties::getDeleteAppWithParent);
 
         // Verify private profile parent
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 13c011a..44dad59 100644
--- a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java
@@ -18,9 +18,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+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.assertNotSame;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -28,6 +30,7 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -49,6 +52,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.SystemService;
+import com.android.server.power.ThermalManagerService.TemperatureWatcher;
 import com.android.server.power.ThermalManagerService.ThermalHalWrapper;
 
 import org.junit.Before;
@@ -65,6 +69,7 @@
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 
 /**
  * atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server
@@ -415,9 +420,9 @@
 
     @Test
     public void testTemperatureWatcherUpdateSevereThresholds() throws RemoteException {
-        ThermalManagerService.TemperatureWatcher watcher = mService.mTemperatureWatcher;
+        TemperatureWatcher watcher = mService.mTemperatureWatcher;
         watcher.mSevereThresholds.erase();
-        watcher.updateSevereThresholds();
+        watcher.updateThresholds();
         assertEquals(1, watcher.mSevereThresholds.size());
         assertEquals("skin1", watcher.mSevereThresholds.keyAt(0));
         Float threshold = watcher.mSevereThresholds.get("skin1");
@@ -426,9 +431,60 @@
     }
 
     @Test
+    public void testTemperatureWatcherUpdateHeadroomThreshold() {
+        TemperatureWatcher watcher = mService.mTemperatureWatcher;
+        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);
+        synchronized (watcher.mSamples) {
+            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);
+        synchronized (watcher.mSamples) {
+            assertArrayEquals(new float[]{Float.NaN, 0.5f, 0.8f, 1.0f, 1.4f, 1.7f, 2.0f},
+                    watcher.mHeadroomThresholds, 0.01f);
+        }
+    }
+
+    @Test
+    public void testGetThermalHeadroomThresholdsOnlyReadOnce() throws Exception {
+        float[] expected = new float[]{Float.NaN, 0.1f, 0.2f, 0.3f, 0.4f, Float.NaN, 0.6f};
+        when(mIThermalServiceMock.getThermalHeadroomThresholds()).thenReturn(expected);
+        Map<Integer, Float> thresholds1 = mPowerManager.getThermalHeadroomThresholds();
+        verify(mIThermalServiceMock, times(1)).getThermalHeadroomThresholds();
+        for (int status = PowerManager.THERMAL_STATUS_LIGHT;
+                status <= PowerManager.THERMAL_STATUS_SHUTDOWN; status++) {
+            if (Float.isNaN(expected[status])) {
+                assertFalse(thresholds1.containsKey(status));
+            } else {
+                assertEquals(expected[status], thresholds1.get(status), 0.01f);
+            }
+        }
+        reset(mIThermalServiceMock);
+        Map<Integer, Float> thresholds2 = mPowerManager.getThermalHeadroomThresholds();
+        verify(mIThermalServiceMock, times(0)).getThermalHeadroomThresholds();
+        assertNotSame(thresholds1, thresholds2);
+        assertEquals(thresholds1, thresholds2);
+    }
+
+    @Test
     public void testTemperatureWatcherGetSlopeOf() throws RemoteException {
-        ThermalManagerService.TemperatureWatcher watcher = mService.mTemperatureWatcher;
-        List<ThermalManagerService.TemperatureWatcher.Sample> samples = new ArrayList<>();
+        TemperatureWatcher watcher = mService.mTemperatureWatcher;
+        List<TemperatureWatcher.Sample> samples = new ArrayList<>();
         for (int i = 0; i < 30; ++i) {
             samples.add(watcher.createSampleForTesting(i, (float) (i / 2 * 2)));
         }
@@ -437,21 +493,23 @@
 
     @Test
     public void testTemperatureWatcherNormalizeTemperature() throws RemoteException {
-        ThermalManagerService.TemperatureWatcher watcher = mService.mTemperatureWatcher;
-        assertEquals(0.5f, watcher.normalizeTemperature(25.0f, 40.0f), 0.0f);
+        assertEquals(0.5f,
+                TemperatureWatcher.normalizeTemperature(25.0f, 40.0f), 0.0f);
 
         // Temperatures more than 30 degrees below the SEVERE threshold should be clamped to 0.0f
-        assertEquals(0.0f, watcher.normalizeTemperature(0.0f, 40.0f), 0.0f);
+        assertEquals(0.0f,
+                TemperatureWatcher.normalizeTemperature(0.0f, 40.0f), 0.0f);
 
         // Temperatures above the SEVERE threshold should not be clamped
-        assertEquals(2.0f, watcher.normalizeTemperature(70.0f, 40.0f), 0.0f);
+        assertEquals(2.0f,
+                TemperatureWatcher.normalizeTemperature(70.0f, 40.0f), 0.0f);
     }
 
     @Test
     public void testTemperatureWatcherGetForecast() throws RemoteException {
-        ThermalManagerService.TemperatureWatcher watcher = mService.mTemperatureWatcher;
+        TemperatureWatcher watcher = mService.mTemperatureWatcher;
 
-        ArrayList<ThermalManagerService.TemperatureWatcher.Sample> samples = new ArrayList<>();
+        ArrayList<TemperatureWatcher.Sample> samples = new ArrayList<>();
 
         // Add a single sample
         samples.add(watcher.createSampleForTesting(0, 25.0f));
@@ -478,7 +536,7 @@
 
     @Test
     public void testTemperatureWatcherGetForecastUpdate() throws Exception {
-        ThermalManagerService.TemperatureWatcher watcher = mService.mTemperatureWatcher;
+        TemperatureWatcher watcher = mService.mTemperatureWatcher;
 
         // Reduce the inactivity threshold to speed up testing
         watcher.mInactivityThresholdMillis = 2000;
@@ -499,7 +557,7 @@
     }
 
     // Helper function to hold mSamples lock, avoid GuardedBy lint errors
-    private boolean isWatcherSamplesEmpty(ThermalManagerService.TemperatureWatcher watcher) {
+    private boolean isWatcherSamplesEmpty(TemperatureWatcher watcher) {
         synchronized (watcher.mSamples) {
             return watcher.mSamples.isEmpty();
         }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 3803244..7fb8b30 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -3239,7 +3239,7 @@
         mService.setPreferencesHelper(mPreferencesHelper);
         when(mPreferencesHelper.getNotificationChannel(
                 anyString(), anyInt(), eq("foo"), anyBoolean())).thenReturn(
-                        new NotificationChannel("foo", "foo", IMPORTANCE_HIGH));
+                    new NotificationChannel("foo", "foo", IMPORTANCE_HIGH));
 
         Notification.TvExtender tv = new Notification.TvExtender().setChannelId("foo");
         mBinderService.enqueueNotificationWithTag(PKG, PKG, "testTvExtenderChannelOverride_onTv", 0,
@@ -9927,6 +9927,174 @@
     }
 
     @Test
+    public void testRestoreConversationChannel_deleted() throws Exception {
+        // Create parent channel
+        when(mPackageManager.isPackageSuspendedForUser(anyString(), anyInt())).thenReturn(false);
+        final NotificationChannel originalChannel = new NotificationChannel("id", "name",
+                IMPORTANCE_DEFAULT);
+        NotificationChannel parentChannel = parcelAndUnparcel(originalChannel,
+                NotificationChannel.CREATOR);
+        assertEquals(originalChannel, parentChannel);
+        mBinderService.createNotificationChannels(PKG,
+                new ParceledListSlice(Arrays.asList(parentChannel)));
+
+        //Create deleted conversation channel
+        mBinderService.createConversationNotificationChannelForPackage(
+                PKG, mUid, parentChannel, VALID_CONVO_SHORTCUT_ID);
+        final NotificationChannel conversationChannel =
+                mBinderService.getConversationNotificationChannel(
+                PKG, mUserId, PKG, originalChannel.getId(), false, VALID_CONVO_SHORTCUT_ID);
+        conversationChannel.setDeleted(true);
+
+        //Create notification record
+        Notification.Builder nb = getMessageStyleNotifBuilder(false /* addDefaultMetadata */,
+                null /* groupKey */, false /* isSummary */);
+        nb.setShortcutId(VALID_CONVO_SHORTCUT_ID);
+        nb.setChannelId(originalChannel.getId());
+        StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1,
+                "tag", mUid, 0, nb.build(), UserHandle.getUserHandleForUid(mUid), null, 0);
+        NotificationRecord nr = new NotificationRecord(mContext, sbn, originalChannel);
+        assertThat(nr.getChannel()).isEqualTo(originalChannel);
+
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, nr.getSbn().getTag(),
+                nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId());
+        waitForIdle();
+
+        // Verify that the channel was changed to the conversation channel and restored
+        assertThat(mService.getNotificationRecord(nr.getKey()).isConversation()).isTrue();
+        assertThat(mService.getNotificationRecord(nr.getKey()).getChannel()).isEqualTo(
+                conversationChannel);
+        assertThat(mService.getNotificationRecord(nr.getKey()).getChannel().isDeleted()).isFalse();
+        assertThat(mService.getNotificationRecord(nr.getKey()).getChannel().getDeletedTimeMs())
+                .isEqualTo(-1);
+    }
+
+    @Test
+    public void testDoNotRestoreParentChannel_deleted() throws Exception {
+        // Create parent channel and set as deleted
+        when(mPackageManager.isPackageSuspendedForUser(anyString(), anyInt())).thenReturn(false);
+        final NotificationChannel originalChannel = new NotificationChannel("id", "name",
+                IMPORTANCE_DEFAULT);
+        NotificationChannel parentChannel = parcelAndUnparcel(originalChannel,
+                NotificationChannel.CREATOR);
+        assertEquals(originalChannel, parentChannel);
+        mBinderService.createNotificationChannels(PKG,
+                new ParceledListSlice(Arrays.asList(parentChannel)));
+        parentChannel.setDeleted(true);
+
+        //Create notification record
+        Notification.Builder nb = getMessageStyleNotifBuilder(false /* addDefaultMetadata */,
+                null /* groupKey */, false /* isSummary */);
+        nb.setShortcutId(VALID_CONVO_SHORTCUT_ID);
+        nb.setChannelId(originalChannel.getId());
+        StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1,
+                "tag", mUid, 0, nb.build(), UserHandle.getUserHandleForUid(mUid), null, 0);
+        NotificationRecord nr = new NotificationRecord(mContext, sbn, originalChannel);
+        assertThat(nr.getChannel()).isEqualTo(originalChannel);
+
+        when(mPermissionHelper.hasPermission(mUid)).thenReturn(true);
+
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, nr.getSbn().getTag(),
+                nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId());
+        waitForIdle();
+
+        // Verify that the channel was not restored and the notification was not posted
+        assertThat(mService.mChannelToastsSent).contains(mUid);
+        assertThat(mService.getNotificationRecord(nr.getKey())).isNull();
+        assertThat(parentChannel.isDeleted()).isTrue();
+    }
+
+    @Test
+    public void testEnqueueToConversationChannel_notDeleted_doesNotRestore() throws Exception {
+        TestableNotificationManagerService service = spy(mService);
+        PreferencesHelper preferencesHelper = spy(mService.mPreferencesHelper);
+        service.setPreferencesHelper(preferencesHelper);
+        // Create parent channel
+        when(mPackageManager.isPackageSuspendedForUser(anyString(), anyInt())).thenReturn(false);
+        final NotificationChannel originalChannel = new NotificationChannel("id", "name",
+                IMPORTANCE_DEFAULT);
+        NotificationChannel parentChannel = parcelAndUnparcel(originalChannel,
+                NotificationChannel.CREATOR);
+        assertEquals(originalChannel, parentChannel);
+        mBinderService.createNotificationChannels(PKG,
+                new ParceledListSlice(Arrays.asList(parentChannel)));
+
+        //Create conversation channel
+        mBinderService.createConversationNotificationChannelForPackage(
+                PKG, mUid, parentChannel, VALID_CONVO_SHORTCUT_ID);
+        final NotificationChannel conversationChannel =
+                mBinderService.getConversationNotificationChannel(
+                    PKG, mUserId, PKG, originalChannel.getId(), false, VALID_CONVO_SHORTCUT_ID);
+
+        //Create notification record
+        Notification.Builder nb = getMessageStyleNotifBuilder(false /* addDefaultMetadata */,
+                null /* groupKey */, false /* isSummary */);
+        nb.setShortcutId(VALID_CONVO_SHORTCUT_ID);
+        nb.setChannelId(originalChannel.getId());
+        StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1,
+                "tag", mUid, 0, nb.build(), UserHandle.getUserHandleForUid(mUid), null, 0);
+        NotificationRecord nr = new NotificationRecord(mContext, sbn, originalChannel);
+        assertThat(nr.getChannel()).isEqualTo(originalChannel);
+
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, nr.getSbn().getTag(),
+                nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId());
+        waitForIdle();
+
+        // Verify that the channel was changed to the conversation channel and not restored
+        assertThat(service.getNotificationRecord(nr.getKey()).isConversation()).isTrue();
+        assertThat(service.getNotificationRecord(nr.getKey()).getChannel()).isEqualTo(
+                conversationChannel);
+        verify(service, never()).handleSavePolicyFile();
+        verify(preferencesHelper, never()).createNotificationChannel(anyString(),
+                anyInt(), any(), anyBoolean(), anyBoolean(), anyInt(), anyBoolean());
+    }
+
+    @Test
+    public void testEnqueueToParentChannel_notDeleted_doesNotRestore() throws Exception {
+        TestableNotificationManagerService service = spy(mService);
+        PreferencesHelper preferencesHelper = spy(mService.mPreferencesHelper);
+        service.setPreferencesHelper(preferencesHelper);
+        // Create parent channel
+        when(mPackageManager.isPackageSuspendedForUser(anyString(), anyInt())).thenReturn(false);
+        final NotificationChannel originalChannel = new NotificationChannel("id", "name",
+                IMPORTANCE_DEFAULT);
+        NotificationChannel parentChannel = parcelAndUnparcel(originalChannel,
+                NotificationChannel.CREATOR);
+        assertEquals(originalChannel, parentChannel);
+        mBinderService.createNotificationChannels(PKG,
+                new ParceledListSlice(Arrays.asList(parentChannel)));
+
+        //Create deleted conversation channel
+        mBinderService.createConversationNotificationChannelForPackage(
+                PKG, mUid, parentChannel, VALID_CONVO_SHORTCUT_ID);
+        final NotificationChannel conversationChannel =
+                mBinderService.getConversationNotificationChannel(
+                    PKG, mUserId, PKG, originalChannel.getId(), false, VALID_CONVO_SHORTCUT_ID);
+
+        //Create notification record without a shortcutId
+        Notification.Builder nb = getMessageStyleNotifBuilder(false /* addDefaultMetadata */,
+                null /* groupKey */, false /* isSummary */);
+        nb.setShortcutId(null);
+        nb.setChannelId(originalChannel.getId());
+        StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1,
+                "tag", mUid, 0, nb.build(), UserHandle.getUserHandleForUid(mUid), null, 0);
+        NotificationRecord nr = new NotificationRecord(mContext, sbn, originalChannel);
+        assertThat(nr.getChannel()).isEqualTo(originalChannel);
+
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, nr.getSbn().getTag(),
+                nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId());
+        waitForIdle();
+
+        // Verify that the channel is the parent channel and no channel was restored
+        //assertThat(service.getNotificationRecord(nr.getKey()).isConversation()).isFalse();
+        assertThat(service.getNotificationRecord(nr.getKey()).getChannel()).isEqualTo(
+                parentChannel);
+        verify(service, never()).handleSavePolicyFile();
+        verify(preferencesHelper, never()).createNotificationChannel(anyString(),
+                anyInt(), any(), anyBoolean(), anyBoolean(), anyInt(), anyBoolean());
+    }
+
+    @Test
     public void testGetConversationsForPackage_hasShortcut() throws Exception {
         mService.setPreferencesHelper(mPreferencesHelper);
         ArrayList<ConversationChannelWrapper> convos = new ArrayList<>();
diff --git a/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
index eab8757..912e1d3d 100644
--- a/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
@@ -16,15 +16,19 @@
 
 package com.android.server.policy;
 
+import static android.provider.Settings.Global.STEM_PRIMARY_BUTTON_DOUBLE_PRESS;
 import static android.provider.Settings.Global.STEM_PRIMARY_BUTTON_LONG_PRESS;
 import static android.provider.Settings.Global.STEM_PRIMARY_BUTTON_SHORT_PRESS;
 import static android.view.KeyEvent.KEYCODE_STEM_PRIMARY;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_PRIMARY_LAUNCH_VOICE_ASSISTANT;
 import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS;
 import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_PRIMARY_LAUNCH_TARGET_ACTIVITY;
 
+import android.app.ActivityManager.RecentTaskInfo;
 import android.content.ComponentName;
+import android.os.RemoteException;
 import android.provider.Settings;
 
 import org.junit.Test;
@@ -120,6 +124,46 @@
         mPhoneWindowManager.assertStatusBarStartAssist();
     }
 
+    @Test
+    public void stemDoubleKey_EarlyShortPress_AllAppsThenSwitchToMostRecent()
+            throws RemoteException {
+        overrideBehavior(STEM_PRIMARY_BUTTON_DOUBLE_PRESS, SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS);
+        setUpPhoneWindowManager(/* supportSettingsUpdate= */ true);
+        mPhoneWindowManager.overrideShouldEarlyShortPressOnStemPrimary(true);
+        mPhoneWindowManager.setKeyguardServiceDelegateIsShowing(false);
+        mPhoneWindowManager.overrideIsUserSetupComplete(true);
+        RecentTaskInfo recentTaskInfo = new RecentTaskInfo();
+        int referenceId = 666;
+        recentTaskInfo.persistentId = referenceId;
+        doReturn(recentTaskInfo).when(
+                mPhoneWindowManager.mActivityTaskManagerInternal).getMostRecentTaskFromBackground();
+
+        sendKey(KEYCODE_STEM_PRIMARY);
+        sendKey(KEYCODE_STEM_PRIMARY);
+
+        mPhoneWindowManager.assertOpenAllAppView();
+        mPhoneWindowManager.assertSwitchToRecent(referenceId);
+    }
+
+    @Test
+    public void stemDoubleKey_NoEarlyShortPress_SwitchToMostRecent() throws RemoteException {
+        overrideBehavior(STEM_PRIMARY_BUTTON_DOUBLE_PRESS, SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS);
+        setUpPhoneWindowManager(/* supportSettingsUpdate= */ true);
+        mPhoneWindowManager.overrideShouldEarlyShortPressOnStemPrimary(false);
+        mPhoneWindowManager.setKeyguardServiceDelegateIsShowing(false);
+        mPhoneWindowManager.overrideIsUserSetupComplete(true);
+        RecentTaskInfo recentTaskInfo = new RecentTaskInfo();
+        int referenceId = 666;
+        recentTaskInfo.persistentId = referenceId;
+        doReturn(recentTaskInfo).when(
+                mPhoneWindowManager.mActivityTaskManagerInternal).getMostRecentTaskFromBackground();
+
+        sendKey(KEYCODE_STEM_PRIMARY);
+        sendKey(KEYCODE_STEM_PRIMARY);
+
+        mPhoneWindowManager.assertNotOpenAllAppView();
+        mPhoneWindowManager.assertSwitchToRecent(referenceId);
+    }
 
     private void overrideBehavior(String key, int expectedBehavior) {
         Settings.Global.putLong(mContext.getContentResolver(), key, expectedBehavior);
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index e26260a..314cd04 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -57,6 +57,7 @@
 
 import android.app.ActivityManagerInternal;
 import android.app.AppOpsManager;
+import android.app.IActivityManager;
 import android.app.NotificationManager;
 import android.app.SearchManager;
 import android.content.ComponentName;
@@ -126,7 +127,8 @@
 
     @Mock private WindowManagerInternal mWindowManagerInternal;
     @Mock private ActivityManagerInternal mActivityManagerInternal;
-    @Mock private ActivityTaskManagerInternal mActivityTaskManagerInternal;
+    @Mock ActivityTaskManagerInternal mActivityTaskManagerInternal;
+    @Mock IActivityManager mActivityManagerService;
     @Mock private InputManagerInternal mInputManagerInternal;
     @Mock private InputManager mInputManager;
     @Mock private SensorPrivacyManager mSensorPrivacyManager;
@@ -181,6 +183,10 @@
         KeyguardServiceDelegate getKeyguardServiceDelegate() {
             return mKeyguardServiceDelegate;
         }
+
+        IActivityManager getActivityManagerService() {
+            return mActivityManagerService;
+        }
     }
 
     TestPhoneWindowManager(Context context, boolean supportSettingsUpdate) {
@@ -347,6 +353,10 @@
         mPhoneWindowManager.mShortPressOnPowerBehavior = behavior;
     }
 
+    void overrideShouldEarlyShortPressOnStemPrimary(boolean shouldEarlyShortPress) {
+        mPhoneWindowManager.mShouldEarlyShortPressOnStemPrimary = shouldEarlyShortPress;
+    }
+
      // Override assist perform function.
     void overrideLongPressOnPower(int behavior) {
         mPhoneWindowManager.mLongPressOnPowerBehavior = behavior;
@@ -667,4 +677,11 @@
                         vendorId, productId, logEvent.getIntValue(), new int[]{expectedKey},
                         expectedModifierState), description(errorMsg));
     }
+
+    void assertSwitchToRecent(int persistentId) throws RemoteException {
+        mTestLooper.dispatchAll();
+        verify(mActivityManagerService,
+                timeout(TEST_SINGLE_KEY_DELAY_MILLIS)).startActivityFromRecents(eq(persistentId),
+                isNull());
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index 6790dc2..afea811 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -117,6 +117,20 @@
     }
 
     @Test
+    public void noBackWhenMoveTaskToBack() {
+        Task taskA = createTask(mDefaultDisplay);
+        ActivityRecord recordA = createActivityRecord(taskA);
+        Mockito.doNothing().when(recordA).reparentSurfaceControl(any(), any());
+
+        final Task topTask = createTopTaskWithActivity();
+        withSystemCallback(topTask);
+        // simulate moveTaskToBack
+        topTask.setVisibleRequested(false);
+        BackNavigationInfo backNavigationInfo = startBackNavigation();
+        assertWithMessage("BackNavigationInfo").that(backNavigationInfo).isNull();
+    }
+
+    @Test
     public void backTypeCrossTaskWhenBackToPreviousTask() {
         Task taskA = createTask(mDefaultDisplay);
         ActivityRecord recordA = createActivityRecord(taskA);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayAreaPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayAreaPolicyTests.java
index 147a44f..fafc035 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayAreaPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayAreaPolicyTests.java
@@ -190,7 +190,7 @@
         final WindowManagerService wms = mWm;
         final DisplayContent displayContent = mock(DisplayContent.class);
         doReturn(true).when(displayContent).isTrusted();
-        doReturn(true).when(displayContent).supportsSystemDecorations();
+        doReturn(true).when(displayContent).isHomeSupported();
         final RootDisplayArea root = new SurfacelessDisplayAreaRoot(wms);
         final TaskDisplayArea taskDisplayAreaWithHome = new TaskDisplayArea(displayContent, wms,
                 "Tasks1", FEATURE_DEFAULT_TASK_CONTAINER);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
index e54b8e5..e2524a2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
@@ -496,6 +496,19 @@
                 mPrivateDisplay.getDisplayInfo());
     }
 
+    @Test
+    public void testClearDisplaySettings() {
+        spyOn(mWm.mDisplayWindowSettings);
+        spyOn(mWm.mDisplayWindowSettingsProvider);
+
+        WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class);
+        DisplayInfo info = mPrivateDisplay.getDisplayInfo();
+        wmInternal.clearDisplaySettings(info.uniqueId, info.type);
+
+        verify(mWm.mDisplayWindowSettings).clearDisplaySettings(info.uniqueId, info.type);
+        verify(mWm.mDisplayWindowSettingsProvider).clearDisplaySettings(info);
+    }
+
     public final class TestSettingsProvider implements DisplayWindowSettings.SettingsProvider {
         Map<DisplayInfo, SettingsEntry> mOverrideSettingsCache = new HashMap<>();
 
@@ -530,5 +543,10 @@
         public void onDisplayRemoved(@NonNull DisplayInfo info) {
             mOverrideSettingsCache.remove(info);
         }
+
+        @Override
+        public void clearDisplaySettings(@NonNull DisplayInfo info) {
+            mOverrideSettingsCache.remove(info);
+        }
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index eaeb804..d08ab51 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -859,9 +859,7 @@
         final int callingUid = Process.FIRST_APPLICATION_UID;
         final int callingPid = 1234;
         final SurfaceControl surfaceControl = mock(SurfaceControl.class);
-        final IWindow window = mock(IWindow.class);
-        final IBinder windowToken = mock(IBinder.class);
-        when(window.asBinder()).thenReturn(windowToken);
+        final IBinder window = new Binder();
         final IBinder focusGrantToken = mock(IBinder.class);
 
         final InputChannel inputChannel = new InputChannel();
@@ -879,9 +877,7 @@
         final int callingUid = Process.SYSTEM_UID;
         final int callingPid = 1234;
         final SurfaceControl surfaceControl = mock(SurfaceControl.class);
-        final IWindow window = mock(IWindow.class);
-        final IBinder windowToken = mock(IBinder.class);
-        when(window.asBinder()).thenReturn(windowToken);
+        final IBinder window = new Binder();
         final IBinder focusGrantToken = mock(IBinder.class);
 
         final InputChannel inputChannel = new InputChannel();
@@ -901,9 +897,7 @@
         final int callingUid = Process.FIRST_APPLICATION_UID;
         final int callingPid = 1234;
         final SurfaceControl surfaceControl = mock(SurfaceControl.class);
-        final IWindow window = mock(IWindow.class);
-        final IBinder windowToken = mock(IBinder.class);
-        when(window.asBinder()).thenReturn(windowToken);
+        final IBinder window = new Binder();
         final IBinder focusGrantToken = mock(IBinder.class);
 
         final InputChannel inputChannel = new InputChannel();
@@ -927,9 +921,7 @@
         final int callingUid = Process.SYSTEM_UID;
         final int callingPid = 1234;
         final SurfaceControl surfaceControl = mock(SurfaceControl.class);
-        final IWindow window = mock(IWindow.class);
-        final IBinder windowToken = mock(IBinder.class);
-        when(window.asBinder()).thenReturn(windowToken);
+        final IBinder window = new Binder();
         final IBinder focusGrantToken = mock(IBinder.class);
 
         final InputChannel inputChannel = new InputChannel();
diff --git a/telecomm/java/android/telecom/InCallService.java b/telecomm/java/android/telecom/InCallService.java
index 13a0458..f3dfcd7 100644
--- a/telecomm/java/android/telecom/InCallService.java
+++ b/telecomm/java/android/telecom/InCallService.java
@@ -384,8 +384,14 @@
 
     /** Manages the binder calls so that the implementor does not need to deal with it. */
     private final class InCallServiceBinder extends IInCallService.Stub {
+        private boolean mInCallAdapterSet;
         @Override
         public void setInCallAdapter(IInCallAdapter inCallAdapter) {
+            if (mInCallAdapterSet) {
+                Log.i(this, "setInCallAdapter: InCallAdapter already set, skipping...");
+                return;
+            }
+            mInCallAdapterSet = true;
             mHandler.obtainMessage(MSG_SET_IN_CALL_ADAPTER, inCallAdapter).sendToTarget();
         }
 
diff --git a/telephony/java/android/telephony/PreciseDataConnectionState.java b/telephony/java/android/telephony/PreciseDataConnectionState.java
index 7f1c14b..b568f07 100644
--- a/telephony/java/android/telephony/PreciseDataConnectionState.java
+++ b/telephony/java/android/telephony/PreciseDataConnectionState.java
@@ -16,6 +16,8 @@
 
 package android.telephony;
 
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
@@ -37,8 +39,11 @@
 import android.telephony.data.DataCallResponse;
 import android.telephony.data.Qos;
 
+import com.android.internal.telephony.flags.Flags;
 import com.android.internal.telephony.util.TelephonyUtils;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.Objects;
 
 
@@ -66,6 +71,53 @@
     private final LinkProperties mLinkProperties;
     private final ApnSetting mApnSetting;
     private final Qos mDefaultQos;
+    private final @NetworkValidationStatus int mNetworkValidationStatus;
+
+    /** @hide */
+    @IntDef(prefix = "NETWORK_VALIDATION_", value = {
+            NETWORK_VALIDATION_UNSUPPORTED,
+            NETWORK_VALIDATION_NOT_REQUESTED,
+            NETWORK_VALIDATION_IN_PROGRESS,
+            NETWORK_VALIDATION_SUCCESS,
+            NETWORK_VALIDATION_FAILURE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface NetworkValidationStatus {}
+
+    /**
+     * Unsupported. The unsupported state is used when the data network cannot support the network
+     * validation function for the current data connection state.
+     */
+    @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
+    public static final int NETWORK_VALIDATION_UNSUPPORTED = 0;
+
+    /**
+     * Not Requested. The not requested status is used when the data network supports the network
+     * validation function, but no network validation is being performed yet.
+     */
+    @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
+    public static final int NETWORK_VALIDATION_NOT_REQUESTED = 1;
+
+    /**
+     * In progress. The in progress state is used when the network validation process for the data
+     * network is in progress. This state is followed by either success or failure.
+     */
+    @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
+    public static final int NETWORK_VALIDATION_IN_PROGRESS = 2;
+
+    /**
+     * Success. The Success status is used when network validation has been completed for the data
+     * network and the result is successful.
+     */
+    @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
+    public static final int NETWORK_VALIDATION_SUCCESS = 3;
+
+    /**
+     * Failure. The Failure status is used when network validation has been completed for the data
+     * network and the result is failure.
+     */
+    @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
+    public static final int NETWORK_VALIDATION_FAILURE = 4;
 
     /**
      * Constructor
@@ -87,7 +139,7 @@
                         .setApnTypeBitmask(apnTypes)
                         .setApnName(apn)
                         .setEntryName(apn)
-                        .build(), null);
+                        .build(), null, NETWORK_VALIDATION_UNSUPPORTED);
     }
 
 
@@ -109,7 +161,8 @@
     private PreciseDataConnectionState(@TransportType int transportType, int id,
             @DataState int state, @NetworkType int networkType,
             @Nullable LinkProperties linkProperties, @DataFailureCause int failCause,
-            @Nullable ApnSetting apnSetting, @Nullable Qos defaultQos) {
+            @Nullable ApnSetting apnSetting, @Nullable Qos defaultQos,
+            @NetworkValidationStatus int networkValidationStatus) {
         mTransportType = transportType;
         mId = id;
         mState = state;
@@ -118,6 +171,7 @@
         mFailCause = failCause;
         mApnSetting = apnSetting;
         mDefaultQos = defaultQos;
+        mNetworkValidationStatus = networkValidationStatus;
     }
 
     /**
@@ -140,6 +194,7 @@
         mDefaultQos = in.readParcelable(
                 Qos.class.getClassLoader(),
                 android.telephony.data.Qos.class);
+        mNetworkValidationStatus = in.readInt();
     }
 
     /**
@@ -289,6 +344,16 @@
         return mDefaultQos;
     }
 
+    /**
+     * Returns the network validation state.
+     *
+     * @return the network validation status of the data call
+     */
+    @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
+    public @NetworkValidationStatus int getNetworkValidationStatus() {
+        return mNetworkValidationStatus;
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -304,6 +369,7 @@
         out.writeInt(mFailCause);
         out.writeParcelable(mApnSetting, flags);
         out.writeParcelable(mDefaultQos, flags);
+        out.writeInt(mNetworkValidationStatus);
     }
 
     public static final @NonNull Parcelable.Creator<PreciseDataConnectionState> CREATOR
@@ -321,7 +387,7 @@
     @Override
     public int hashCode() {
         return Objects.hash(mTransportType, mId, mState, mNetworkType, mFailCause,
-                mLinkProperties, mApnSetting, mDefaultQos);
+                mLinkProperties, mApnSetting, mDefaultQos, mNetworkValidationStatus);
     }
 
 
@@ -337,7 +403,8 @@
                 && mFailCause == that.mFailCause
                 && Objects.equals(mLinkProperties, that.mLinkProperties)
                 && Objects.equals(mApnSetting, that.mApnSetting)
-                && Objects.equals(mDefaultQos, that.mDefaultQos);
+                && Objects.equals(mDefaultQos, that.mDefaultQos)
+                && mNetworkValidationStatus == that.mNetworkValidationStatus;
     }
 
     @NonNull
@@ -354,11 +421,34 @@
         sb.append(", link properties: " + mLinkProperties);
         sb.append(", default QoS: " + mDefaultQos);
         sb.append(", fail cause: " + DataFailCause.toString(mFailCause));
+        sb.append(", network validation status: "
+                + networkValidationStatusToString(mNetworkValidationStatus));
 
         return sb.toString();
     }
 
     /**
+     * Convert a network validation status to string.
+     *
+     * @param networkValidationStatus network validation status.
+     * @return string of validation status.
+     *
+     * @hide
+     */
+    @NonNull
+    public static String networkValidationStatusToString(
+            @NetworkValidationStatus int networkValidationStatus) {
+        switch (networkValidationStatus) {
+            case NETWORK_VALIDATION_UNSUPPORTED: return "unsupported";
+            case NETWORK_VALIDATION_NOT_REQUESTED: return "not requested";
+            case NETWORK_VALIDATION_IN_PROGRESS: return "in progress";
+            case NETWORK_VALIDATION_SUCCESS: return "success";
+            case NETWORK_VALIDATION_FAILURE: return "failure";
+            default: return Integer.toString(networkValidationStatus);
+        }
+    }
+
+    /**
      * {@link PreciseDataConnectionState} builder
      *
      * @hide
@@ -394,6 +484,10 @@
         /** The Default QoS for this EPS/5GS bearer or null otherwise */
         private @Nullable Qos mDefaultQos;
 
+        /** The network validation status for the data connection. */
+        private @NetworkValidationStatus int mNetworkValidationStatus =
+                NETWORK_VALIDATION_UNSUPPORTED;
+
         /**
          * Set the transport type of the data connection.
          *
@@ -486,13 +580,27 @@
         }
 
         /**
+         * Set the network validation state for the data connection.
+         *
+         * @param networkValidationStatus the network validation status of the data call
+         * @return The builder
+         */
+        @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
+        public @NonNull Builder setNetworkValidationStatus(
+                @NetworkValidationStatus int networkValidationStatus) {
+            mNetworkValidationStatus = networkValidationStatus;
+            return this;
+        }
+
+        /**
          * Build the {@link PreciseDataConnectionState} instance.
          *
          * @return The {@link PreciseDataConnectionState} instance
          */
         public PreciseDataConnectionState build() {
             return new PreciseDataConnectionState(mTransportType, mId, mState, mNetworkType,
-                    mLinkProperties, mFailCause, mApnSetting, mDefaultQos);
+                    mLinkProperties, mFailCause, mApnSetting, mDefaultQos,
+                    mNetworkValidationStatus);
         }
     }
 }
diff --git a/telephony/java/android/telephony/data/DataCallResponse.java b/telephony/java/android/telephony/data/DataCallResponse.java
index c7f0c5f..9dd83d1 100644
--- a/telephony/java/android/telephony/data/DataCallResponse.java
+++ b/telephony/java/android/telephony/data/DataCallResponse.java
@@ -17,6 +17,7 @@
 
 package android.telephony.data;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
@@ -27,9 +28,11 @@
 import android.os.Parcelable;
 import android.telephony.Annotation.DataFailureCause;
 import android.telephony.DataFailCause;
+import android.telephony.PreciseDataConnectionState;
 import android.telephony.data.ApnSetting.ProtocolType;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.flags.Flags;
 import com.android.internal.util.Preconditions;
 
 import java.lang.annotation.Retention;
@@ -123,7 +126,6 @@
      * Indicates that the pdu session id is not set.
      */
     public static final int PDU_SESSION_ID_NOT_SET = 0;
-
     private final @DataFailureCause int mCause;
     private final long mSuggestedRetryTime;
     private final int mId;
@@ -143,6 +145,7 @@
     private final List<QosBearerSession> mQosBearerSessions;
     private final NetworkSliceInfo mSliceInfo;
     private final List<TrafficDescriptor> mTrafficDescriptors;
+    private final @PreciseDataConnectionState.NetworkValidationStatus int mNetworkValidationStatus;
 
     /**
      * @param cause Data call fail cause. {@link DataFailCause#NONE} indicates no error.
@@ -185,7 +188,8 @@
                 HANDOVER_FAILURE_MODE_LEGACY, PDU_SESSION_ID_NOT_SET,
                 null /* defaultQos */, Collections.emptyList() /* qosBearerSessions */,
                 null /* sliceInfo */,
-                Collections.emptyList() /* trafficDescriptors */);
+                Collections.emptyList(), /* trafficDescriptors */
+                PreciseDataConnectionState.NETWORK_VALIDATION_UNSUPPORTED);
     }
 
     private DataCallResponse(@DataFailureCause int cause, long suggestedRetryTime, int id,
@@ -196,7 +200,8 @@
             @HandoverFailureMode int handoverFailureMode, int pduSessionId,
             @Nullable Qos defaultQos, @NonNull List<QosBearerSession> qosBearerSessions,
             @Nullable NetworkSliceInfo sliceInfo,
-            @NonNull List<TrafficDescriptor> trafficDescriptors) {
+            @NonNull List<TrafficDescriptor> trafficDescriptors,
+            @PreciseDataConnectionState.NetworkValidationStatus int networkValidationStatus) {
         mCause = cause;
         mSuggestedRetryTime = suggestedRetryTime;
         mId = id;
@@ -216,6 +221,7 @@
         mQosBearerSessions = new ArrayList<>(qosBearerSessions);
         mSliceInfo = sliceInfo;
         mTrafficDescriptors = new ArrayList<>(trafficDescriptors);
+        mNetworkValidationStatus = networkValidationStatus;
 
         if (mLinkStatus == LINK_STATUS_ACTIVE
                 || mLinkStatus == LINK_STATUS_DORMANT) {
@@ -270,6 +276,7 @@
         source.readList(mTrafficDescriptors,
                 TrafficDescriptor.class.getClassLoader(),
                 android.telephony.data.TrafficDescriptor.class);
+        mNetworkValidationStatus = source.readInt();
     }
 
     /**
@@ -442,6 +449,17 @@
         return Collections.unmodifiableList(mTrafficDescriptors);
     }
 
+    /**
+     * Return the network validation status that was initiated by {@link
+     * DataService.DataServiceProvider#requestValidation}
+     *
+     * @return The network validation status of data connection.
+     */
+    @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
+    public @PreciseDataConnectionState.NetworkValidationStatus int getNetworkValidationStatus() {
+        return mNetworkValidationStatus;
+    }
+
     @NonNull
     @Override
     public String toString() {
@@ -466,6 +484,8 @@
            .append(" qosBearerSessions=").append(mQosBearerSessions)
            .append(" sliceInfo=").append(mSliceInfo)
            .append(" trafficDescriptors=").append(mTrafficDescriptors)
+           .append(" networkValidationStatus=").append(PreciseDataConnectionState
+                        .networkValidationStatusToString(mNetworkValidationStatus))
            .append("}");
         return sb.toString();
     }
@@ -504,7 +524,8 @@
                 && mQosBearerSessions.containsAll(other.mQosBearerSessions) // non-null
                 && Objects.equals(mSliceInfo, other.mSliceInfo)
                 && mTrafficDescriptors.size() == other.mTrafficDescriptors.size() // non-null
-                && mTrafficDescriptors.containsAll(other.mTrafficDescriptors); // non-null
+                && mTrafficDescriptors.containsAll(other.mTrafficDescriptors) // non-null
+                && mNetworkValidationStatus == other.mNetworkValidationStatus;
     }
 
     @Override
@@ -513,7 +534,7 @@
                 mInterfaceName, Set.copyOf(mAddresses), Set.copyOf(mDnsAddresses),
                 Set.copyOf(mGatewayAddresses), Set.copyOf(mPcscfAddresses), mMtu, mMtuV4, mMtuV6,
                 mHandoverFailureMode, mPduSessionId, mDefaultQos, Set.copyOf(mQosBearerSessions),
-                mSliceInfo, Set.copyOf(mTrafficDescriptors));
+                mSliceInfo, Set.copyOf(mTrafficDescriptors), mNetworkValidationStatus);
     }
 
     @Override
@@ -542,6 +563,7 @@
         dest.writeList(mQosBearerSessions);
         dest.writeParcelable(mSliceInfo, flags);
         dest.writeList(mTrafficDescriptors);
+        dest.writeInt(mNetworkValidationStatus);
     }
 
     public static final @android.annotation.NonNull Parcelable.Creator<DataCallResponse> CREATOR =
@@ -629,6 +651,9 @@
 
         private List<TrafficDescriptor> mTrafficDescriptors = new ArrayList<>();
 
+        private @PreciseDataConnectionState.NetworkValidationStatus int mNetworkValidationStatus =
+                PreciseDataConnectionState.NETWORK_VALIDATION_UNSUPPORTED;
+
         /**
          * Default constructor for Builder.
          */
@@ -905,6 +930,20 @@
         }
 
         /**
+         * Set the network validation status that corresponds to the state of the network validation
+         * request started by {@link DataService.DataServiceProvider#requestValidation}
+         *
+         * @param status The network validation status.
+         * @return The same instance of the builder.
+         */
+        @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
+        public @NonNull Builder setNetworkValidationStatus(
+                @PreciseDataConnectionState.NetworkValidationStatus int status) {
+            mNetworkValidationStatus = status;
+            return this;
+        }
+
+        /**
          * Build the DataCallResponse.
          *
          * @return the DataCallResponse object.
@@ -913,7 +952,8 @@
             return new DataCallResponse(mCause, mSuggestedRetryTime, mId, mLinkStatus,
                     mProtocolType, mInterfaceName, mAddresses, mDnsAddresses, mGatewayAddresses,
                     mPcscfAddresses, mMtu, mMtuV4, mMtuV6, mHandoverFailureMode, mPduSessionId,
-                    mDefaultQos, mQosBearerSessions, mSliceInfo, mTrafficDescriptors);
+                    mDefaultQos, mQosBearerSessions, mSliceInfo, mTrafficDescriptors,
+                    mNetworkValidationStatus);
         }
     }
 }
diff --git a/telephony/java/android/telephony/data/DataService.java b/telephony/java/android/telephony/data/DataService.java
index d8b2cbe..80e91a3 100644
--- a/telephony/java/android/telephony/data/DataService.java
+++ b/telephony/java/android/telephony/data/DataService.java
@@ -16,6 +16,8 @@
 
 package android.telephony.data;
 
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
@@ -26,6 +28,7 @@
 import android.content.Intent;
 import android.net.LinkProperties;
 import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.Looper;
@@ -36,6 +39,9 @@
 import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.IIntegerConsumer;
+import com.android.internal.telephony.flags.Flags;
+import com.android.internal.util.FunctionalUtils;
 import com.android.telephony.Rlog;
 
 import java.lang.annotation.Retention;
@@ -44,6 +50,8 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
 
 /**
  * Base class of data service. Services that extend DataService must register the service in
@@ -113,11 +121,14 @@
     private static final int DATA_SERVICE_REQUEST_REGISTER_APN_UNTHROTTLED             = 14;
     private static final int DATA_SERVICE_REQUEST_UNREGISTER_APN_UNTHROTTLED           = 15;
     private static final int DATA_SERVICE_INDICATION_APN_UNTHROTTLED                   = 16;
+    private static final int DATA_SERVICE_REQUEST_VALIDATION                           = 17;
 
     private final HandlerThread mHandlerThread;
 
     private final DataServiceHandler mHandler;
 
+    private final Executor mHandlerExecutor;
+
     private final SparseArray<DataServiceProvider> mServiceMap = new SparseArray<>();
 
     /** @hide */
@@ -379,6 +390,43 @@
             }
         }
 
+        /**
+         * Request validation check to see if the network is working properly for a given data call.
+         *
+         * <p>This request is completed immediately after submitting the request to the data service
+         * provider and receiving {@link DataServiceCallback.ResultCode}, and progress status or
+         * validation results are notified through {@link
+         * DataCallResponse#getNetworkValidationStatus}.
+         *
+         * <p> If the network validation request is submitted successfully, {@link
+         * DataServiceCallback#RESULT_SUCCESS} is passed to {@code resultCodeCallback}. If the
+         * network validation feature is not supported by the data service provider itself, {@link
+         * DataServiceCallback#RESULT_ERROR_UNSUPPORTED} is passed to {@code resultCodeCallback}.
+         * See {@link DataServiceCallback.ResultCode} for the type of response that indicates
+         * whether the request was successfully submitted or had an error.
+         *
+         * <p>In response to this network validation request, providers can validate the data call
+         * in their own way. For example, in IWLAN, the DPD (Dead Peer Detection) can be used as a
+         * tool to check whether a data call is alive.
+         *
+         * @param cid The identifier of the data call which is provided in {@link DataCallResponse}
+         * @param executor The callback executor for the response.
+         * @param resultCodeCallback Listener for the {@link DataServiceCallback.ResultCode} that
+         *     request validation to the DataService and checks if the request has been submitted.
+         */
+        @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
+        public void requestValidation(int cid,
+                @NonNull @CallbackExecutor Executor executor,
+                @NonNull @DataServiceCallback.ResultCode Consumer<Integer> resultCodeCallback) {
+            Objects.requireNonNull(executor, "executor cannot be null");
+            Objects.requireNonNull(resultCodeCallback, "resultCodeCallback cannot be null");
+
+            Log.d(TAG, "requestValidation: " + cid);
+
+            // The default implementation is to return unsupported.
+            executor.execute(() -> resultCodeCallback
+                    .accept(DataServiceCallback.RESULT_ERROR_UNSUPPORTED));
+        }
 
         /**
          * Notify the system that current data call list changed. Data service must invoke this
@@ -537,6 +585,17 @@
         }
     }
 
+    private static final class ValidationRequest {
+        public final int cid;
+        public final Executor executor;
+        public final IIntegerConsumer callback;
+        ValidationRequest(int cid, Executor executor, IIntegerConsumer callback) {
+            this.cid = cid;
+            this.executor = executor;
+            this.callback = callback;
+        }
+    }
+
     private class DataServiceHandler extends Handler {
 
         DataServiceHandler(Looper looper) {
@@ -679,6 +738,15 @@
                         loge("Failed to call onApnUnthrottled. " + e);
                     }
                     break;
+                case DATA_SERVICE_REQUEST_VALIDATION:
+                    if (serviceProvider == null) break;
+                    ValidationRequest validationRequest = (ValidationRequest) message.obj;
+                    serviceProvider.requestValidation(
+                            validationRequest.cid,
+                            validationRequest.executor,
+                            FunctionalUtils
+                                    .ignoreRemoteException(validationRequest.callback::accept));
+                    break;
             }
         }
     }
@@ -691,6 +759,7 @@
         mHandlerThread.start();
 
         mHandler = new DataServiceHandler(mHandlerThread.getLooper());
+        mHandlerExecutor = new HandlerExecutor(mHandler);
         log("Data service created");
     }
 
@@ -853,6 +922,18 @@
             mHandler.obtainMessage(DATA_SERVICE_REQUEST_UNREGISTER_APN_UNTHROTTLED,
                     slotIndex, 0, callback).sendToTarget();
         }
+
+        @Override
+        public void requestValidation(int slotIndex, int cid, IIntegerConsumer resultCodeCallback) {
+            if (resultCodeCallback == null) {
+                loge("requestValidation: resultCodeCallback is null");
+                return;
+            }
+            ValidationRequest validationRequest =
+                    new ValidationRequest(cid, mHandlerExecutor, resultCodeCallback);
+            mHandler.obtainMessage(DATA_SERVICE_REQUEST_VALIDATION,
+                    slotIndex, 0, validationRequest).sendToTarget();
+        }
     }
 
     private void log(String s) {
diff --git a/telephony/java/android/telephony/data/IDataService.aidl b/telephony/java/android/telephony/data/IDataService.aidl
index 1346946..15f8881 100644
--- a/telephony/java/android/telephony/data/IDataService.aidl
+++ b/telephony/java/android/telephony/data/IDataService.aidl
@@ -22,6 +22,8 @@
 import android.telephony.data.NetworkSliceInfo;
 import android.telephony.data.TrafficDescriptor;
 
+import com.android.internal.telephony.IIntegerConsumer;
+
 /**
  * {@hide}
  */
@@ -46,4 +48,5 @@
     void cancelHandover(int slotId, int cid, IDataServiceCallback callback);
     void registerForUnthrottleApn(int slotIndex, IDataServiceCallback callback);
     void unregisterForUnthrottleApn(int slotIndex, IDataServiceCallback callback);
+    void requestValidation(int slotId, int cid, IIntegerConsumer callback);
 }
diff --git a/telephony/java/android/telephony/data/IQualifiedNetworksServiceCallback.aidl b/telephony/java/android/telephony/data/IQualifiedNetworksServiceCallback.aidl
index 32ffdbc..bdd212a 100644
--- a/telephony/java/android/telephony/data/IQualifiedNetworksServiceCallback.aidl
+++ b/telephony/java/android/telephony/data/IQualifiedNetworksServiceCallback.aidl
@@ -16,6 +16,8 @@
 
 package android.telephony.data;
 
+import com.android.internal.telephony.IIntegerConsumer;
+
 /**
  * The qualified networks service call back interface
  * @hide
@@ -23,4 +25,5 @@
 oneway interface IQualifiedNetworksServiceCallback
 {
     void onQualifiedNetworkTypesChanged(int apnTypes, in int[] qualifiedNetworkTypes);
+    void onNetworkValidationRequested(int networkCapability, IIntegerConsumer callback);
 }
diff --git a/telephony/java/android/telephony/data/QualifiedNetworksService.java b/telephony/java/android/telephony/data/QualifiedNetworksService.java
index 56f0f9f..c3ba092 100644
--- a/telephony/java/android/telephony/data/QualifiedNetworksService.java
+++ b/telephony/java/android/telephony/data/QualifiedNetworksService.java
@@ -16,6 +16,8 @@
 
 package android.telephony.data;
 
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 import android.app.Service;
@@ -29,13 +31,23 @@
 import android.telephony.AccessNetworkConstants;
 import android.telephony.AccessNetworkConstants.AccessNetworkType;
 import android.telephony.Annotation.ApnType;
+import android.telephony.Annotation.NetCapability;
+import android.telephony.PreciseDataConnectionState;
 import android.util.Log;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.IIntegerConsumer;
+import com.android.internal.telephony.flags.FeatureFlags;
+import com.android.internal.telephony.flags.FeatureFlagsImpl;
+import com.android.internal.telephony.flags.Flags;
+import com.android.internal.util.FunctionalUtils;
 import com.android.telephony.Rlog;
 
 import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
 
 /**
  * Base class of the qualified networks service, which is a vendor service providing up-to-date
@@ -69,6 +81,10 @@
     private static final int QNS_UPDATE_QUALIFIED_NETWORKS                          = 4;
     private static final int QNS_APN_THROTTLE_STATUS_CHANGED                        = 5;
     private static final int QNS_EMERGENCY_DATA_NETWORK_PREFERRED_TRANSPORT_CHANGED = 6;
+    private static final int QNS_REQUEST_NETWORK_VALIDATION                         = 7;
+
+    /** Feature flags */
+    private static final FeatureFlags sFeatureFlag = new FeatureFlagsImpl();
 
     private final HandlerThread mHandlerThread;
 
@@ -208,6 +224,72 @@
         }
 
         /**
+         * Request network validation to the connected data network for given a network capability.
+         *
+         * <p>This network validation can only be performed when a data network is in connected
+         * state, and will not be triggered if the data network does not support network validation
+         * feature or network validation is not in connected state.
+         *
+         * <p>See {@link DataServiceCallback.ResultCode} for the type of response that indicates
+         * whether the request was successfully submitted or had an error.
+         *
+         * <p>If network validation is requested, monitor network validation status in {@link
+         * PreciseDataConnectionState#getNetworkValidationStatus()}.
+         *
+         * @param networkCapability A network capability. (Note that only APN-type capabilities are
+         *     supported.
+         * @param executor executor The callback executor that responds whether the request has been
+         *     successfully submitted or not.
+         * @param resultCodeCallback A callback to determine whether the request was successfully
+         *     submitted or not.
+         */
+        @FlaggedApi(Flags.FLAG_NETWORK_VALIDATION)
+        public void requestNetworkValidation(
+                @NetCapability int networkCapability,
+                @NonNull @CallbackExecutor Executor executor,
+                @NonNull @DataServiceCallback.ResultCode Consumer<Integer> resultCodeCallback) {
+            Objects.requireNonNull(executor, "executor cannot be null");
+            Objects.requireNonNull(resultCodeCallback, "resultCodeCallback cannot be null");
+
+            if (!sFeatureFlag.networkValidation()) {
+                loge("networkValidation feature is disabled");
+                executor.execute(
+                        () ->
+                                resultCodeCallback.accept(
+                                        DataServiceCallback.RESULT_ERROR_UNSUPPORTED));
+                return;
+            }
+
+            IIntegerConsumer callback = new IIntegerConsumer.Stub() {
+                @Override
+                public void accept(int result) {
+                    executor.execute(() -> resultCodeCallback.accept(result));
+                }
+            };
+
+            // Move to the internal handler and process it.
+            mHandler.obtainMessage(
+                            QNS_REQUEST_NETWORK_VALIDATION,
+                            mSlotIndex,
+                            0,
+                            new NetworkValidationRequestData(networkCapability, callback))
+                    .sendToTarget();
+        }
+
+        /** Process a network validation request on the internal handler. */
+        private void onRequestNetworkValidation(NetworkValidationRequestData data) {
+            try {
+                log("onRequestNetworkValidation");
+                // Callback to request a network validation.
+                mCallback.onNetworkValidationRequested(data.mNetworkCapability, data.mCallback);
+            } catch (RemoteException | NullPointerException e) {
+                loge("Failed to call onRequestNetworkValidation. " + e);
+                FunctionalUtils.ignoreRemoteException(data.mCallback::accept)
+                        .accept(DataServiceCallback.RESULT_ERROR_UNSUPPORTED);
+            }
+        }
+
+        /**
          * Called when the qualified networks provider is removed. The extended class should
          * implement this method to perform cleanup works.
          */
@@ -280,6 +362,10 @@
                     if (provider == null) break;
                     provider.onUpdateQualifiedNetworkTypes(message.arg2, (int[]) message.obj);
                     break;
+
+                case QNS_REQUEST_NETWORK_VALIDATION:
+                    if (provider == null) break;
+                    provider.onRequestNetworkValidation((NetworkValidationRequestData) message.obj);
             }
         }
     }
@@ -364,6 +450,17 @@
         }
     }
 
+    private static final class NetworkValidationRequestData {
+        final @NetCapability int mNetworkCapability;
+        final IIntegerConsumer mCallback;
+
+        private NetworkValidationRequestData(@NetCapability int networkCapability,
+                @NonNull IIntegerConsumer callback) {
+            mNetworkCapability = networkCapability;
+            mCallback = callback;
+        }
+    }
+
     private void log(String s) {
         Rlog.d(TAG, s);
     }
diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java
index ae7c2a9..4548a7d 100644
--- a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java
+++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java
@@ -78,14 +78,15 @@
     // TODO(b/293651105): Unhardcode category fps range mapping
     private static final FpsRange FRAME_RATE_CATEGORY_HIGH = new FpsRange(90, 120);
     private static final FpsRange FRAME_RATE_CATEGORY_NORMAL = new FpsRange(60, 90);
-    private static final FpsRange FRAME_RATE_CATEGORY_LOW = new FpsRange(30, 60);
+    private static final FpsRange FRAME_RATE_CATEGORY_LOW = new FpsRange(30, 30);
 
     private DisplayManager mDisplayManager;
     private SurfaceView mSurfaceView;
     private Handler mHandler = new Handler(Looper.getMainLooper());
     private final Object mLock = new Object();
     private Surface mSurface = null;
-    private float mDeviceFrameRate;
+    private float mDisplayModeRefreshRate;
+    private float mDisplayRefreshRate;
     private ModeChangedEvents mModeChangedEvents = new ModeChangedEvents();
 
     private enum ActivityState { RUNNING, PAUSED, DESTROYED }
@@ -123,14 +124,20 @@
                 return;
             }
             synchronized (mLock) {
-                Display.Mode mode = mDisplayManager.getDisplay(displayId).getMode();
+                Display display = mDisplayManager.getDisplay(displayId);
+                Display.Mode mode = display.getMode();
                 mModeChangedEvents.add(mode);
-                float frameRate = mode.getRefreshRate();
-                if (frameRate != mDeviceFrameRate) {
+                float displayModeRefreshRate = mode.getRefreshRate();
+                float displayRefreshRate = display.getRefreshRate();
+                if (displayModeRefreshRate != mDisplayModeRefreshRate
+                        || displayRefreshRate != mDisplayRefreshRate) {
                     Log.i(TAG,
-                            String.format("Frame rate changed: %.2f --> %.2f", mDeviceFrameRate,
-                                    frameRate));
-                    mDeviceFrameRate = frameRate;
+                            String.format("Refresh rate changed: (mode) %.2f --> %.2f, "
+                                            + "(display) %.2f --> %.2f",
+                                    mDisplayModeRefreshRate, displayModeRefreshRate,
+                                    mDisplayRefreshRate, displayRefreshRate));
+                    mDisplayModeRefreshRate = displayModeRefreshRate;
+                    mDisplayRefreshRate = displayRefreshRate;
                     mLock.notify();
                 }
             }
@@ -317,8 +324,10 @@
         super.onCreate(savedInstanceState);
         synchronized (mLock) {
             mDisplayManager = getSystemService(DisplayManager.class);
-            Display.Mode mode = getDisplay().getMode();
-            mDeviceFrameRate = mode.getRefreshRate();
+            Display display = getDisplay();
+            Display.Mode mode = display.getMode();
+            mDisplayModeRefreshRate = mode.getRefreshRate();
+            mDisplayRefreshRate = display.getRefreshRate();
             // Insert the initial mode so we have the full display mode history.
             mModeChangedEvents.add(mode);
             mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
@@ -516,22 +525,25 @@
             if (expectedFrameRate > FRAME_RATE_TOLERANCE) { // expectedFrameRate > 0
                 // Wait until we switch to a compatible frame rate.
                 Log.i(TAG,
-                        "Verifying expected frame rate: actual (device)=" + mDeviceFrameRate
-                                + " expected=" + expectedFrameRate);
+                        String.format(
+                                "Verifying expected frame rate: actual=%.2f, expected=%.2f",
+                                multiplesAllowed ? mDisplayModeRefreshRate : mDisplayRefreshRate,
+                                expectedFrameRate));
                 if (multiplesAllowed) {
-                    while (!isFrameRateMultiple(mDeviceFrameRate, expectedFrameRate)
+                    while (!isFrameRateMultiple(mDisplayModeRefreshRate, expectedFrameRate)
                             && !waitForEvents(gracePeriodEndTimeNanos, surfaces)) {
                         // Empty
                     }
                 } else {
-                    while (!frameRateEquals(mDeviceFrameRate, expectedFrameRate)
+                    while (!frameRateEquals(mDisplayRefreshRate, expectedFrameRate)
                             && !waitForEvents(gracePeriodEndTimeNanos, surfaces)) {
                         // Empty
                     }
                 }
                 nowNanos = System.nanoTime();
                 if (nowNanos >= gracePeriodEndTimeNanos) {
-                    throw new FrameRateTimeoutException(expectedFrameRate, mDeviceFrameRate);
+                    throw new FrameRateTimeoutException(expectedFrameRate,
+                            multiplesAllowed ? mDisplayModeRefreshRate : mDisplayRefreshRate);
                 }
             }
 
@@ -541,7 +553,10 @@
             while (endTimeNanos > nowNanos) {
                 int numModeChangedEvents = mModeChangedEvents.size();
                 if (waitForEvents(endTimeNanos, surfaces)) {
-                    Log.i(TAG, String.format("Stable frame rate %.2f verified", mDeviceFrameRate));
+                    Log.i(TAG,
+                            String.format("Stable frame rate %.2f verified",
+                                    multiplesAllowed ? mDisplayModeRefreshRate
+                                                     : mDisplayRefreshRate));
                     return;
                 }
                 nowNanos = System.nanoTime();
diff --git a/tests/MotionPrediction/Android.bp b/tests/MotionPrediction/Android.bp
index 6cda8f0..b4a4359 100644
--- a/tests/MotionPrediction/Android.bp
+++ b/tests/MotionPrediction/Android.bp
@@ -26,5 +26,8 @@
 android_app {
     name: "MotionPrediction",
     srcs: ["**/*.kt"],
+    kotlincflags: [
+        "-Werror",
+    ],
     sdk_version: "current",
 }
diff --git a/tests/MultiDeviceInput/Android.bp b/tests/MultiDeviceInput/Android.bp
new file mode 100644
index 0000000..3c80873
--- /dev/null
+++ b/tests/MultiDeviceInput/Android.bp
@@ -0,0 +1,33 @@
+//
+// Copyright 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_app {
+    name: "MultiDeviceInput",
+    srcs: ["**/*.kt"],
+    kotlincflags: [
+        "-Werror",
+    ],
+    sdk_version: "current",
+}
diff --git a/tests/MultiDeviceInput/AndroidManifest.xml b/tests/MultiDeviceInput/AndroidManifest.xml
new file mode 100644
index 0000000..ed8cadb
--- /dev/null
+++ b/tests/MultiDeviceInput/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="test.multideviceinput">
+
+    <application android:allowBackup="false"
+         android:icon="@mipmap/ic_launcher"
+         android:label="@string/app_name"
+         android:supportsRtl="true"
+         android:theme="@style/AppTheme">
+        <activity android:name=".MainActivity"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/tests/MultiDeviceInput/OWNERS b/tests/MultiDeviceInput/OWNERS
new file mode 100644
index 0000000..c88bfe9
--- /dev/null
+++ b/tests/MultiDeviceInput/OWNERS
@@ -0,0 +1 @@
+include platform/frameworks/base:/INPUT_OWNERS
diff --git a/tests/MultiDeviceInput/README.md b/tests/MultiDeviceInput/README.md
new file mode 100644
index 0000000..5fcdeda
--- /dev/null
+++ b/tests/MultiDeviceInput/README.md
@@ -0,0 +1,19 @@
+# MultiDeviceInput test app #
+
+This demo app is for manual testing of the multi-device input feature.
+It creates two windows - one on the left and one on the right. You can use different input devices
+in these windows.
+
+## Installation ##
+Install this using:
+```
+APP=MultiDeviceInput; m $APP && adb install $ANDROID_PRODUCT_OUT/system/app/$APP/$APP.apk
+```
+
+## Features ##
+
+* Touch in one window, use stylus in another window, at the same time
+* Visualize hovering stylus
+* Pinch zoom in one window to affect the line thickness in another window
+* Check whether stylus rejects touch in the same window
+* (in the future) Check stylus and touch operation in the same window
diff --git a/tests/MultiDeviceInput/res/layout/activity_main.xml b/tests/MultiDeviceInput/res/layout/activity_main.xml
new file mode 100644
index 0000000..a6a6f891
--- /dev/null
+++ b/tests/MultiDeviceInput/res/layout/activity_main.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:paddingBottom="@dimen/activity_vertical_margin"
+    android:paddingLeft="@dimen/activity_horizontal_margin"
+    android:paddingRight="@dimen/activity_horizontal_margin"
+    android:paddingTop="@dimen/activity_vertical_margin"
+    tools:context="test.multideviceinput.MainActivity">
+
+</LinearLayout>
diff --git a/tests/MultiDeviceInput/res/mipmap-hdpi/ic_launcher.png b/tests/MultiDeviceInput/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cde69bc
--- /dev/null
+++ b/tests/MultiDeviceInput/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/tests/MultiDeviceInput/res/mipmap-mdpi/ic_launcher.png b/tests/MultiDeviceInput/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c133a0c
--- /dev/null
+++ b/tests/MultiDeviceInput/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/tests/MultiDeviceInput/res/mipmap-xhdpi/ic_launcher.png b/tests/MultiDeviceInput/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..bfa42f0
--- /dev/null
+++ b/tests/MultiDeviceInput/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/tests/MultiDeviceInput/res/mipmap-xxhdpi/ic_launcher.png b/tests/MultiDeviceInput/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..324e72c
--- /dev/null
+++ b/tests/MultiDeviceInput/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/tests/MultiDeviceInput/res/mipmap-xxxhdpi/ic_launcher.png b/tests/MultiDeviceInput/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..aee44e1
--- /dev/null
+++ b/tests/MultiDeviceInput/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/tests/MultiDeviceInput/res/values-w820dp/dimens.xml b/tests/MultiDeviceInput/res/values-w820dp/dimens.xml
new file mode 100644
index 0000000..b14a560
--- /dev/null
+++ b/tests/MultiDeviceInput/res/values-w820dp/dimens.xml
@@ -0,0 +1,20 @@
+<!-- Copyright 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+    <!-- Example customization of dimensions originally defined in res/values/dimens.xml
+         (such as screen margins) for screens with more than 820dp of available width. This
+         would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
+    <dimen name="activity_horizontal_margin">64dp</dimen>
+</resources>
diff --git a/tests/MultiDeviceInput/res/values/colors.xml b/tests/MultiDeviceInput/res/values/colors.xml
new file mode 100644
index 0000000..c37df9f
--- /dev/null
+++ b/tests/MultiDeviceInput/res/values/colors.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+    <color name="colorPrimary">#3F51B5</color>
+    <color name="colorPrimaryDark">#303F9F</color>
+    <color name="colorAccent">#FF4081</color>
+</resources>
diff --git a/tests/MultiDeviceInput/res/values/dimens.xml b/tests/MultiDeviceInput/res/values/dimens.xml
new file mode 100644
index 0000000..bdb8ede
--- /dev/null
+++ b/tests/MultiDeviceInput/res/values/dimens.xml
@@ -0,0 +1,19 @@
+<!-- Copyright 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+    <!-- Default screen margins, per the Android Design guidelines. -->
+    <dimen name="activity_horizontal_margin">16dp</dimen>
+    <dimen name="activity_vertical_margin">16dp</dimen>
+</resources>
diff --git a/tests/MultiDeviceInput/res/values/strings.xml b/tests/MultiDeviceInput/res/values/strings.xml
new file mode 100644
index 0000000..3827c34
--- /dev/null
+++ b/tests/MultiDeviceInput/res/values/strings.xml
@@ -0,0 +1,17 @@
+<!-- Copyright 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+    <string name="app_name">Simultaneous touch and stylus</string>
+</resources>
diff --git a/tests/MultiDeviceInput/res/values/styles.xml b/tests/MultiDeviceInput/res/values/styles.xml
new file mode 100644
index 0000000..a563e7e
--- /dev/null
+++ b/tests/MultiDeviceInput/res/values/styles.xml
@@ -0,0 +1,23 @@
+<!-- Copyright 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="@android:style/Theme.Material.Light.DarkActionBar">
+        <!-- Customize your theme here. -->
+        <item name="android:colorPrimary">@color/colorPrimary</item>
+        <item name="android:colorPrimaryDark">@color/colorPrimaryDark</item>
+        <item name="android:colorAccent">@color/colorAccent</item>
+    </style>
+</resources>
diff --git a/tests/MultiDeviceInput/src/test/multideviceinput/DrawingView.kt b/tests/MultiDeviceInput/src/test/multideviceinput/DrawingView.kt
new file mode 100644
index 0000000..b5bd9ca
--- /dev/null
+++ b/tests/MultiDeviceInput/src/test/multideviceinput/DrawingView.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package test.multideviceinput
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.util.AttributeSet
+import android.view.InputDevice.SOURCE_STYLUS
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_HOVER_EXIT
+import android.view.MotionEvent.ACTION_UP
+import android.view.ScaleGestureDetector
+import android.view.View
+
+import java.util.Vector
+
+private fun drawLine(canvas: Canvas, from: MotionEvent, to: MotionEvent, paint: Paint) {
+    // Correct implementation here would require us to build a set of pointers and then iterate
+    // through them. Instead, we are taking a few shortcuts and ignore some of the events, which
+    // causes occasional gaps in the drawings.
+    if (from.pointerCount != to.pointerCount) {
+        return
+    }
+    // Now, 'from' is guaranteed to have as many pointers as the 'to' event. It doesn't
+    // necessarily mean they are the same pointers, though.
+    for (p in 0..<from.pointerCount) {
+        val x0 = from.getX(p)
+        val y0 = from.getY(p)
+        if (to.getPointerId(p) == from.getPointerId(p)) {
+            // This only works when the i-th pointer in "to" is the same pointer
+            // as the i-th pointer in "from"`. It's not guaranteed by the input APIs,
+            // but it works in practice.
+            val x1 = to.getX(p)
+            val y1 = to.getY(p)
+            // Ignoring historical data here for simplicity
+            canvas.drawLine(x0, y0, x1, y1, paint)
+        }
+    }
+}
+
+private fun drawCircle(canvas: Canvas, event: MotionEvent, paint: Paint, radius: Float) {
+    val x = event.getX()
+    val y = event.getY()
+    canvas.drawCircle(x, y, radius, paint)
+}
+
+/**
+ * Draw the current stroke
+ */
+class DrawingView : View {
+    private val TAG = "DrawingView"
+
+    private var myState: SharedScaledPointerSize? = null
+    private var otherState: SharedScaledPointerSize? = null
+
+    constructor(
+            context: Context,
+            myState: SharedScaledPointerSize,
+            otherState: SharedScaledPointerSize
+            ) : super(context) {
+        this.myState = myState
+        this.otherState = otherState
+        init()
+    }
+
+    constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
+        init()
+    }
+
+    val touchEvents = mutableMapOf<Int, Vector<Pair<MotionEvent, Paint>>>()
+    val hoverEvents = mutableMapOf<Int, MotionEvent>()
+
+    val scaleGestureListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
+
+        override fun onScaleBegin(scaleGestureDetector: ScaleGestureDetector): Boolean {
+            return true
+        }
+
+        override fun onScale(scaleGestureDetector: ScaleGestureDetector): Boolean {
+            val scaleFactor = scaleGestureDetector.scaleFactor
+            when (otherState?.state) {
+                PointerState.DOWN -> {
+                    otherState?.lineSize = (otherState?.lineSize ?: 5f) * scaleFactor
+                }
+                PointerState.HOVER -> {
+                    otherState?.circleSize = (otherState?.circleSize ?: 20f) * scaleFactor
+                }
+                else -> {}
+            }
+            return true
+        }
+    }
+    private val scaleGestureDetector = ScaleGestureDetector(context, scaleGestureListener, null)
+
+    private var touchPaint = Paint()
+    private var stylusPaint = Paint()
+
+    private fun init() {
+        touchPaint.color = Color.RED
+        touchPaint.setStrokeWidth(5f)
+        stylusPaint.color = Color.YELLOW
+        stylusPaint.setStrokeWidth(5f)
+
+        setOnHoverListener { _, event -> processHoverEvent(event); true }
+    }
+
+    private fun processTouchEvent(event: MotionEvent) {
+        scaleGestureDetector.onTouchEvent(event)
+        if (event.actionMasked == ACTION_DOWN) {
+            touchEvents.remove(event.deviceId)
+            myState?.state = PointerState.DOWN
+        } else if (event.actionMasked == ACTION_UP) {
+            myState?.state = PointerState.NONE
+        }
+        var vec = touchEvents.getOrPut(event.deviceId) { Vector<Pair<MotionEvent, Paint>>() }
+
+        val paint = if (event.isFromSource(SOURCE_STYLUS)) {
+            val size = myState?.lineSize ?: 5f
+            stylusPaint.setStrokeWidth(size)
+            Paint(stylusPaint)
+        } else {
+            val size = myState?.lineSize ?: 5f
+            touchPaint.setStrokeWidth(size)
+            Paint(touchPaint)
+        }
+        vec.add(Pair(MotionEvent.obtain(event), paint))
+        invalidate()
+    }
+
+    private fun processHoverEvent(event: MotionEvent) {
+        hoverEvents.remove(event.deviceId)
+        if (event.getActionMasked() != ACTION_HOVER_EXIT) {
+            hoverEvents.put(event.deviceId, MotionEvent.obtain(event))
+            myState?.state = PointerState.HOVER
+        } else {
+            myState?.state = PointerState.NONE
+        }
+        invalidate()
+    }
+
+    public override fun onTouchEvent(event: MotionEvent): Boolean {
+        processTouchEvent(event)
+        return true
+    }
+
+    public override fun onDraw(canvas: Canvas) {
+        super.onDraw(canvas)
+
+        // Draw touch and stylus MotionEvents
+        for ((_, vec) in touchEvents ) {
+            for (i in 1 until vec.size) {
+                drawLine(canvas, vec[i - 1].first, vec[i].first, vec[i].second)
+            }
+        }
+        // Draw hovers
+        for ((_, event) in hoverEvents ) {
+            if (event.isFromSource(SOURCE_STYLUS)) {
+                val size = myState?.circleSize ?: 20f
+                drawCircle(canvas, event, stylusPaint, size)
+            } else {
+                val size = myState?.circleSize ?: 20f
+                drawCircle(canvas, event, touchPaint, size)
+            }
+        }
+    }
+}
diff --git a/tests/MultiDeviceInput/src/test/multideviceinput/MainActivity.kt b/tests/MultiDeviceInput/src/test/multideviceinput/MainActivity.kt
new file mode 100644
index 0000000..9112085
--- /dev/null
+++ b/tests/MultiDeviceInput/src/test/multideviceinput/MainActivity.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package test.multideviceinput
+
+import android.app.Activity
+import android.graphics.Color
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowInsets.Type
+import android.view.WindowManager
+
+
+enum class PointerState {
+    DOWN, // One or more pointer(s) down, lines are being drawn
+    HOVER, // Pointer is hovering
+    NONE, // Nothing is touching or hovering
+}
+
+data class SharedScaledPointerSize(
+        var lineSize: Float,
+        var circleSize: Float,
+        var state: PointerState
+)
+
+class MainActivity : Activity() {
+    val TAG = "MultiDeviceInput"
+    private val leftState = SharedScaledPointerSize(5f, 20f, PointerState.NONE)
+    private val rightState = SharedScaledPointerSize(5f, 20f, PointerState.NONE)
+    private lateinit var left: View
+    private lateinit var right: View
+
+    override fun onResume() {
+        super.onResume()
+
+        val wm = getSystemService(WindowManager::class.java)
+        val wmlp = WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION)
+        wmlp.flags = (wmlp.flags or
+                      WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
+                      WindowManager.LayoutParams.FLAG_SPLIT_TOUCH)
+
+        val windowMetrics = windowManager.currentWindowMetrics
+        val insets = windowMetrics.windowInsets.getInsetsIgnoringVisibility(Type.systemBars())
+        val width = windowMetrics.bounds.width() - insets.left - insets.right
+        val height = windowMetrics.bounds.height() - insets.top - insets.bottom
+
+        wmlp.width = width * 24 / 50
+        wmlp.height = height * 35 / 50
+
+        val vglp = ViewGroup.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT
+        )
+
+        wmlp.setTitle("Left -- " + getPackageName())
+        wmlp.gravity = Gravity.CENTER_VERTICAL or Gravity.START
+        left = DrawingView(this, leftState, rightState)
+        left.setBackgroundColor(Color.LTGRAY)
+        left.setLayoutParams(vglp)
+        wm.addView(left, wmlp)
+
+        wmlp.setTitle("Right -- " + getPackageName())
+        wmlp.gravity = Gravity.CENTER_VERTICAL or Gravity.END
+        right = DrawingView(this, rightState, leftState)
+        right.setBackgroundColor(Color.LTGRAY)
+        right.setLayoutParams(vglp)
+        wm.addView(right, wmlp)
+    }
+}
diff --git a/tools/hoststubgen/hoststubgen/Android.bp b/tools/hoststubgen/hoststubgen/Android.bp
index fd4ec8b..5949bca 100644
--- a/tools/hoststubgen/hoststubgen/Android.bp
+++ b/tools/hoststubgen/hoststubgen/Android.bp
@@ -284,6 +284,9 @@
         "hoststubgen-helper-runtime.ravenwood",
         "framework-minus-apex.ravenwood",
     ],
+    static_libs: [
+        "core-xml-for-device",
+    ],
 }
 
 // Defaults for host side test modules.
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/Parcel_host.java b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/Parcel_host.java
index 12c7841..4a3a798 100644
--- a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/Parcel_host.java
+++ b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/Parcel_host.java
@@ -286,11 +286,15 @@
     }
 
     public static byte[] nativeReadBlob(long nativePtr) {
+        var p = getInstance(nativePtr);
+        if (p.mSize - p.mPos < 4) {
+            // Match native impl that returns "null" when not enough data
+            return null;
+        }
         final var size = nativeReadInt(nativePtr);
         if (size == -1) {
             return null;
         }
-        var p = getInstance(nativePtr);
         try {
             p.ensureDataAvailable(align4(size));
         } catch (Exception e) {