Merge "Adding supportedModes to DisplayDeviceConfig SensorData" into main
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/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/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/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..4d89508 100644
--- a/core/java/android/hardware/input/VirtualKeyEvent.java
+++ b/core/java/android/hardware/input/VirtualKeyEvent.java
@@ -205,6 +205,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/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/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/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/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/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