Merge "Fix global ref table overflow issue" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 8b88689a..cbb6566 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -226,6 +226,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"],
 }
 
@@ -255,6 +256,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",
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/core/api/current.txt b/core/api/current.txt
index 91e3a3a..4256d51 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -44755,10 +44755,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..a7ea753 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -14837,6 +14837,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 +14874,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 +14954,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 +15016,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/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/content/Intent.java b/core/java/android/content/Intent.java
index ea54c91..6b39f26 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -2800,6 +2800,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 +2813,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)
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/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/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/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/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/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/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/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/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_media_AudioRecord.cpp b/core/jni/android_media_AudioRecord.cpp
index b1dab85..8fa179b 100644
--- a/core/jni/android_media_AudioRecord.cpp
+++ b/core/jni/android_media_AudioRecord.cpp
@@ -574,7 +574,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/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index dc2b056..b065611 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",
@@ -1213,12 +1207,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 +2221,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 +2725,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 +3079,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/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/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/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/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/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..8f31a2d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -51,8 +51,8 @@
 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. */
@@ -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..2c4ed58 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()
@@ -222,11 +191,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/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/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index f93efa1..5f54a98 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -130,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;
@@ -186,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/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/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/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/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/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 52844cf..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
@@ -76,20 +76,12 @@
     @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)
         }
 
@@ -142,29 +134,19 @@
     @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()
@@ -176,7 +158,6 @@
 
             assertThat(message?.text).isEmpty()
             assertThat(pin).isEmpty()
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
         }
 
     @Test
@@ -224,9 +205,7 @@
                 collectLastValue(authenticationInteractor.authenticationChallengeResult)
             lockDeviceAndOpenPinBouncer()
 
-            FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
-                underTest.onPinButtonClicked(digit)
-            }
+            FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked)
 
             underTest.onAuthenticateButtonClicked()
 
@@ -274,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()
@@ -292,9 +269,7 @@
                 collectLastValue(authenticationInteractor.authenticationChallengeResult)
             lockDeviceAndOpenPinBouncer()
 
-            FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
-                underTest.onPinButtonClicked(digit)
-            }
+            FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked)
 
             assertThat(authResult).isTrue()
         }
@@ -323,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
@@ -414,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/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/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/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/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 027c11c..251718d 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();
@@ -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/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 7e83034..aa5f987 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -162,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;
@@ -543,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/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..3e54c7a 100644
--- a/ravenwood/framework-minus-apex-ravenwood-policies.txt
+++ b/ravenwood/framework-minus-apex-ravenwood-policies.txt
@@ -103,7 +103,21 @@
 # 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
diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt
index 776a19a..1ac6bf0 100644
--- a/ravenwood/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/ravenwood-annotation-allowed-classes.txt
@@ -2,8 +2,11 @@
 
 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
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/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/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..8046dbf 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;
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/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/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/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/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/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/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..3775ac94 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;
@@ -2317,11 +2318,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 +2350,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/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/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/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/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/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/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/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.