Merge "Add method to create a deep copy of a ZenMode" into main
diff --git a/core/api/current.txt b/core/api/current.txt
index d610f4c..c5a70df 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -34161,6 +34161,7 @@
     field public static final int USAGE_CLASS_UNKNOWN = 0; // 0x0
     field public static final int USAGE_COMMUNICATION_REQUEST = 65; // 0x41
     field public static final int USAGE_HARDWARE_FEEDBACK = 50; // 0x32
+    field @FlaggedApi("android.os.vibrator.vibration_attribute_ime_usage_api") public static final int USAGE_IME_FEEDBACK = 82; // 0x52
     field public static final int USAGE_MEDIA = 19; // 0x13
     field public static final int USAGE_NOTIFICATION = 49; // 0x31
     field public static final int USAGE_PHYSICAL_EMULATION = 34; // 0x22
diff --git a/core/java/android/app/Person.java b/core/java/android/app/Person.java
index 96f6f4e..c7432c5 100644
--- a/core/java/android/app/Person.java
+++ b/core/java/android/app/Person.java
@@ -189,10 +189,8 @@
      */
     public void visitUris(@NonNull Consumer<Uri> visitor) {
         visitor.accept(getIconUri());
-        if (Flags.visitPersonUri()) {
-            if (mUri != null && !mUri.isEmpty()) {
-                visitor.accept(Uri.parse(mUri));
-            }
+        if (mUri != null && !mUri.isEmpty()) {
+            visitor.accept(Uri.parse(mUri));
         }
     }
 
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index 19de793..a1ae9da 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -84,6 +84,13 @@
 }
 
 flag {
+     namespace: "virtual_devices"
+     name: "enforce_remote_device_opt_out_on_all_virtual_displays"
+     description: "Respect canDisplayOnRemoteDevices on all virtual displays"
+     bug: "338973239"
+}
+
+flag {
     namespace: "virtual_devices"
     name: "virtual_display_multi_window_mode_support"
     description: "Add support for WINDOWING_MODE_MULTI_WINDOW to virtual displays by default"
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index 6952a09..481e6b5 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -617,7 +617,7 @@
      */
     public static final int FLAG_ENABLE_VR_MODE = 0x8000;
     /**
-     * Bit in {@link #flags} indicating if the activity can be displayed on a remote device.
+     * Bit in {@link #flags} indicating if the activity can be displayed on a virtual display.
      * Corresponds to {@link android.R.attr#canDisplayOnRemoteDevices}
      * @hide
      */
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 88fbbdd..6882d5c 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -359,3 +359,13 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+
+flag {
+  name: "show_different_creation_error_for_unsupported_devices"
+  namespace: "profile_experiences"
+  description: "On private space create error due to child account added/fully managed user show message with link to the Help Center to find out more."
+  bug: "340130375"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index fe14d457..00ce949 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -1171,12 +1171,14 @@
             }
             switch (motionEvent.getAction()) {
                 case MotionEvent.ACTION_DOWN:
+                case MotionEvent.ACTION_HOVER_ENTER:
                     // Consume and ignore all touches while stylus is down to prevent
                     // accidental touches from going to the app while writing.
                     mPrivOps.setHandwritingSurfaceNotTouchable(false);
                     break;
                 case MotionEvent.ACTION_UP:
                 case MotionEvent.ACTION_CANCEL:
+                case MotionEvent.ACTION_HOVER_EXIT:
                     // Go back to only consuming stylus events so that the user
                     // can continue to interact with the app using touch
                     // when the stylus is not down.
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index 136c45d..47096db 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -434,7 +434,6 @@
     @RavenwoodThrow
     private static native void nativeWriteStrongBinder(long nativePtr, IBinder val);
     @FastNative
-    @RavenwoodThrow
     private static native void nativeWriteFileDescriptor(long nativePtr, FileDescriptor val);
 
     private static native byte[] nativeCreateByteArray(long nativePtr);
@@ -456,7 +455,6 @@
     @RavenwoodThrow
     private static native IBinder nativeReadStrongBinder(long nativePtr);
     @FastNative
-    @RavenwoodThrow
     private static native FileDescriptor nativeReadFileDescriptor(long nativePtr);
 
     private static native long nativeCreate();
diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java
index 71957ee..464df23 100644
--- a/core/java/android/os/ParcelFileDescriptor.java
+++ b/core/java/android/os/ParcelFileDescriptor.java
@@ -381,6 +381,8 @@
     }
 
     private static void closeInternal$ravenwood(FileDescriptor fd) {
+        // Desktop JVM doesn't have FileDescriptor.close(), so we'll need to go to the ravenwood
+        // side to close it.
         native_close$ravenwood(fd);
     }
 
diff --git a/core/java/android/os/VibrationAttributes.java b/core/java/android/os/VibrationAttributes.java
index 9df5b85..da863e5 100644
--- a/core/java/android/os/VibrationAttributes.java
+++ b/core/java/android/os/VibrationAttributes.java
@@ -16,6 +16,9 @@
 
 package android.os;
 
+import static android.os.vibrator.Flags.FLAG_VIBRATION_ATTRIBUTE_IME_USAGE_API;
+
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -55,6 +58,7 @@
             USAGE_PHYSICAL_EMULATION,
             USAGE_RINGTONE,
             USAGE_TOUCH,
+            USAGE_IME_FEEDBACK,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface Usage {}
@@ -136,6 +140,12 @@
      */
     public static final int USAGE_ACCESSIBILITY = 0x40 | USAGE_CLASS_FEEDBACK;
     /**
+     * Usage value to use for input method editor (IME) haptic feedback.
+     */
+    @FlaggedApi(FLAG_VIBRATION_ATTRIBUTE_IME_USAGE_API)
+    public static final int USAGE_IME_FEEDBACK = 0x50 | USAGE_CLASS_FEEDBACK;
+
+    /**
      * Usage value to use for media vibrations, such as music, movie, soundtrack, animations, games,
      * or any interactive media that isn't for touch feedback specifically.
      */
@@ -174,7 +184,6 @@
             FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF,
             FLAG_INVALIDATE_SETTINGS_CACHE,
             FLAG_PIPELINED_EFFECT,
-            FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface Flag{}
@@ -228,31 +237,12 @@
     public static final int FLAG_PIPELINED_EFFECT = 1 << 3;
 
     /**
-     * Flag requesting that this vibration effect to be played without applying the user
-     * intensity setting to scale the vibration.
-     *
-     * <p>The user setting is still applied to enable/disable the vibration, but the vibration
-     * effect strength will not be scaled based on the enabled setting value.
-     *
-     * <p>This is intended to be used on scenarios where the system needs to enforce a specific
-     * strength for the vibration effect, regardless of the user preference. Only privileged apps
-     * can ignore user settings, and this flag will be ignored otherwise.
-     *
-     * <p>If you need to bypass the user setting when it's disabling vibrations then this also
-     * needs the flag {@link #FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF} to be set.
-     *
-     * @hide
-     */
-    public static final int FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE = 1 << 4;
-
-    /**
      * All flags supported by vibrator service, update it when adding new flag.
      * @hide
      */
     public static final int FLAG_ALL_SUPPORTED =
             FLAG_BYPASS_INTERRUPTION_POLICY | FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF
-                    | FLAG_INVALIDATE_SETTINGS_CACHE | FLAG_PIPELINED_EFFECT
-                    | FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE;
+                    | FLAG_INVALIDATE_SETTINGS_CACHE | FLAG_PIPELINED_EFFECT;
 
     /** Creates a new {@link VibrationAttributes} instance with given usage. */
     public static @NonNull VibrationAttributes createForUsage(@Usage int usage) {
@@ -349,6 +339,7 @@
             case USAGE_RINGTONE:
                 return AudioAttributes.USAGE_NOTIFICATION_RINGTONE;
             case USAGE_TOUCH:
+            case USAGE_IME_FEEDBACK:
                 return AudioAttributes.USAGE_ASSISTANCE_SONIFICATION;
             case USAGE_ALARM:
                 return AudioAttributes.USAGE_ALARM;
@@ -447,6 +438,8 @@
                 return "PHYSICAL_EMULATION";
             case USAGE_HARDWARE_FEEDBACK:
                 return "HARDWARE_FEEDBACK";
+            case USAGE_IME_FEEDBACK:
+                return "IME";
             default:
                 return "unknown usage " + usage;
         }
diff --git a/core/java/android/os/vibrator/VibrationConfig.java b/core/java/android/os/vibrator/VibrationConfig.java
index f6e73b3..a4164e9 100644
--- a/core/java/android/os/vibrator/VibrationConfig.java
+++ b/core/java/android/os/vibrator/VibrationConfig.java
@@ -20,6 +20,7 @@
 import static android.os.VibrationAttributes.USAGE_ALARM;
 import static android.os.VibrationAttributes.USAGE_COMMUNICATION_REQUEST;
 import static android.os.VibrationAttributes.USAGE_HARDWARE_FEEDBACK;
+import static android.os.VibrationAttributes.USAGE_IME_FEEDBACK;
 import static android.os.VibrationAttributes.USAGE_MEDIA;
 import static android.os.VibrationAttributes.USAGE_NOTIFICATION;
 import static android.os.VibrationAttributes.USAGE_PHYSICAL_EMULATION;
@@ -67,6 +68,8 @@
     private final int mDefaultNotificationVibrationIntensity;
     @VibrationIntensity
     private final int mDefaultRingVibrationIntensity;
+    @VibrationIntensity
+    private final int mDefaultKeyboardVibrationIntensity;
 
     private final boolean mKeyboardVibrationSettingsSupported;
 
@@ -98,6 +101,8 @@
                 com.android.internal.R.integer.config_defaultNotificationVibrationIntensity);
         mDefaultRingVibrationIntensity = loadDefaultIntensity(resources,
                 com.android.internal.R.integer.config_defaultRingVibrationIntensity);
+        mDefaultKeyboardVibrationIntensity = loadDefaultIntensity(resources,
+                com.android.internal.R.integer.config_defaultKeyboardVibrationIntensity);
     }
 
     @VibrationIntensity
@@ -213,6 +218,9 @@
             case USAGE_PHYSICAL_EMULATION:
             case USAGE_ACCESSIBILITY:
                 return mDefaultHapticFeedbackIntensity;
+            case USAGE_IME_FEEDBACK:
+                return isKeyboardVibrationSettingsSupported()
+                        ? mDefaultKeyboardVibrationIntensity : mDefaultHapticFeedbackIntensity;
             case USAGE_MEDIA:
             case USAGE_UNKNOWN:
                 // fall through
@@ -236,6 +244,7 @@
                 + ", mDefaultMediaIntensity=" + mDefaultMediaVibrationIntensity
                 + ", mDefaultNotificationIntensity=" + mDefaultNotificationVibrationIntensity
                 + ", mDefaultRingIntensity=" + mDefaultRingVibrationIntensity
+                + ", mDefaultKeyboardIntensity=" + mDefaultKeyboardVibrationIntensity
                 + ", mKeyboardVibrationSettingsSupported=" + mKeyboardVibrationSettingsSupported
                 + "}";
     }
diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig
index 62b3682..67c3464 100644
--- a/core/java/android/os/vibrator/flags.aconfig
+++ b/core/java/android/os/vibrator/flags.aconfig
@@ -74,3 +74,14 @@
       purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    namespace: "haptics"
+    name: "vibration_attribute_ime_usage_api"
+    is_exported: true
+    description: "A public API for IME usage vibration attribute"
+    bug: "332661766"
+    metadata {
+        purpose: PURPOSE_FEATURE
+    }
+}
diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java
index a78a417..6b1aef7 100644
--- a/core/java/android/text/DynamicLayout.java
+++ b/core/java/android/text/DynamicLayout.java
@@ -1320,7 +1320,11 @@
                         // It's possible that a Span is removed when the text covering it is
                         // deleted, in this case, the original start and end of the span might be
                         // OOB. So it'll reflow the entire string instead.
-                        reflow(s, 0, 0, s.length());
+                        if (Flags.insertModeCrashUpdateLayoutSpan()) {
+                            transformAndReflow(s, 0, s.length());
+                        } else {
+                            reflow(s, 0, 0, s.length());
+                        }
                     } else {
                         reflow(s, start, end - start, end - start);
                     }
@@ -1343,7 +1347,11 @@
                         // When text is changed, it'll also trigger onSpanChanged. In this case we
                         // can't determine the updated range in the transformed text. So it'll
                         // reflow the entire range instead.
-                        reflow(s, 0, 0, s.length());
+                        if (Flags.insertModeCrashUpdateLayoutSpan()) {
+                            transformAndReflow(s, 0, s.length());
+                        } else {
+                            reflow(s, 0, 0, s.length());
+                        }
                     } else {
                         reflow(s, start, end - start, end - start);
                         reflow(s, nstart, nend - nstart, nend - nstart);
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index 155a3e4..88a1b9c 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -125,6 +125,16 @@
 }
 
 flag {
+  name: "insert_mode_crash_update_layout_span"
+  namespace: "text"
+  description: "Fix insert mode crash when the text has UpdateLayout span attached."
+  bug: "355137282"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
   name: "icu_bidi_migration"
   namespace: "text"
   description: "A flag for replacing AndroidBidi with android.icu.text.Bidi."
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 725d496..e5a9b6a 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -65,14 +65,6 @@
 }
 
 flag {
-    name: "defer_display_updates"
-    namespace: "windowing_frontend"
-    description: "Feature flag for deferring DisplayManager updates to WindowManager if Shell transition is running"
-    bug: "259220649"
-    is_fixed_read_only: true
-}
-
-flag {
   name: "close_to_square_config_includes_status_bar"
   namespace: "windowing_frontend"
   description: "On close to square display, when necessary, configuration includes status bar"
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index ab456a8..6258f5c 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -544,6 +544,14 @@
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
+        if (Settings.Secure.getIntForUser(getContentResolver(),
+                Settings.Secure.SECURE_FRP_MODE, 0,
+                getUserId()) == 1) {
+            Log.e(TAG, "Sharing disabled due to active FRP lock.");
+            super.onCreate(savedInstanceState);
+            finish();
+            return;
+        }
         final long intentReceivedTime = System.currentTimeMillis();
         mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET);
 
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 2e3dbda..0be33c2 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -3296,8 +3296,8 @@
              usually TVs.
              <p>Requires permission {@code android.permission.DISABLE_SYSTEM_SOUND_EFFECTS}. -->
         <attr name="playHomeTransitionSound" format="boolean"/>
-        <!-- Indicates whether the activity can be displayed on a remote device which may or
-             may not be running Android. -->
+        <!-- Indicates whether the activity can be displayed on a display that may belong to a
+             remote device which may or may not be running Android. -->
         <attr name="canDisplayOnRemoteDevices" format="boolean"/>
         <attr name="allowUntrustedActivityEmbedding" />
         <attr name="knownActivityEmbeddingCerts" />
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 2afc303..8ed444d 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1399,6 +1399,10 @@
          Settings.System.RING_VIBRATION_INTENSITY more details on the constant values and
          meanings. -->
     <integer name="config_defaultRingVibrationIntensity">2</integer>
+    <!-- The default intensity level for keyboard vibrations. Note that this will only be applied
+         on devices where config_keyboardVibrationSettingsSupported is true, otherwise the
+         keyboard vibration will follow config_defaultHapticFeedbackIntensity -->
+    <integer name="config_defaultKeyboardVibrationIntensity">2</integer>
 
     <!-- Whether to use the strict phone number matcher by default. -->
     <bool name="config_use_strict_phone_number_comparation">false</bool>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index bc8c778..6b58396 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4223,6 +4223,7 @@
   <java-symbol type="integer" name="config_defaultMediaVibrationIntensity" />
   <java-symbol type="integer" name="config_defaultNotificationVibrationIntensity" />
   <java-symbol type="integer" name="config_defaultRingVibrationIntensity" />
+  <java-symbol type="integer" name="config_defaultKeyboardVibrationIntensity" />
 
   <java-symbol type="bool" name="config_maskMainBuiltInDisplayCutout" />
 
diff --git a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java
index a102b3e..eb463fd 100644
--- a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java
+++ b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java
@@ -30,9 +30,9 @@
 import android.view.Choreographer;
 import android.view.animation.LinearInterpolator;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.MediumTest;
 import androidx.test.rule.ActivityTestRule;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.After;
 import org.junit.Before;
diff --git a/core/tests/coretests/src/android/companion/virtual/audio/VirtualAudioSessionTest.java b/core/tests/coretests/src/android/companion/virtual/audio/VirtualAudioSessionTest.java
index e025fae..b91263e 100644
--- a/core/tests/coretests/src/android/companion/virtual/audio/VirtualAudioSessionTest.java
+++ b/core/tests/coretests/src/android/companion/virtual/audio/VirtualAudioSessionTest.java
@@ -35,7 +35,7 @@
 import android.platform.test.annotations.Presubmit;
 
 import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import org.junit.Before;
 import org.junit.Rule;
diff --git a/core/tests/coretests/src/android/debug/AdbNotificationsTest.java b/core/tests/coretests/src/android/debug/AdbNotificationsTest.java
index 3496e2c..10eeb35 100644
--- a/core/tests/coretests/src/android/debug/AdbNotificationsTest.java
+++ b/core/tests/coretests/src/android/debug/AdbNotificationsTest.java
@@ -25,8 +25,8 @@
 import android.text.TextUtils;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/core/tests/coretests/src/android/graphics/FontListParserTest.java b/core/tests/coretests/src/android/graphics/FontListParserTest.java
index 5f96c17..52f53dd 100644
--- a/core/tests/coretests/src/android/graphics/FontListParserTest.java
+++ b/core/tests/coretests/src/android/graphics/FontListParserTest.java
@@ -16,16 +16,16 @@
 
 package android.graphics;
 
+import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE;
+import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL;
+import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY;
+import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT;
 import static android.graphics.fonts.FontStyle.FONT_SLANT_ITALIC;
 import static android.graphics.fonts.FontStyle.FONT_SLANT_UPRIGHT;
 import static android.graphics.fonts.FontStyle.FONT_WEIGHT_NORMAL;
 import static android.text.FontConfig.FontFamily.VARIANT_COMPACT;
 import static android.text.FontConfig.FontFamily.VARIANT_DEFAULT;
 import static android.text.FontConfig.FontFamily.VARIANT_ELEGANT;
-import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE;
-import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY;
-import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL;
-import static android.graphics.fonts.FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -38,8 +38,8 @@
 import android.text.FontConfig;
 import android.util.Xml;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/graphics/RectTest.java b/core/tests/coretests/src/android/graphics/RectTest.java
index 2918f44..d0cb5d5 100644
--- a/core/tests/coretests/src/android/graphics/RectTest.java
+++ b/core/tests/coretests/src/android/graphics/RectTest.java
@@ -24,8 +24,8 @@
 
 import android.platform.test.annotations.Presubmit;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/graphics/TypefaceEqualsTest.java b/core/tests/coretests/src/android/graphics/TypefaceEqualsTest.java
index 6ae7eb7..a94f412 100644
--- a/core/tests/coretests/src/android/graphics/TypefaceEqualsTest.java
+++ b/core/tests/coretests/src/android/graphics/TypefaceEqualsTest.java
@@ -23,8 +23,8 @@
 import android.graphics.fonts.Font;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java
index 0d687b2..10aed8d 100644
--- a/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java
+++ b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java
@@ -39,8 +39,8 @@
 import android.util.ArrayMap;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.After;
 import org.junit.Before;
diff --git a/core/tests/coretests/src/android/graphics/TypefaceTest.java b/core/tests/coretests/src/android/graphics/TypefaceTest.java
index 6bf8f56..80efa51 100644
--- a/core/tests/coretests/src/android/graphics/TypefaceTest.java
+++ b/core/tests/coretests/src/android/graphics/TypefaceTest.java
@@ -30,10 +30,10 @@
 import android.util.ArrayMap;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.MediumTest;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.frameworks.coretests.R;
 
diff --git a/core/tests/coretests/src/android/graphics/drawable/DrawableWrapperTest.java b/core/tests/coretests/src/android/graphics/drawable/DrawableWrapperTest.java
index d0a6ff9..4991cd0 100644
--- a/core/tests/coretests/src/android/graphics/drawable/DrawableWrapperTest.java
+++ b/core/tests/coretests/src/android/graphics/drawable/DrawableWrapperTest.java
@@ -25,8 +25,8 @@
 import android.graphics.PorterDuffXfermode;
 import android.graphics.Xfermode;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/hardware/input/InputFlagsTest.java b/core/tests/coretests/src/android/hardware/input/InputFlagsTest.java
index 5aeab42..b4f1dee 100644
--- a/core/tests/coretests/src/android/hardware/input/InputFlagsTest.java
+++ b/core/tests/coretests/src/android/hardware/input/InputFlagsTest.java
@@ -21,8 +21,8 @@
 
 import android.platform.test.annotations.Presubmit;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/preference/PreferenceIconSpaceTest.java b/core/tests/coretests/src/android/preference/PreferenceIconSpaceTest.java
index 0deb77e..55a347e 100644
--- a/core/tests/coretests/src/android/preference/PreferenceIconSpaceTest.java
+++ b/core/tests/coretests/src/android/preference/PreferenceIconSpaceTest.java
@@ -27,8 +27,8 @@
 import android.widget.ImageView;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java b/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java
index c25aa51..746c8ca 100644
--- a/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java
+++ b/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java
@@ -42,9 +42,9 @@
 import android.print.test.services.StubbablePrinterDiscoverySession;
 import android.printservice.recommendation.IRecommendationsChangeListener;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.MediumTest;
-import androidx.test.runner.AndroidJUnit4;
 import androidx.test.uiautomator.UiDevice;
 
 import org.junit.Before;
diff --git a/core/tests/coretests/src/android/provider/DeviceConfigServiceManagerTest.java b/core/tests/coretests/src/android/provider/DeviceConfigServiceManagerTest.java
index e20258a..a60746f 100644
--- a/core/tests/coretests/src/android/provider/DeviceConfigServiceManagerTest.java
+++ b/core/tests/coretests/src/android/provider/DeviceConfigServiceManagerTest.java
@@ -23,8 +23,8 @@
 
 import android.platform.test.annotations.Presubmit;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/core/tests/coretests/src/android/provider/DeviceConfigTest.java b/core/tests/coretests/src/android/provider/DeviceConfigTest.java
index 9300d1e..681396e 100644
--- a/core/tests/coretests/src/android/provider/DeviceConfigTest.java
+++ b/core/tests/coretests/src/android/provider/DeviceConfigTest.java
@@ -29,9 +29,9 @@
 import android.platform.test.annotations.Presubmit;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.FlakyTest;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.After;
 import org.junit.Assert;
diff --git a/core/tests/coretests/src/android/provider/FontsContractE2ETest.java b/core/tests/coretests/src/android/provider/FontsContractE2ETest.java
index 7e02be8..4010171 100644
--- a/core/tests/coretests/src/android/provider/FontsContractE2ETest.java
+++ b/core/tests/coretests/src/android/provider/FontsContractE2ETest.java
@@ -33,8 +33,8 @@
 import android.provider.FontsContract.FontFamilyResult;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.After;
 import org.junit.Before;
diff --git a/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java b/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java
index 4d446901..6eaf2e4 100644
--- a/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java
+++ b/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java
@@ -45,9 +45,9 @@
 import android.service.controls.actions.ControlActionWrapper;
 import android.service.controls.templates.ThumbnailTemplate;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.R;
 
diff --git a/core/tests/coretests/src/android/service/controls/actions/ControlActionTest.java b/core/tests/coretests/src/android/service/controls/actions/ControlActionTest.java
index d8088b7..44bdc53 100644
--- a/core/tests/coretests/src/android/service/controls/actions/ControlActionTest.java
+++ b/core/tests/coretests/src/android/service/controls/actions/ControlActionTest.java
@@ -23,8 +23,8 @@
 
 import android.os.Parcel;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/service/controls/templates/ControlTemplateTest.java b/core/tests/coretests/src/android/service/controls/templates/ControlTemplateTest.java
index 91a3ba7..73b6f648 100644
--- a/core/tests/coretests/src/android/service/controls/templates/ControlTemplateTest.java
+++ b/core/tests/coretests/src/android/service/controls/templates/ControlTemplateTest.java
@@ -25,8 +25,8 @@
 import android.graphics.drawable.Icon;
 import android.os.Parcel;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.frameworks.coretests.R;
 
diff --git a/core/tests/coretests/src/android/service/euicc/EuiccProfileInfoTest.java b/core/tests/coretests/src/android/service/euicc/EuiccProfileInfoTest.java
index 6792d0b..f4206c8 100644
--- a/core/tests/coretests/src/android/service/euicc/EuiccProfileInfoTest.java
+++ b/core/tests/coretests/src/android/service/euicc/EuiccProfileInfoTest.java
@@ -26,8 +26,8 @@
 import android.service.carrier.CarrierIdentifier;
 import android.telephony.UiccAccessRule;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/service/notification/NotificationListenerFilterTest.java b/core/tests/coretests/src/android/service/notification/NotificationListenerFilterTest.java
index a121941..44456e9 100644
--- a/core/tests/coretests/src/android/service/notification/NotificationListenerFilterTest.java
+++ b/core/tests/coretests/src/android/service/notification/NotificationListenerFilterTest.java
@@ -27,8 +27,8 @@
 import android.os.Parcel;
 import android.util.ArraySet;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/service/notification/StatusBarNotificationTest.java b/core/tests/coretests/src/android/service/notification/StatusBarNotificationTest.java
index 76c9f88..5042408 100644
--- a/core/tests/coretests/src/android/service/notification/StatusBarNotificationTest.java
+++ b/core/tests/coretests/src/android/service/notification/StatusBarNotificationTest.java
@@ -37,8 +37,8 @@
 import android.os.UserHandle;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 
diff --git a/core/tests/coretests/src/android/service/quicksettings/TileTest.java b/core/tests/coretests/src/android/service/quicksettings/TileTest.java
index ca6c3b4..43f9122 100644
--- a/core/tests/coretests/src/android/service/quicksettings/TileTest.java
+++ b/core/tests/coretests/src/android/service/quicksettings/TileTest.java
@@ -18,8 +18,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/service/settings/suggestions/SuggestionServiceTest.java b/core/tests/coretests/src/android/service/settings/suggestions/SuggestionServiceTest.java
index 64edda5..85659d6 100644
--- a/core/tests/coretests/src/android/service/settings/suggestions/SuggestionServiceTest.java
+++ b/core/tests/coretests/src/android/service/settings/suggestions/SuggestionServiceTest.java
@@ -23,9 +23,9 @@
 import android.os.RemoteException;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 import androidx.test.rule.ServiceTestRule;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.After;
 import org.junit.Before;
diff --git a/core/tests/coretests/src/android/service/settings/suggestions/SuggestionTest.java b/core/tests/coretests/src/android/service/settings/suggestions/SuggestionTest.java
index e0eb197..03096de 100644
--- a/core/tests/coretests/src/android/service/settings/suggestions/SuggestionTest.java
+++ b/core/tests/coretests/src/android/service/settings/suggestions/SuggestionTest.java
@@ -26,8 +26,8 @@
 import android.os.Parcel;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/core/tests/coretests/src/android/telephony/PinResultTest.java b/core/tests/coretests/src/android/telephony/PinResultTest.java
index c260807..f5432ee 100644
--- a/core/tests/coretests/src/android/telephony/PinResultTest.java
+++ b/core/tests/coretests/src/android/telephony/PinResultTest.java
@@ -18,7 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/text/DynamicLayoutOffsetMappingTest.java b/core/tests/coretests/src/android/text/DynamicLayoutOffsetMappingTest.java
index 5ff659b..8a41678 100644
--- a/core/tests/coretests/src/android/text/DynamicLayoutOffsetMappingTest.java
+++ b/core/tests/coretests/src/android/text/DynamicLayoutOffsetMappingTest.java
@@ -21,11 +21,18 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.text.method.OffsetMapping;
+import android.text.style.UpdateLayout;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.text.flags.Flags;
+
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -36,6 +43,9 @@
     private static final int WIDTH = 10000;
     private static final TextPaint sTextPaint = new TextPaint();
 
+    @Rule
+    public CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Test
     public void textWithOffsetMapping() {
         final String text = "abcde";
@@ -120,6 +130,84 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_INSERT_MODE_CRASH_UPDATE_LAYOUT_SPAN)
+    public void textWithOffsetMapping_deletion_withUpdateLayoutSpan() {
+        final String text = "abcdef";
+        final SpannableStringBuilder spannable = new SpannableStringBuilder(text);
+        // UpdateLayout span covers the letter 'd'.
+        spannable.setSpan(new UpdateLayout() {}, 3, 4, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+
+        final CharSequence transformedText =
+                new TestOffsetMapping(spannable, 3, "\n\n");
+
+        final DynamicLayout layout = DynamicLayout.Builder.obtain(spannable, sTextPaint, WIDTH)
+                .setAlignment(ALIGN_NORMAL)
+                .setIncludePad(false)
+                .setDisplayText(transformedText)
+                .build();
+
+        // delete character 'c', original text becomes "abdef"
+        spannable.delete(2, 3);
+        assertThat(transformedText.toString()).isEqualTo("ab\n\ndef");
+        assertLineRange(layout, /* lineBreaks */ 0, 3, 4, 7);
+
+        // delete character 'd', original text becomes "abef"
+        spannable.delete(2, 3);
+        assertThat(transformedText.toString()).isEqualTo("ab\n\nef");
+        assertLineRange(layout, /* lineBreaks */ 0, 3, 4, 6);
+
+        // delete "be", original text becomes "af"
+        spannable.delete(1, 3);
+        assertThat(transformedText.toString()).isEqualTo("a\n\nf");
+        assertLineRange(layout, /* lineBreaks */ 0, 2, 3, 4);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_INSERT_MODE_CRASH_UPDATE_LAYOUT_SPAN)
+    public void textWithOffsetMapping_insert_withUpdateLayoutSpan() {
+        final String text = "abcdef";
+        final SpannableStringBuilder spannable = new SpannableStringBuilder(text);
+        final CharSequence transformedText = new TestOffsetMapping(spannable, 3, "\n\n");
+
+        // UpdateLayout span covers the letter 'de'.
+        spannable.setSpan(new UpdateLayout() {}, 3, 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+
+        final DynamicLayout layout = DynamicLayout.Builder.obtain(spannable, sTextPaint, WIDTH)
+                .setAlignment(ALIGN_NORMAL)
+                .setIncludePad(false)
+                .setDisplayText(transformedText)
+                .build();
+
+        spannable.insert(3, "x");
+        assertThat(transformedText.toString()).isEqualTo("abcx\n\ndef");
+        assertLineRange(layout, /* lineBreaks */ 0, 5, 6, 9);
+
+        spannable.insert(5, "x");
+        assertThat(transformedText.toString()).isEqualTo("abcx\n\ndxef");
+        assertLineRange(layout, /* lineBreaks */ 0, 5, 6, 10);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_INSERT_MODE_CRASH_UPDATE_LAYOUT_SPAN)
+    public void textWithOffsetMapping_replace_withUpdateLayoutSpan() {
+        final String text = "abcdef";
+        final SpannableStringBuilder spannable = new SpannableStringBuilder(text);
+        final CharSequence transformedText = new TestOffsetMapping(spannable, 3, "\n\n");
+        // UpdateLayout span covers the letter 'de'.
+        spannable.setSpan(new UpdateLayout() {}, 3, 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+
+        final DynamicLayout layout = DynamicLayout.Builder.obtain(spannable, sTextPaint, WIDTH)
+                .setAlignment(ALIGN_NORMAL)
+                .setIncludePad(false)
+                .setDisplayText(transformedText)
+                .build();
+
+        spannable.replace(2, 4, "xx");
+        assertThat(transformedText.toString()).isEqualTo("abxx\n\nef");
+        assertLineRange(layout, /* lineBreaks */ 0, 5, 6, 8);
+    }
+
+    @Test
     public void textWithOffsetMapping_blockBeforeTextChanged_deletion() {
         final String text = "abcdef";
         final SpannableStringBuilder spannable = new TestNoBeforeTextChangeSpannableString(text);
diff --git a/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java b/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java
index df9a89e..bbeb18d 100644
--- a/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java
+++ b/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java
@@ -37,7 +37,7 @@
 import android.util.proto.ProtoInputStream;
 import android.util.proto.ProtoOutputStream;
 
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import com.google.common.truth.Truth;
 import com.google.protobuf.InvalidProtocolBufferException;
diff --git a/core/tests/coretests/src/android/transition/AutoTransitionTest.java b/core/tests/coretests/src/android/transition/AutoTransitionTest.java
index deae967..5d58fead 100644
--- a/core/tests/coretests/src/android/transition/AutoTransitionTest.java
+++ b/core/tests/coretests/src/android/transition/AutoTransitionTest.java
@@ -20,8 +20,8 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/com/android/internal/infra/AndroidFutureTest.java b/core/tests/coretests/src/com/android/internal/infra/AndroidFutureTest.java
index 3a27225..178e93a 100644
--- a/core/tests/coretests/src/com/android/internal/infra/AndroidFutureTest.java
+++ b/core/tests/coretests/src/com/android/internal/infra/AndroidFutureTest.java
@@ -22,7 +22,7 @@
 
 import android.os.Parcel;
 
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/com/android/internal/infra/ServiceConnectorTest.java b/core/tests/coretests/src/com/android/internal/infra/ServiceConnectorTest.java
index 725dcf3..3d1b565 100644
--- a/core/tests/coretests/src/com/android/internal/infra/ServiceConnectorTest.java
+++ b/core/tests/coretests/src/com/android/internal/infra/ServiceConnectorTest.java
@@ -29,8 +29,8 @@
 import android.os.UserHandle;
 
 import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.frameworks.coretests.aidl.ITestServiceConnectorService;
 import com.android.internal.infra.ServiceConnectorTest.CapturingServiceLifecycleCallbacks.ServiceLifeCycleEvent;
diff --git a/core/tests/coretests/src/com/android/internal/logging/MetricsLoggerTest.java b/core/tests/coretests/src/com/android/internal/logging/MetricsLoggerTest.java
index 7054cc0..b86cb4a 100644
--- a/core/tests/coretests/src/com/android/internal/logging/MetricsLoggerTest.java
+++ b/core/tests/coretests/src/com/android/internal/logging/MetricsLoggerTest.java
@@ -20,8 +20,8 @@
 
 import android.metrics.LogMaker;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.logging.nano.MetricsProto;
 import com.android.internal.logging.testing.FakeMetricsLogger;
diff --git a/core/tests/coretests/src/com/android/internal/logging/UiEventLoggerTest.java b/core/tests/coretests/src/com/android/internal/logging/UiEventLoggerTest.java
index 7840f71..fc28627 100644
--- a/core/tests/coretests/src/com/android/internal/logging/UiEventLoggerTest.java
+++ b/core/tests/coretests/src/com/android/internal/logging/UiEventLoggerTest.java
@@ -18,8 +18,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.logging.testing.UiEventLoggerFake;
 
diff --git a/core/tests/coretests/src/com/android/internal/ravenwood/RavenwoodEnvironmentTest.java b/core/tests/coretests/src/com/android/internal/ravenwood/RavenwoodEnvironmentTest.java
index d1ef61b..d1c0668 100644
--- a/core/tests/coretests/src/com/android/internal/ravenwood/RavenwoodEnvironmentTest.java
+++ b/core/tests/coretests/src/com/android/internal/ravenwood/RavenwoodEnvironmentTest.java
@@ -19,7 +19,7 @@
 
 import android.platform.test.ravenwood.RavenwoodRule;
 
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import org.junit.Rule;
 import org.junit.Test;
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 269a586..606ebb4 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -554,15 +554,10 @@
          enable_windowing_edge_drag_resize is disabled. -->
     <dimen name="freeform_resize_corner">44dp</dimen>
 
-    <!-- The width of the area at the sides of the screen where a freeform task will transition to
-    split select if dragged until the touch input is within the range. -->
-    <dimen name="desktop_mode_transition_area_width">32dp</dimen>
+    <!-- The thickness in dp for all desktop drag transition regions. -->
+    <dimen name="desktop_mode_transition_region_thickness">44dp</dimen>
 
-    <!-- The width of the area where a desktop task will transition to fullscreen. -->
-    <dimen name="desktop_mode_fullscreen_from_desktop_width">80dp</dimen>
-
-    <!-- The height of the area where a desktop task will transition to fullscreen. -->
-    <dimen name="desktop_mode_fullscreen_from_desktop_height">40dp</dimen>
+    <item type="dimen" format="float" name="desktop_mode_fullscreen_region_scale">0.4</item>
 
     <!-- The height on the screen where drag to the left or right edge will result in a
     desktop task snapping to split size. The empty space between this and the top is to allow
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
index 2b01eac..1304969 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
@@ -43,7 +43,8 @@
   APP_HEADER_WITH_TASK_DENSITY(Flags::enableAppHeaderWithTaskDensity, true),
   TASK_STACK_OBSERVER_IN_SHELL(Flags::enableTaskStackObserverInShell, true),
   SIZE_CONSTRAINTS(Flags::enableDesktopWindowingSizeConstraints, true),
-  DYNAMIC_INITIAL_BOUNDS(Flags::enableWindowingDynamicInitialBounds, true);
+  DYNAMIC_INITIAL_BOUNDS(Flags::enableWindowingDynamicInitialBounds, true),
+  ENABLE_DESKTOP_WINDOWING_TASK_LIMIT(Flags::enableDesktopWindowingTaskLimit, true);
 
   /**
    * Determines state of flag based on the actual flag and desktop mode developer option overrides.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 35d3876..0262507 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -16,7 +16,7 @@
 
 package com.android.wm.shell.dagger;
 
-import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.DESKTOP_WINDOWING_MODE;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASK_LIMIT;
 
 import android.annotation.Nullable;
 import android.app.KeyguardManager;
@@ -569,7 +569,7 @@
             ShellTaskOrganizer shellTaskOrganizer) {
         int maxTaskLimit = DesktopModeStatus.getMaxTaskLimit(context);
         if (!DesktopModeStatus.canEnterDesktopMode(context)
-                || !DESKTOP_WINDOWING_MODE.isEnabled(context)
+                || !ENABLE_DESKTOP_WINDOWING_TASK_LIMIT.isEnabled(context)
                 || maxTaskLimit <= 0) {
             return Optional.empty();
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index 4299841..4bec788 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -270,13 +270,37 @@
             logW("Unminimize Task: display=%d, task=%d, no task data", displayId, taskId)
     }
 
-    /** Removes task from the ordered list. */
+    private fun getDisplayIdForTask(taskId: Int): Int? {
+        desktopTaskDataByDisplayId.forEach { displayId, data ->
+            if (taskId in data.freeformTasksInZOrder) {
+                return displayId
+            }
+        }
+        logW("No display id found for task: taskId=%d", taskId)
+        return null
+    }
+
+    /**
+     * Removes [taskId] from the respective display. If [INVALID_DISPLAY], the original display id
+     * will be looked up from the task id.
+     */
     fun removeFreeformTask(displayId: Int, taskId: Int) {
         logD("Removes freeform task: taskId=%d", taskId)
+        if (displayId == INVALID_DISPLAY) {
+            // Removes the original display id of the task.
+            getDisplayIdForTask(taskId)?.let { removeTaskFromDisplay(it, taskId) }
+        } else {
+            removeTaskFromDisplay(displayId, taskId)
+        }
+    }
+
+    /** Removes given task from a valid [displayId]. */
+    private fun removeTaskFromDisplay(displayId: Int, taskId: Int) {
+        logD("Removes freeform task: taskId=%d, displayId=%d", taskId, displayId)
         desktopTaskDataByDisplayId[displayId]?.freeformTasksInZOrder?.remove(taskId)
         boundsBeforeMaximizeByTaskId.remove(taskId)
-        logD("Remaining freeform tasks: %d",
-            desktopTaskDataByDisplayId[displayId]?.freeformTasksInZOrder?.toDumpString() ?: "")
+        logD("Remaining freeform tasks: %s",
+            desktopTaskDataByDisplayId[displayId]?.freeformTasksInZOrder?.toDumpString())
     }
 
     /**
@@ -358,3 +382,4 @@
 
 private fun <T> Iterable<T>.toDumpString(): String =
     joinToString(separator = ", ", prefix = "[", postfix = "]")
+
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
index ed0d2b8..6011db7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
@@ -105,7 +105,7 @@
         // If we are in freeform, we don't want a visible indicator in the "freeform" drag zone.
         IndicatorType result = IndicatorType.NO_INDICATOR;
         final int transitionAreaWidth = mContext.getResources().getDimensionPixelSize(
-                com.android.wm.shell.R.dimen.desktop_mode_transition_area_width);
+                com.android.wm.shell.R.dimen.desktop_mode_transition_region_thickness);
         // Because drags in freeform use task position for indicator calculation, we need to
         // account for the possibility of the task going off the top of the screen by captionHeight
         final int captionHeight = mContext.getResources().getDimensionPixelSize(
@@ -140,18 +140,19 @@
         final Region region = new Region();
         int transitionHeight = windowingMode == WINDOWING_MODE_FREEFORM
                 ? mContext.getResources().getDimensionPixelSize(
-                com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_height)
+                com.android.wm.shell.R.dimen.desktop_mode_transition_region_thickness)
                 : 2 * layout.stableInsets().top;
-        // A thin, short Rect at the top of the screen.
+        // A Rect at the top of the screen that takes up the center 40%.
         if (windowingMode == WINDOWING_MODE_FREEFORM) {
-            int fromFreeformWidth = mContext.getResources().getDimensionPixelSize(
-                    com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_width);
-            region.union(new Rect((layout.width() / 2) - (fromFreeformWidth / 2),
+            final float toFullscreenScale = mContext.getResources().getFloat(
+                    R.dimen.desktop_mode_fullscreen_region_scale);
+            final float toFullscreenWidth = (layout.width() * toFullscreenScale);
+            region.union(new Rect((int) ((layout.width() / 2f) - (toFullscreenWidth / 2f)),
                     -captionHeight,
-                    (layout.width() / 2) + (fromFreeformWidth / 2),
+                    (int) ((layout.width() / 2f) + (toFullscreenWidth / 2f)),
                     transitionHeight));
         }
-        // A screen-wide, shorter Rect if the task is in fullscreen or split.
+        // A screen-wide Rect if the task is in fullscreen or split.
         if (windowingMode == WINDOWING_MODE_FULLSCREEN
                 || windowingMode == WINDOWING_MODE_MULTI_WINDOW) {
             region.union(new Rect(0,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
index a011ff5..87d1800 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
@@ -33,7 +33,8 @@
  * Limits the number of tasks shown in Desktop Mode.
  *
  * This class should only be used if
- * [com.android.window.flags.Flags.enableDesktopWindowingTaskLimit()] is true.
+ * [com.android.wm.shell.shared.desktopmode.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASK_LIMIT]
+ * is enabled and [maxTasksLimit] is strictly greater than 0.
  */
 class DesktopTasksLimiter (
         transitions: Transitions,
@@ -86,10 +87,10 @@
         }
 
         /**
-         * Returns whether the given Task is being reordered to the back in the given transition, or
-         * is already invisible.
+         * Returns whether the Task [taskDetails] is being reordered to the back in the transition
+         * [info], or is already invisible.
          *
-         * <p> This check can be used to double-check that a task was indeed minimized before
+         * This check can be used to double-check that a task was indeed minimized before
          * marking it as such.
          */
         private fun isTaskReorderedToBackOrInvisible(
@@ -149,8 +150,8 @@
     }
 
     /**
-     * Mark a task as minimized, this should only be done after the corresponding transition has
-     * finished so we don't minimize the task if the transition fails.
+     * Mark [taskId], which must be on [displayId], as minimized, this should only be done after the
+     * corresponding transition has finished so we don't minimize the task if the transition fails.
      */
     private fun markTaskMinimized(displayId: Int, taskId: Int) {
         ProtoLog.v(
@@ -161,11 +162,9 @@
 
     /**
      * Add a minimize-transition to [wct] if adding [newFrontTaskInfo] brings us over the task
-     * limit.
+     * limit, returning the task to minimize.
      *
-     * @param transition the transition that the minimize-transition will be appended to, or null if
-     * the transition will be started later.
-     * @return the ID of the minimized task, or null if no task is being minimized.
+     * The task must be on [displayId].
      */
     fun addAndGetMinimizeTaskChangesIfNeeded(
             displayId: Int,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 7451d22..284620e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -272,6 +272,7 @@
         final boolean changed = onDisplayRotationChanged(mContext, outBounds, currentBounds,
                 mTmpInsetBounds, displayId, fromRotation, toRotation, t);
         if (changed) {
+            mMenuController.hideMenu();
             // If the pip was in the offset zone earlier, adjust the new bounds to the bottom of the
             // movement bounds
             mTouchHandler.adjustBoundsForRotation(outBounds, mPipBoundsState.getBounds(),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index 0a5672d..9dc3401 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -468,8 +468,42 @@
     }
 
     @Test
+    fun removeFreeformTask_invalidDisplay_removesTaskFromFreeformTasks() {
+        repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1)
+
+        repo.removeFreeformTask(INVALID_DISPLAY, taskId = 1)
+
+        val invalidDisplayTasks = repo.getFreeformTasksInZOrder(INVALID_DISPLAY)
+        assertThat(invalidDisplayTasks).isEmpty()
+        val validDisplayTasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY)
+        assertThat(validDisplayTasks).isEmpty()
+    }
+
+    @Test
+    fun removeFreeformTask_validDisplay_removesTaskFromFreeformTasks() {
+        repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1)
+
+        repo.removeFreeformTask(DEFAULT_DISPLAY, taskId = 1)
+
+        val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY)
+        assertThat(tasks).isEmpty()
+    }
+
+    @Test
+    fun removeFreeformTask_validDisplay_differentDisplay_doesNotRemovesTask() {
+        repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1)
+
+        repo.removeFreeformTask(SECOND_DISPLAY, taskId = 1)
+
+        val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY)
+        assertThat(tasks).containsExactly(1)
+    }
+
+    @Test
     fun removeFreeformTask_removesTaskBoundsBeforeMaximize() {
         val taskId = 1
+        repo.addActiveTask(THIRD_DISPLAY, taskId)
+        repo.addOrMoveFreeformTaskToTop(THIRD_DISPLAY, taskId)
         repo.saveBoundsBeforeMaximize(taskId, Rect(0, 0, 200, 200))
 
         repo.removeFreeformTask(THIRD_DISPLAY, taskId)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
index bd39aa6..2dea43b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
@@ -61,20 +61,23 @@
 
     @Test
     fun testFullscreenRegionCalculation() {
-        val transitionHeight = context.resources.getDimensionPixelSize(
-            R.dimen.desktop_mode_fullscreen_from_desktop_height)
-        val fromFreeformWidth = mContext.resources.getDimensionPixelSize(
-            R.dimen.desktop_mode_fullscreen_from_desktop_width
-        )
         var testRegion = visualIndicator.calculateFullscreenRegion(displayLayout,
             WINDOWING_MODE_FULLSCREEN, CAPTION_HEIGHT)
         assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, 2 * STABLE_INSETS.top))
         testRegion = visualIndicator.calculateFullscreenRegion(displayLayout,
             WINDOWING_MODE_FREEFORM, CAPTION_HEIGHT)
+
+        val transitionHeight = context.resources.getDimensionPixelSize(
+            R.dimen.desktop_mode_transition_region_thickness)
+        val toFullscreenScale = mContext.resources.getFloat(
+            R.dimen.desktop_mode_fullscreen_region_scale
+        )
+        val toFullscreenWidth = displayLayout.width() * toFullscreenScale
+
         assertThat(testRegion.bounds).isEqualTo(Rect(
-            DISPLAY_BOUNDS.width() / 2 - fromFreeformWidth / 2,
+            (DISPLAY_BOUNDS.width() / 2f - toFullscreenWidth / 2f).toInt(),
             -50,
-            DISPLAY_BOUNDS.width() / 2 + fromFreeformWidth / 2,
+            (DISPLAY_BOUNDS.width() / 2f + toFullscreenWidth / 2f).toInt(),
             transitionHeight))
         testRegion = visualIndicator.calculateFullscreenRegion(displayLayout,
             WINDOWING_MODE_MULTI_WINDOW, CAPTION_HEIGHT)
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index e302fa8..d71f3b6 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -736,6 +736,7 @@
 
 cc_test {
     name: "hwui_unit_tests",
+    test_config: "tests/unit/AndroidTest.xml",
     defaults: [
         "hwui_test_defaults",
         "android_graphics_apex",
@@ -803,6 +804,7 @@
 
 cc_benchmark {
     name: "hwuimacro",
+    test_config: "tests/macrobench/AndroidTest.xml",
     defaults: ["hwui_test_defaults"],
 
     static_libs: ["libhwui"],
@@ -822,6 +824,7 @@
 
 cc_benchmark {
     name: "hwuimicro",
+    test_config: "tests/microbench/AndroidTest.xml",
     defaults: ["hwui_test_defaults"],
 
     static_libs: ["libhwui_static"],
diff --git a/libs/hwui/AndroidTest.xml b/libs/hwui/tests/macrobench/AndroidTest.xml
similarity index 60%
copy from libs/hwui/AndroidTest.xml
copy to libs/hwui/tests/macrobench/AndroidTest.xml
index 75f61f5..5b8576d 100644
--- a/libs/hwui/AndroidTest.xml
+++ b/libs/hwui/tests/macrobench/AndroidTest.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
+<!-- Copyright 2024 The Android Open Source Project
 
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
@@ -13,24 +13,13 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<configuration description="Config for hwuimicro">
+<configuration description="Config for hwuimacro">
     <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
         <option name="cleanup" value="true" />
-        <option name="push" value="hwui_unit_tests->/data/local/tmp/nativetest/hwui_unit_tests" />
-        <option name="push" value="hwuimicro->/data/local/tmp/benchmarktest/hwuimicro" />
         <option name="push" value="hwuimacro->/data/local/tmp/benchmarktest/hwuimacro" />
     </target_preparer>
     <option name="test-suite-tag" value="apct" />
     <option name="not-shardable" value="true" />
-    <test class="com.android.tradefed.testtype.GTest" >
-        <option name="native-test-device-path" value="/data/local/tmp/nativetest" />
-        <option name="module-name" value="hwui_unit_tests" />
-    </test>
-    <test class="com.android.tradefed.testtype.GoogleBenchmarkTest" >
-        <option name="native-benchmark-device-path" value="/data/local/tmp/benchmarktest" />
-        <option name="benchmark-module-name" value="hwuimicro" />
-        <option name="file-exclusion-filter-regex" value=".*\.config$" />
-    </test>
     <test class="com.android.tradefed.testtype.GoogleBenchmarkTest" >
         <option name="native-benchmark-device-path" value="/data/local/tmp/benchmarktest" />
         <option name="benchmark-module-name" value="hwuimacro" />
diff --git a/libs/hwui/tests/macrobench/how_to_run.txt b/libs/hwui/tests/macrobench/how_to_run.txt
index 3c3d36a..59ef25a 100644
--- a/libs/hwui/tests/macrobench/how_to_run.txt
+++ b/libs/hwui/tests/macrobench/how_to_run.txt
@@ -3,3 +3,7 @@
 adb shell /data/benchmarktest/hwuimacro/hwuimacro shadowgrid2 --onscreen
 
 Pass --help to get help
+
+OR (if you don't need to pass arguments)
+
+atest hwuimacro
diff --git a/libs/hwui/AndroidTest.xml b/libs/hwui/tests/microbench/AndroidTest.xml
similarity index 63%
rename from libs/hwui/AndroidTest.xml
rename to libs/hwui/tests/microbench/AndroidTest.xml
index 75f61f5..d67305df 100644
--- a/libs/hwui/AndroidTest.xml
+++ b/libs/hwui/tests/microbench/AndroidTest.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
+<!-- Copyright 2024 The Android Open Source Project
 
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
@@ -16,24 +16,13 @@
 <configuration description="Config for hwuimicro">
     <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
         <option name="cleanup" value="true" />
-        <option name="push" value="hwui_unit_tests->/data/local/tmp/nativetest/hwui_unit_tests" />
         <option name="push" value="hwuimicro->/data/local/tmp/benchmarktest/hwuimicro" />
-        <option name="push" value="hwuimacro->/data/local/tmp/benchmarktest/hwuimacro" />
     </target_preparer>
     <option name="test-suite-tag" value="apct" />
     <option name="not-shardable" value="true" />
-    <test class="com.android.tradefed.testtype.GTest" >
-        <option name="native-test-device-path" value="/data/local/tmp/nativetest" />
-        <option name="module-name" value="hwui_unit_tests" />
-    </test>
     <test class="com.android.tradefed.testtype.GoogleBenchmarkTest" >
         <option name="native-benchmark-device-path" value="/data/local/tmp/benchmarktest" />
         <option name="benchmark-module-name" value="hwuimicro" />
         <option name="file-exclusion-filter-regex" value=".*\.config$" />
     </test>
-    <test class="com.android.tradefed.testtype.GoogleBenchmarkTest" >
-        <option name="native-benchmark-device-path" value="/data/local/tmp/benchmarktest" />
-        <option name="benchmark-module-name" value="hwuimacro" />
-        <option name="file-exclusion-filter-regex" value=".*\.config$" />
-    </test>
 </configuration>
diff --git a/libs/hwui/tests/microbench/how_to_run.txt b/libs/hwui/tests/microbench/how_to_run.txt
index 915fe5d..c7ddc1a 100755
--- a/libs/hwui/tests/microbench/how_to_run.txt
+++ b/libs/hwui/tests/microbench/how_to_run.txt
@@ -1,3 +1,7 @@
 mmm -j8 frameworks/base/libs/hwui &&
 adb push $OUT/data/benchmarktest/hwuimicro/hwuimicro /data/benchmarktest/hwuimicro/hwuimicro &&
 adb shell /data/benchmarktest/hwuimicro/hwuimicro
+
+OR
+
+atest hwuimicro
diff --git a/libs/hwui/tests/unit/AndroidTest.xml b/libs/hwui/tests/unit/AndroidTest.xml
new file mode 100644
index 0000000..dc586c9
--- /dev/null
+++ b/libs/hwui/tests/unit/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Config for hwui_unit_tests">
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="cleanup" value="true" />
+        <option name="push" value="hwui_unit_tests->/data/local/tmp/nativetest/hwui_unit_tests" />
+    </target_preparer>
+    <option name="test-suite-tag" value="apct" />
+    <option name="not-shardable" value="true" />
+    <test class="com.android.tradefed.testtype.GTest" >
+        <option name="native-test-device-path" value="/data/local/tmp/nativetest" />
+        <option name="module-name" value="hwui_unit_tests" />
+    </test>
+</configuration>
diff --git a/libs/hwui/tests/unit/how_to_run.txt b/libs/hwui/tests/unit/how_to_run.txt
index c11d6eb3..1a35adf 100755
--- a/libs/hwui/tests/unit/how_to_run.txt
+++ b/libs/hwui/tests/unit/how_to_run.txt
@@ -2,3 +2,11 @@
 adb push $ANDROID_PRODUCT_OUT/data/nativetest/hwui_unit_tests/hwui_unit_tests \
     /data/nativetest/hwui_unit_tests/hwui_unit_tests &&
 adb shell /data/nativetest/hwui_unit_tests/hwui_unit_tests
+
+OR
+
+atest hwui_unit_tests
+
+OR, if you need arguments, they can be passed as native-test-flags, as in:
+
+atest hwui_unit_tests -- --test-arg com.android.tradefed.testtype.GTest:native-test-flag:"--renderer=skiavk"
diff --git a/libs/hwui/tests/unit/main.cpp b/libs/hwui/tests/unit/main.cpp
index 76cbc8a..3fd15c4 100644
--- a/libs/hwui/tests/unit/main.cpp
+++ b/libs/hwui/tests/unit/main.cpp
@@ -15,6 +15,7 @@
  */
 
 #include <getopt.h>
+#include <log/log.h>
 #include <signal.h>
 
 #include "Properties.h"
@@ -65,6 +66,19 @@
     return RenderPipelineType::SkiaGL;
 }
 
+static constexpr const char* renderPipelineTypeName(const RenderPipelineType renderPipelineType) {
+    switch (renderPipelineType) {
+        case RenderPipelineType::SkiaGL:
+            return "SkiaGL";
+        case RenderPipelineType::SkiaVulkan:
+            return "SkiaVulkan";
+        case RenderPipelineType::SkiaCpu:
+            return "SkiaCpu";
+        case RenderPipelineType::NotInitialized:
+            return "NotInitialized";
+    }
+}
+
 struct Options {
     RenderPipelineType renderer = RenderPipelineType::SkiaGL;
 };
@@ -118,6 +132,7 @@
 
     auto opts = parseOptions(argc, argv);
     Properties::overrideRenderPipelineType(opts.renderer);
+    ALOGI("Starting HWUI unit tests with %s pipeline", renderPipelineTypeName(opts.renderer));
 
     // Run the tests
     testing::InitGoogleTest(&argc, argv);
diff --git a/packages/InputDevices/res/raw/keyboard_layout_arabic.kcm b/packages/InputDevices/res/raw/keyboard_layout_arabic.kcm
index 9c2064c..8c6880b 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_arabic.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_arabic.kcm
@@ -58,7 +58,8 @@
     label:                              '5'
     base:                               '\u0665'
     capslock:                           '5'
-    shift:                              '%'
+    shift:                              '\u066a'
+    shift+capslock:                     '%'
 }
 
 key 6 {
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index ce997bf..5c4cdb2 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1010,8 +1010,8 @@
     <!-- UI debug setting: force allow on external summary [CHAR LIMIT=150] -->
     <string name="force_resizable_activities_summary">Make all activities resizable for multi-window, regardless of manifest values.</string>
 
-    <!-- Title for a toggle that enables support for windows to be in freeform (apps run in resizable windows). [CHAR LIMIT=50] -->
-    <string name="enable_freeform_support">Enable freeform window support</string>
+    <!-- Title for a toggle that enables support for windows to be in freeform. Freeform windows enables users to freely arrange and resize overlapping apps. [CHAR LIMIT=50] -->
+    <string name="enable_freeform_support">Enable freeform windows</string>
 
     <!-- Local (desktop) backup password menu title [CHAR LIMIT=25] -->
     <string name="local_backup_password_title">Desktop backup password</string>
@@ -1164,7 +1164,7 @@
     <!-- [CHAR_LIMIT=40] Label for battery level chart when charging with duration -->
     <string name="power_charging_duration"><xliff:g id="level">%1$s</xliff:g> - <xliff:g id="time">%2$s</xliff:g> left until full</string>
     <!-- [CHAR_LIMIT=80] Label for battery level chart when charge been limited -->
-    <string name="power_charging_limited"><xliff:g id="level">%1$s</xliff:g> - Charging optimized</string>
+    <string name="power_charging_limited"><xliff:g id="level">%1$s</xliff:g> - Charging on hold to protect battery</string>
     <!-- [CHAR_LIMIT=80] Label for battery charging future pause -->
     <string name="power_charging_future_paused"><xliff:g id="level">%1$s</xliff:g> - Charging</string>
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index 3be5231..368085f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -32,6 +32,7 @@
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.compose.animation.scene.Back
+import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.ElementMatcher
@@ -70,7 +71,7 @@
 }
 
 object AllElements : ElementMatcher {
-    override fun matches(key: ElementKey, scene: SceneKey) = true
+    override fun matches(key: ElementKey, content: ContentKey) = true
 }
 
 private object TransitionDuration {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
index 859c036..df068c4 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
@@ -92,7 +92,7 @@
     fun SceneScope.Notifications(burnInParams: BurnInParameters?, modifier: Modifier = Modifier) {
         val areNotificationsVisible by
             lockscreenContentViewModel
-                .areNotificationsVisible(sceneKey)
+                .areNotificationsVisible(contentKey)
                 .collectAsStateWithLifecycle(initialValue = false)
         if (!areNotificationsVisible) {
             return
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt
new file mode 100644
index 0000000..4b3a39b
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.notifications.ui.composable
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.offset
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.unit.IntOffset
+import com.android.compose.nestedscroll.PriorityNestedScrollConnection
+import kotlin.math.roundToInt
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+@Composable
+fun Modifier.stackVerticalOverscroll(
+    coroutineScope: CoroutineScope,
+    canScrollForward: () -> Boolean
+): Modifier {
+    val overscrollOffset = remember { Animatable(0f) }
+    val stackNestedScrollConnection = remember {
+        NotificationStackNestedScrollConnection(
+            stackOffset = { overscrollOffset.value },
+            canScrollForward = canScrollForward,
+            onScroll = { offsetAvailable ->
+                coroutineScope.launch {
+                    overscrollOffset.snapTo(overscrollOffset.value + offsetAvailable * 0.3f)
+                }
+            },
+            onStop = { velocityAvailable ->
+                coroutineScope.launch {
+                    overscrollOffset.animateTo(
+                        targetValue = 0f,
+                        initialVelocity = velocityAvailable,
+                        animationSpec = tween()
+                    )
+                }
+            }
+        )
+    }
+
+    return this.then(
+        Modifier.nestedScroll(stackNestedScrollConnection).offset {
+            IntOffset(x = 0, y = overscrollOffset.value.roundToInt())
+        }
+    )
+}
+
+fun NotificationStackNestedScrollConnection(
+    stackOffset: () -> Float,
+    canScrollForward: () -> Boolean,
+    onStart: (Float) -> Unit = {},
+    onScroll: (Float) -> Unit,
+    onStop: (Float) -> Unit = {},
+): PriorityNestedScrollConnection {
+    return PriorityNestedScrollConnection(
+        orientation = Orientation.Vertical,
+        canStartPreScroll = { _, _ -> false },
+        canStartPostScroll = { offsetAvailable, offsetBeforeStart ->
+            offsetAvailable < 0f && offsetBeforeStart < 0f && !canScrollForward()
+        },
+        canStartPostFling = { velocityAvailable -> velocityAvailable < 0f && !canScrollForward() },
+        canContinueScroll = { source ->
+            if (source == NestedScrollSource.SideEffect) {
+                stackOffset() > STACK_OVERSCROLL_FLING_MIN_OFFSET
+            } else {
+                true
+            }
+        },
+        canScrollOnFling = true,
+        onStart = { offsetAvailable -> onStart(offsetAvailable) },
+        onScroll = { offsetAvailable ->
+            onScroll(offsetAvailable)
+            offsetAvailable
+        },
+        onStop = { velocityAvailable ->
+            onStop(velocityAvailable)
+            velocityAvailable
+        },
+    )
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index 76a7a10..2eb7b3f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -474,6 +474,7 @@
                         .thenIf(shadeMode == ShadeMode.Single) {
                             Modifier.nestedScroll(scrimNestedScrollConnection)
                         }
+                        .stackVerticalOverscroll(coroutineScope) { scrollState.canScrollForward }
                         .verticalScroll(scrollState)
                         .padding(top = topPadding)
                         .fillMaxWidth()
@@ -671,3 +672,4 @@
 private val DEBUG_BOX_COLOR = Color(0f, 1f, 0f, 0.2f)
 private const val HUN_SNOOZE_POSITIONAL_THRESHOLD_FRACTION = 0.25f
 private const val HUN_SNOOZE_VELOCITY_THRESHOLD = -70f
+internal const val STACK_OVERSCROLL_FLING_MIN_OFFSET = -100f
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
index 114dcf4..afbc8e7 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
@@ -67,15 +67,15 @@
 /**
  * Animate a scene Int value.
  *
- * @see SceneScope.animateSceneValueAsState
+ * @see ContentScope.animateContentValueAsState
  */
 @Composable
-fun SceneScope.animateSceneIntAsState(
+fun ContentScope.animateContentIntAsState(
     value: Int,
     key: ValueKey,
     canOverflow: Boolean = true,
 ): AnimatedState<Int> {
-    return animateSceneValueAsState(value, key, SharedIntType, canOverflow)
+    return animateContentValueAsState(value, key, SharedIntType, canOverflow)
 }
 
 /**
@@ -107,17 +107,28 @@
 /**
  * Animate a scene Float value.
  *
- * @see SceneScope.animateSceneValueAsState
+ * @see ContentScope.animateContentValueAsState
  */
 @Composable
-fun SceneScope.animateSceneFloatAsState(
+fun ContentScope.animateContentFloatAsState(
     value: Float,
     key: ValueKey,
     canOverflow: Boolean = true,
 ): AnimatedState<Float> {
-    return animateSceneValueAsState(value, key, SharedFloatType, canOverflow)
+    return animateContentValueAsState(value, key, SharedFloatType, canOverflow)
 }
 
+@Deprecated(
+    "Use animateSceneFloatAsState() instead",
+    replaceWith = ReplaceWith("animateContentFloatAsState(value, key, canOverflow)")
+)
+@Composable
+fun ContentScope.animateSceneFloatAsState(
+    value: Float,
+    key: ValueKey,
+    canOverflow: Boolean = true,
+) = animateContentFloatAsState(value, key, canOverflow)
+
 /**
  * Animate a shared element Float value.
  *
@@ -147,17 +158,28 @@
 /**
  * Animate a scene Dp value.
  *
- * @see SceneScope.animateSceneValueAsState
+ * @see ContentScope.animateContentValueAsState
  */
 @Composable
-fun SceneScope.animateSceneDpAsState(
+fun ContentScope.animateContentDpAsState(
     value: Dp,
     key: ValueKey,
     canOverflow: Boolean = true,
 ): AnimatedState<Dp> {
-    return animateSceneValueAsState(value, key, SharedDpType, canOverflow)
+    return animateContentValueAsState(value, key, SharedDpType, canOverflow)
 }
 
+@Deprecated(
+    "Use animateSceneDpAsState() instead",
+    replaceWith = ReplaceWith("animateContentDpAsState(value, key, canOverflow)")
+)
+@Composable
+fun ContentScope.animateSceneDpAsState(
+    value: Dp,
+    key: ValueKey,
+    canOverflow: Boolean = true,
+) = animateContentDpAsState(value, key, canOverflow)
+
 /**
  * Animate a shared element Dp value.
  *
@@ -188,14 +210,14 @@
 /**
  * Animate a scene Color value.
  *
- * @see SceneScope.animateSceneValueAsState
+ * @see ContentScope.animateContentValueAsState
  */
 @Composable
-fun SceneScope.animateSceneColorAsState(
+fun ContentScope.animateContentColorAsState(
     value: Color,
     key: ValueKey,
 ): AnimatedState<Color> {
-    return animateSceneValueAsState(value, key, SharedColorType, canOverflow = false)
+    return animateContentValueAsState(value, key, SharedColorType, canOverflow = false)
 }
 
 /**
@@ -261,24 +283,24 @@
 @Composable
 internal fun <T> animateSharedValueAsState(
     layoutImpl: SceneTransitionLayoutImpl,
-    scene: SceneKey,
+    content: ContentKey,
     element: ElementKey?,
     key: ValueKey,
     value: T,
     type: SharedValueType<T, *>,
     canOverflow: Boolean,
 ): AnimatedState<T> {
-    DisposableEffect(layoutImpl, scene, element, key) {
-        // Create the associated maps that hold the current value for each (element, scene) pair.
+    DisposableEffect(layoutImpl, content, element, key) {
+        // Create the associated maps that hold the current value for each (element, content) pair.
         val valueMap = layoutImpl.sharedValues.getOrPut(key) { mutableMapOf() }
         val sharedValue = valueMap.getOrPut(element) { SharedValue(type) } as SharedValue<T, *>
         val targetValues = sharedValue.targetValues
-        targetValues[scene] = value
+        targetValues[content] = value
 
         onDispose {
             // Remove the value associated to the current scene, and eventually remove the maps if
             // they are empty.
-            targetValues.remove(scene)
+            targetValues.remove(content)
 
             if (targetValues.isEmpty() && valueMap[element] === sharedValue) {
                 valueMap.remove(element)
@@ -297,11 +319,11 @@
             error("value is equal to $value, which is the undefined value for this type.")
         }
 
-        sharedValue<T, Any>(layoutImpl, key, element).targetValues[scene] = value
+        sharedValue<T, Any>(layoutImpl, key, element).targetValues[content] = value
     }
 
-    return remember(layoutImpl, scene, element, canOverflow) {
-        AnimatedStateImpl<T, Any>(layoutImpl, scene, element, key, canOverflow)
+    return remember(layoutImpl, content, element, canOverflow) {
+        AnimatedStateImpl<T, Any>(layoutImpl, content, element, key, canOverflow)
     }
 }
 
@@ -322,8 +344,8 @@
 internal class SharedValue<T, Delta>(
     val type: SharedValueType<T, Delta>,
 ) {
-    /** The target value of this shared value for each scene. */
-    val targetValues = SnapshotStateMap<SceneKey, T>()
+    /** The target value of this shared value for each content. */
+    val targetValues = SnapshotStateMap<ContentKey, T>()
 
     /** The last value of this shared value. */
     var lastValue: T = type.unspecifiedValue
@@ -340,7 +362,7 @@
 
 private class AnimatedStateImpl<T, Delta>(
     private val layoutImpl: SceneTransitionLayoutImpl,
-    private val scene: SceneKey,
+    private val content: ContentKey,
     private val element: ElementKey?,
     private val key: ValueKey,
     private val canOverflow: Boolean,
@@ -356,14 +378,14 @@
                 // TODO(b/311600838): Remove this. We should not have to fallback to the current
                 // scene value, but we have to because code of removed nodes can still run if they
                 // are placed with a graphics layer.
-                ?: sharedValue[scene]
+                ?: sharedValue[content]
                 ?: error(valueReadTooEarlyMessage(key))
         val interruptedValue = computeInterruptedValue(sharedValue, transition, value)
         sharedValue.lastValue = interruptedValue
         return interruptedValue
     }
 
-    private operator fun SharedValue<T, *>.get(scene: SceneKey): T? = targetValues[scene]
+    private operator fun SharedValue<T, *>.get(content: ContentKey): T? = targetValues[content]
 
     private fun valueOrNull(
         sharedValue: SharedValue<T, *>,
@@ -401,7 +423,7 @@
         val targetValues = sharedValue.targetValues
         val transition =
             if (element != null) {
-                layoutImpl.elements[element]?.sceneStates?.let { sceneStates ->
+                layoutImpl.elements[element]?.stateByContent?.let { sceneStates ->
                     layoutImpl.state.currentTransitions.fastLastOrNull { transition ->
                         transition.fromScene in sceneStates || transition.toScene in sceneStates
                     }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index fb13b57..67d1b59 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -30,6 +30,7 @@
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.round
 import com.android.compose.animation.scene.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified
+import com.android.compose.animation.scene.content.Scene
 import com.android.compose.nestedscroll.PriorityNestedScrollConnection
 import kotlin.math.absoluteValue
 import kotlinx.coroutines.CoroutineScope
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 3ad07d0..0b5e58f 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
@@ -48,6 +48,7 @@
 import androidx.compose.ui.util.fastCoerceIn
 import androidx.compose.ui.util.fastLastOrNull
 import androidx.compose.ui.util.lerp
+import com.android.compose.animation.scene.content.Content
 import com.android.compose.animation.scene.transformation.PropertyTransformation
 import com.android.compose.animation.scene.transformation.SharedElementTransformation
 import com.android.compose.ui.util.lerp
@@ -57,30 +58,30 @@
 /** An element on screen, that can be composed in one or more scenes. */
 @Stable
 internal class Element(val key: ElementKey) {
-    /** The mapping between a scene and the state this element has in that scene, if any. */
+    /** The mapping between a content and the state this element has in that content, if any. */
     // TODO(b/316901148): Make this a normal map instead once we can make sure that new transitions
     // are first seen by composition then layout/drawing code. See b/316901148#comment2 for details.
-    val sceneStates = SnapshotStateMap<SceneKey, SceneState>()
+    val stateByContent = SnapshotStateMap<ContentKey, State>()
 
     /**
      * The last transition that was used when computing the state (size, position and alpha) of this
-     * element in any scene, or `null` if it was last laid out when idle.
+     * element in any content, or `null` if it was last laid out when idle.
      */
     var lastTransition: TransitionState.Transition? = null
 
-    /** Whether this element was ever drawn in a scene. */
-    var wasDrawnInAnyScene = false
+    /** Whether this element was ever drawn in a content. */
+    var wasDrawnInAnyContent = false
 
     override fun toString(): String {
         return "Element(key=$key)"
     }
 
-    /** The last and target state of this element in a given scene. */
+    /** The last and target state of this element in a given content. */
     @Stable
-    class SceneState(val scene: SceneKey) {
+    class State(val content: ContentKey) {
         /**
-         * The *target* state of this element in this scene, i.e. the state of this element when we
-         * are idle on this scene.
+         * The *target* state of this element in this content, i.e. the state of this element when
+         * we are idle on this content.
          */
         var targetSize by mutableStateOf(SizeUnspecified)
         var targetOffset by mutableStateOf(Offset.Unspecified)
@@ -91,7 +92,9 @@
         var lastScale = Scale.Unspecified
         var lastAlpha = AlphaUnspecified
 
-        /** The state of this element in this scene right before the last interruption (if any). */
+        /**
+         * The state of this element in this content right before the last interruption (if any).
+         */
         var offsetBeforeInterruption = Offset.Unspecified
         var sizeBeforeInterruption = SizeUnspecified
         var scaleBeforeInterruption = Scale.Unspecified
@@ -109,7 +112,7 @@
         var alphaInterruptionDelta = 0f
 
         /**
-         * The attached [ElementNode] a Modifier.element() for a given element and scene. During
+         * The attached [ElementNode] a Modifier.element() for a given element and content. 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.
          */
@@ -130,19 +133,19 @@
     }
 }
 
-/** The implementation of [SceneScope.element]. */
+/** The implementation of [ContentScope.element]. */
 @Stable
 internal fun Modifier.element(
     layoutImpl: SceneTransitionLayoutImpl,
-    scene: Scene,
+    content: Content,
     key: ElementKey,
 ): Modifier {
     // Make sure that we read the current transitions during composition and not during
     // layout/drawing.
     // TODO(b/341072461): Revert this and read the current transitions in ElementNode directly once
-    // we can ensure that SceneTransitionLayoutImpl will compose new scenes first.
+    // we can ensure that SceneTransitionLayoutImpl will compose new contents first.
     val currentTransitions = layoutImpl.state.currentTransitions
-    return then(ElementModifier(layoutImpl, currentTransitions, scene, key)).testTag(key.testTag)
+    return then(ElementModifier(layoutImpl, currentTransitions, content, key)).testTag(key.testTag)
 }
 
 /**
@@ -152,92 +155,92 @@
 private data class ElementModifier(
     private val layoutImpl: SceneTransitionLayoutImpl,
     private val currentTransitions: List<TransitionState.Transition>,
-    private val scene: Scene,
+    private val content: Content,
     private val key: ElementKey,
 ) : ModifierNodeElement<ElementNode>() {
-    override fun create(): ElementNode = ElementNode(layoutImpl, currentTransitions, scene, key)
+    override fun create(): ElementNode = ElementNode(layoutImpl, currentTransitions, content, key)
 
     override fun update(node: ElementNode) {
-        node.update(layoutImpl, currentTransitions, scene, key)
+        node.update(layoutImpl, currentTransitions, content, key)
     }
 }
 
 internal class ElementNode(
     private var layoutImpl: SceneTransitionLayoutImpl,
     private var currentTransitions: List<TransitionState.Transition>,
-    private var scene: Scene,
+    private var content: Content,
     private var key: ElementKey,
 ) : Modifier.Node(), DrawModifierNode, ApproachLayoutModifierNode, TraversableNode {
     private var _element: Element? = null
     private val element: Element
         get() = _element!!
 
-    private var _sceneState: Element.SceneState? = null
-    private val sceneState: Element.SceneState
-        get() = _sceneState!!
+    private var _stateInContent: Element.State? = null
+    private val stateInContent: Element.State
+        get() = _stateInContent!!
 
     override val traverseKey: Any = ElementTraverseKey
 
     override fun onAttach() {
         super.onAttach()
-        updateElementAndSceneValues()
-        addNodeToSceneState()
+        updateElementAndContentValues()
+        addNodeToContentState()
     }
 
-    private fun updateElementAndSceneValues() {
+    private fun updateElementAndContentValues() {
         val element =
             layoutImpl.elements[key] ?: Element(key).also { layoutImpl.elements[key] = it }
         _element = element
-        _sceneState =
-            element.sceneStates[scene.key]
-                ?: Element.SceneState(scene.key).also { element.sceneStates[scene.key] = it }
+        _stateInContent =
+            element.stateByContent[content.key]
+                ?: Element.State(content.key).also { element.stateByContent[content.key] = it }
     }
 
-    private fun addNodeToSceneState() {
-        sceneState.nodes.add(this)
+    private fun addNodeToContentState() {
+        stateInContent.nodes.add(this)
 
         coroutineScope.launch {
             // At this point all [CodeLocationNode] have been attached or detached, which means that
-            // [sceneState.codeLocations] should have exactly 1 element, otherwise this means that
-            // this element was composed multiple times in the same scene.
-            val nCodeLocations = sceneState.nodes.size
-            if (nCodeLocations != 1 || !sceneState.nodes.contains(this@ElementNode)) {
-                error("$key was composed $nCodeLocations times in ${sceneState.scene}")
+            // [elementState.codeLocations] should have exactly 1 element, otherwise this means that
+            // this element was composed multiple times in the same content.
+            val nCodeLocations = stateInContent.nodes.size
+            if (nCodeLocations != 1 || !stateInContent.nodes.contains(this@ElementNode)) {
+                error("$key was composed $nCodeLocations times in ${stateInContent.content}")
             }
         }
     }
 
     override fun onDetach() {
         super.onDetach()
-        removeNodeFromSceneState()
-        maybePruneMaps(layoutImpl, element, sceneState)
+        removeNodeFromContentState()
+        maybePruneMaps(layoutImpl, element, stateInContent)
 
         _element = null
-        _sceneState = null
+        _stateInContent = null
     }
 
-    private fun removeNodeFromSceneState() {
-        sceneState.nodes.remove(this)
+    private fun removeNodeFromContentState() {
+        stateInContent.nodes.remove(this)
     }
 
     fun update(
         layoutImpl: SceneTransitionLayoutImpl,
         currentTransitions: List<TransitionState.Transition>,
-        scene: Scene,
+        content: Content,
         key: ElementKey,
     ) {
-        check(layoutImpl == this.layoutImpl && scene == this.scene)
+        check(layoutImpl == this.layoutImpl && content == this.content)
         this.currentTransitions = currentTransitions
 
-        removeNodeFromSceneState()
+        removeNodeFromContentState()
 
         val prevElement = this.element
-        val prevSceneState = this.sceneState
+        val prevElementState = this.stateInContent
         this.key = key
-        updateElementAndSceneValues()
+        updateElementAndContentValues()
 
-        addNodeToSceneState()
-        maybePruneMaps(layoutImpl, prevElement, prevSceneState)
+        addNodeToContentState()
+        maybePruneMaps(layoutImpl, prevElement, prevElementState)
     }
 
     override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
@@ -262,15 +265,15 @@
         check(isLookingAhead)
 
         return measurable.measure(constraints).run {
-            // Update the size this element has in this scene when idle.
-            sceneState.targetSize = size()
+            // Update the size this element has in this content when idle.
+            stateInContent.targetSize = size()
 
             layout(width, height) {
                 // Update the offset (relative to the SceneTransitionLayout) this element has in
-                // this scene when idle.
+                // this content when idle.
                 coordinates?.let { coords ->
                     with(layoutImpl.lookaheadScope) {
-                        sceneState.targetOffset =
+                        stateInContent.targetOffset =
                             lookaheadScopeCoordinates.localLookaheadPositionOf(coords)
                     }
                 }
@@ -287,22 +290,22 @@
         val transition = elementTransition(layoutImpl, element, transitions)
 
         // If this element is not supposed to be laid out now, either because it is not part of any
-        // ongoing transition or the other scene of its transition is overscrolling, then lay out
+        // ongoing transition or the other content of its transition is overscrolling, then lay out
         // the element normally and don't place it.
         val overscrollScene = transition?.currentOverscrollSpec?.scene
-        val isOtherSceneOverscrolling = overscrollScene != null && overscrollScene != scene.key
+        val isOtherSceneOverscrolling = overscrollScene != null && overscrollScene != content.key
         val isNotPartOfAnyOngoingTransitions = transitions.isNotEmpty() && transition == null
         if (isNotPartOfAnyOngoingTransitions || isOtherSceneOverscrolling) {
             recursivelyClearPlacementValues()
-            sceneState.lastSize = Element.SizeUnspecified
+            stateInContent.lastSize = Element.SizeUnspecified
 
             val placeable = measurable.measure(constraints)
             return layout(placeable.width, placeable.height) { /* Do not place */ }
         }
 
         val placeable =
-            measure(layoutImpl, element, transition, sceneState, measurable, constraints)
-        sceneState.lastSize = placeable.size()
+            measure(layoutImpl, element, transition, stateInContent, measurable, constraints)
+        stateInContent.lastSize = placeable.size()
         return layout(placeable.width, placeable.height) { place(transition, placeable) }
     }
 
@@ -312,12 +315,12 @@
     ) {
         with(layoutImpl.lookaheadScope) {
             // Update the offset (relative to the SceneTransitionLayout) this element has in this
-            // scene when idle.
+            // content when idle.
             val coords =
                 coordinates ?: error("Element ${element.key} does not have any coordinates")
 
-            // No need to place the element in this scene if we don't want to draw it anyways.
-            if (!shouldPlaceElement(layoutImpl, scene.key, element, transition)) {
+            // No need to place the element in this content if we don't want to draw it anyways.
+            if (!shouldPlaceElement(layoutImpl, content.key, element, transition)) {
                 recursivelyClearPlacementValues()
                 return
             }
@@ -326,10 +329,10 @@
             val targetOffset =
                 computeValue(
                     layoutImpl,
-                    sceneState,
+                    stateInContent,
                     element,
                     transition,
-                    sceneValue = { it.targetOffset },
+                    contentValue = { it.targetOffset },
                     transformation = { it.offset },
                     currentValue = { currentOffset },
                     isSpecified = { it != Offset.Unspecified },
@@ -343,17 +346,17 @@
                     value = targetOffset,
                     unspecifiedValue = Offset.Unspecified,
                     zeroValue = Offset.Zero,
-                    getValueBeforeInterruption = { sceneState.offsetBeforeInterruption },
-                    setValueBeforeInterruption = { sceneState.offsetBeforeInterruption = it },
-                    getInterruptionDelta = { sceneState.offsetInterruptionDelta },
+                    getValueBeforeInterruption = { stateInContent.offsetBeforeInterruption },
+                    setValueBeforeInterruption = { stateInContent.offsetBeforeInterruption = it },
+                    getInterruptionDelta = { stateInContent.offsetInterruptionDelta },
                     setInterruptionDelta = { delta ->
                         setPlacementInterruptionDelta(
                             element = element,
-                            sceneState = sceneState,
+                            stateInContent = stateInContent,
                             transition = transition,
                             delta = delta,
-                            setter = { sceneState, delta ->
-                                sceneState.offsetInterruptionDelta = delta
+                            setter = { stateInContent, delta ->
+                                stateInContent.offsetInterruptionDelta = delta
                             },
                         )
                     },
@@ -361,14 +364,15 @@
                     add = { a, b, bProgress -> a + b * bProgress },
                 )
 
-            sceneState.lastOffset = interruptedOffset
+            stateInContent.lastOffset = interruptedOffset
 
             val offset = (interruptedOffset - currentOffset).round()
             if (
-                isElementOpaque(scene, element, transition) &&
-                    interruptedAlpha(layoutImpl, element, transition, sceneState, alpha = 1f) == 1f
+                isElementOpaque(content, element, transition) &&
+                    interruptedAlpha(layoutImpl, element, transition, stateInContent, alpha = 1f) ==
+                        1f
             ) {
-                sceneState.lastAlpha = 1f
+                stateInContent.lastAlpha = 1f
 
                 // TODO(b/291071158): Call placeWithLayer() if offset != IntOffset.Zero and size is
                 // not animated once b/305195729 is fixed. Test that drawing is not invalidated in
@@ -387,11 +391,11 @@
                     }
 
                     val transition = elementTransition(layoutImpl, element, currentTransitions)
-                    if (!shouldPlaceElement(layoutImpl, scene.key, element, transition)) {
+                    if (!shouldPlaceElement(layoutImpl, content.key, element, transition)) {
                         return@placeWithLayer
                     }
 
-                    alpha = elementAlpha(layoutImpl, element, transition, sceneState)
+                    alpha = elementAlpha(layoutImpl, element, transition, stateInContent)
                     compositingStrategy = CompositingStrategy.ModulateAlpha
                 }
             }
@@ -404,24 +408,24 @@
      * for the descendants for which approachMeasure() won't be called.
      */
     private fun recursivelyClearPlacementValues() {
-        fun Element.SceneState.clearLastPlacementValues() {
+        fun Element.State.clearLastPlacementValues() {
             lastOffset = Offset.Unspecified
             lastScale = Scale.Unspecified
             lastAlpha = Element.AlphaUnspecified
         }
 
-        sceneState.clearLastPlacementValues()
+        stateInContent.clearLastPlacementValues()
         traverseDescendants(ElementTraverseKey) { node ->
-            (node as ElementNode)._sceneState?.clearLastPlacementValues()
+            (node as ElementNode)._stateInContent?.clearLastPlacementValues()
             TraversableNode.Companion.TraverseDescendantsAction.ContinueTraversal
         }
     }
 
     override fun ContentDrawScope.draw() {
-        element.wasDrawnInAnyScene = true
+        element.wasDrawnInAnyContent = true
 
         val transition = elementTransition(layoutImpl, element, currentTransitions)
-        val drawScale = getDrawScale(layoutImpl, element, transition, sceneState)
+        val drawScale = getDrawScale(layoutImpl, element, transition, stateInContent)
         if (drawScale == Scale.Default) {
             drawContent()
         } else {
@@ -441,16 +445,21 @@
         private fun maybePruneMaps(
             layoutImpl: SceneTransitionLayoutImpl,
             element: Element,
-            sceneState: Element.SceneState,
+            stateInContent: Element.State,
         ) {
-            // If element is not composed from this scene anymore, remove the scene values. This
+            // If element is not composed in this content anymore, remove the content 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 (sceneState.nodes.isEmpty() && element.sceneStates[sceneState.scene] == sceneState) {
-                element.sceneStates.remove(sceneState.scene)
+            if (
+                stateInContent.nodes.isEmpty() &&
+                    element.stateByContent[stateInContent.content] == stateInContent
+            ) {
+                element.stateByContent.remove(stateInContent.content)
 
-                // If the element is not composed in any scene, remove it from the elements map.
-                if (element.sceneStates.isEmpty() && layoutImpl.elements[element.key] == element) {
+                // If the element is not composed in any content, remove it from the elements map.
+                if (
+                    element.stateByContent.isEmpty() && layoutImpl.elements[element.key] == element
+                ) {
                     layoutImpl.elements.remove(element.key)
                 }
             }
@@ -460,7 +469,7 @@
 
 /**
  * The transition that we should consider for [element]. This is the last transition where one of
- * its scenes contains the element.
+ * its contents contains the element.
  */
 private fun elementTransition(
     layoutImpl: SceneTransitionLayoutImpl,
@@ -469,7 +478,8 @@
 ): TransitionState.Transition? {
     val transition =
         transitions.fastLastOrNull { transition ->
-            transition.fromScene in element.sceneStates || transition.toScene in element.sceneStates
+            transition.fromScene in element.stateByContent ||
+                transition.toScene in element.stateByContent
         }
 
     val previousTransition = element.lastTransition
@@ -480,7 +490,7 @@
         prepareInterruption(layoutImpl, element, transition, previousTransition)
     } else if (transition == null && previousTransition != null) {
         // The transition was just finished.
-        element.sceneStates.values.forEach {
+        element.stateByContent.values.forEach {
             it.clearValuesBeforeInterruption()
             it.clearInterruptionDeltas()
         }
@@ -499,32 +509,32 @@
         return
     }
 
-    val sceneStates = element.sceneStates
-    fun updatedSceneState(key: SceneKey): Element.SceneState? {
-        return sceneStates[key]?.also { it.selfUpdateValuesBeforeInterruption() }
+    val stateByContent = element.stateByContent
+    fun updateStateInContent(key: ContentKey): Element.State? {
+        return stateByContent[key]?.also { it.selfUpdateValuesBeforeInterruption() }
     }
 
-    val previousFromState = updatedSceneState(previousTransition.fromScene)
-    val previousToState = updatedSceneState(previousTransition.toScene)
-    val fromState = updatedSceneState(transition.fromScene)
-    val toState = updatedSceneState(transition.toScene)
+    val previousFromState = updateStateInContent(previousTransition.fromScene)
+    val previousToState = updateStateInContent(previousTransition.toScene)
+    val fromState = updateStateInContent(transition.fromScene)
+    val toState = updateStateInContent(transition.toScene)
 
     reconcileStates(element, previousTransition)
     reconcileStates(element, transition)
 
-    // Remove the interruption values to all scenes but the scene(s) where the element will be
+    // Remove the interruption values to all contents but the content(s) where the element will be
     // placed, to make sure that interruption deltas are computed only right after this interruption
     // is prepared.
-    fun cleanInterruptionValues(sceneState: Element.SceneState) {
-        sceneState.sizeInterruptionDelta = IntSize.Zero
-        sceneState.offsetInterruptionDelta = Offset.Zero
-        sceneState.alphaInterruptionDelta = 0f
-        sceneState.scaleInterruptionDelta = Scale.Zero
+    fun cleanInterruptionValues(stateInContent: Element.State) {
+        stateInContent.sizeInterruptionDelta = IntSize.Zero
+        stateInContent.offsetInterruptionDelta = Offset.Zero
+        stateInContent.alphaInterruptionDelta = 0f
+        stateInContent.scaleInterruptionDelta = Scale.Zero
 
-        if (!shouldPlaceElement(layoutImpl, sceneState.scene, element, transition)) {
-            sceneState.offsetBeforeInterruption = Offset.Unspecified
-            sceneState.alphaBeforeInterruption = Element.AlphaUnspecified
-            sceneState.scaleBeforeInterruption = Scale.Unspecified
+        if (!shouldPlaceElement(layoutImpl, stateInContent.content, element, transition)) {
+            stateInContent.offsetBeforeInterruption = Offset.Unspecified
+            stateInContent.alphaBeforeInterruption = Element.AlphaUnspecified
+            stateInContent.scaleBeforeInterruption = Scale.Unspecified
         }
     }
 
@@ -542,8 +552,8 @@
     element: Element,
     transition: TransitionState.Transition,
 ) {
-    val fromSceneState = element.sceneStates[transition.fromScene] ?: return
-    val toSceneState = element.sceneStates[transition.toScene] ?: return
+    val fromSceneState = element.stateByContent[transition.fromScene] ?: return
+    val toSceneState = element.stateByContent[transition.toScene] ?: return
     if (!isSharedElementEnabled(element.key, transition)) {
         return
     }
@@ -563,7 +573,7 @@
     }
 }
 
-private fun Element.SceneState.selfUpdateValuesBeforeInterruption() {
+private fun Element.State.selfUpdateValuesBeforeInterruption() {
     sizeBeforeInterruption = lastSize
 
     if (lastAlpha > 0f) {
@@ -571,7 +581,7 @@
         scaleBeforeInterruption = lastScale
         alphaBeforeInterruption = lastAlpha
     } else {
-        // Consider the element as not placed in this scene if it was fully transparent.
+        // Consider the element as not placed in this content if it was fully transparent.
         // TODO(b/290930950): Look into using derived state inside place() instead to not even place
         // the element at all when alpha == 0f.
         offsetBeforeInterruption = Offset.Unspecified
@@ -580,7 +590,7 @@
     }
 }
 
-private fun Element.SceneState.updateValuesBeforeInterruption(lastState: Element.SceneState) {
+private fun Element.State.updateValuesBeforeInterruption(lastState: Element.State) {
     offsetBeforeInterruption = lastState.offsetBeforeInterruption
     sizeBeforeInterruption = lastState.sizeBeforeInterruption
     scaleBeforeInterruption = lastState.scaleBeforeInterruption
@@ -589,14 +599,14 @@
     clearInterruptionDeltas()
 }
 
-private fun Element.SceneState.clearInterruptionDeltas() {
+private fun Element.State.clearInterruptionDeltas() {
     offsetInterruptionDelta = Offset.Zero
     sizeInterruptionDelta = IntSize.Zero
     scaleInterruptionDelta = Scale.Zero
     alphaInterruptionDelta = 0f
 }
 
-private fun Element.SceneState.clearValuesBeforeInterruption() {
+private fun Element.State.clearValuesBeforeInterruption() {
     offsetBeforeInterruption = Offset.Unspecified
     scaleBeforeInterruption = Scale.Unspecified
     alphaBeforeInterruption = Element.AlphaUnspecified
@@ -655,13 +665,13 @@
  */
 private inline fun <T> setPlacementInterruptionDelta(
     element: Element,
-    sceneState: Element.SceneState,
+    stateInContent: Element.State,
     transition: TransitionState.Transition?,
     delta: T,
-    setter: (Element.SceneState, T) -> Unit,
+    setter: (Element.State, T) -> Unit,
 ) {
-    // Set the interruption delta on the current scene.
-    setter(sceneState, delta)
+    // Set the interruption delta on the current content.
+    setter(stateInContent, delta)
 
     if (transition == null) {
         return
@@ -670,8 +680,9 @@
     // If the element is shared, also set the delta on the other scene so that it is used by that
     // scene if we start overscrolling it and change the scene where the element is placed.
     val otherScene =
-        if (sceneState.scene == transition.fromScene) transition.toScene else transition.fromScene
-    val otherSceneState = element.sceneStates[otherScene] ?: return
+        if (stateInContent.content == transition.fromScene) transition.toScene
+        else transition.fromScene
+    val otherSceneState = element.stateByContent[otherScene] ?: return
     if (isSharedElementEnabled(element.key, transition)) {
         setter(otherSceneState, delta)
     }
@@ -679,7 +690,7 @@
 
 private fun shouldPlaceElement(
     layoutImpl: SceneTransitionLayoutImpl,
-    scene: SceneKey,
+    content: ContentKey,
     element: Element,
     transition: TransitionState.Transition?,
 ): Boolean {
@@ -688,15 +699,16 @@
         return true
     }
 
-    // Don't place the element in this scene if this scene is not part of the current element
+    // Don't place the element in this content if this content is not part of the current element
     // transition.
-    if (scene != transition.fromScene && scene != transition.toScene) {
+    if (content != transition.fromScene && content != transition.toScene) {
         return false
     }
 
     // Place the element if it is not shared.
     if (
-        transition.fromScene !in element.sceneStates || transition.toScene !in element.sceneStates
+        transition.fromScene !in element.stateByContent ||
+            transition.toScene !in element.stateByContent
     ) {
         return true
     }
@@ -708,7 +720,7 @@
 
     return shouldPlaceOrComposeSharedElement(
         layoutImpl,
-        scene,
+        content,
         element.key,
         transition,
     )
@@ -716,14 +728,14 @@
 
 internal fun shouldPlaceOrComposeSharedElement(
     layoutImpl: SceneTransitionLayoutImpl,
-    scene: SceneKey,
+    content: ContentKey,
     element: ElementKey,
     transition: TransitionState.Transition,
 ): Boolean {
     // If we are overscrolling, only place/compose the element in the overscrolling scene.
     val overscrollScene = transition.currentOverscrollSpec?.scene
     if (overscrollScene != null) {
-        return scene == overscrollScene
+        return content == overscrollScene
     }
 
     val scenePicker = element.scenePicker
@@ -738,7 +750,7 @@
             toSceneZIndex = layoutImpl.scenes.getValue(toScene).zIndex,
         ) ?: return false
 
-    return pickedScene == scene
+    return pickedScene == content
 }
 
 private fun isSharedElementEnabled(
@@ -775,7 +787,7 @@
  * placement and we don't want to read the transition progress in that phase.
  */
 private fun isElementOpaque(
-    scene: Scene,
+    content: Content,
     element: Element,
     transition: TransitionState.Transition?,
 ): Boolean {
@@ -785,8 +797,8 @@
 
     val fromScene = transition.fromScene
     val toScene = transition.toScene
-    val fromState = element.sceneStates[fromScene]
-    val toState = element.sceneStates[toScene]
+    val fromState = element.stateByContent[fromScene]
+    val toState = element.stateByContent[toScene]
 
     if (fromState == null && toState == null) {
         // TODO(b/311600838): Throw an exception instead once layers of disposed elements are not
@@ -799,7 +811,7 @@
         return true
     }
 
-    return transition.transformationSpec.transformations(element.key, scene.key).alpha == null
+    return transition.transformationSpec.transformations(element.key, content.key).alpha == null
 }
 
 /**
@@ -814,15 +826,15 @@
     layoutImpl: SceneTransitionLayoutImpl,
     element: Element,
     transition: TransitionState.Transition?,
-    sceneState: Element.SceneState,
+    stateInContent: Element.State,
 ): Float {
     val alpha =
         computeValue(
                 layoutImpl,
-                sceneState,
+                stateInContent,
                 element,
                 transition,
-                sceneValue = { 1f },
+                contentValue = { 1f },
                 transformation = { it.alpha },
                 currentValue = { 1f },
                 isSpecified = { true },
@@ -832,12 +844,12 @@
 
     // If the element is fading during this transition and that it is drawn for the first time, make
     // sure that it doesn't instantly appear on screen.
-    if (!element.wasDrawnInAnyScene && alpha > 0f) {
-        element.sceneStates.forEach { it.value.alphaBeforeInterruption = 0f }
+    if (!element.wasDrawnInAnyContent && alpha > 0f) {
+        element.stateByContent.forEach { it.value.alphaBeforeInterruption = 0f }
     }
 
-    val interruptedAlpha = interruptedAlpha(layoutImpl, element, transition, sceneState, alpha)
-    sceneState.lastAlpha = interruptedAlpha
+    val interruptedAlpha = interruptedAlpha(layoutImpl, element, transition, stateInContent, alpha)
+    stateInContent.lastAlpha = interruptedAlpha
     return interruptedAlpha
 }
 
@@ -845,7 +857,7 @@
     layoutImpl: SceneTransitionLayoutImpl,
     element: Element,
     transition: TransitionState.Transition?,
-    sceneState: Element.SceneState,
+    stateInContent: Element.State,
     alpha: Float,
 ): Float {
     return computeInterruptedValue(
@@ -854,16 +866,16 @@
         value = alpha,
         unspecifiedValue = Element.AlphaUnspecified,
         zeroValue = 0f,
-        getValueBeforeInterruption = { sceneState.alphaBeforeInterruption },
-        setValueBeforeInterruption = { sceneState.alphaBeforeInterruption = it },
-        getInterruptionDelta = { sceneState.alphaInterruptionDelta },
+        getValueBeforeInterruption = { stateInContent.alphaBeforeInterruption },
+        setValueBeforeInterruption = { stateInContent.alphaBeforeInterruption = it },
+        getInterruptionDelta = { stateInContent.alphaInterruptionDelta },
         setInterruptionDelta = { delta ->
             setPlacementInterruptionDelta(
                 element = element,
-                sceneState = sceneState,
+                stateInContent = stateInContent,
                 transition = transition,
                 delta = delta,
-                setter = { sceneState, delta -> sceneState.alphaInterruptionDelta = delta },
+                setter = { stateInContent, delta -> stateInContent.alphaInterruptionDelta = delta },
             )
         },
         diff = { a, b -> a - b },
@@ -875,7 +887,7 @@
     layoutImpl: SceneTransitionLayoutImpl,
     element: Element,
     transition: TransitionState.Transition?,
-    sceneState: Element.SceneState,
+    stateInContent: Element.State,
     measurable: Measurable,
     constraints: Constraints,
 ): Placeable {
@@ -887,10 +899,10 @@
     val targetSize =
         computeValue(
             layoutImpl,
-            sceneState,
+            stateInContent,
             element,
             transition,
-            sceneValue = { it.targetSize },
+            contentValue = { it.targetSize },
             transformation = { it.size },
             currentValue = { measurable.measure(constraints).also { maybePlaceable = it }.size() },
             isSpecified = { it != Element.SizeUnspecified },
@@ -900,8 +912,8 @@
     // The measurable was already measured, so we can't take interruptions into account here given
     // that we are not allowed to measure the same measurable twice.
     maybePlaceable?.let { placeable ->
-        sceneState.sizeBeforeInterruption = Element.SizeUnspecified
-        sceneState.sizeInterruptionDelta = IntSize.Zero
+        stateInContent.sizeBeforeInterruption = Element.SizeUnspecified
+        stateInContent.sizeInterruptionDelta = IntSize.Zero
         return placeable
     }
 
@@ -912,10 +924,10 @@
             value = targetSize,
             unspecifiedValue = Element.SizeUnspecified,
             zeroValue = IntSize.Zero,
-            getValueBeforeInterruption = { sceneState.sizeBeforeInterruption },
-            setValueBeforeInterruption = { sceneState.sizeBeforeInterruption = it },
-            getInterruptionDelta = { sceneState.sizeInterruptionDelta },
-            setInterruptionDelta = { sceneState.sizeInterruptionDelta = it },
+            getValueBeforeInterruption = { stateInContent.sizeBeforeInterruption },
+            setValueBeforeInterruption = { stateInContent.sizeBeforeInterruption = it },
+            getInterruptionDelta = { stateInContent.sizeInterruptionDelta },
+            setInterruptionDelta = { stateInContent.sizeInterruptionDelta = it },
             diff = { a, b -> IntSize(a.width - b.width, a.height - b.height) },
             add = { a, b, bProgress ->
                 IntSize(
@@ -939,15 +951,15 @@
     layoutImpl: SceneTransitionLayoutImpl,
     element: Element,
     transition: TransitionState.Transition?,
-    sceneState: Element.SceneState,
+    stateInContent: Element.State,
 ): Scale {
     val scale =
         computeValue(
             layoutImpl,
-            sceneState,
+            stateInContent,
             element,
             transition,
-            sceneValue = { Scale.Default },
+            contentValue = { Scale.Default },
             transformation = { it.drawScale },
             currentValue = { Scale.Default },
             isSpecified = { true },
@@ -965,16 +977,18 @@
             value = scale,
             unspecifiedValue = Scale.Unspecified,
             zeroValue = Scale.Zero,
-            getValueBeforeInterruption = { sceneState.scaleBeforeInterruption },
-            setValueBeforeInterruption = { sceneState.scaleBeforeInterruption = it },
-            getInterruptionDelta = { sceneState.scaleInterruptionDelta },
+            getValueBeforeInterruption = { stateInContent.scaleBeforeInterruption },
+            setValueBeforeInterruption = { stateInContent.scaleBeforeInterruption = it },
+            getInterruptionDelta = { stateInContent.scaleInterruptionDelta },
             setInterruptionDelta = { delta ->
                 setPlacementInterruptionDelta(
                     element = element,
-                    sceneState = sceneState,
+                    stateInContent = stateInContent,
                     transition = transition,
                     delta = delta,
-                    setter = { sceneState, delta -> sceneState.scaleInterruptionDelta = delta },
+                    setter = { stateInContent, delta ->
+                        stateInContent.scaleInterruptionDelta = delta
+                    },
                 )
             },
             diff = { a, b ->
@@ -1003,7 +1017,7 @@
             }
         )
 
-    sceneState.lastScale = interruptedScale
+    stateInContent.lastScale = interruptedScale
     return interruptedScale
 }
 
@@ -1015,11 +1029,11 @@
  * Measurable.
  *
  * @param layoutImpl the [SceneTransitionLayoutImpl] associated to [element].
- * @param currentSceneState the scene state of the scene for which we are computing the value. Note
- *   that during interruptions, this could be the state of a scene that is neither
+ * @param currentContentState the content state of the content for which we are computing the value.
+ *   Note that during interruptions, this could be the state of a content that is neither
  *   [transition.toScene] nor [transition.fromScene].
  * @param element the element being animated.
- * @param sceneValue the value being animated.
+ * @param contentValue the value being animated.
  * @param transformation the transformation associated to the value being animated.
  * @param currentValue the value that would be used if it is not transformed. Note that this is
  *   different than [idleValue] even if the value is not transformed directly because it could be
@@ -1030,10 +1044,10 @@
  */
 private inline fun <T> computeValue(
     layoutImpl: SceneTransitionLayoutImpl,
-    currentSceneState: Element.SceneState,
+    currentContentState: Element.State,
     element: Element,
     transition: TransitionState.Transition?,
-    sceneValue: (Element.SceneState) -> T,
+    contentValue: (Element.State) -> T,
     transformation: (ElementTransformations) -> PropertyTransformation<T>?,
     currentValue: () -> T,
     isSpecified: (T) -> Boolean,
@@ -1050,16 +1064,16 @@
     val fromScene = transition.fromScene
     val toScene = transition.toScene
 
-    val fromState = element.sceneStates[fromScene]
-    val toState = element.sceneStates[toScene]
+    val fromState = element.stateByContent[fromScene]
+    val toState = element.stateByContent[toScene]
 
     if (fromState == null && toState == null) {
         // TODO(b/311600838): Throw an exception instead once layers of disposed elements are not
         // run anymore.
-        return sceneValue(currentSceneState)
+        return contentValue(currentContentState)
     }
 
-    val currentScene = currentSceneState.scene
+    val currentScene = currentContentState.content
     if (transition is TransitionState.HasOverscrollProperties) {
         val overscroll = transition.currentOverscrollSpec
         if (overscroll?.scene == currentScene) {
@@ -1067,7 +1081,7 @@
                 overscroll.transformationSpec.transformations(element.key, currentScene)
             val propertySpec = transformation(elementSpec) ?: return currentValue()
             val overscrollState = checkNotNull(if (currentScene == toScene) toState else fromState)
-            val idleValue = sceneValue(overscrollState)
+            val idleValue = contentValue(overscrollState)
             val targetValue =
                 propertySpec.transform(
                     layoutImpl,
@@ -1102,8 +1116,8 @@
     // elements follow the finger direction.
     val isSharedElement = fromState != null && toState != null
     if (isSharedElement && isSharedElementEnabled(element.key, transition)) {
-        val start = sceneValue(fromState!!)
-        val end = sceneValue(toState!!)
+        val start = contentValue(fromState!!)
+        val end = contentValue(toState!!)
 
         // TODO(b/316901148): Remove checks to isSpecified() once the lookahead pass runs for all
         // nodes before the intermediate layout pass.
@@ -1117,7 +1131,7 @@
 
     // Get the transformed value, i.e. the target value at the beginning (for entering elements) or
     // end (for leaving elements) of the transition.
-    val sceneState =
+    val contentState =
         checkNotNull(
             when {
                 isSharedElement && currentScene == fromScene -> fromState
@@ -1129,26 +1143,26 @@
     // The scene for which we compute the transformation. Note that this is not necessarily
     // [currentScene] because [currentScene] could be a different scene than the transition
     // fromScene or toScene during interruptions.
-    val scene = sceneState.scene
+    val content = contentState.content
 
     val transformation =
-        transformation(transition.transformationSpec.transformations(element.key, scene))
+        transformation(transition.transformationSpec.transformations(element.key, content))
 
     val previewTransformation =
         transition.previewTransformationSpec?.let {
-            transformation(it.transformations(element.key, scene))
+            transformation(it.transformations(element.key, content))
         }
     if (previewTransformation != null) {
         val isInPreviewStage = transition.isInPreviewStage
 
-        val idleValue = sceneValue(sceneState)
-        val isEntering = scene == toScene
+        val idleValue = contentValue(contentState)
+        val isEntering = content == toScene
         val previewTargetValue =
             previewTransformation.transform(
                 layoutImpl,
-                scene,
+                content,
                 element,
-                sceneState,
+                contentState,
                 transition,
                 idleValue,
             )
@@ -1156,9 +1170,9 @@
         val targetValueOrNull =
             transformation?.transform(
                 layoutImpl,
-                scene,
+                content,
                 element,
-                sceneState,
+                contentState,
                 transition,
                 idleValue,
             )
@@ -1226,13 +1240,13 @@
         return currentValue()
     }
 
-    val idleValue = sceneValue(sceneState)
+    val idleValue = contentValue(contentState)
     val targetValue =
         transformation.transform(
             layoutImpl,
-            scene,
+            content,
             element,
-            sceneState,
+            contentState,
             transition,
             idleValue,
         )
@@ -1248,7 +1262,7 @@
     val rangeProgress = transformation.range?.progress(progress) ?: progress
 
     // Interpolate between the value at rest and the value before entering/after leaving.
-    val isEntering = scene == toScene
+    val isEntering = content == toScene
     return if (isEntering) {
         lerp(targetValue, idleValue, rangeProgress)
     } else {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt
index 98dbb67..ca68c25 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt
@@ -18,20 +18,23 @@
 
 /** An interface to match one or more elements. */
 interface ElementMatcher {
-    /** Whether the element with key [key] in scene [scene] matches this matcher. */
-    fun matches(key: ElementKey, scene: SceneKey): Boolean
+    /** Whether the element with key [key] in scene [content] matches this matcher. */
+    fun matches(key: ElementKey, content: ContentKey): Boolean
 }
 
 /**
- * Returns an [ElementMatcher] that matches elements in [scene] also matching [this]
+ * Returns an [ElementMatcher] that matches elements in [content] also matching [this]
  * [ElementMatcher].
  */
-fun ElementMatcher.inScene(scene: SceneKey): ElementMatcher {
+fun ElementMatcher.inContent(content: ContentKey): ElementMatcher {
     val delegate = this
-    val matcherScene = scene
+    val matcherScene = content
     return object : ElementMatcher {
-        override fun matches(key: ElementKey, scene: SceneKey): Boolean {
-            return scene == matcherScene && delegate.matches(key, scene)
+        override fun matches(key: ElementKey, content: ContentKey): Boolean {
+            return content == matcherScene && delegate.matches(key, content)
         }
     }
 }
+
+@Deprecated("Use inContent() instead", replaceWith = ReplaceWith("inContent(scene)"))
+fun ElementMatcher.inScene(scene: SceneKey) = inContent(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 9770399..a9edf0a 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
@@ -40,15 +40,20 @@
     }
 }
 
+/** The key for a content (scene or overlay). */
+sealed class ContentKey(debugName: String, identity: Any) : Key(debugName, identity) {
+    @VisibleForTesting
+    // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can
+    // access internal members.
+    abstract val testTag: String
+}
+
 /** Key for a scene. */
 class SceneKey(
     debugName: String,
     identity: Any = Object(),
-) : Key(debugName, identity) {
-    @VisibleForTesting
-    // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can
-    // access internal members.
-    val testTag: String = "scene:$debugName"
+) : ContentKey(debugName, identity) {
+    override val testTag: String = "scene:$debugName"
 
     /** The unique [ElementKey] identifying this scene's root element. */
     val rootElementKey = ElementKey(debugName, identity)
@@ -74,7 +79,7 @@
     // access internal members.
     val testTag: String = "element:$debugName"
 
-    override fun matches(key: ElementKey, scene: SceneKey): Boolean {
+    override fun matches(key: ElementKey, content: ContentKey): Boolean {
         return key == this
     }
 
@@ -86,7 +91,7 @@
         /** Matches any element whose [key identity][ElementKey.identity] matches [predicate]. */
         fun withIdentity(predicate: (Any) -> Boolean): ElementMatcher {
             return object : ElementMatcher {
-                override fun matches(key: ElementKey, scene: SceneKey): Boolean {
+                override fun matches(key: ElementKey, content: ContentKey): Boolean {
                     return predicate(key.identity)
                 }
             }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt
index 32eadde..e556f6f 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt
@@ -27,21 +27,22 @@
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.util.fastLastOrNull
+import com.android.compose.animation.scene.content.Content
 
 @Composable
 internal fun Element(
     layoutImpl: SceneTransitionLayoutImpl,
-    scene: Scene,
+    sceneOrOverlay: Content,
     key: ElementKey,
     modifier: Modifier,
     content: @Composable ElementScope<ElementContentScope>.() -> Unit,
 ) {
-    Box(modifier.element(layoutImpl, scene, key)) {
-        val sceneScope = scene.scope
+    Box(modifier.element(layoutImpl, sceneOrOverlay, key)) {
+        val contentScope = sceneOrOverlay.scope
         val boxScope = this
         val elementScope =
-            remember(layoutImpl, key, scene, sceneScope, boxScope) {
-                ElementScopeImpl(layoutImpl, key, scene, sceneScope, boxScope)
+            remember(layoutImpl, key, sceneOrOverlay, contentScope, boxScope) {
+                ElementScopeImpl(layoutImpl, key, sceneOrOverlay, contentScope, boxScope)
             }
 
         content(elementScope)
@@ -51,17 +52,17 @@
 @Composable
 internal fun MovableElement(
     layoutImpl: SceneTransitionLayoutImpl,
-    scene: Scene,
+    sceneOrOverlay: Content,
     key: ElementKey,
     modifier: Modifier,
     content: @Composable ElementScope<MovableElementContentScope>.() -> Unit,
 ) {
-    Box(modifier.element(layoutImpl, scene, key)) {
-        val sceneScope = scene.scope
+    Box(modifier.element(layoutImpl, sceneOrOverlay, key)) {
+        val contentScope = sceneOrOverlay.scope
         val boxScope = this
         val elementScope =
-            remember(layoutImpl, key, scene, sceneScope, boxScope) {
-                MovableElementScopeImpl(layoutImpl, key, scene, sceneScope, boxScope)
+            remember(layoutImpl, key, sceneOrOverlay, contentScope, boxScope) {
+                MovableElementScopeImpl(layoutImpl, key, sceneOrOverlay, contentScope, boxScope)
             }
 
         content(elementScope)
@@ -71,7 +72,7 @@
 private abstract class BaseElementScope<ContentScope>(
     private val layoutImpl: SceneTransitionLayoutImpl,
     private val element: ElementKey,
-    private val scene: Scene,
+    private val sceneOrOverlay: Content,
 ) : ElementScope<ContentScope> {
     @Composable
     override fun <T> animateElementValueAsState(
@@ -82,7 +83,7 @@
     ): AnimatedState<T> {
         return animateSharedValueAsState(
             layoutImpl,
-            scene.key,
+            sceneOrOverlay.key,
             element,
             key,
             value,
@@ -95,12 +96,12 @@
 private class ElementScopeImpl(
     layoutImpl: SceneTransitionLayoutImpl,
     element: ElementKey,
-    scene: Scene,
-    private val sceneScope: SceneScope,
+    content: Content,
+    private val delegateContentScope: ContentScope,
     private val boxScope: BoxScope,
-) : BaseElementScope<ElementContentScope>(layoutImpl, element, scene) {
+) : BaseElementScope<ElementContentScope>(layoutImpl, element, content) {
     private val contentScope =
-        object : ElementContentScope, SceneScope by sceneScope, BoxScope by boxScope {}
+        object : ElementContentScope, ContentScope by delegateContentScope, BoxScope by boxScope {}
 
     @Composable
     override fun content(content: @Composable ElementContentScope.() -> Unit) {
@@ -111,12 +112,15 @@
 private class MovableElementScopeImpl(
     private val layoutImpl: SceneTransitionLayoutImpl,
     private val element: ElementKey,
-    private val scene: Scene,
-    private val sceneScope: BaseSceneScope,
+    private val content: Content,
+    private val baseContentScope: BaseContentScope,
     private val boxScope: BoxScope,
-) : BaseElementScope<MovableElementContentScope>(layoutImpl, element, scene) {
+) : BaseElementScope<MovableElementContentScope>(layoutImpl, element, content) {
     private val contentScope =
-        object : MovableElementContentScope, BaseSceneScope by sceneScope, BoxScope by boxScope {}
+        object :
+            MovableElementContentScope,
+            BaseContentScope by baseContentScope,
+            BoxScope by boxScope {}
 
     @Composable
     override fun content(content: @Composable MovableElementContentScope.() -> Unit) {
@@ -126,9 +130,10 @@
         // during the transition.
         // TODO(b/317026105): Use derivedStateOf only if the scene picker reads the progress in its
         // logic.
+        val contentKey = this@MovableElementScopeImpl.content.key
         val shouldComposeMovableElement by
-            remember(layoutImpl, scene.key, element) {
-                derivedStateOf { shouldComposeMovableElement(layoutImpl, scene.key, element) }
+            remember(layoutImpl, contentKey, element) {
+                derivedStateOf { shouldComposeMovableElement(layoutImpl, contentKey, element) }
             }
 
         if (shouldComposeMovableElement) {
@@ -152,7 +157,7 @@
                 val size =
                     placeholderContentSize(
                         layoutImpl,
-                        scene.key,
+                        contentKey,
                         layoutImpl.elements.getValue(element),
                     )
                 layout(size.width, size.height) {}
@@ -163,7 +168,7 @@
 
 private fun shouldComposeMovableElement(
     layoutImpl: SceneTransitionLayoutImpl,
-    scene: SceneKey,
+    content: ContentKey,
     element: ElementKey,
 ): Boolean {
     val transitions = layoutImpl.state.currentTransitions
@@ -171,7 +176,7 @@
         // If we are idle, there is only one [scene] that is composed so we can compose our
         // movable content here. We still check that [scene] is equal to the current idle scene, to
         // make sure we only compose it there.
-        return layoutImpl.state.transitionState.currentScene == scene
+        return layoutImpl.state.transitionState.currentScene == content
     }
 
     // The current transition for this element is the last transition in which either fromScene or
@@ -189,7 +194,7 @@
     // Always compose movable elements in the scene picked by their scene picker.
     return shouldPlaceOrComposeSharedElement(
         layoutImpl,
-        scene,
+        content,
         element,
         transition,
     )
@@ -201,12 +206,12 @@
  */
 private fun placeholderContentSize(
     layoutImpl: SceneTransitionLayoutImpl,
-    scene: SceneKey,
+    content: ContentKey,
     element: Element,
 ): IntSize {
     // If the content of the movable element was already composed in this scene before, use that
     // target size.
-    val targetValueInScene = element.sceneStates.getValue(scene).targetSize
+    val targetValueInScene = element.stateByContent.getValue(content).targetSize
     if (targetValueInScene != Element.SizeUnspecified) {
         return targetValueInScene
     }
@@ -219,8 +224,9 @@
     // doesn't change between scenes.
     // TODO(b/317026105): Provide a way to give a hint size/content for cases where this is not
     // true.
-    val otherScene = if (transition.fromScene == scene) transition.toScene else transition.fromScene
-    val targetValueInOtherScene = element.sceneStates[otherScene]?.targetSize
+    val otherScene =
+        if (transition.fromScene == content) transition.toScene else transition.fromScene
+    val targetValueInOtherScene = element.stateByContent[otherScene]?.targetSize
     if (targetValueInOtherScene != null && targetValueInOtherScene != Element.SizeUnspecified) {
         return targetValueInOtherScene
     }
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 2fc4526..3401af8 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
@@ -34,7 +34,6 @@
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
-import com.android.compose.animation.scene.UserAction.Resolved
 
 /**
  * [SceneTransitionLayout] is a container that automatically animates its content whenever its state
@@ -85,7 +84,7 @@
     fun scene(
         key: SceneKey,
         userActions: Map<UserAction, UserActionResult> = emptyMap(),
-        content: @Composable SceneScope.() -> Unit,
+        content: @Composable ContentScope.() -> Unit,
     )
 }
 
@@ -118,25 +117,25 @@
 
 @Stable
 @ElementDsl
-interface BaseSceneScope : ElementStateScope {
-    /** The key of this scene. */
-    val sceneKey: SceneKey
+interface BaseContentScope : ElementStateScope {
+    /** The key of this content. */
+    val contentKey: ContentKey
 
-    /** The state of the [SceneTransitionLayout] in which this scene is contained. */
+    /** The state of the [SceneTransitionLayout] in which this content is contained. */
     val layoutState: SceneTransitionLayoutState
 
     /**
      * Tag an element identified by [key].
      *
      * Tagging an element will allow you to reference that element when defining transitions, so
-     * that the element can be transformed and animated when the scene transitions in or out.
+     * that the element can be transformed and animated when the content transitions in or out.
      *
-     * Additionally, this [key] will be used to detect elements that are shared between scenes to
+     * Additionally, this [key] will be used to detect elements that are shared between contents to
      * automatically interpolate their size and offset. If you need to animate shared element values
-     * (i.e. values associated to this element that change depending on which scene it is composed
+     * (i.e. values associated to this element that change depending on which content it is composed
      * in), use [Element] instead.
      *
-     * Note that shared elements tagged using this function will be duplicated in each scene they
+     * Note that shared elements tagged using this function will be duplicated in each content they
      * are part of, so any **internal** state (e.g. state created using `remember {
      * mutableStateOf(...) }`) will be lost. If you need to preserve internal state, you should use
      * [MovableElement] instead.
@@ -150,7 +149,7 @@
      * Create an element identified by [key].
      *
      * Similar to [element], this creates an element that will be automatically shared when present
-     * in multiple scenes and that can be transformed during transitions, the same way that
+     * in multiple contents and that can be transformed during transitions, the same way that
      * [element] does.
      *
      * The only difference with [element] is that the provided [ElementScope] allows you to
@@ -177,7 +176,7 @@
      * Create a *movable* element identified by [key].
      *
      * Similar to [Element], this creates an element that will be automatically shared when present
-     * in multiple scenes and that can be transformed during transitions, and you can also use the
+     * in multiple contents and that can be transformed during transitions, and you can also use the
      * provided [ElementScope] to [animate element values][ElementScope.animateElementValueAsState].
      *
      * The important difference with [element] and [Element] is that this element
@@ -232,24 +231,26 @@
     fun Modifier.noResizeDuringTransitions(): Modifier
 }
 
+typealias SceneScope = ContentScope
+
 @Stable
 @ElementDsl
-interface SceneScope : BaseSceneScope {
+interface ContentScope : BaseContentScope {
     /**
-     * Animate some value at the scene level.
+     * Animate some value at the content level.
      *
      * @param value the value of this shared value in the current scene.
      * @param key the key of this shared value.
      * @param type the [SharedValueType] of this animated value.
      * @param canOverflow whether this value can overflow past the values it is interpolated
      *   between, for instance because the transition is animated using a bouncy spring.
-     * @see animateSceneIntAsState
-     * @see animateSceneFloatAsState
-     * @see animateSceneDpAsState
-     * @see animateSceneColorAsState
+     * @see animateContentIntAsState
+     * @see animateContentFloatAsState
+     * @see animateContentDpAsState
+     * @see animateContentColorAsState
      */
     @Composable
-    fun <T> animateSceneValueAsState(
+    fun <T> animateContentValueAsState(
         value: T,
         key: ValueKey,
         type: SharedValueType<T, *>,
@@ -259,7 +260,7 @@
 
 /**
  * The type of a shared value animated using [ElementScope.animateElementValueAsState] or
- * [SceneScope.animateSceneValueAsState].
+ * [ContentScope.animateContentValueAsState].
  */
 @Stable
 interface SharedValueType<T, Delta> {
@@ -321,8 +322,9 @@
  * The exact same scope as [androidx.compose.foundation.layout.BoxScope].
  *
  * We can't reuse BoxScope directly because of the @LayoutScopeMarker annotation on it, which would
- * prevent us from calling Modifier.element() and other methods of [SceneScope] inside any Box {} in
- * the [content][ElementScope.content] of a [SceneScope.Element] or a [SceneScope.MovableElement].
+ * prevent us from calling Modifier.element() and other methods of [ContentScope] inside any Box {}
+ * in the [content][ElementScope.content] of a [ContentScope.Element] or a
+ * [ContentScope.MovableElement].
  */
 @Stable
 @ElementDsl
@@ -335,16 +337,16 @@
 }
 
 /** The scope for "normal" (not movable) elements. */
-@Stable @ElementDsl interface ElementContentScope : SceneScope, ElementBoxScope
+@Stable @ElementDsl interface ElementContentScope : ContentScope, ElementBoxScope
 
 /**
  * The scope for the content of movable elements.
  *
- * Note that it extends [BaseSceneScope] and not [SceneScope] because movable elements should not
- * call [SceneScope.animateSceneValueAsState], given that their content is not composed in all
- * scenes.
+ * Note that it extends [BaseContentScope] and not [ContentScope] because movable elements should
+ * not call [ContentScope.animateContentValueAsState], given that their content is not composed in
+ * all scenes.
  */
-@Stable @ElementDsl interface MovableElementContentScope : BaseSceneScope, ElementBoxScope
+@Stable @ElementDsl interface MovableElementContentScope : BaseContentScope, ElementBoxScope
 
 /** An action performed by the user. */
 sealed class UserAction {
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 32db0b7..062d553 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
@@ -36,6 +36,8 @@
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastForEachReversed
+import com.android.compose.animation.scene.content.Content
+import com.android.compose.animation.scene.content.Scene
 import com.android.compose.ui.util.lerp
 import kotlinx.coroutines.CoroutineScope
 
@@ -84,7 +86,7 @@
 
     /**
      * The different values of a shared value keyed by a a [ValueKey] and the different elements and
-     * scenes it is associated to.
+     * contents it is associated to.
      */
     private var _sharedValues: MutableMap<ValueKey, MutableMap<ElementKey?, SharedValue<*, *>>>? =
         null
@@ -149,6 +151,12 @@
         return scenes[key] ?: error("Scene $key is not configured")
     }
 
+    internal fun content(key: ContentKey): Content {
+        return when (key) {
+            is SceneKey -> scene(key)
+        }
+    }
+
     internal fun updateScenes(
         builder: SceneTransitionLayoutScope.() -> Unit,
         layoutDirection: LayoutDirection,
@@ -164,7 +172,7 @@
                 override fun scene(
                     key: SceneKey,
                     userActions: Map<UserAction, UserActionResult>,
-                    content: @Composable SceneScope.() -> Unit,
+                    content: @Composable ContentScope.() -> Unit,
                 ) {
                     scenesToRemove.remove(key)
 
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 06b093d..cfa4c70 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
@@ -302,18 +302,18 @@
     override val distance: UserActionDistance?,
     override val transformations: List<Transformation>,
 ) : TransformationSpec {
-    private val cache = mutableMapOf<ElementKey, MutableMap<SceneKey, ElementTransformations>>()
+    private val cache = mutableMapOf<ElementKey, MutableMap<ContentKey, ElementTransformations>>()
 
-    internal fun transformations(element: ElementKey, scene: SceneKey): ElementTransformations {
+    internal fun transformations(element: ElementKey, content: ContentKey): ElementTransformations {
         return cache
             .getOrPut(element) { mutableMapOf() }
-            .getOrPut(scene) { computeTransformations(element, scene) }
+            .getOrPut(content) { computeTransformations(element, content) }
     }
 
     /** Filter [transformations] to compute the [ElementTransformations] of [element]. */
     private fun computeTransformations(
         element: ElementKey,
-        scene: SceneKey,
+        content: ContentKey,
     ): ElementTransformations {
         var shared: SharedElementTransformation? = null
         var offset: PropertyTransformation<Offset>? = null
@@ -351,7 +351,7 @@
         }
 
         transformations.fastForEach { transformation ->
-            if (!transformation.matcher.matches(element, scene)) {
+            if (!transformation.matcher.matches(element, content)) {
                 return@fastForEach
             }
 
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index a2118b2..f062146 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -31,6 +31,7 @@
 import androidx.compose.ui.node.TraversableNode
 import androidx.compose.ui.node.findNearestAncestor
 import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.content.Scene
 
 /**
  * Configures the swipeable behavior of a [SceneTransitionLayout] depending on the current state.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
index 3a87d41..06be86d 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -239,10 +239,11 @@
      * should not be drawn or composed in neither [transition.fromScene] nor [transition.toScene],
      * return `null`.
      *
-     * Important: For [MovableElements][SceneScope.MovableElement], this scene picker will *always*
-     * be used during transitions to decide whether we should compose that element in a given scene
-     * or not. Therefore, you should make sure that the returned [SceneKey] contains the movable
-     * element, otherwise that element will not be composed in any scene during the transition.
+     * Important: For [MovableElements][ContentScope.MovableElement], this scene picker will
+     * *always* be used during transitions to decide whether we should compose that element in a
+     * given scene or not. Therefore, you should make sure that the returned [SceneKey] contains the
+     * movable element, otherwise that element will not be composed in any scene during the
+     * transition.
      */
     fun sceneDuringTransition(
         element: ElementKey,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
index b7abb33..0f66804 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
@@ -23,13 +23,13 @@
     private val layoutImpl: SceneTransitionLayoutImpl,
 ) : ElementStateScope {
     override fun ElementKey.targetSize(scene: SceneKey): IntSize? {
-        return layoutImpl.elements[this]?.sceneStates?.get(scene)?.targetSize.takeIf {
+        return layoutImpl.elements[this]?.stateByContent?.get(scene)?.targetSize.takeIf {
             it != Element.SizeUnspecified
         }
     }
 
     override fun ElementKey.targetOffset(scene: SceneKey): Offset? {
-        return layoutImpl.elements[this]?.sceneStates?.get(scene)?.targetOffset.takeIf {
+        return layoutImpl.elements[this]?.stateByContent?.get(scene)?.targetOffset.takeIf {
             it != Offset.Unspecified
         }
     }
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/content/Content.kt
similarity index 63%
rename from packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
rename to packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
index a49f1af..492d211 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/content/Content.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.compose.animation.scene
+package com.android.compose.animation.scene.content
 
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.layout.Box
@@ -29,24 +29,44 @@
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.zIndex
+import com.android.compose.animation.scene.AnimatedState
+import com.android.compose.animation.scene.ContentKey
+import com.android.compose.animation.scene.ContentScope
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementContentScope
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.ElementScope
+import com.android.compose.animation.scene.ElementStateScope
+import com.android.compose.animation.scene.MovableElement
+import com.android.compose.animation.scene.MovableElementContentScope
+import com.android.compose.animation.scene.NestedScrollBehavior
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.SceneTransitionLayoutState
+import com.android.compose.animation.scene.SharedValueType
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.compose.animation.scene.ValueKey
+import com.android.compose.animation.scene.animateSharedValueAsState
+import com.android.compose.animation.scene.element
 import com.android.compose.animation.scene.modifiers.noResizeDuringTransitions
+import com.android.compose.animation.scene.nestedScrollToScene
 
-/** A scene in a [SceneTransitionLayout]. */
+/** A content defined in a [SceneTransitionLayout], i.e. a scene or an overlay. */
 @Stable
-internal class Scene(
-    val key: SceneKey,
-    layoutImpl: SceneTransitionLayoutImpl,
-    content: @Composable SceneScope.() -> Unit,
+internal sealed class Content(
+    open val key: ContentKey,
+    val layoutImpl: SceneTransitionLayoutImpl,
+    content: @Composable ContentScope.() -> Unit,
     actions: Map<UserAction.Resolved, UserActionResult>,
     zIndex: Float,
 ) {
-    internal val scope = SceneScopeImpl(layoutImpl, this)
+    internal val scope = ContentScopeImpl(layoutImpl, content = this)
 
     var content by mutableStateOf(content)
-    private var _userActions by mutableStateOf(checkValid(actions))
     var zIndex by mutableFloatStateOf(zIndex)
     var targetSize by mutableStateOf(IntSize.Zero)
 
+    private var _userActions by mutableStateOf(checkValid(actions))
     var userActions
         get() = _userActions
         set(value) {
@@ -59,8 +79,8 @@
         userActions.forEach { (action, result) ->
             if (key == result.toScene) {
                 error(
-                    "Transition to the same scene is not supported. Scene $key, action $action," +
-                        " result $result"
+                    "Transition to the same content (scene/overlay) is not supported. Content " +
+                        "$key, action $action, result $result"
                 )
             }
         }
@@ -73,7 +93,7 @@
             modifier
                 .zIndex(zIndex)
                 .approachLayout(
-                    isMeasurementApproachInProgress = { scope.layoutState.isTransitioning() }
+                    isMeasurementApproachInProgress = { layoutImpl.state.isTransitioning() }
                 ) { measurable, constraints ->
                     targetSize = lookaheadSize
                     val placeable = measurable.measure(constraints)
@@ -84,21 +104,19 @@
             scope.content()
         }
     }
-
-    override fun toString(): String {
-        return "Scene(key=$key)"
-    }
 }
 
-internal class SceneScopeImpl(
+internal class ContentScopeImpl(
     private val layoutImpl: SceneTransitionLayoutImpl,
-    private val scene: Scene,
-) : SceneScope, ElementStateScope by layoutImpl.elementStateScope {
-    override val sceneKey: SceneKey = scene.key
+    private val content: Content,
+) : ContentScope, ElementStateScope by layoutImpl.elementStateScope {
+    override val contentKey: ContentKey
+        get() = content.key
+
     override val layoutState: SceneTransitionLayoutState = layoutImpl.state
 
     override fun Modifier.element(key: ElementKey): Modifier {
-        return element(layoutImpl, scene, key)
+        return element(layoutImpl, content, key)
     }
 
     @Composable
@@ -107,7 +125,7 @@
         modifier: Modifier,
         content: @Composable (ElementScope<ElementContentScope>.() -> Unit)
     ) {
-        Element(layoutImpl, scene, key, modifier, content)
+        Element(layoutImpl, this@ContentScopeImpl.content, key, modifier, content)
     }
 
     @Composable
@@ -116,19 +134,19 @@
         modifier: Modifier,
         content: @Composable (ElementScope<MovableElementContentScope>.() -> Unit)
     ) {
-        MovableElement(layoutImpl, scene, key, modifier, content)
+        MovableElement(layoutImpl, this@ContentScopeImpl.content, key, modifier, content)
     }
 
     @Composable
-    override fun <T> animateSceneValueAsState(
+    override fun <T> animateContentValueAsState(
         value: T,
         key: ValueKey,
         type: SharedValueType<T, *>,
-        canOverflow: Boolean
+        canOverflow: Boolean,
     ): AnimatedState<T> {
         return animateSharedValueAsState(
             layoutImpl = layoutImpl,
-            scene = scene.key,
+            content = content.key,
             element = null,
             key = key,
             value = value,
@@ -141,27 +159,29 @@
         leftBehavior: NestedScrollBehavior,
         rightBehavior: NestedScrollBehavior,
         isExternalOverscrollGesture: () -> Boolean,
-    ): Modifier =
-        nestedScrollToScene(
+    ): Modifier {
+        return nestedScrollToScene(
             layoutImpl = layoutImpl,
             orientation = Orientation.Horizontal,
             topOrLeftBehavior = leftBehavior,
             bottomOrRightBehavior = rightBehavior,
             isExternalOverscrollGesture = isExternalOverscrollGesture,
         )
+    }
 
     override fun Modifier.verticalNestedScrollToScene(
         topBehavior: NestedScrollBehavior,
         bottomBehavior: NestedScrollBehavior,
         isExternalOverscrollGesture: () -> Boolean,
-    ): Modifier =
-        nestedScrollToScene(
+    ): Modifier {
+        return nestedScrollToScene(
             layoutImpl = layoutImpl,
             orientation = Orientation.Vertical,
             topOrLeftBehavior = topBehavior,
             bottomOrRightBehavior = bottomBehavior,
             isExternalOverscrollGesture = isExternalOverscrollGesture,
         )
+    }
 
     override fun Modifier.noResizeDuringTransitions(): Modifier {
         return noResizeDuringTransitions(layoutState = layoutImpl.state)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Scene.kt
new file mode 100644
index 0000000..4a7a94d
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Scene.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.content
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import com.android.compose.animation.scene.ContentScope
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+
+/** A scene defined in a [SceneTransitionLayout]. */
+@Stable
+internal class Scene(
+    override val key: SceneKey,
+    layoutImpl: SceneTransitionLayoutImpl,
+    content: @Composable ContentScope.() -> Unit,
+    actions: Map<UserAction.Resolved, UserActionResult>,
+    zIndex: Float,
+) : Content(key, layoutImpl, content, actions, zIndex) {
+    override fun toString(): String {
+        return "Scene(key=$key)"
+    }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
index 73ee451..65d4d2d 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
@@ -17,6 +17,7 @@
 package com.android.compose.animation.scene.transformation
 
 import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.ElementMatcher
@@ -33,15 +34,15 @@
 ) : PropertyTransformation<IntSize> {
     override fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: SceneKey,
+        content: ContentKey,
         element: Element,
-        sceneState: Element.SceneState,
+        sceneState: Element.State,
         transition: TransitionState.Transition,
         value: IntSize,
     ): IntSize {
         fun anchorSizeIn(scene: SceneKey): IntSize {
             val size =
-                layoutImpl.elements[anchor]?.sceneStates?.get(scene)?.targetSize?.takeIf {
+                layoutImpl.elements[anchor]?.stateByContent?.get(scene)?.targetSize?.takeIf {
                     it != Element.SizeUnspecified
                 }
                     ?: throwMissingAnchorException(
@@ -59,7 +60,7 @@
         // This simple implementation assumes that the size of [element] is the same as the size of
         // the [anchor] in [scene], so simply transform to the size of the anchor in the other
         // scene.
-        return if (scene == transition.fromScene) {
+        return if (content == transition.fromScene) {
             anchorSizeIn(transition.toScene)
         } else {
             anchorSizeIn(transition.fromScene)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
index 70dca4c..8d7e1c9 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.isSpecified
+import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.ElementMatcher
@@ -32,9 +33,9 @@
 ) : PropertyTransformation<Offset> {
     override fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: SceneKey,
+        content: ContentKey,
         element: Element,
-        sceneState: Element.SceneState,
+        sceneState: Element.State,
         transition: TransitionState.Transition,
         value: Offset,
     ): Offset {
@@ -48,7 +49,7 @@
 
         val anchor = layoutImpl.elements[anchor] ?: throwException(scene = null)
         fun anchorOffsetIn(scene: SceneKey): Offset? {
-            return anchor.sceneStates[scene]?.targetOffset?.takeIf { it.isSpecified }
+            return anchor.stateByContent[scene]?.targetOffset?.takeIf { it.isSpecified }
         }
 
         // [element] will move the same amount as [anchor] does.
@@ -60,7 +61,7 @@
             anchorOffsetIn(transition.toScene) ?: throwException(transition.toScene)
         val offset = anchorToOffset - anchorFromOffset
 
-        return if (scene == transition.toScene) {
+        return if (content == transition.toScene) {
             Offset(
                 value.x - offset.x,
                 value.y - offset.y,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
index 98c2dd3..f010c3b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
@@ -17,10 +17,10 @@
 package com.android.compose.animation.scene.transformation
 
 import androidx.compose.ui.geometry.Offset
+import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementMatcher
 import com.android.compose.animation.scene.Scale
-import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.TransitionState
 
@@ -37,9 +37,9 @@
 
     override fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: SceneKey,
+        content: ContentKey,
         element: Element,
-        sceneState: Element.SceneState,
+        sceneState: Element.State,
         transition: TransitionState.Transition,
         value: Scale,
     ): Scale {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
index 7daefd0..dfce997 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
@@ -17,10 +17,10 @@
 package com.android.compose.animation.scene.transformation
 
 import androidx.compose.ui.geometry.Offset
+import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.TransitionState
 
@@ -32,13 +32,13 @@
 ) : PropertyTransformation<Offset> {
     override fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: SceneKey,
+        content: ContentKey,
         element: Element,
-        sceneState: Element.SceneState,
+        sceneState: Element.State,
         transition: TransitionState.Transition,
         value: Offset
     ): Offset {
-        val sceneSize = layoutImpl.scene(scene).targetSize
+        val sceneSize = layoutImpl.content(content).targetSize
         val elementSize = sceneState.targetSize
         if (elementSize == Element.SizeUnspecified) {
             return value
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
index ada814e..c1bb017 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
@@ -16,9 +16,9 @@
 
 package com.android.compose.animation.scene.transformation
 
+import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.TransitionState
 
@@ -28,9 +28,9 @@
 ) : PropertyTransformation<Float> {
     override fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: SceneKey,
+        content: ContentKey,
         element: Element,
-        sceneState: Element.SceneState,
+        sceneState: Element.State,
         transition: TransitionState.Transition,
         value: Float
     ): Float {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
index dca8f85..5adbf7e 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
@@ -17,9 +17,9 @@
 package com.android.compose.animation.scene.transformation
 
 import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.TransitionState
 import kotlin.math.roundToInt
@@ -35,9 +35,9 @@
 ) : PropertyTransformation<IntSize> {
     override fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: SceneKey,
+        content: ContentKey,
         element: Element,
-        sceneState: Element.SceneState,
+        sceneState: Element.State,
         transition: TransitionState.Transition,
         value: IntSize,
     ): IntSize {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
index 7be9ce1..24b7194 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
@@ -19,9 +19,9 @@
 import androidx.compose.ui.util.fastCoerceAtLeast
 import androidx.compose.ui.util.fastCoerceAtMost
 import androidx.compose.ui.util.fastCoerceIn
+import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.TransitionState
 
@@ -61,9 +61,9 @@
     // to these internal classes.
     fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: SceneKey,
+        content: ContentKey,
         element: Element,
-        sceneState: Element.SceneState,
+        sceneState: Element.State,
         transition: TransitionState.Transition,
         value: T,
     ): T
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
index f066511..123756a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
@@ -19,10 +19,10 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.Element
 import com.android.compose.animation.scene.ElementMatcher
 import com.android.compose.animation.scene.OverscrollScope
-import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayoutImpl
 import com.android.compose.animation.scene.TransitionState
 
@@ -33,9 +33,9 @@
 ) : PropertyTransformation<Offset> {
     override fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: SceneKey,
+        content: ContentKey,
         element: Element,
-        sceneState: Element.SceneState,
+        sceneState: Element.State,
         transition: TransitionState.Transition,
         value: Offset,
     ): Offset {
@@ -55,9 +55,9 @@
 ) : PropertyTransformation<Offset> {
     override fun transform(
         layoutImpl: SceneTransitionLayoutImpl,
-        scene: SceneKey,
+        content: ContentKey,
         element: Element,
-        sceneState: Element.SceneState,
+        sceneState: Element.State,
         transition: TransitionState.Transition,
         value: Offset,
     ): Offset {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt
index 8e35988..ae3169b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt
@@ -57,7 +57,7 @@
             minHeight() < currentHeight && currentHeight < maxHeight()
         },
         canScrollOnFling = true,
-        onStart = { /* do nothing */},
+        onStart = { /* do nothing */ },
         onScroll = { offsetAvailable ->
             val currentHeight = height()
             val amountConsumed =
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt
index ac11d30..228f7ba 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt
@@ -38,7 +38,7 @@
     private val canStartPreScroll: (offsetAvailable: Offset, offsetBeforeStart: Offset) -> Boolean,
     private val canStartPostScroll: (offsetAvailable: Offset, offsetBeforeStart: Offset) -> Boolean,
     private val canStartPostFling: (velocityAvailable: Velocity) -> Boolean,
-    private val canContinueScroll: () -> Boolean,
+    private val canContinueScroll: (source: NestedScrollSource) -> Boolean,
     private val canScrollOnFling: Boolean,
     private val onStart: (offsetAvailable: Offset) -> Unit,
     private val onScroll: (offsetAvailable: Offset) -> Offset,
@@ -61,7 +61,7 @@
 
         if (
             isPriorityMode ||
-                (source == NestedScrollSource.Fling && !canScrollOnFling) ||
+                (source == NestedScrollSource.SideEffect && !canScrollOnFling) ||
                 !canStartPostScroll(available, offsetBeforeStart)
         ) {
             // The priority mode cannot start so we won't consume the available offset.
@@ -73,7 +73,7 @@
 
     override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
         if (!isPriorityMode) {
-            if (source != NestedScrollSource.Fling || canScrollOnFling) {
+            if (source == NestedScrollSource.UserInput || canScrollOnFling) {
                 if (canStartPreScroll(available, offsetScrolledBeforePriorityMode)) {
                     return onPriorityStart(available)
                 }
@@ -84,7 +84,7 @@
             return Offset.Zero
         }
 
-        if (!canContinueScroll()) {
+        if (!canContinueScroll(source)) {
             // Step 3a: We have lost priority and we no longer need to intercept scroll events.
             onPriorityStop(velocity = Velocity.Zero)
 
@@ -170,7 +170,7 @@
     canStartPreScroll: (offsetAvailable: Float, offsetBeforeStart: Float) -> Boolean,
     canStartPostScroll: (offsetAvailable: Float, offsetBeforeStart: Float) -> Boolean,
     canStartPostFling: (velocityAvailable: Float) -> Boolean,
-    canContinueScroll: () -> Boolean,
+    canContinueScroll: (source: NestedScrollSource) -> Boolean,
     canScrollOnFling: Boolean,
     onStart: (offsetAvailable: Float) -> Unit,
     onScroll: (offsetAvailable: Float) -> Float,
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
index a7889e2..0f33303 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
@@ -67,7 +67,7 @@
     }
 
     @Composable
-    private fun SceneScope.Foo(
+    private fun ContentScope.Foo(
         targetValues: Values,
         onCurrentValueChanged: (Values) -> Unit,
     ) {
@@ -87,7 +87,7 @@
     }
 
     @Composable
-    private fun SceneScope.MovableFoo(
+    private fun ContentScope.MovableFoo(
         targetValues: Values,
         onCurrentValueChanged: (Values) -> Unit,
     ) {
@@ -105,14 +105,14 @@
     }
 
     @Composable
-    private fun SceneScope.SceneValues(
+    private fun ContentScope.SceneValues(
         targetValues: Values,
         onCurrentValueChanged: (Values) -> Unit,
     ) {
-        val int by animateSceneIntAsState(targetValues.int, key = TestValues.Value1)
-        val float by animateSceneFloatAsState(targetValues.float, key = TestValues.Value2)
-        val dp by animateSceneDpAsState(targetValues.dp, key = TestValues.Value3)
-        val color by animateSceneColorAsState(targetValues.color, key = TestValues.Value4)
+        val int by animateContentIntAsState(targetValues.int, key = TestValues.Value1)
+        val float by animateContentFloatAsState(targetValues.float, key = TestValues.Value2)
+        val dp by animateContentDpAsState(targetValues.dp, key = TestValues.Value3)
+        val color by animateContentColorAsState(targetValues.color, key = TestValues.Value4)
 
         LaunchedEffect(Unit) {
             snapshotFlow { Values(int, float, dp, color) }.collect(onCurrentValueChanged)
@@ -292,7 +292,7 @@
     fun readingAnimatedStateValueDuringCompositionThrows() {
         assertThrows(IllegalStateException::class.java) {
             rule.testTransition(
-                fromSceneContent = { animateSceneIntAsState(0, TestValues.Value1).value },
+                fromSceneContent = { animateContentIntAsState(0, TestValues.Value1).value },
                 toSceneContent = {},
                 transition = {},
             ) {}
@@ -302,21 +302,21 @@
     @Test
     fun readingAnimatedStateValueDuringCompositionIsStillPossible() {
         @Composable
-        fun SceneScope.SceneValuesDuringComposition(
+        fun ContentScope.SceneValuesDuringComposition(
             targetValues: Values,
             onCurrentValueChanged: (Values) -> Unit,
         ) {
             val int by
-                animateSceneIntAsState(targetValues.int, key = TestValues.Value1)
+                animateContentIntAsState(targetValues.int, key = TestValues.Value1)
                     .unsafeCompositionState(targetValues.int)
             val float by
-                animateSceneFloatAsState(targetValues.float, key = TestValues.Value2)
+                animateContentFloatAsState(targetValues.float, key = TestValues.Value2)
                     .unsafeCompositionState(targetValues.float)
             val dp by
-                animateSceneDpAsState(targetValues.dp, key = TestValues.Value3)
+                animateContentDpAsState(targetValues.dp, key = TestValues.Value3)
                     .unsafeCompositionState(targetValues.dp)
             val color by
-                animateSceneColorAsState(targetValues.color, key = TestValues.Value4)
+                animateContentColorAsState(targetValues.color, key = TestValues.Value4)
                     .unsafeCompositionState(targetValues.color)
 
             val values = Values(int, float, dp, color)
@@ -397,14 +397,14 @@
 
         val foo = ValueKey("foo")
         val bar = ValueKey("bar")
-        val lastValues = mutableMapOf<ValueKey, MutableMap<SceneKey, Float>>()
+        val lastValues = mutableMapOf<ValueKey, MutableMap<ContentKey, Float>>()
 
         @Composable
-        fun SceneScope.animateFloat(value: Float, key: ValueKey) {
-            val animatedValue = animateSceneFloatAsState(value, key)
+        fun ContentScope.animateFloat(value: Float, key: ValueKey) {
+            val animatedValue = animateContentFloatAsState(value, key)
             LaunchedEffect(animatedValue) {
                 snapshotFlow { animatedValue.value }
-                    .collect { lastValues.getOrPut(key) { mutableMapOf() }[sceneKey] = it }
+                    .collect { lastValues.getOrPut(key) { mutableMapOf() }[contentKey] = it }
             }
         }
 
@@ -458,13 +458,13 @@
             }
 
         val key = ValueKey("foo")
-        val lastValues = mutableMapOf<SceneKey, Float>()
+        val lastValues = mutableMapOf<ContentKey, Float>()
 
         @Composable
-        fun SceneScope.animateFloat(value: Float, key: ValueKey) {
-            val animatedValue = animateSceneFloatAsState(value, key)
+        fun ContentScope.animateFloat(value: Float, key: ValueKey) {
+            val animatedValue = animateContentFloatAsState(value, key)
             LaunchedEffect(animatedValue) {
-                snapshotFlow { animatedValue.value }.collect { lastValues[sceneKey] = it }
+                snapshotFlow { animatedValue.value }.collect { lastValues[contentKey] = it }
             }
         }
 
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 1d9e9b7..329257e 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
@@ -86,7 +86,7 @@
     @get:Rule val rule = createComposeRule()
 
     @Composable
-    private fun SceneScope.Element(
+    private fun ContentScope.Element(
         key: ElementKey,
         size: Dp,
         offset: Dp,
@@ -380,7 +380,7 @@
 
         assertThat(layoutImpl.elements.keys).containsExactly(key)
         val element = layoutImpl.elements.getValue(key)
-        assertThat(element.sceneStates.keys).containsExactly(SceneB)
+        assertThat(element.stateByContent.keys).containsExactly(SceneB)
 
         // Scene C, state 0: the same element is reused.
         rule.runOnUiThread { state.setTargetScene(SceneC, coroutineScope) }
@@ -389,13 +389,13 @@
 
         assertThat(layoutImpl.elements.keys).containsExactly(key)
         assertThat(layoutImpl.elements.getValue(key)).isSameInstanceAs(element)
-        assertThat(element.sceneStates.keys).containsExactly(SceneC)
+        assertThat(element.stateByContent.keys).containsExactly(SceneC)
 
         // Scene C, state 1: the element is removed from the map.
         sceneCState = 1
         rule.waitForIdle()
 
-        assertThat(element.sceneStates).isEmpty()
+        assertThat(element.stateByContent).isEmpty()
         assertThat(layoutImpl.elements).isEmpty()
     }
 
@@ -405,7 +405,7 @@
 
         assertThrows(IllegalStateException::class.java) {
             rule.setContent {
-                TestSceneScope {
+                TestContentScope {
                     Column {
                         Box(Modifier.element(key))
                         Box(Modifier.element(key))
@@ -421,7 +421,7 @@
 
         assertThrows(IllegalStateException::class.java) {
             rule.setContent {
-                TestSceneScope {
+                TestContentScope {
                     Column {
                         val childModifier = Modifier.element(key)
                         Box(childModifier)
@@ -439,7 +439,7 @@
         assertThrows(IllegalStateException::class.java) {
             var nElements by mutableStateOf(1)
             rule.setContent {
-                TestSceneScope {
+                TestContentScope {
                     Column {
                         val childModifier = Modifier.element(key)
                         repeat(nElements) { Box(childModifier) }
@@ -457,7 +457,7 @@
         assertThrows(IllegalStateException::class.java) {
             var key by mutableStateOf(TestElements.Foo)
             rule.setContent {
-                TestSceneScope {
+                TestContentScope {
                     Column {
                         Box(Modifier.element(key))
                         Box(Modifier.element(TestElements.Bar))
@@ -491,7 +491,7 @@
         // There is only Foo in the elements map.
         assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Foo)
         val fooElement = layoutImpl.elements.getValue(TestElements.Foo)
-        assertThat(fooElement.sceneStates.keys).containsExactly(SceneA)
+        assertThat(fooElement.stateByContent.keys).containsExactly(SceneA)
 
         key = TestElements.Bar
 
@@ -499,8 +499,8 @@
         rule.waitForIdle()
         assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Bar)
         val barElement = layoutImpl.elements.getValue(TestElements.Bar)
-        assertThat(barElement.sceneStates.keys).containsExactly(SceneA)
-        assertThat(fooElement.sceneStates).isEmpty()
+        assertThat(barElement.stateByContent.keys).containsExactly(SceneA)
+        assertThat(fooElement.stateByContent).isEmpty()
     }
 
     @Test
@@ -553,7 +553,7 @@
         // There is only Foo in the elements map.
         assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Foo)
         val element = layoutImpl.elements.getValue(TestElements.Foo)
-        val sceneValues = element.sceneStates
+        val sceneValues = element.stateByContent
         assertThat(sceneValues.keys).containsExactly(SceneA)
 
         // Get the ElementModifier node that should be reused later on when coming back to this
@@ -576,7 +576,7 @@
 
         assertThat(layoutImpl.elements.keys).containsExactly(TestElements.Foo)
         val newElement = layoutImpl.elements.getValue(TestElements.Foo)
-        val newSceneValues = newElement.sceneStates
+        val newSceneValues = newElement.stateByContent
         assertThat(newElement).isNotEqualTo(element)
         assertThat(newSceneValues).isNotEqualTo(sceneValues)
         assertThat(newSceneValues.keys).containsExactly(SceneA)
@@ -677,7 +677,7 @@
                 modifier = Modifier.size(layoutWidth, layoutHeight)
             ) {
                 scene(key = SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
-                    animateSceneFloatAsState(
+                    animateContentFloatAsState(
                         value = animatedFloatRange.start,
                         key = TestValues.Value1,
                         false
@@ -686,7 +686,7 @@
                 }
                 scene(SceneB) {
                     val animatedFloat by
-                        animateSceneFloatAsState(
+                        animateContentFloatAsState(
                             value = animatedFloatRange.endInclusive,
                             key = TestValues.Value1,
                             canOverflow = false
@@ -1215,15 +1215,15 @@
             }
 
         val layoutSize = DpSize(200.dp, 100.dp)
-        val lastValues = mutableMapOf<SceneKey, Float>()
+        val lastValues = mutableMapOf<ContentKey, Float>()
 
         @Composable
-        fun SceneScope.Foo(size: Dp, value: Float, modifier: Modifier = Modifier) {
-            val sceneKey = this.sceneKey
+        fun ContentScope.Foo(size: Dp, value: Float, modifier: Modifier = Modifier) {
+            val contentKey = this.contentKey
             Element(TestElements.Foo, modifier.size(size)) {
                 val animatedValue = animateElementFloatAsState(value, TestValues.Value1)
                 LaunchedEffect(animatedValue) {
-                    snapshotFlow { animatedValue.value }.collect { lastValues[sceneKey] = it }
+                    snapshotFlow { animatedValue.value }.collect { lastValues[contentKey] = it }
                 }
             }
         }
@@ -1388,8 +1388,8 @@
 
         // The interruption values should be unspecified and deltas should be set to zero.
         val foo = layoutImpl.elements.getValue(TestElements.Foo)
-        assertThat(foo.sceneStates.keys).containsExactly(SceneC)
-        val stateInC = foo.sceneStates.getValue(SceneC)
+        assertThat(foo.stateByContent.keys).containsExactly(SceneC)
+        val stateInC = foo.stateByContent.getValue(SceneC)
         assertThat(stateInC.offsetBeforeInterruption).isEqualTo(Offset.Unspecified)
         assertThat(stateInC.sizeBeforeInterruption).isEqualTo(Element.SizeUnspecified)
         assertThat(stateInC.scaleBeforeInterruption).isEqualTo(Scale.Unspecified)
@@ -1423,7 +1423,7 @@
             }
 
         @Composable
-        fun SceneScope.Foo(modifier: Modifier = Modifier) {
+        fun ContentScope.Foo(modifier: Modifier = Modifier) {
             Box(modifier.element(TestElements.Foo).size(fooSize))
         }
 
@@ -1542,8 +1542,8 @@
         assertThat(layoutImpl.elements).containsKey(TestElements.Foo)
         val foo = layoutImpl.elements.getValue(TestElements.Foo)
 
-        assertThat(foo.sceneStates).containsKey(SceneB)
-        val bState = foo.sceneStates.getValue(SceneB)
+        assertThat(foo.stateByContent).containsKey(SceneB)
+        val bState = foo.stateByContent.getValue(SceneB)
 
         assertThat(bState.targetSize).isNotEqualTo(Element.SizeUnspecified)
         assertThat(bState.targetOffset).isNotEqualTo(Offset.Unspecified)
@@ -1583,9 +1583,9 @@
         rule.waitForIdle()
 
         val foo = checkNotNull(layoutImpl.elements[TestElements.Foo])
-        assertThat(foo.sceneStates[SceneA]).isNull()
+        assertThat(foo.stateByContent[SceneA]).isNull()
 
-        val fooInB = foo.sceneStates[SceneB]
+        val fooInB = foo.stateByContent[SceneB]
         assertThat(fooInB).isNotNull()
         assertThat(fooInB!!.lastAlpha).isEqualTo(0.5f)
 
@@ -1599,7 +1599,7 @@
             state.startTransition(transition(from = SceneB, to = SceneC, progress = { 0.3f }))
         }
         rule.waitForIdle()
-        val fooInC = foo.sceneStates[SceneC]
+        val fooInC = foo.stateByContent[SceneC]
         assertThat(fooInC).isNotNull()
         assertThat(fooInC!!.lastAlpha).isEqualTo(1f)
         assertThat(fooInB.lastAlpha).isEqualTo(Element.AlphaUnspecified)
@@ -1645,7 +1645,7 @@
         rule.waitForIdle()
 
         // Alpha of Foo should be 0f at interruption progress 100%.
-        val fooInB = layoutImpl.elements.getValue(TestElements.Foo).sceneStates.getValue(SceneB)
+        val fooInB = layoutImpl.elements.getValue(TestElements.Foo).stateByContent.getValue(SceneB)
         assertThat(fooInB.lastAlpha).isEqualTo(0f)
 
         // Alpha of Foo should be 0.6f at interruption progress 0%.
@@ -1673,7 +1673,7 @@
             }
 
         @Composable
-        fun SceneScope.Foo() {
+        fun ContentScope.Foo() {
             Box(Modifier.element(TestElements.Foo).size(10.dp))
         }
 
@@ -1724,7 +1724,7 @@
         val fooInB = "fooInB"
 
         @Composable
-        fun SceneScope.MovableFoo(text: String, modifier: Modifier = Modifier) {
+        fun ContentScope.MovableFoo(text: String, modifier: Modifier = Modifier) {
             MovableElement(TestElements.Foo, modifier) { content { Text(text) } }
         }
 
@@ -1773,7 +1773,7 @@
             }
 
         @Composable
-        fun SceneScope.SceneWithFoo(offset: DpOffset, modifier: Modifier = Modifier) {
+        fun ContentScope.SceneWithFoo(offset: DpOffset, modifier: Modifier = Modifier) {
             Box(modifier.fillMaxSize()) {
                 Box(Modifier.offset(offset.x, offset.y).element(TestElements.Foo).size(100.dp))
             }
@@ -1856,7 +1856,7 @@
         val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) }
 
         @Composable
-        fun SceneScope.NestedFooBar() {
+        fun ContentScope.NestedFooBar() {
             Box(Modifier.element(TestElements.Foo)) {
                 Box(Modifier.element(TestElements.Bar).size(10.dp))
             }
@@ -1881,13 +1881,13 @@
         val foo = layoutImpl.elements.getValue(TestElements.Foo)
         val bar = layoutImpl.elements.getValue(TestElements.Bar)
 
-        assertThat(foo.sceneStates).containsKey(SceneA)
-        assertThat(bar.sceneStates).containsKey(SceneA)
-        assertThat(foo.sceneStates).doesNotContainKey(SceneB)
-        assertThat(bar.sceneStates).doesNotContainKey(SceneB)
+        assertThat(foo.stateByContent).containsKey(SceneA)
+        assertThat(bar.stateByContent).containsKey(SceneA)
+        assertThat(foo.stateByContent).doesNotContainKey(SceneB)
+        assertThat(bar.stateByContent).doesNotContainKey(SceneB)
 
-        val fooInA = foo.sceneStates.getValue(SceneA)
-        val barInA = bar.sceneStates.getValue(SceneA)
+        val fooInA = foo.stateByContent.getValue(SceneA)
+        val barInA = bar.stateByContent.getValue(SceneA)
         assertThat(fooInA.lastOffset).isNotEqualTo(Offset.Unspecified)
         assertThat(fooInA.lastAlpha).isNotEqualTo(Element.AlphaUnspecified)
         assertThat(fooInA.lastScale).isNotEqualTo(Scale.Unspecified)
@@ -1903,11 +1903,11 @@
         rule.onNode(isElement(TestElements.Foo, SceneB)).assertIsDisplayed()
         rule.onNode(isElement(TestElements.Bar, SceneB)).assertIsDisplayed()
 
-        assertThat(foo.sceneStates).containsKey(SceneB)
-        assertThat(bar.sceneStates).containsKey(SceneB)
+        assertThat(foo.stateByContent).containsKey(SceneB)
+        assertThat(bar.stateByContent).containsKey(SceneB)
 
-        val fooInB = foo.sceneStates.getValue(SceneB)
-        val barInB = bar.sceneStates.getValue(SceneB)
+        val fooInB = foo.stateByContent.getValue(SceneB)
+        val barInB = bar.stateByContent.getValue(SceneB)
         assertThat(fooInA.lastOffset).isEqualTo(Offset.Unspecified)
         assertThat(fooInA.lastAlpha).isEqualTo(Element.AlphaUnspecified)
         assertThat(fooInA.lastScale).isEqualTo(Scale.Unspecified)
@@ -1938,8 +1938,8 @@
             }
 
         @Composable
-        fun SceneScope.Foo() {
-            Box(Modifier.testTag("fooParentIn${sceneKey.debugName}")) {
+        fun ContentScope.Foo() {
+            Box(Modifier.testTag("fooParentIn${contentKey.debugName}")) {
                 Box(Modifier.element(TestElements.Foo).size(20.dp))
             }
         }
@@ -1973,7 +1973,7 @@
         val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) }
 
         @Composable
-        fun SceneScope.Foo(offset: Dp) {
+        fun ContentScope.Foo(offset: Dp) {
             Box(Modifier.fillMaxSize()) {
                 Box(Modifier.offset(offset, offset).element(TestElements.Foo).size(20.dp))
             }
@@ -2041,7 +2041,7 @@
             }
 
         @Composable
-        fun SceneScope.Foo() {
+        fun ContentScope.Foo() {
             Box(Modifier.element(TestElements.Foo).size(10.dp))
         }
 
@@ -2062,7 +2062,11 @@
         rule.waitForIdle()
 
         assertThat(
-                layoutImpl.elements.getValue(TestElements.Foo).sceneStates.getValue(SceneB).lastSize
+                layoutImpl.elements
+                    .getValue(TestElements.Foo)
+                    .stateByContent
+                    .getValue(SceneB)
+                    .lastSize
             )
             .isEqualTo(Element.SizeUnspecified)
     }
@@ -2078,8 +2082,8 @@
                             // In A => B, Foo is not shared and first fades out from A then fades in
                             // B.
                             sharedElement(TestElements.Foo, enabled = false)
-                            fractionRange(end = 0.5f) { fade(TestElements.Foo.inScene(SceneA)) }
-                            fractionRange(start = 0.5f) { fade(TestElements.Foo.inScene(SceneB)) }
+                            fractionRange(end = 0.5f) { fade(TestElements.Foo.inContent(SceneA)) }
+                            fractionRange(start = 0.5f) { fade(TestElements.Foo.inContent(SceneB)) }
                         }
 
                         from(SceneB, to = SceneA) {
@@ -2091,7 +2095,7 @@
             }
 
         @Composable
-        fun SceneScope.Foo(modifier: Modifier = Modifier) {
+        fun ContentScope.Foo(modifier: Modifier = Modifier) {
             Box(modifier.element(TestElements.Foo).size(10.dp))
         }
 
@@ -2149,7 +2153,7 @@
         val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) }
 
         @Composable
-        fun SceneScope.Foo(modifier: Modifier = Modifier) {
+        fun ContentScope.Foo(modifier: Modifier = Modifier) {
             Box(modifier.element(TestElements.Foo).size(10.dp))
         }
 
@@ -2216,7 +2220,7 @@
 
         // verify that preview transition for exiting elements is halfway played from
         // current-scene-value -> preview-target-value
-        val exiting1InB = layoutImpl.elements.getValue(exiting1).sceneStates.getValue(SceneB)
+        val exiting1InB = layoutImpl.elements.getValue(exiting1).stateByContent.getValue(SceneB)
         // e.g. exiting1 is half scaled...
         assertThat(exiting1InB.lastScale).isEqualTo(Scale(0.9f, 0.9f, Offset.Unspecified))
         // ...and exiting2 is halfway translated from 0.dp to 20.dp...
@@ -2228,7 +2232,7 @@
         // verify that preview transition for entering elements is halfway played from
         // preview-target-value -> transition-target-value (or target-scene-value if no
         // transition-target-value defined).
-        val entering1InA = layoutImpl.elements.getValue(entering1).sceneStates.getValue(SceneA)
+        val entering1InA = layoutImpl.elements.getValue(entering1).stateByContent.getValue(SceneA)
         // e.g. entering1 is half scaled between 0f and 0.5f -> 0.25f...
         assertThat(entering1InA.lastScale).isEqualTo(Scale(0.25f, 0.25f, Offset.Unspecified))
         // ...and entering2 is half way translated between 30.dp and 0.dp
@@ -2272,7 +2276,7 @@
 
         // verify that exiting elements remain in the preview-end state if no further transition is
         // defined for them in the second stage
-        val exiting1InB = layoutImpl.elements.getValue(exiting1).sceneStates.getValue(SceneB)
+        val exiting1InB = layoutImpl.elements.getValue(exiting1).stateByContent.getValue(SceneB)
         // i.e. exiting1 remains half scaled
         assertThat(exiting1InB.lastScale).isEqualTo(Scale(0.9f, 0.9f, Offset.Unspecified))
         // in case there is an additional transition defined for the second stage, verify that the
@@ -2286,7 +2290,7 @@
         rule.onNode(isElement(exiting3)).assertSizeIsEqualTo(90.dp, 90.dp)
 
         // verify that entering elements animate seamlessly to their target state
-        val entering1InA = layoutImpl.elements.getValue(entering1).sceneStates.getValue(SceneA)
+        val entering1InA = layoutImpl.elements.getValue(entering1).stateByContent.getValue(SceneA)
         // e.g. entering1, which was scaled from 0f to 0.25f during the preview phase, should now be
         // half way scaled between 0.25f and its target-state of 1f -> 0.625f
         assertThat(entering1InA.lastScale).isEqualTo(Scale(0.625f, 0.625f, Offset.Unspecified))
@@ -2318,7 +2322,7 @@
             }
 
         @Composable
-        fun SceneScope.Foo(elementKey: ElementKey) {
+        fun ContentScope.Foo(elementKey: ElementKey) {
             Box(Modifier.element(elementKey).size(100.dp))
         }
 
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt
index 9523896..821cc29 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt
@@ -62,7 +62,7 @@
     }
 
     @Composable
-    private fun SceneScope.MovableCounter(key: ElementKey, modifier: Modifier) {
+    private fun ContentScope.MovableCounter(key: ElementKey, modifier: Modifier) {
         MovableElement(key, modifier) { content { Counter() } }
     }
 
@@ -264,7 +264,7 @@
     @Test
     fun movableElementContentIsRecomposedIfContentParametersChange() {
         @Composable
-        fun SceneScope.MovableFoo(text: String, modifier: Modifier = Modifier) {
+        fun ContentScope.MovableFoo(text: String, modifier: Modifier = Modifier) {
             MovableElement(TestElements.Foo, modifier) { content { Text(text) } }
         }
 
@@ -298,7 +298,7 @@
     @Test
     fun elementScopeExtendsBoxScope() {
         rule.setContent {
-            TestSceneScope {
+            TestContentScope {
                 Element(TestElements.Foo, Modifier.size(200.dp)) {
                     content {
                         Box(Modifier.testTag("bottomEnd").align(Alignment.BottomEnd))
@@ -315,7 +315,7 @@
     @Test
     fun movableElementScopeExtendsBoxScope() {
         rule.setContent {
-            TestSceneScope {
+            TestContentScope {
                 MovableElement(TestElements.Foo, Modifier.size(200.dp)) {
                     content {
                         Box(Modifier.testTag("bottomEnd").align(Alignment.BottomEnd))
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt
index 311a580..9ebc426 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt
@@ -48,7 +48,7 @@
     private val layoutHeight = 400.dp
 
     private fun setup2ScenesAndScrollTouchSlop(
-        modifierSceneA: @Composable SceneScope.() -> Modifier = { Modifier },
+        modifierSceneA: @Composable ContentScope.() -> Modifier = { Modifier },
     ): MutableSceneTransitionLayoutState {
         val state =
             rule.runOnUiThread {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
index 1ec1079..32f3bac 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -129,7 +129,7 @@
     }
 
     @Composable
-    private fun SceneScope.SharedFoo(size: Dp, childOffset: Dp, modifier: Modifier = Modifier) {
+    private fun ContentScope.SharedFoo(size: Dp, childOffset: Dp, modifier: Modifier = Modifier) {
         Element(TestElements.Foo, modifier.size(size).background(Color.Red)) {
             // Offset the single child of Foo by some animated shared offset.
             val offset by animateElementDpAsState(childOffset, TestValues.Value1)
@@ -479,14 +479,14 @@
     fun sceneKeyInScope() {
         val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
 
-        var keyInA: SceneKey? = null
-        var keyInB: SceneKey? = null
-        var keyInC: SceneKey? = null
+        var keyInA: ContentKey? = null
+        var keyInB: ContentKey? = null
+        var keyInC: ContentKey? = null
         rule.setContent {
             SceneTransitionLayout(state) {
-                scene(SceneA) { keyInA = sceneKey }
-                scene(SceneB) { keyInB = sceneKey }
-                scene(SceneC) { keyInC = sceneKey }
+                scene(SceneA) { keyInA = contentKey }
+                scene(SceneB) { keyInB = contentKey }
+                scene(SceneC) { keyInC = contentKey }
             }
         }
 
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt
index 6233608..c9f71da 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt
@@ -25,7 +25,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.ContentScope
 import com.android.compose.animation.scene.TestElements
 import com.android.compose.animation.scene.TransitionBuilder
 import com.android.compose.animation.scene.TransitionRecordingSpec
@@ -108,8 +108,8 @@
     }
 
     private fun assertBarSizeMatchesGolden(
-        fromSceneContent: @Composable SceneScope.() -> Unit,
-        toSceneContent: @Composable SceneScope.() -> Unit,
+        fromSceneContent: @Composable ContentScope.() -> Unit,
+        toSceneContent: @Composable ContentScope.() -> Unit,
         transition: TransitionBuilder.() -> Unit,
     ) {
         val recordingSpec =
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt
index 8001f41..00acb13 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt
@@ -31,7 +31,7 @@
 import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.TestElements
 import com.android.compose.animation.scene.TestScenes
-import com.android.compose.animation.scene.inScene
+import com.android.compose.animation.scene.inContent
 import com.android.compose.animation.scene.testTransition
 import com.android.compose.test.assertSizeIsEqualTo
 import org.junit.Rule
@@ -125,10 +125,10 @@
                 sharedElement(TestElements.Foo, enabled = false)
 
                 // In SceneA, Foo leaves to the left edge.
-                translate(TestElements.Foo.inScene(TestScenes.SceneA), Edge.Left)
+                translate(TestElements.Foo.inContent(TestScenes.SceneA), Edge.Left)
 
                 // In SceneB, Foo comes from the bottom edge.
-                translate(TestElements.Foo.inScene(TestScenes.SceneB), Edge.Bottom)
+                translate(TestElements.Foo.inContent(TestScenes.SceneB), Edge.Bottom)
             },
         ) {
             before { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp) }
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
similarity index 85%
rename from packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt
rename to packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
index fbd557f..00adefb 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
@@ -20,11 +20,13 @@
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 
-/** `SceneScope` for tests, which allows a single scene to be drawn in a [SceneTransitionLayout]. */
+/**
+ * [ContentScope] for tests, which allows a single scene to be drawn in a [SceneTransitionLayout].
+ */
 @Composable
-fun TestSceneScope(
+fun TestContentScope(
     modifier: Modifier = Modifier,
-    content: @Composable SceneScope.() -> Unit,
+    content: @Composable ContentScope.() -> Unit,
 ) {
     val currentScene = remember { SceneKey("current") }
     val state = remember { MutableSceneTransitionLayoutState(currentScene) }
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
index a37d78e..7f26b98 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
@@ -18,9 +18,7 @@
 
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
 import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.semantics.SemanticsNode
 import androidx.compose.ui.test.SemanticsNodeInteraction
@@ -87,8 +85,8 @@
  * @sample com.android.compose.animation.scene.transformation.TranslateTest
  */
 fun ComposeContentTestRule.testTransition(
-    fromSceneContent: @Composable SceneScope.() -> Unit,
-    toSceneContent: @Composable SceneScope.() -> Unit,
+    fromSceneContent: @Composable ContentScope.() -> Unit,
+    toSceneContent: @Composable ContentScope.() -> Unit,
     transition: TransitionBuilder.() -> Unit,
     layoutModifier: Modifier = Modifier,
     fromScene: SceneKey = TestScenes.SceneA,
@@ -134,8 +132,8 @@
 
 /** Records the transition between two scenes of [transitionLayout][SceneTransitionLayout]. */
 fun MotionTestRule<ComposeToolkit>.recordTransition(
-    fromSceneContent: @Composable SceneScope.() -> Unit,
-    toSceneContent: @Composable SceneScope.() -> Unit,
+    fromSceneContent: @Composable ContentScope.() -> Unit,
+    toSceneContent: @Composable ContentScope.() -> Unit,
     transition: TransitionBuilder.() -> Unit,
     recordingSpec: TransitionRecordingSpec,
     layoutModifier: Modifier = Modifier,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
index 6ad4b31..3146318 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
@@ -675,6 +675,24 @@
             assertThat(tiles!!.size).isEqualTo(3)
         }
 
+    @Test
+    fun changeInPackagesTiles_doesntTriggerUserChange_logged() =
+        testScope.runTest(USER_INFO_0) {
+            val specs =
+                listOf(
+                    TileSpec.create("a"),
+                )
+            tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+            runCurrent()
+            // Settled on the same list of tiles.
+            assertThat(underTest.currentTilesSpecs).isEqualTo(specs)
+
+            installedTilesPackageRepository.setInstalledPackagesForUser(USER_INFO_0.id, emptySet())
+            runCurrent()
+
+            verify(logger, never()).logTileUserChanged(TileSpec.create("a"), 0)
+        }
+
     private fun QSTile.State.fillIn(state: Int, label: CharSequence, secondaryLabel: CharSequence) {
         this.state = state
         this.label = label
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index d7c3527..ba37d58 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3681,7 +3681,10 @@
     <!-- Touchpad back gesture action name in tutorial [CHAR LIMIT=NONE] -->
     <string name="touchpad_back_gesture_action_title">Go back</string>
     <!-- Touchpad back gesture guidance in gestures tutorial [CHAR LIMIT=NONE] -->
-    <string name="touchpad_back_gesture_guidance">To go back, swipe left or right using three fingers anywhere on the touchpad.</string>
+    <string name="touchpad_back_gesture_guidance">To go back, swipe left or right using three fingers anywhere on the touchpad.\n\nYou can also use the keyboard shortcut
+Action + ESC for this.</string>
+    <!-- Text shown to the user after they complete back gesture tutorial [CHAR LIMIT=NONE] -->
+    <string name="touchpad_back_gesture_finished">You completed the go back gesture.</string>
     <string name="touchpad_back_gesture_animation_content_description">Touchpad showing three fingers moving right and left</string>
     <string name="touchpad_back_gesture_screen_animation_content_description">Device screen showing animation for back gesture</string>
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 88601da..4286646 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.dreams.DreamMonitor
 import com.android.systemui.dreams.homecontrols.HomeControlsDreamStartable
 import com.android.systemui.globalactions.GlobalActionsComponent
+import com.android.systemui.inputdevice.oobe.KeyboardTouchpadOobeTutorialCoreStartable
 import com.android.systemui.keyboard.KeyboardUI
 import com.android.systemui.keyboard.PhysicalKeyboardCoreStartable
 import com.android.systemui.keyguard.KeyguardViewConfigurator
@@ -257,6 +258,13 @@
 
     @Binds
     @IntoMap
+    @ClassKey(KeyboardTouchpadOobeTutorialCoreStartable::class)
+    abstract fun bindOobeSchedulerCoreStartable(
+        listener: KeyboardTouchpadOobeTutorialCoreStartable
+    ): CoreStartable
+
+    @Binds
+    @IntoMap
     @ClassKey(PhysicalKeyboardCoreStartable::class)
     abstract fun bindKeyboardCoreStartable(listener: PhysicalKeyboardCoreStartable): CoreStartable
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 15ddf5b..a448072 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -144,6 +144,7 @@
 import com.android.systemui.statusbar.window.StatusBarWindowModule;
 import com.android.systemui.telephony.data.repository.TelephonyRepositoryModule;
 import com.android.systemui.temporarydisplay.dagger.TemporaryDisplayModule;
+import com.android.systemui.touchpad.TouchpadModule;
 import com.android.systemui.tuner.dagger.TunerModule;
 import com.android.systemui.user.UserModule;
 import com.android.systemui.user.domain.UserDomainLayerModule;
@@ -259,6 +260,7 @@
         CommonSystemUIUnfoldModule.class,
         TelephonyRepositoryModule.class,
         TemporaryDisplayModule.class,
+        TouchpadModule.class,
         TunerModule.class,
         UserDomainLayerModule.class,
         UserModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt
index 3b161b6..5a008bd 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt
@@ -45,7 +45,7 @@
 
     data class DeviceAdded(val deviceId: Int) : DeviceChange
 
-    data object DeviceRemoved : DeviceChange
+    data class DeviceRemoved(val deviceId: Int) : DeviceChange
 
     data object FreshStart : DeviceChange
 
@@ -72,7 +72,7 @@
 
                         override fun onInputDeviceRemoved(deviceId: Int) {
                             connectedDevices = connectedDevices - deviceId
-                            sendWithLogging(connectedDevices to DeviceRemoved)
+                            sendWithLogging(connectedDevices to DeviceRemoved(deviceId))
                         }
                     }
                 sendWithLogging(connectedDevices to FreshStart)
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/KeyboardTouchpadOobeTutorialCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/KeyboardTouchpadOobeTutorialCoreStartable.kt
new file mode 100644
index 0000000..dbfea76
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/KeyboardTouchpadOobeTutorialCoreStartable.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.inputdevice.oobe
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.inputdevice.oobe.domain.interactor.OobeTutorialSchedulerInteractor
+import com.android.systemui.shared.Flags.newTouchpadGesturesTutorial
+import dagger.Lazy
+import javax.inject.Inject
+
+/** A [CoreStartable] to launch a scheduler for keyboard and touchpad OOBE education */
+@SysUISingleton
+class KeyboardTouchpadOobeTutorialCoreStartable
+@Inject
+constructor(private val oobeTutorialSchedulerInteractor: Lazy<OobeTutorialSchedulerInteractor>) :
+    CoreStartable {
+    override fun start() {
+        if (newTouchpadGesturesTutorial()) {
+            oobeTutorialSchedulerInteractor.get().start()
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/domain/interactor/OobeTutorialSchedulerInteractor.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/domain/interactor/OobeTutorialSchedulerInteractor.kt
new file mode 100644
index 0000000..0d69081
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/domain/interactor/OobeTutorialSchedulerInteractor.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.inputdevice.oobe.domain.interactor
+
+import android.content.Context
+import android.content.Intent
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyboard.data.repository.KeyboardRepository
+import com.android.systemui.touchpad.data.repository.TouchpadRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/** When keyboards or touchpads are connected, schedule a tutorial after given time has elapsed */
+@SysUISingleton
+class OobeTutorialSchedulerInteractor
+@Inject
+constructor(
+    @Application private val context: Context,
+    @Application private val applicationScope: CoroutineScope,
+    keyboardRepository: KeyboardRepository,
+    touchpadRepository: TouchpadRepository
+) {
+    private val isAnyKeyboardConnected = keyboardRepository.isAnyKeyboardConnected
+    private val isAnyTouchpadConnected = touchpadRepository.isAnyTouchpadConnected
+
+    fun start() {
+        applicationScope.launch { isAnyKeyboardConnected.collect { startOobe() } }
+        applicationScope.launch { isAnyTouchpadConnected.collect { startOobe() } }
+    }
+
+    private fun startOobe() {
+        val intent = Intent(TUTORIAL_ACTION)
+        intent.addCategory(Intent.CATEGORY_DEFAULT)
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        context.startActivity(intent)
+    }
+
+    companion object {
+        const val TAG = "OobeSchedulerInteractor"
+        const val TUTORIAL_ACTION = "com.android.systemui.action.TOUCHPAD_TUTORIAL"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
index 817849c..b654307 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
@@ -41,6 +41,7 @@
 import kotlinx.coroutines.flow.asFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.flatMapConcat
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.flowOn
@@ -78,9 +79,15 @@
 ) : KeyboardRepository {
 
     private val keyboardsChange: Flow<Pair<Collection<Int>, DeviceChange>> =
-        inputDeviceRepository.deviceChange.map { (ids, change) ->
-            ids.filter { id -> isPhysicalFullKeyboard(id) } to change
-        }
+        inputDeviceRepository.deviceChange
+            .map { (ids, change) -> ids.filter { id -> isPhysicalFullKeyboard(id) } to change }
+            .filter { (_, change) ->
+                when (change) {
+                    FreshStart -> true
+                    is DeviceAdded -> isPhysicalFullKeyboard(change.deviceId)
+                    is DeviceRemoved -> isPhysicalFullKeyboard(change.deviceId)
+                }
+            }
 
     @FlowPreview
     override val newlyConnectedKeyboard: Flow<Keyboard> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
index af755d3..58719fe 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
@@ -24,6 +24,9 @@
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsFocusedAsState
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxScope
@@ -77,11 +80,16 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.drawscope.Stroke
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.LocalContext
@@ -100,6 +108,7 @@
 import androidx.compose.ui.util.fastFirstOrNull
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastForEachIndexed
+import androidx.compose.ui.zIndex
 import com.android.compose.ui.graphics.painter.rememberDrawablePainter
 import com.android.compose.windowsizeclass.LocalWindowSizeClass
 import com.android.systemui.keyboard.shortcut.shared.model.Shortcut
@@ -405,7 +414,7 @@
         Row(Modifier.fillMaxWidth()) {
             StartSidePanel(
                 onSearchQueryChanged = onSearchQueryChanged,
-                modifier = Modifier.fillMaxWidth(fraction = 0.32f),
+                modifier = Modifier.width(200.dp),
                 categories = categories,
                 onKeyboardSettingsClicked = onKeyboardSettingsClicked,
                 selectedCategory = selectedCategoryType,
@@ -462,7 +471,18 @@
 
 @Composable
 private fun ShortcutView(modifier: Modifier, searchQuery: String, shortcut: Shortcut) {
-    Row(modifier) {
+    val interactionSource = remember { MutableInteractionSource() }
+    val isFocused by interactionSource.collectIsFocusedAsState()
+    Row(
+        modifier
+            .focusable(interactionSource = interactionSource)
+            .outlineFocusModifier(
+                isFocused = isFocused,
+                focusColor = MaterialTheme.colorScheme.secondary,
+                padding = 8.dp,
+                cornerRadius = 16.dp
+            )
+    ) {
         Row(
             modifier = Modifier.width(128.dp).align(Alignment.CenterVertically),
             horizontalArrangement = Arrangement.spacedBy(16.dp),
@@ -670,10 +690,23 @@
     colors: NavigationDrawerItemColors =
         NavigationDrawerItemDefaults.colors(unselectedContainerColor = Color.Transparent),
 ) {
+    val interactionSource = remember { MutableInteractionSource() }
+    val isFocused by interactionSource.collectIsFocusedAsState()
+
     Surface(
         selected = selected,
         onClick = onClick,
-        modifier = Modifier.semantics { role = Role.Tab }.heightIn(min = 64.dp).fillMaxWidth(),
+        modifier =
+            Modifier.semantics { role = Role.Tab }
+                .heightIn(min = 64.dp)
+                .fillMaxWidth()
+                .focusable(interactionSource = interactionSource)
+                .outlineFocusModifier(
+                    isFocused = isFocused,
+                    focusColor = MaterialTheme.colorScheme.secondary,
+                    padding = 2.dp,
+                    cornerRadius = 33.dp
+                ),
         shape = RoundedCornerShape(28.dp),
         color = colors.containerColor(selected).value,
     ) {
@@ -697,6 +730,39 @@
     }
 }
 
+private fun Modifier.outlineFocusModifier(
+    isFocused: Boolean,
+    focusColor: Color,
+    padding: Dp,
+    cornerRadius: Dp
+): Modifier {
+    if (isFocused) {
+        return this.drawWithContent {
+                val focusOutline =
+                    Rect(Offset.Zero, size).let {
+                        if (padding > 0.dp) {
+                            it.inflate(padding.toPx())
+                        } else {
+                            it.deflate(padding.unaryMinus().toPx())
+                        }
+                    }
+                drawContent()
+                drawRoundRect(
+                    color = focusColor,
+                    style = Stroke(width = 3.dp.toPx()),
+                    topLeft = focusOutline.topLeft,
+                    size = focusOutline.size,
+                    cornerRadius = CornerRadius(cornerRadius.toPx())
+                )
+            }
+            // Increasing Z-Index so focus outline is drawn on top of "selected" category
+            // background.
+            .zIndex(1f)
+    } else {
+        return this
+    }
+}
+
 @Composable
 @OptIn(ExperimentalMaterial3Api::class)
 private fun TitleBar() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
index e2d7851..04ea37e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
@@ -112,14 +112,18 @@
                         keyguardInteractor.isActiveDreamLockscreenHosted,
                         communalSceneInteractor.isIdleOnCommunal
                     )
-                    .filterRelevantKeyguardState()
-                    .collect {
-                        (isBouncerShowing, isAwake, isActiveDreamLockscreenHosted, isIdleOnCommunal)
-                        ->
+                    .filterRelevantKeyguardStateAnd { (isBouncerShowing, _, _, _) ->
+                        // TODO(b/307976454) - See if we need to listen for SHOW_WHEN_LOCKED
+                        // activities showing up over the bouncer. Camera launch can't show up over
+                        // bouncer since the first power press hides bouncer. Do occluding
+                        // activities auto hide bouncer? Not sure.
+                        !isBouncerShowing
+                    }
+                    .collect { (_, isAwake, isActiveDreamLockscreenHosted, isIdleOnCommunal) ->
                         if (
                             !maybeStartTransitionToOccludedOrInsecureCamera { state, reason ->
                                 startTransitionTo(state, ownerReason = reason)
-                            } && !isBouncerShowing && isAwake && !isActiveDreamLockscreenHosted
+                            } && isAwake && !isActiveDreamLockscreenHosted
                         ) {
                             val toState =
                                 if (isIdleOnCommunal) {
@@ -254,6 +258,6 @@
         val TO_GONE_SHORT_DURATION = 200.milliseconds
         val TO_LOCKSCREEN_DURATION = 450.milliseconds
         val TO_GLANCEABLE_HUB_DURATION = DEFAULT_DURATION
-        val TO_GONE_SURFACE_BEHIND_VISIBLE_THRESHOLD = 0.5f
+        val TO_GONE_SURFACE_BEHIND_VISIBLE_THRESHOLD = 0.1f
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt
index 805dbb0..2ebd9e8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt
@@ -30,6 +30,7 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch
@@ -52,11 +53,12 @@
      * then we'll seed the repository with a transition from OFF -> GONE.
      */
     @OptIn(ExperimentalCoroutinesApi::class)
-    private val showLockscreenOnBoot =
+    private val showLockscreenOnBoot: Flow<Boolean> by lazy {
         deviceProvisioningInteractor.isDeviceProvisioned.map { provisioned ->
             (provisioned || deviceEntryInteractor.isAuthenticationRequired()) &&
                 deviceEntryInteractor.isLockscreenEnabled()
         }
+    }
 
     override fun start() {
         scope.launch {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
index 4bfefda..3fffeff 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import android.content.res.Resources
-import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.ContentKey
 import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.dagger.SysUISingleton
@@ -90,12 +90,12 @@
 
     /**
      * Returns a flow that indicates whether lockscreen notifications should be rendered in the
-     * given [sceneKey].
+     * given [contentKey].
      */
-    fun areNotificationsVisible(sceneKey: SceneKey): Flow<Boolean> {
+    fun areNotificationsVisible(contentKey: ContentKey): Flow<Boolean> {
         // `Scenes.NotificationsShade` renders its own separate notifications stack, so when it's
         // open we avoid rendering the lockscreen notifications stack.
-        if (sceneKey == Scenes.NotificationsShade) {
+        if (contentKey == Scenes.NotificationsShade) {
             return flowOf(false)
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
index 97b5e87..02379e6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
@@ -46,7 +46,7 @@
 import com.android.systemui.retail.data.repository.RetailModeRepository
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.user.data.repository.UserRepository
-import com.android.systemui.util.kotlin.pairwise
+import com.android.systemui.util.kotlin.pairwiseBy
 import dagger.Lazy
 import java.io.PrintWriter
 import javax.inject.Inject
@@ -63,7 +63,6 @@
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
@@ -169,17 +168,19 @@
     private val userAndTiles =
         currentUser
             .flatMapLatest { userId ->
-                tileSpecRepository.tilesSpecs(userId).map { UserAndTiles(userId, it) }
+                val currentTiles = tileSpecRepository.tilesSpecs(userId)
+                val installedComponents =
+                    installedTilesComponentRepository.getInstalledTilesComponents(userId)
+                currentTiles.combine(installedComponents) { tiles, components ->
+                    UserTilesAndComponents(userId, tiles, components)
+                }
             }
             .distinctUntilChanged()
-            .pairwise(UserAndTiles(-1, emptyList()))
+            .pairwiseBy(UserTilesAndComponents(-1, emptyList(), emptySet())) { prev, new ->
+                DataWithUserChange(data = new, userChange = prev.userId != new.userId)
+            }
             .flowOn(backgroundDispatcher)
 
-    private val installedPackagesWithTiles =
-        currentUser.flatMapLatest {
-            installedTilesComponentRepository.getInstalledTilesComponents(it)
-        }
-
     private val minTiles: Int
         get() =
             if (retailModeRepository.inRetailMode) {
@@ -194,7 +195,6 @@
         }
     }
 
-    @OptIn(ExperimentalCoroutinesApi::class)
     private fun startTileCollection() {
         scope.launch {
             launch {
@@ -205,95 +205,82 @@
             }
 
             launch(backgroundDispatcher) {
-                userAndTiles
-                    .combine(installedPackagesWithTiles) { usersAndTiles, packages ->
-                        Data(
-                            usersAndTiles.previousValue,
-                            usersAndTiles.newValue,
-                            packages,
-                        )
-                    }
-                    .collectLatest {
-                        val newTileList = it.newData.tiles
-                        val userChanged = it.oldData.userId != it.newData.userId
-                        val newUser = it.newData.userId
-                        val components = it.installedComponents
+                userAndTiles.collectLatest {
+                    val newUser = it.userId
+                    val newTileList = it.tiles
+                    val components = it.installedComponents
+                    val userChanged = it.userChange
 
-                        // Destroy all tiles that are not in the new set
-                        specsToTiles
-                            .filter {
-                                it.key !in newTileList && it.value is TileOrNotInstalled.Tile
-                            }
-                            .forEach { entry ->
-                                logger.logTileDestroyed(
-                                    entry.key,
-                                    if (userChanged) {
-                                        QSPipelineLogger.TileDestroyedReason
-                                            .TILE_NOT_PRESENT_IN_NEW_USER
-                                    } else {
-                                        QSPipelineLogger.TileDestroyedReason.TILE_REMOVED
-                                    }
-                                )
-                                (entry.value as TileOrNotInstalled.Tile).tile.destroy()
-                            }
-                        // MutableMap will keep the insertion order
-                        val newTileMap = mutableMapOf<TileSpec, TileOrNotInstalled>()
-
-                        newTileList.forEach { tileSpec ->
-                            if (tileSpec !in newTileMap) {
-                                if (
-                                    tileSpec is TileSpec.CustomTileSpec &&
-                                        tileSpec.componentName !in components
-                                ) {
-                                    newTileMap[tileSpec] = TileOrNotInstalled.NotInstalled
+                    // Destroy all tiles that are not in the new set
+                    specsToTiles
+                        .filter { it.key !in newTileList && it.value is TileOrNotInstalled.Tile }
+                        .forEach { entry ->
+                            logger.logTileDestroyed(
+                                entry.key,
+                                if (userChanged) {
+                                    QSPipelineLogger.TileDestroyedReason
+                                        .TILE_NOT_PRESENT_IN_NEW_USER
                                 } else {
-                                    // Create tile here will never try to create a CustomTile that
-                                    // is not installed
-                                    val newTile =
-                                        if (tileSpec in specsToTiles) {
-                                            processExistingTile(
-                                                tileSpec,
-                                                specsToTiles.getValue(tileSpec),
-                                                userChanged,
-                                                newUser
-                                            )
-                                                ?: createTile(tileSpec)
-                                        } else {
-                                            createTile(tileSpec)
-                                        }
-                                    if (newTile != null) {
-                                        newTileMap[tileSpec] = TileOrNotInstalled.Tile(newTile)
+                                    QSPipelineLogger.TileDestroyedReason.TILE_REMOVED
+                                }
+                            )
+                            (entry.value as TileOrNotInstalled.Tile).tile.destroy()
+                        }
+                    // MutableMap will keep the insertion order
+                    val newTileMap = mutableMapOf<TileSpec, TileOrNotInstalled>()
+
+                    newTileList.forEach { tileSpec ->
+                        if (tileSpec !in newTileMap) {
+                            if (
+                                tileSpec is TileSpec.CustomTileSpec &&
+                                    tileSpec.componentName !in components
+                            ) {
+                                newTileMap[tileSpec] = TileOrNotInstalled.NotInstalled
+                            } else {
+                                // Create tile here will never try to create a CustomTile that
+                                // is not installed
+                                val newTile =
+                                    if (tileSpec in specsToTiles) {
+                                        processExistingTile(
+                                            tileSpec,
+                                            specsToTiles.getValue(tileSpec),
+                                            userChanged,
+                                            newUser
+                                        ) ?: createTile(tileSpec)
+                                    } else {
+                                        createTile(tileSpec)
                                     }
+                                if (newTile != null) {
+                                    newTileMap[tileSpec] = TileOrNotInstalled.Tile(newTile)
                                 }
                             }
                         }
-
-                        val resolvedSpecs = newTileMap.keys.toList()
-                        specsToTiles.clear()
-                        specsToTiles.putAll(newTileMap)
-                        val newResolvedTiles =
-                            newTileMap
-                                .filter { it.value is TileOrNotInstalled.Tile }
-                                .map {
-                                    TileModel(it.key, (it.value as TileOrNotInstalled.Tile).tile)
-                                }
-
-                        _currentSpecsAndTiles.value = newResolvedTiles
-                        logger.logTilesNotInstalled(
-                            newTileMap.filter { it.value is TileOrNotInstalled.NotInstalled }.keys,
-                            newUser
-                        )
-                        if (newResolvedTiles.size < minTiles) {
-                            // We ended up with not enough tiles (some may be not installed).
-                            // Prepend the default set of tiles
-                            launch { tileSpecRepository.prependDefault(currentUser.value) }
-                        } else if (resolvedSpecs != newTileList) {
-                            // There were some tiles that couldn't be created. Change the value in
-                            // the
-                            // repository
-                            launch { tileSpecRepository.setTiles(currentUser.value, resolvedSpecs) }
-                        }
                     }
+
+                    val resolvedSpecs = newTileMap.keys.toList()
+                    specsToTiles.clear()
+                    specsToTiles.putAll(newTileMap)
+                    val newResolvedTiles =
+                        newTileMap
+                            .filter { it.value is TileOrNotInstalled.Tile }
+                            .map { TileModel(it.key, (it.value as TileOrNotInstalled.Tile).tile) }
+
+                    _currentSpecsAndTiles.value = newResolvedTiles
+                    logger.logTilesNotInstalled(
+                        newTileMap.filter { it.value is TileOrNotInstalled.NotInstalled }.keys,
+                        newUser
+                    )
+                    if (newResolvedTiles.size < minTiles) {
+                        // We ended up with not enough tiles (some may be not installed).
+                        // Prepend the default set of tiles
+                        launch { tileSpecRepository.prependDefault(currentUser.value) }
+                    } else if (resolvedSpecs != newTileList) {
+                        // There were some tiles that couldn't be created. Change the value in
+                        // the
+                        // repository
+                        launch { tileSpecRepository.setTiles(currentUser.value, resolvedSpecs) }
+                    }
+                }
             }
         }
     }
@@ -362,8 +349,7 @@
                     newQSTileFactory.get().createTile(spec.spec)
                 } else {
                     null
-                }
-                    ?: tileFactory.createTile(spec.spec)
+                } ?: tileFactory.createTile(spec.spec)
             }
         if (tile == null) {
             logger.logTileNotFoundInFactory(spec)
@@ -436,15 +422,25 @@
 
         @JvmInline value class Tile(val tile: QSTile) : TileOrNotInstalled
     }
-
-    private data class UserAndTiles(
-        val userId: Int,
-        val tiles: List<TileSpec>,
-    )
-
-    private data class Data(
-        val oldData: UserAndTiles,
-        val newData: UserAndTiles,
-        val installedComponents: Set<ComponentName>,
-    )
 }
+
+private data class UserTilesAndComponents(
+    val userId: Int,
+    val tiles: List<TileSpec>,
+    val installedComponents: Set<ComponentName>
+)
+
+private data class DataWithUserChange(
+    val userId: Int,
+    val tiles: List<TileSpec>,
+    val installedComponents: Set<ComponentName>,
+    val userChange: Boolean,
+)
+
+private fun DataWithUserChange(data: UserTilesAndComponents, userChange: Boolean) =
+    DataWithUserChange(
+        data.userId,
+        data.tiles,
+        data.installedComponents,
+        userChange,
+    )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
index d9546ec..1750347 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
@@ -106,7 +106,8 @@
     @Override
     @MainThread
     public void onManagedProfileRemoved() {
-        mHost.removeTile(getTileSpec());
+        // No OP as this may race with the user change in CurrentTilesInteractor.
+        // If the tile needs to be removed, AutoAdd (or AutoTileManager) will take care of that.
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt
index e9e9d8b..cdcefdb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt
@@ -22,12 +22,15 @@
 import com.android.internal.logging.InstanceId
 import com.android.systemui.qs.pipeline.shared.TileSpec
 
-data class QSTileConfig(
+data class QSTileConfig
+@JvmOverloads
+constructor(
     val tileSpec: TileSpec,
     val uiConfig: QSTileUIConfig,
     val instanceId: InstanceId,
     val metricsSpec: String = tileSpec.spec,
     val policy: QSTilePolicy = QSTilePolicy.NoRestrictions,
+    val autoRemoveOnUnavailable: Boolean = true,
 )
 
 /**
@@ -38,6 +41,7 @@
 
     val iconRes: Int
         @DrawableRes get
+
     val labelRes: Int
         @StringRes get
 
@@ -48,6 +52,7 @@
     data object Empty : QSTileUIConfig {
         override val iconRes: Int
             get() = Resources.ID_NULL
+
         override val labelRes: Int
             get() = Resources.ID_NULL
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index 2cdcc24..c6f9ae8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -70,7 +70,7 @@
             applicationScope.launch {
                 launch {
                     qsTileViewModel.isAvailable.collectIndexed { index, isAvailable ->
-                        if (!isAvailable) {
+                        if (!isAvailable && qsTileViewModel.config.autoRemoveOnUnavailable) {
                             qsHost.removeTile(tileSpec)
                         }
                         // qsTileViewModel.isAvailable flow often starts with isAvailable == true.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
index ac2a0d8..1e0e597a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
@@ -20,10 +20,14 @@
 import android.os.UserHandle
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.server.notification.Flags.screenshareNotificationHiding
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
 import com.android.systemui.statusbar.notification.DynamicPrivacyController
 import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.ListEntry
@@ -32,27 +36,33 @@
 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import dagger.Binds
 import dagger.Module
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.launch
 
 @Module(includes = [PrivateSensitiveContentCoordinatorModule::class])
 interface SensitiveContentCoordinatorModule
 
 @Module
 interface PrivateSensitiveContentCoordinatorModule {
-    @Binds
-    fun bindCoordinator(impl: SensitiveContentCoordinatorImpl): SensitiveContentCoordinator
+    @Binds fun bindCoordinator(impl: SensitiveContentCoordinatorImpl): SensitiveContentCoordinator
 }
 
 /** Coordinates re-inflation and post-processing of sensitive notification content. */
 interface SensitiveContentCoordinator : Coordinator
 
 @CoordinatorScope
-class SensitiveContentCoordinatorImpl @Inject constructor(
+class SensitiveContentCoordinatorImpl
+@Inject
+constructor(
     private val dynamicPrivacyController: DynamicPrivacyController,
     private val lockscreenUserManager: NotificationLockscreenUserManager,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
@@ -61,45 +71,85 @@
     private val selectedUserInteractor: SelectedUserInteractor,
     private val sensitiveNotificationProtectionController:
         SensitiveNotificationProtectionController,
-) : Invalidator("SensitiveContentInvalidator"),
-        SensitiveContentCoordinator,
-        DynamicPrivacyController.Listener,
-        OnBeforeRenderListListener {
-    private val onSensitiveStateChanged = Runnable() {
-        invalidateList("onSensitiveStateChanged")
-    }
+    private val deviceEntryInteractor: DeviceEntryInteractor,
+    private val sceneInteractor: SceneInteractor,
+    @Application private val scope: CoroutineScope,
+) :
+    Invalidator("SensitiveContentInvalidator"),
+    SensitiveContentCoordinator,
+    DynamicPrivacyController.Listener,
+    OnBeforeRenderListListener {
+    private var inTransitionFromLockedToGone = false
 
-    private val screenshareSecretFilter = object : NotifFilter("ScreenshareSecretFilter") {
-        val NotificationEntry.isSecret
-            get() = channel?.lockscreenVisibility == Notification.VISIBILITY_SECRET ||
-                sbn.notification?.visibility == Notification.VISIBILITY_SECRET
-        override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean {
-            return screenshareNotificationHiding() &&
-                sensitiveNotificationProtectionController.isSensitiveStateActive &&
-                entry.isSecret
+    private val onSensitiveStateChanged = Runnable() { invalidateList("onSensitiveStateChanged") }
+
+    private val screenshareSecretFilter =
+        object : NotifFilter("ScreenshareSecretFilter") {
+            val NotificationEntry.isSecret
+                get() =
+                    channel?.lockscreenVisibility == Notification.VISIBILITY_SECRET ||
+                        sbn.notification?.visibility == Notification.VISIBILITY_SECRET
+
+            override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean {
+                return screenshareNotificationHiding() &&
+                    sensitiveNotificationProtectionController.isSensitiveStateActive &&
+                    entry.isSecret
+            }
         }
-    }
 
     override fun attach(pipeline: NotifPipeline) {
         dynamicPrivacyController.addListener(this)
         if (screenshareNotificationHiding()) {
-            sensitiveNotificationProtectionController
-                .registerSensitiveStateListener(onSensitiveStateChanged)
+            sensitiveNotificationProtectionController.registerSensitiveStateListener(
+                onSensitiveStateChanged
+            )
         }
         pipeline.addOnBeforeRenderListListener(this)
         pipeline.addPreRenderInvalidator(this)
         if (screenshareNotificationHiding()) {
             pipeline.addFinalizeFilter(screenshareSecretFilter)
         }
+
+        if (SceneContainerFlag.isEnabled) {
+            scope.launch {
+                sceneInteractor.transitionState
+                    .mapNotNull {
+                        val transitioningToGone = it.isTransitioning(to = Scenes.Gone)
+                        val deviceEntered = deviceEntryInteractor.isDeviceEntered.value
+                        when {
+                            transitioningToGone && !deviceEntered -> true
+                            !transitioningToGone -> false
+                            else -> null
+                        }
+                    }
+                    .distinctUntilChanged()
+                    .collect {
+                        inTransitionFromLockedToGone = it
+                        invalidateList("inTransitionFromLockedToGoneChanged")
+                    }
+            }
+        }
     }
 
     override fun onDynamicPrivacyChanged(): Unit = invalidateList("onDynamicPrivacyChanged")
 
+    private val isKeyguardGoingAway: Boolean
+        get() {
+            if (SceneContainerFlag.isEnabled) {
+                return inTransitionFromLockedToGone
+            } else {
+                return keyguardStateController.isKeyguardGoingAway
+            }
+        }
+
     override fun onBeforeRenderList(entries: List<ListEntry>) {
-        if (keyguardStateController.isKeyguardGoingAway ||
+        if (
+            isKeyguardGoingAway ||
                 statusBarStateController.state == StatusBarState.KEYGUARD &&
-                keyguardUpdateMonitor.getUserUnlockedWithBiometricAndIsBypassing(
-                        selectedUserInteractor.getSelectedUserId())) {
+                    keyguardUpdateMonitor.getUserUnlockedWithBiometricAndIsBypassing(
+                        selectedUserInteractor.getSelectedUserId()
+                    )
+        ) {
             // don't update yet if:
             // - the keyguard is currently going away
             // - LS is about to be dismissed by a biometric that bypasses LS (avoid notif flash)
@@ -109,35 +159,40 @@
             return
         }
 
-        val isSensitiveContentProtectionActive = screenshareNotificationHiding() &&
-            sensitiveNotificationProtectionController.isSensitiveStateActive
+        val isSensitiveContentProtectionActive =
+            screenshareNotificationHiding() &&
+                sensitiveNotificationProtectionController.isSensitiveStateActive
         val currentUserId = lockscreenUserManager.currentUserId
         val devicePublic = lockscreenUserManager.isLockscreenPublicMode(currentUserId)
-        val deviceSensitive = (devicePublic &&
+        val deviceSensitive =
+            (devicePublic &&
                 !lockscreenUserManager.userAllowsPrivateNotificationsInPublic(currentUserId)) ||
                 isSensitiveContentProtectionActive
         val dynamicallyUnlocked = dynamicPrivacyController.isDynamicallyUnlocked
         for (entry in extractAllRepresentativeEntries(entries).filter { it.rowExists() }) {
             val notifUserId = entry.sbn.user.identifier
-            val userLockscreen = devicePublic ||
-                    lockscreenUserManager.isLockscreenPublicMode(notifUserId)
-            val userPublic = when {
-                // if we're not on the lockscreen, we're definitely private
-                !userLockscreen -> false
-                // we are on the lockscreen, so unless we're dynamically unlocked, we're
-                // definitely public
-                !dynamicallyUnlocked -> true
-                // we're dynamically unlocked, but check if the notification needs
-                // a separate challenge if it's from a work profile
-                else -> when (notifUserId) {
-                    currentUserId -> false
-                    UserHandle.USER_ALL -> false
-                    else -> lockscreenUserManager.needsSeparateWorkChallenge(notifUserId)
+            val userLockscreen =
+                devicePublic || lockscreenUserManager.isLockscreenPublicMode(notifUserId)
+            val userPublic =
+                when {
+                    // if we're not on the lockscreen, we're definitely private
+                    !userLockscreen -> false
+                    // we are on the lockscreen, so unless we're dynamically unlocked, we're
+                    // definitely public
+                    !dynamicallyUnlocked -> true
+                    // we're dynamically unlocked, but check if the notification needs
+                    // a separate challenge if it's from a work profile
+                    else ->
+                        when (notifUserId) {
+                            currentUserId -> false
+                            UserHandle.USER_ALL -> false
+                            else -> lockscreenUserManager.needsSeparateWorkChallenge(notifUserId)
+                        }
                 }
-            }
 
-            val shouldProtectNotification = screenshareNotificationHiding() &&
-                sensitiveNotificationProtectionController.shouldProtectNotification(entry)
+            val shouldProtectNotification =
+                screenshareNotificationHiding() &&
+                    sensitiveNotificationProtectionController.shouldProtectNotification(entry)
 
             val needsRedaction = lockscreenUserManager.needsRedaction(entry)
             val isSensitive = userPublic && needsRedaction
@@ -149,9 +204,7 @@
     }
 }
 
-private fun extractAllRepresentativeEntries(
-    entries: List<ListEntry>
-): Sequence<NotificationEntry> =
+private fun extractAllRepresentativeEntries(entries: List<ListEntry>): Sequence<NotificationEntry> =
     entries.asSequence().flatMap(::extractAllRepresentativeEntries)
 
 private fun extractAllRepresentativeEntries(listEntry: ListEntry): Sequence<NotificationEntry> =
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 2a8db56..a6ca3ab 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
@@ -98,7 +98,8 @@
         globalSettings.registerContentObserverSync(
             globalSettings.getUriFor(HEADS_UP_NOTIFICATIONS_ENABLED),
             /* notifyForDescendants = */ true,
-            observer)
+            observer
+        )
 
         // QQQ: Do we need to register for SETTING_HEADS_UP_TICKER? It seems unused.
 
@@ -147,12 +148,12 @@
         }
 }
 
-class PeekDndSuppressor() :
+class PeekDndSuppressor :
     VisualInterruptionFilter(types = setOf(PEEK), reason = "suppressed by DND") {
     override fun shouldSuppress(entry: NotificationEntry) = entry.shouldSuppressPeek()
 }
 
-class PeekNotImportantSuppressor() :
+class PeekNotImportantSuppressor :
     VisualInterruptionFilter(types = setOf(PEEK), reason = "importance < HIGH") {
     override fun shouldSuppress(entry: NotificationEntry) = entry.importance < IMPORTANCE_HIGH
 }
@@ -170,7 +171,10 @@
 
 class PeekOldWhenSuppressor(private val systemClock: SystemClock) :
     VisualInterruptionFilter(
-        types = setOf(PEEK), reason = "has old `when`", uiEventId = HUN_SUPPRESSED_OLD_WHEN) {
+        types = setOf(PEEK),
+        reason = "has old `when`",
+        uiEventId = HUN_SUPPRESSED_OLD_WHEN
+    ) {
     private fun whenAge(entry: NotificationEntry) =
         systemClock.currentTimeMillis() - entry.sbn.notification.getWhen()
 
@@ -190,47 +194,51 @@
         }
 }
 
-class PulseEffectSuppressor() :
+class PulseEffectSuppressor :
     VisualInterruptionFilter(types = setOf(PULSE), reason = "suppressed by DND") {
     override fun shouldSuppress(entry: NotificationEntry) = entry.shouldSuppressAmbient()
 }
 
-class PulseLockscreenVisibilityPrivateSuppressor() :
+class PulseLockscreenVisibilityPrivateSuppressor :
     VisualInterruptionFilter(
-        types = setOf(PULSE), reason = "hidden by lockscreen visibility override") {
+        types = setOf(PULSE),
+        reason = "hidden by lockscreen visibility override"
+    ) {
     override fun shouldSuppress(entry: NotificationEntry) =
         entry.ranking.lockscreenVisibilityOverride == VISIBILITY_PRIVATE
 }
 
-class PulseLowImportanceSuppressor() :
+class PulseLowImportanceSuppressor :
     VisualInterruptionFilter(types = setOf(PULSE), reason = "importance < DEFAULT") {
     override fun shouldSuppress(entry: NotificationEntry) = entry.importance < IMPORTANCE_DEFAULT
 }
 
-class HunGroupAlertBehaviorSuppressor() :
+class HunGroupAlertBehaviorSuppressor :
     VisualInterruptionFilter(
-        types = setOf(PEEK, PULSE), reason = "suppressive group alert behavior") {
+        types = setOf(PEEK, PULSE),
+        reason = "suppressive group alert behavior"
+    ) {
     override fun shouldSuppress(entry: NotificationEntry) =
         entry.sbn.let { it.isGroup && it.notification.suppressAlertingDueToGrouping() }
 }
 
-class HunSilentNotificationSuppressor() :
+class HunSilentNotificationSuppressor :
     VisualInterruptionFilter(types = setOf(PEEK, PULSE), reason = "notification isSilent") {
     override fun shouldSuppress(entry: NotificationEntry) =
         entry.sbn.let { Flags.notificationSilentFlag() && it.notification.isSilent }
 }
 
-class HunJustLaunchedFsiSuppressor() :
+class HunJustLaunchedFsiSuppressor :
     VisualInterruptionFilter(types = setOf(PEEK, PULSE), reason = "just launched FSI") {
     override fun shouldSuppress(entry: NotificationEntry) = entry.hasJustLaunchedFullScreenIntent()
 }
 
-class BubbleNotAllowedSuppressor() :
-    VisualInterruptionFilter(types = setOf(BUBBLE), reason = "cannot bubble") {
+class BubbleNotAllowedSuppressor :
+    VisualInterruptionFilter(types = setOf(BUBBLE), reason = "cannot bubble", isSpammy = true) {
     override fun shouldSuppress(entry: NotificationEntry) = !entry.canBubble()
 }
 
-class BubbleNoMetadataSuppressor() :
+class BubbleNoMetadataSuppressor :
     VisualInterruptionFilter(types = setOf(BUBBLE), reason = "has no or invalid bubble metadata") {
 
     private fun isValidMetadata(metadata: BubbleMetadata?) =
@@ -253,6 +261,7 @@
 
 /**
  * Set with:
+ *
  * adb shell setprop persist.force_show_avalanche_edu_once 1 && adb shell stop; adb shell start
  */
 private const val FORCE_SHOW_AVALANCHE_EDU_ONCE = "persist.force_show_avalanche_edu_once"
@@ -368,7 +377,8 @@
         val bundle = Bundle()
         bundle.putString(
             Notification.EXTRA_SUBSTITUTE_APP_NAME,
-            context.getString(com.android.internal.R.string.android_system_label))
+            context.getString(com.android.internal.R.string.android_system_label)
+        )
 
         val builder =
             Notification.Builder(context, NotificationChannels.ALERTS)
@@ -390,8 +400,10 @@
     }
 
     private fun calculateState(entry: NotificationEntry): State {
-        if (entry.ranking.isConversation &&
-            entry.sbn.notification.getWhen() > avalancheProvider.startTime) {
+        if (
+            entry.ranking.isConversation &&
+                entry.sbn.notification.getWhen() > avalancheProvider.startTime
+        ) {
             uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_NEW_CONVERSATION)
             return State.ALLOW_CONVERSATION_AFTER_AVALANCHE
         }
@@ -424,8 +436,10 @@
             uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_COLORIZED)
             return State.ALLOW_COLORIZED
         }
-        if (packageManager.checkPermission(RECEIVE_EMERGENCY_BROADCAST, entry.sbn.packageName) ==
-            PERMISSION_GRANTED) {
+        if (
+            packageManager.checkPermission(RECEIVE_EMERGENCY_BROADCAST, entry.sbn.packageName) ==
+                PERMISSION_GRANTED
+        ) {
             uiEventLogger.log(AvalancheEvent.AVALANCHE_SUPPRESSOR_HUN_ALLOWED_EMERGENCY)
             return State.ALLOW_EMERGENCY
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
index 1470b03..c204ea9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification.interruption
 
+import android.util.Log
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel.DEBUG
 import com.android.systemui.log.core.LogLevel.INFO
@@ -24,11 +25,15 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider.FullScreenIntentDecision
 import com.android.systemui.statusbar.notification.logKey
+import com.android.systemui.util.Compile
 import javax.inject.Inject
 
 class VisualInterruptionDecisionLogger
 @Inject
 constructor(@NotificationInterruptLog val buffer: LogBuffer) {
+
+    val spew: Boolean = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE)
+
     fun logHeadsUpFeatureChanged(isEnabled: Boolean) {
         buffer.log(
             TAG,
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 c0d27cb..8e8d9b6 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
@@ -95,7 +95,8 @@
     private constructor(
         val decision: DecisionImpl,
         override val uiEventId: UiEventEnum? = null,
-        override val eventLogData: EventLogData? = null
+        override val eventLogData: EventLogData? = null,
+        val isSpammy: Boolean = false,
     ) : Loggable {
         companion object {
             val unsuppressed =
@@ -113,7 +114,8 @@
                 LoggableDecision(
                     DecisionImpl(shouldInterrupt = false, logReason = suppressor.reason),
                     uiEventId = suppressor.uiEventId,
-                    eventLogData = suppressor.eventLogData
+                    eventLogData = suppressor.eventLogData,
+                    isSpammy = suppressor.isSpammy,
                 )
         }
     }
@@ -185,8 +187,15 @@
 
         if (NotificationAvalancheSuppression.isEnabled) {
             addFilter(
-                AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor,
-                    packageManager, uiEventLogger, context, notificationManager)
+                AvalancheSuppressor(
+                    avalancheProvider,
+                    systemClock,
+                    settingsInteractor,
+                    packageManager,
+                    uiEventLogger,
+                    context,
+                    notificationManager
+                )
             )
             avalancheProvider.register()
         }
@@ -280,7 +289,9 @@
         entry: NotificationEntry,
         loggableDecision: LoggableDecision
     ) {
-        logger.logDecision(type.name, entry, loggableDecision.decision)
+        if (!loggableDecision.isSpammy || logger.spew) {
+            logger.logDecision(type.name, entry, loggableDecision.decision)
+        }
         logEvents(entry, loggableDecision)
     }
 
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 ee79727..5fe75c0 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
@@ -59,6 +59,10 @@
     /** Optional data to be logged in the EventLog when this suppresses an interruption. */
     val eventLogData: EventLogData?
 
+    /** Whether the interruption is spammy and should be dropped under normal circumstances. */
+    val isSpammy: Boolean
+        get() = false
+
     /**
      * Called after the suppressor is added to the [VisualInterruptionDecisionProvider] but before
      * any other methods are called on the suppressor.
@@ -76,7 +80,7 @@
     constructor(
         types: Set<VisualInterruptionType>,
         reason: String
-    ) : this(types, reason, /* uiEventId = */ null)
+    ) : this(types, reason, /* uiEventId= */ null)
 
     /** @return true if these interruptions should be suppressed right now. */
     abstract fun shouldSuppress(): Boolean
@@ -87,12 +91,13 @@
     override val types: Set<VisualInterruptionType>,
     override val reason: String,
     override val uiEventId: UiEventEnum? = null,
-    override val eventLogData: EventLogData? = null
+    override val eventLogData: EventLogData? = null,
+    override val isSpammy: Boolean = false,
 ) : VisualInterruptionSuppressor {
     constructor(
         types: Set<VisualInterruptionType>,
         reason: String
-    ) : this(types, reason, /* uiEventId = */ null)
+    ) : this(types, reason, /* uiEventId= */ null)
 
     /**
      * @param entry the notification to consider suppressing
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 461a38d..b6de78e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -2995,7 +2995,7 @@
                 @Override
                 public void onFalse() {
                     // Hides quick settings, bouncer, and quick-quick settings.
-                    mStatusBarKeyguardViewManager.reset(true);
+                    mStatusBarKeyguardViewManager.reset(true, /* isFalsingReset= */true);
                 }
             };
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 41b69a7..88a2b23 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -708,7 +708,7 @@
      * Shows the notification keyguard or the bouncer depending on
      * {@link #needsFullscreenBouncer()}.
      */
-    protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing) {
+    protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing, boolean isFalsingReset) {
         boolean isDozing = mDozing;
         if (Flags.simPinRaceConditionOnRestart()) {
             KeyguardState toState = mKeyguardTransitionInteractor.getTransitionState().getValue()
@@ -734,8 +734,12 @@
                         mPrimaryBouncerInteractor.show(/* isScrimmed= */ true);
                     }
                 }
-            } else {
-                Log.e(TAG, "Attempted to show the sim bouncer when it is already showing.");
+            } else if (!isFalsingReset) {
+                // Falsing resets can cause this to flicker, so don't reset in this case
+                Log.i(TAG, "Sim bouncer is already showing, issuing a refresh");
+                mPrimaryBouncerInteractor.hide();
+                mPrimaryBouncerInteractor.show(/* isScrimmed= */ true);
+
             }
         } else {
             mCentralSurfaces.showKeyguard();
@@ -957,6 +961,10 @@
 
     @Override
     public void reset(boolean hideBouncerWhenShowing) {
+        reset(hideBouncerWhenShowing, /* isFalsingReset= */false);
+    }
+
+    public void reset(boolean hideBouncerWhenShowing, boolean isFalsingReset) {
         if (mKeyguardStateController.isShowing() && !bouncerIsAnimatingAway()) {
             final boolean isOccluded = mKeyguardStateController.isOccluded();
             // Hide quick settings.
@@ -968,7 +976,7 @@
                     hideBouncer(false /* destroyView */);
                 }
             } else {
-                showBouncerOrKeyguard(hideBouncerWhenShowing);
+                showBouncerOrKeyguard(hideBouncerWhenShowing, isFalsingReset);
             }
             if (hideBouncerWhenShowing) {
                 hideAlternateBouncer(true);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
index a7c5f78..03ec41d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
@@ -20,6 +20,7 @@
 import android.telephony.TelephonyCallback
 import android.telephony.TelephonyManager
 import android.telephony.satellite.NtnSignalStrengthCallback
+import android.telephony.satellite.SatelliteCommunicationAllowedStateCallback
 import android.telephony.satellite.SatelliteManager
 import android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_SUCCESS
 import android.telephony.satellite.SatelliteModemStateCallback
@@ -37,7 +38,6 @@
 import com.android.systemui.statusbar.pipeline.dagger.DeviceBasedSatelliteInputLog
 import com.android.systemui.statusbar.pipeline.dagger.VerboseDeviceBasedSatelliteInputLog
 import com.android.systemui.statusbar.pipeline.satellite.data.RealDeviceBasedSatelliteRepository
-import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Companion.whenSupported
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.NotSupported
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Supported
@@ -60,11 +60,9 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.mapNotNull
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.stateIn
@@ -122,15 +120,9 @@
 }
 
 /**
- * Basically your everyday run-of-the-mill system service listener, with three notable exceptions.
+ * Basically your everyday run-of-the-mill system service listener, with two notable exceptions.
  *
- * First, there is an availability bit that we are tracking via [SatelliteManager]. See
- * [isSatelliteAllowedForCurrentLocation] for the implementation details. The thing to note about
- * this bit is that there is no callback that exists. Therefore we implement a simple polling
- * mechanism here. Since the underlying bit is location-dependent, we simply poll every hour (see
- * [POLLING_INTERVAL_MS]) and see what the current state is.
- *
- * Secondly, there are cases when simply requesting information from SatelliteManager can fail. See
+ * First, there are cases when simply requesting information from SatelliteManager can fail. See
  * [SatelliteSupport] for details on how we track the state. What's worth noting here is that
  * SUPPORTED is a stronger guarantee than [satelliteManager] being null. Therefore, the fundamental
  * data flows here ([connectionState], [signalStrength],...) are wrapped in the convenience method
@@ -138,7 +130,7 @@
  * [SupportedSatelliteManager], we can guarantee that the manager is non-null AND that it has told
  * us that satellite is supported. Therefore, we don't expect exceptions to be thrown.
  *
- * Lastly, this class is designed to wait a full minute of process uptime before making any requests
+ * Second, this class is designed to wait a full minute of process uptime before making any requests
  * to the satellite manager. The hope is that by waiting we don't have to retry due to a modem that
  * is still booting up or anything like that. We can tune or remove this behavior in the future if
  * necessary.
@@ -158,8 +150,6 @@
 
     private val satelliteManager: SatelliteManager?
 
-    override val isSatelliteAllowedForCurrentLocation: MutableStateFlow<Boolean>
-
     // Some calls into satellite manager will throw exceptions if it is not supported.
     // This is never expected to change after boot, but may need to be retried in some cases
     @get:VisibleForTesting
@@ -221,8 +211,6 @@
     init {
         satelliteManager = satelliteManagerOpt.getOrNull()
 
-        isSatelliteAllowedForCurrentLocation = MutableStateFlow(false)
-
         if (satelliteManager != null) {
             // Outer scope launch allows us to delay until MIN_UPTIME
             scope.launch {
@@ -233,10 +221,7 @@
                     { "Checked for system support. support=$str1" },
                 )
 
-                // Second, launch a job to poll for service availability based on location
-                scope.launch { pollForAvailabilityBasedOnLocation() }
-
-                // Third, register a listener to let us know if there are changes to support
+                // Second, register a listener to let us know if there are changes to support
                 scope.launch { listenForChangesToSatelliteSupport(satelliteManager) }
             }
         } else {
@@ -259,28 +244,43 @@
         return sm.checkSatelliteSupported()
     }
 
-    /*
-     * As there is no listener available for checking satellite allowed, we must poll the service.
-     * Defaulting to polling at most once every 20m while active. Subsequent OOS events will restart
-     * the job, so a flaky connection might cause more frequent checks.
-     */
-    private suspend fun pollForAvailabilityBasedOnLocation() {
+    override val isSatelliteAllowedForCurrentLocation =
         satelliteSupport
             .whenSupported(
-                supported = ::isSatelliteAllowedHasListener,
+                supported = ::isSatelliteAvailableFlow,
                 orElse = flowOf(false),
                 retrySignal = telephonyProcessCrashedEvent,
             )
-            .collectLatest { hasSubscribers ->
-                if (hasSubscribers) {
-                    while (true) {
-                        logBuffer.i { "requestIsCommunicationAllowedForCurrentLocation" }
-                        checkIsSatelliteAllowed()
-                        delay(POLLING_INTERVAL_MS)
+            .stateIn(scope, SharingStarted.Lazily, false)
+
+    private fun isSatelliteAvailableFlow(sm: SupportedSatelliteManager): Flow<Boolean> =
+        conflatedCallbackFlow {
+                val callback = SatelliteCommunicationAllowedStateCallback { allowed ->
+                    logBuffer.i({ bool1 = allowed }) {
+                        "onSatelliteCommunicationAllowedStateChanged: $bool1"
+                    }
+
+                    trySend(allowed)
+                }
+
+                var registered = false
+                try {
+                    sm.registerForCommunicationAllowedStateChanged(
+                        bgDispatcher.asExecutor(),
+                        callback
+                    )
+                    registered = true
+                } catch (e: Exception) {
+                    logBuffer.e("Error calling registerForCommunicationAllowedStateChanged", e)
+                }
+
+                awaitClose {
+                    if (registered) {
+                        sm.unregisterForCommunicationAllowedStateChanged(callback)
                     }
                 }
             }
-    }
+            .flowOn(bgDispatcher)
 
     /**
      * Register a callback with [SatelliteManager] to let us know if there is a change in satellite
@@ -410,14 +410,6 @@
             }
         }
 
-    /**
-     * Signal that we should start polling [checkIsSatelliteAllowed]. We only need to poll if there
-     * are active listeners to [isSatelliteAllowedForCurrentLocation]
-     */
-    @SuppressWarnings("unused")
-    private fun isSatelliteAllowedHasListener(sm: SupportedSatelliteManager): Flow<Boolean> =
-        isSatelliteAllowedForCurrentLocation.subscriptionCount.map { it > 0 }.distinctUntilChanged()
-
     override val connectionState =
         satelliteSupport
             .whenSupported(
@@ -485,28 +477,6 @@
             }
             .flowOn(bgDispatcher)
 
-    /** Fire off a request to check for satellite availability. Always runs on the bg context */
-    private suspend fun checkIsSatelliteAllowed() =
-        withContext(bgDispatcher) {
-            satelliteManager?.requestIsCommunicationAllowedForCurrentLocation(
-                bgDispatcher.asExecutor(),
-                object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> {
-                    override fun onError(e: SatelliteManager.SatelliteException) {
-                        logBuffer.e(
-                            "Found exception when checking availability",
-                            e,
-                        )
-                        isSatelliteAllowedForCurrentLocation.value = false
-                    }
-
-                    override fun onResult(allowed: Boolean) {
-                        logBuffer.i { "isSatelliteAllowedForCurrentLocation: $allowed" }
-                        isSatelliteAllowedForCurrentLocation.value = allowed
-                    }
-                }
-            )
-        }
-
     private suspend fun SatelliteManager.checkSatelliteSupported(): SatelliteSupport =
         suspendCancellableCoroutine { continuation ->
             val cb =
@@ -546,9 +516,6 @@
         }
 
     companion object {
-        // TTL for satellite polling is twenty minutes
-        const val POLLING_INTERVAL_MS: Long = 1000 * 60 * 20
-
         // Let the system boot up and stabilize before we check for system support
         const val MIN_UPTIME: Long = 1000 * 60
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
index f693409..21ec14f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
@@ -289,6 +289,7 @@
                         labelRes = R.string.quick_settings_work_mode_label,
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
+                autoRemoveOnUnavailable = false,
             )
 
         /** Inject work mode into tileViewModelMap in QSModule */
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/TouchpadModule.kt b/packages/SystemUI/src/com/android/systemui/touchpad/TouchpadModule.kt
new file mode 100644
index 0000000..c86ac2f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/TouchpadModule.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.touchpad
+
+import com.android.systemui.touchpad.data.repository.TouchpadRepository
+import com.android.systemui.touchpad.data.repository.TouchpadRepositoryImpl
+import dagger.Binds
+import dagger.Module
+
+@Module
+abstract class TouchpadModule {
+
+    @Binds
+    abstract fun bindTouchpadRepository(repository: TouchpadRepositoryImpl): TouchpadRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/data/repository/TouchpadRepository.kt b/packages/SystemUI/src/com/android/systemui/touchpad/data/repository/TouchpadRepository.kt
new file mode 100644
index 0000000..7131546
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/data/repository/TouchpadRepository.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.touchpad.data.repository
+
+import android.hardware.input.InputManager
+import android.view.InputDevice.SOURCE_TOUCHPAD
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.inputdevice.data.repository.InputDeviceRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+interface TouchpadRepository {
+    /** Emits true if any touchpad is connected to the device, false otherwise. */
+    val isAnyTouchpadConnected: Flow<Boolean>
+}
+
+@SysUISingleton
+class TouchpadRepositoryImpl
+@Inject
+constructor(
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val inputManager: InputManager,
+    inputDeviceRepository: InputDeviceRepository
+) : TouchpadRepository {
+
+    override val isAnyTouchpadConnected: Flow<Boolean> =
+        inputDeviceRepository.deviceChange
+            .map { (ids, _) -> ids.any { id -> isTouchpad(id) } }
+            .distinctUntilChanged()
+            .flowOn(backgroundDispatcher)
+
+    private fun isTouchpad(deviceId: Int): Boolean {
+        val device = inputManager.getInputDevice(deviceId) ?: return false
+        return device.supportsSource(SOURCE_TOUCHPAD)
+    }
+
+    companion object {
+        const val TAG = "TouchpadRepositoryImpl"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
index 94ff65e..51dfef0 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
@@ -26,6 +26,7 @@
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
@@ -49,6 +50,7 @@
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
+import com.airbnb.lottie.LottieComposition
 import com.airbnb.lottie.LottieProperty
 import com.airbnb.lottie.compose.LottieAnimation
 import com.airbnb.lottie.compose.LottieCompositionSpec
@@ -61,6 +63,9 @@
 import com.airbnb.lottie.compose.rememberLottieDynamicProperty
 import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.systemui.res.R
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS
 import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGesture.BACK
 import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureHandler
 
@@ -78,23 +83,49 @@
 ) {
     val screenColors = rememberScreenColors()
     BackHandler(onBack = onBack)
-    var gestureDone by remember { mutableStateOf(false) }
+    var gestureState by remember { mutableStateOf(GestureState.NOT_STARTED) }
     val swipeDistanceThresholdPx =
         LocalContext.current.resources.getDimensionPixelSize(
             com.android.internal.R.dimen.system_gestures_distance_threshold
         )
     val gestureHandler =
         remember(swipeDistanceThresholdPx) {
-            TouchpadGestureHandler(BACK, swipeDistanceThresholdPx, onDone = { gestureDone = true })
+            TouchpadGestureHandler(
+                BACK,
+                swipeDistanceThresholdPx,
+                onGestureStateChanged = { gestureState = it }
+            )
         }
+    TouchpadGesturesHandlingBox(gestureHandler, gestureState) {
+        GestureTutorialContent(gestureState, onDoneButtonClicked, screenColors)
+    }
+}
+
+@Composable
+private fun TouchpadGesturesHandlingBox(
+    gestureHandler: TouchpadGestureHandler,
+    gestureState: GestureState,
+    modifier: Modifier = Modifier,
+    content: @Composable BoxScope.() -> Unit
+) {
     Box(
         modifier =
-            Modifier.fillMaxSize()
+            modifier
+                .fillMaxSize()
                 // we need to use pointerInteropFilter because some info about touchpad gestures is
                 // only available in MotionEvent
-                .pointerInteropFilter(onTouchEvent = gestureHandler::onMotionEvent)
+                .pointerInteropFilter(
+                    onTouchEvent = { event ->
+                        // FINISHED is the final state so we don't need to process touches anymore
+                        if (gestureState != FINISHED) {
+                            gestureHandler.onMotionEvent(event)
+                        } else {
+                            false
+                        }
+                    }
+                )
     ) {
-        GestureTutorialContent(gestureDone, onDoneButtonClicked, screenColors)
+        content()
     }
 }
 
@@ -126,14 +157,14 @@
 
 @Composable
 private fun GestureTutorialContent(
-    gestureDone: Boolean,
+    gestureState: GestureState,
     onDoneButtonClicked: () -> Unit,
     screenColors: TutorialScreenColors
 ) {
     val animatedColor by
         animateColorAsState(
             targetValue =
-                if (gestureDone) screenColors.successBackgroundColor
+                if (gestureState == FINISHED) screenColors.successBackgroundColor
                 else screenColors.backgroundColor,
             animationSpec = tween(durationMillis = 150, easing = LinearEasing),
             label = "backgroundColor"
@@ -148,15 +179,17 @@
         Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
             TutorialDescription(
                 titleTextId =
-                    if (gestureDone) R.string.touchpad_tutorial_gesture_done
+                    if (gestureState == FINISHED) R.string.touchpad_tutorial_gesture_done
                     else R.string.touchpad_back_gesture_action_title,
                 titleColor = screenColors.titleColor,
-                bodyTextId = R.string.touchpad_back_gesture_guidance,
+                bodyTextId =
+                    if (gestureState == FINISHED) R.string.touchpad_back_gesture_finished
+                    else R.string.touchpad_back_gesture_guidance,
                 modifier = Modifier.weight(1f)
             )
             Spacer(modifier = Modifier.width(76.dp))
             TutorialAnimation(
-                gestureDone,
+                gestureState,
                 screenColors.animationProperties,
                 modifier = Modifier.weight(1f).padding(top = 8.dp)
             )
@@ -189,27 +222,38 @@
 
 @Composable
 fun TutorialAnimation(
-    gestureDone: Boolean,
+    gestureState: GestureState,
     animationProperties: LottieDynamicProperties,
     modifier: Modifier = Modifier
 ) {
     Column(modifier = modifier.fillMaxWidth()) {
-        val resId = if (gestureDone) R.raw.trackpad_back_success else R.raw.trackpad_back_edu
+        val resId =
+            if (gestureState == FINISHED) R.raw.trackpad_back_success else R.raw.trackpad_back_edu
         val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(resId))
-        val progress by
-            animateLottieCompositionAsState(
-                composition,
-                iterations = if (gestureDone) 1 else LottieConstants.IterateForever
-            )
+        val progress = progressForGestureState(composition, gestureState)
         LottieAnimation(
             composition = composition,
-            progress = { progress },
+            progress = progress,
             dynamicProperties = animationProperties
         )
     }
 }
 
 @Composable
+private fun progressForGestureState(
+    composition: LottieComposition?,
+    gestureState: GestureState
+): () -> Float {
+    if (gestureState == IN_PROGRESS) {
+        return { 0f } // when gesture is in progress, animation should freeze on 1st frame
+    } else {
+        val iterations = if (gestureState == FINISHED) 1 else LottieConstants.IterateForever
+        val animationState by animateLottieCompositionAsState(composition, iterations = iterations)
+        return { animationState }
+    }
+}
+
+@Composable
 fun rememberColorFilterProperty(
     layerName: String,
     color: Color
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
index 1fa7a0c..6fa9bcd 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
@@ -17,23 +17,26 @@
 package com.android.systemui.touchpad.tutorial.ui.gesture
 
 import android.view.MotionEvent
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NOT_STARTED
 import kotlin.math.abs
 
 /**
- * Monitor for touchpad gestures that calls [gestureDoneCallback] when gesture was successfully
- * done. All tracked motion events should be passed to [processTouchpadEvent]
+ * Monitor for touchpad gestures that calls [gestureStateChangedCallback] when [GestureState]
+ * changes. All tracked motion events should be passed to [processTouchpadEvent]
  */
 interface TouchpadGestureMonitor {
 
     val gestureDistanceThresholdPx: Int
-    val gestureDoneCallback: () -> Unit
+    val gestureStateChangedCallback: (GestureState) -> Unit
 
     fun processTouchpadEvent(event: MotionEvent)
 }
 
 class BackGestureMonitor(
     override val gestureDistanceThresholdPx: Int,
-    override val gestureDoneCallback: () -> Unit
+    override val gestureStateChangedCallback: (GestureState) -> Unit
 ) : TouchpadGestureMonitor {
 
     private var xStart = 0f
@@ -44,13 +47,16 @@
             MotionEvent.ACTION_DOWN -> {
                 if (isThreeFingerTouchpadSwipe(event)) {
                     xStart = event.x
+                    gestureStateChangedCallback(IN_PROGRESS)
                 }
             }
             MotionEvent.ACTION_UP -> {
                 if (isThreeFingerTouchpadSwipe(event)) {
                     val distance = abs(event.x - xStart)
                     if (distance >= gestureDistanceThresholdPx) {
-                        gestureDoneCallback()
+                        gestureStateChangedCallback(FINISHED)
+                    } else {
+                        gestureStateChangedCallback(NOT_STARTED)
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt
new file mode 100644
index 0000000..446875a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.touchpad.tutorial.ui.gesture
+
+enum class GestureState {
+    NOT_STARTED,
+    IN_PROGRESS,
+    FINISHED
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt
index 4ae9c7b..190da62 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt
@@ -22,10 +22,10 @@
 
     fun toMonitor(
         swipeDistanceThresholdPx: Int,
-        gestureDoneCallback: () -> Unit
+        onStateChanged: (GestureState) -> Unit
     ): TouchpadGestureMonitor {
         return when (this) {
-            BACK -> BackGestureMonitor(swipeDistanceThresholdPx, gestureDoneCallback)
+            BACK -> BackGestureMonitor(swipeDistanceThresholdPx, onStateChanged)
             else -> throw IllegalArgumentException("Not implemented yet")
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
index dc8471c..cac2a99 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
@@ -26,11 +26,11 @@
 class TouchpadGestureHandler(
     touchpadGesture: TouchpadGesture,
     swipeDistanceThresholdPx: Int,
-    onDone: () -> Unit
+    onGestureStateChanged: (GestureState) -> Unit
 ) {
 
     private val gestureRecognition =
-        touchpadGesture.toMonitor(swipeDistanceThresholdPx, gestureDoneCallback = onDone)
+        touchpadGesture.toMonitor(swipeDistanceThresholdPx, onStateChanged = onGestureStateChanged)
 
     fun onMotionEvent(event: MotionEvent): Boolean {
         // events from touchpad have SOURCE_MOUSE and not SOURCE_TOUCHPAD because of legacy reasons
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
index efaca7a..5d8b6f1 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
@@ -19,8 +19,8 @@
 import android.content.ContentResolver
 import android.content.Context
 import android.media.AudioManager
-import com.android.settingslib.bluetooth.BluetoothUtils
 import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.flags.Flags
 import com.android.settingslib.notification.domain.interactor.NotificationsSoundPolicyInteractor
 import com.android.settingslib.volume.data.repository.AudioRepository
 import com.android.settingslib.volume.data.repository.AudioRepositoryImpl
@@ -80,7 +80,7 @@
             @Application coroutineScope: CoroutineScope,
             @Background coroutineContext: CoroutineContext,
         ): AudioSharingRepository =
-            if (BluetoothUtils.isAudioSharingEnabled() && localBluetoothManager != null) {
+            if (Flags.enableLeAudioSharing() && localBluetoothManager != null) {
                 AudioSharingRepositoryImpl(
                     contentResolver,
                     localBluetoothManager,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt
index e01744e..6a43a61 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt
@@ -20,6 +20,7 @@
 import android.content.ContextWrapper
 import android.content.SharedPreferences
 import android.os.Handler
+import android.platform.test.annotations.DisableFlags
 import android.provider.Settings
 import android.provider.Settings.Global.ZEN_MODE_NO_INTERRUPTIONS
 import android.provider.Settings.Global.ZEN_MODE_OFF
@@ -61,6 +62,7 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
+@DisableFlags(android.app.Flags.FLAG_MODES_UI)
 class DndTileTest : SysuiTestCase() {
 
     companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
index 0505727..689fc7c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
@@ -28,7 +28,11 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.server.notification.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.RankingBuilder
 import com.android.systemui.statusbar.StatusBarState
@@ -45,6 +49,7 @@
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController
+import com.android.systemui.testKosmos
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
@@ -52,6 +57,7 @@
 import com.android.systemui.util.mockito.withArgCaptor
 import dagger.BindsInstance
 import dagger.Component
+import kotlinx.coroutines.CoroutineScope
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Test
@@ -64,6 +70,8 @@
 @RunWith(AndroidJUnit4::class)
 class SensitiveContentCoordinatorTest : SysuiTestCase() {
 
+    val kosmos = testKosmos()
+
     val dynamicPrivacyController: DynamicPrivacyController = mock()
     val lockscreenUserManager: NotificationLockscreenUserManager = mock()
     val pipeline: NotifPipeline = mock()
@@ -73,6 +81,8 @@
     val mSelectedUserInteractor: SelectedUserInteractor = mock()
     val sensitiveNotificationProtectionController: SensitiveNotificationProtectionController =
         mock()
+    val deviceEntryInteractor: DeviceEntryInteractor = mock()
+    val sceneInteractor: SceneInteractor = mock()
 
     val coordinator: SensitiveContentCoordinator =
         DaggerTestSensitiveContentCoordinatorComponent.factory()
@@ -83,7 +93,10 @@
                 statusBarStateController,
                 keyguardStateController,
                 mSelectedUserInteractor,
-                sensitiveNotificationProtectionController
+                sensitiveNotificationProtectionController,
+                deviceEntryInteractor,
+                sceneInteractor,
+                kosmos.applicationCoroutineScope,
             )
             .coordinator
 
@@ -136,8 +149,7 @@
     @Test
     @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
     fun screenshareSecretFilter_sensitiveInctive_noFiltersSecret() {
-        whenever(sensitiveNotificationProtectionController.isSensitiveStateActive)
-            .thenReturn(false)
+        whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(false)
 
         coordinator.attach(pipeline)
         val filter = withArgCaptor<NotifFilter> { verify(pipeline).addFinalizeFilter(capture()) }
@@ -683,10 +695,11 @@
         val mockSbn: StatusBarNotification =
             mock<StatusBarNotification>().apply { whenever(user).thenReturn(mockUserHandle) }
         val mockRow: ExpandableNotificationRow = mock<ExpandableNotificationRow>()
-        val mockEntry = mock<NotificationEntry>().apply {
-            whenever(sbn).thenReturn(mockSbn)
-            whenever(row).thenReturn(mockRow)
-        }
+        val mockEntry =
+            mock<NotificationEntry>().apply {
+                whenever(sbn).thenReturn(mockSbn)
+                whenever(row).thenReturn(mockRow)
+            }
         whenever(lockscreenUserManager.needsRedaction(mockEntry)).thenReturn(needsRedaction)
         whenever(mockEntry.rowExists()).thenReturn(true)
         return object : ListEntry("key", 0) {
@@ -737,6 +750,9 @@
             @BindsInstance selectedUserInteractor: SelectedUserInteractor,
             @BindsInstance
             sensitiveNotificationProtectionController: SensitiveNotificationProtectionController,
+            @BindsInstance deviceEntryInteractor: DeviceEntryInteractor,
+            @BindsInstance sceneInteractor: SceneInteractor,
+            @BindsInstance @Application scope: CoroutineScope,
         ): TestSensitiveContentCoordinatorComponent
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index af5e60e..9b61105 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -1068,7 +1068,7 @@
     public void testShowBouncerOrKeyguard_needsFullScreen() {
         when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
                 KeyguardSecurityModel.SecurityMode.SimPin);
-        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false);
+        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false);
         verify(mCentralSurfaces).hideKeyguard();
         verify(mPrimaryBouncerInteractor).show(true);
     }
@@ -1084,7 +1084,7 @@
                 .thenReturn(KeyguardState.LOCKSCREEN);
 
         reset(mCentralSurfaces);
-        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false);
+        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false);
         verify(mPrimaryBouncerInteractor).show(true);
         verify(mCentralSurfaces).showKeyguard();
     }
@@ -1092,11 +1092,26 @@
     @Test
     @DisableSceneContainer
     public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing() {
+        boolean isFalsingReset = false;
         when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
                 KeyguardSecurityModel.SecurityMode.SimPin);
         when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
-        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false);
+        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset);
         verify(mCentralSurfaces, never()).hideKeyguard();
+        verify(mPrimaryBouncerInteractor).show(true);
+    }
+
+    @Test
+    @DisableSceneContainer
+    public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing_onFalsing() {
+        boolean isFalsingReset = true;
+        when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
+                KeyguardSecurityModel.SecurityMode.SimPin);
+        when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
+        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset);
+        verify(mCentralSurfaces, never()).hideKeyguard();
+
+        // Do not refresh the full screen bouncer if the call is from falsing
         verify(mPrimaryBouncerInteractor, never()).show(true);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
index 73ac6e3..af4f647 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
@@ -22,6 +22,7 @@
 import android.telephony.TelephonyManager
 import android.telephony.satellite.NtnSignalStrength
 import android.telephony.satellite.NtnSignalStrengthCallback
+import android.telephony.satellite.SatelliteCommunicationAllowedStateCallback
 import android.telephony.satellite.SatelliteManager
 import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_CONNECTED
 import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_RETRYING
@@ -44,7 +45,6 @@
 import com.android.systemui.log.core.FakeLogBuffer
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.MIN_UPTIME
-import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS
 import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.whenever
@@ -54,11 +54,8 @@
 import java.util.Optional
 import kotlin.test.Test
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -71,6 +68,7 @@
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.doThrow
 
 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -186,152 +184,86 @@
         }
 
     @Test
-    fun isSatelliteAllowed_readsSatelliteManagerState_enabled() =
+    fun isSatelliteAllowed_listensToSatelliteManagerCallback() =
         testScope.runTest {
             setupDefaultRepo()
-            // GIVEN satellite is allowed in this location
-            val allowed = true
-
-            doAnswer {
-                    val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
-                    receiver.onResult(allowed)
-                    null
-                }
-                .`when`(satelliteManager)
-                .requestIsCommunicationAllowedForCurrentLocation(
-                    any(),
-                    any<OutcomeReceiver<Boolean, SatelliteException>>()
-                )
 
             val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+            runCurrent()
 
-            assertThat(latest).isTrue()
-        }
-
-    @Test
-    fun isSatelliteAllowed_readsSatelliteManagerState_disabled() =
-        testScope.runTest {
-            setupDefaultRepo()
-            // GIVEN satellite is not allowed in this location
-            val allowed = false
-
-            doAnswer {
-                    val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
-                    receiver.onResult(allowed)
-                    null
+            val callback =
+                withArgCaptor<SatelliteCommunicationAllowedStateCallback> {
+                    verify(satelliteManager)
+                        .registerForCommunicationAllowedStateChanged(any(), capture())
                 }
-                .`when`(satelliteManager)
-                .requestIsCommunicationAllowedForCurrentLocation(
-                    any(),
-                    any<OutcomeReceiver<Boolean, SatelliteException>>()
-                )
 
-            val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+            // WHEN satellite manager says it's not available
+            callback.onSatelliteCommunicationAllowedStateChanged(false)
 
-            assertThat(latest).isFalse()
-        }
-
-    @Test
-    fun isSatelliteAllowed_pollsOnTimeout() =
-        testScope.runTest {
-            setupDefaultRepo()
-            // GIVEN satellite is not allowed in this location
-            var allowed = false
-
-            doAnswer {
-                    val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
-                    receiver.onResult(allowed)
-                    null
-                }
-                .`when`(satelliteManager)
-                .requestIsCommunicationAllowedForCurrentLocation(
-                    any(),
-                    any<OutcomeReceiver<Boolean, SatelliteException>>()
-                )
-
-            val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
-
+            // THEN it's not!
             assertThat(latest).isFalse()
 
-            // WHEN satellite becomes enabled
-            allowed = true
+            // WHEN satellite manager says it's changed to available
+            callback.onSatelliteCommunicationAllowedStateChanged(true)
 
-            // WHEN the timeout has not yet been reached
-            advanceTimeBy(POLLING_INTERVAL_MS / 2)
-
-            // THEN the value is still false
-            assertThat(latest).isFalse()
-
-            // WHEN time advances beyond the polling interval
-            advanceTimeBy(POLLING_INTERVAL_MS / 2 + 1)
-
-            // THEN then new value is emitted
+            // THEN it is!
             assertThat(latest).isTrue()
         }
 
     @Test
-    fun isSatelliteAllowed_pollingRestartsWhenCollectionRestarts() =
-        testScope.runTest {
-            setupDefaultRepo()
-            // Use the old school launch/cancel so we can simulate subscribers arriving and leaving
-
-            var latest: Boolean? = false
-            var job =
-                underTest.isSatelliteAllowedForCurrentLocation.onEach { latest = it }.launchIn(this)
-
-            // GIVEN satellite is not allowed in this location
-            var allowed = false
-
-            doAnswer {
-                    val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
-                    receiver.onResult(allowed)
-                    null
-                }
-                .`when`(satelliteManager)
-                .requestIsCommunicationAllowedForCurrentLocation(
-                    any(),
-                    any<OutcomeReceiver<Boolean, SatelliteException>>()
-                )
-
-            assertThat(latest).isFalse()
-
-            // WHEN satellite becomes enabled
-            allowed = true
-
-            // WHEN the job is restarted
-            advanceTimeBy(POLLING_INTERVAL_MS / 2)
-
-            job.cancel()
-            job =
-                underTest.isSatelliteAllowedForCurrentLocation.onEach { latest = it }.launchIn(this)
-
-            // THEN the value is re-fetched
-            assertThat(latest).isTrue()
-
-            job.cancel()
-        }
-
-    @Test
     fun isSatelliteAllowed_falseWhenErrorOccurs() =
         testScope.runTest {
             setupDefaultRepo()
-            doAnswer {
-                    val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
-                    receiver.onError(SatelliteException(1 /* unused */))
-                    null
-                }
-                .`when`(satelliteManager)
-                .requestIsCommunicationAllowedForCurrentLocation(
-                    any(),
-                    any<OutcomeReceiver<Boolean, SatelliteException>>()
-                )
 
+            // GIVEN SatelliteManager gon' throw exceptions when we ask to register the callback
+            doThrow(RuntimeException("Test exception"))
+                .`when`(satelliteManager)
+                .registerForCommunicationAllowedStateChanged(any(), any())
+
+            // WHEN the latest value is requested (and thus causes an exception to be thrown)
             val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
 
+            // THEN the value is just false, and we didn't crash!
             assertThat(latest).isFalse()
         }
 
     @Test
+    fun isSatelliteAllowed_reRegistersOnTelephonyProcessCrash() =
+        testScope.runTest {
+            setupDefaultRepo()
+            val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+            runCurrent()
+
+            val callback =
+                withArgCaptor<SatelliteCommunicationAllowedStateCallback> {
+                    verify(satelliteManager)
+                        .registerForCommunicationAllowedStateChanged(any(), capture())
+                }
+
+            val telephonyCallback =
+                MobileTelephonyHelpers.getTelephonyCallbackForType<
+                    TelephonyCallback.RadioPowerStateListener
+                >(
+                    telephonyManager
+                )
+
+            // GIVEN satellite is currently provisioned
+            callback.onSatelliteCommunicationAllowedStateChanged(true)
+
+            assertThat(latest).isTrue()
+
+            // WHEN a crash event happens (detected by radio state change)
+            telephonyCallback.onRadioPowerStateChanged(TelephonyManager.RADIO_POWER_ON)
+            runCurrent()
+            telephonyCallback.onRadioPowerStateChanged(TelephonyManager.RADIO_POWER_OFF)
+            runCurrent()
+
+            // THEN listener is re-registered
+            verify(satelliteManager, times(2))
+                .registerForCommunicationAllowedStateChanged(any(), any())
+        }
+
+    @Test
     fun satelliteProvisioned_notSupported_defaultFalse() =
         testScope.runTest {
             // GIVEN satellite is not supported
@@ -363,24 +295,21 @@
         testScope.runTest {
             // GIVEN satellite is supported on device
             doAnswer {
-                val callback: OutcomeReceiver<Boolean, SatelliteException> =
-                    it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException>
-                callback.onResult(true)
-            }
+                    val callback: OutcomeReceiver<Boolean, SatelliteException> =
+                        it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException>
+                    callback.onResult(true)
+                }
                 .whenever(satelliteManager)
                 .requestIsSupported(any(), any())
 
             // GIVEN satellite returns an error when asked if provisioned
             doAnswer {
-                val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
-                receiver.onError(SatelliteException(SATELLITE_RESULT_ERROR))
-                null
-            }
+                    val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
+                    receiver.onError(SatelliteException(SATELLITE_RESULT_ERROR))
+                    null
+                }
                 .whenever(satelliteManager)
-                .requestIsProvisioned(
-                    any(),
-                    any<OutcomeReceiver<Boolean, SatelliteException>>()
-                )
+                .requestIsProvisioned(any(), any<OutcomeReceiver<Boolean, SatelliteException>>())
 
             // GIVEN we've been up long enough to start querying
             systemClock.setUptimeMillis(Process.getStartUptimeMillis() + MIN_UPTIME)
@@ -409,10 +338,10 @@
         testScope.runTest {
             // GIVEN satellite is supported on device
             doAnswer {
-                val callback: OutcomeReceiver<Boolean, SatelliteException> =
-                    it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException>
-                callback.onResult(true)
-            }
+                    val callback: OutcomeReceiver<Boolean, SatelliteException> =
+                        it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException>
+                    callback.onResult(true)
+                }
                 .whenever(satelliteManager)
                 .requestIsSupported(any(), any())
 
@@ -779,10 +708,10 @@
             .requestIsSupported(any(), any())
 
         doAnswer {
-            val callback: OutcomeReceiver<Boolean, SatelliteException> =
-                it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException>
-            callback.onResult(initialSatelliteIsProvisioned)
-        }
+                val callback: OutcomeReceiver<Boolean, SatelliteException> =
+                    it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException>
+                callback.onResult(initialSatelliteIsProvisioned)
+            }
             .whenever(satelliteManager)
             .requestIsProvisioned(any(), any())
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/data/repository/TouchpadRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/data/repository/TouchpadRepositoryTest.kt
new file mode 100644
index 0000000..3783af5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/data/repository/TouchpadRepositoryTest.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.touchpad.data.repository
+
+import android.hardware.input.FakeInputManager
+import android.hardware.input.InputManager.InputDeviceListener
+import android.hardware.input.fakeInputManager
+import android.testing.TestableLooper
+import android.view.InputDevice
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.inputdevice.data.repository.InputDeviceRepository
+import com.android.systemui.testKosmos
+import com.android.systemui.utils.os.FakeHandler
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Captor
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidJUnit4::class)
+class TouchpadRepositoryTest : SysuiTestCase() {
+
+    @Captor private lateinit var deviceListenerCaptor: ArgumentCaptor<InputDeviceListener>
+    private lateinit var fakeInputManager: FakeInputManager
+
+    private lateinit var underTest: TouchpadRepository
+    private lateinit var dispatcher: CoroutineDispatcher
+    private lateinit var inputDeviceRepo: InputDeviceRepository
+    private lateinit var testScope: TestScope
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        fakeInputManager = testKosmos().fakeInputManager
+        dispatcher = StandardTestDispatcher()
+        testScope = TestScope(dispatcher)
+        val handler = FakeHandler(TestableLooper.get(this).looper)
+        inputDeviceRepo =
+            InputDeviceRepository(handler, testScope.backgroundScope, fakeInputManager.inputManager)
+        underTest =
+            TouchpadRepositoryImpl(dispatcher, fakeInputManager.inputManager, inputDeviceRepo)
+    }
+
+    @Test
+    fun emitsDisconnected_ifNothingIsConnected() =
+        testScope.runTest {
+            val initialState = underTest.isAnyTouchpadConnected.first()
+            assertThat(initialState).isFalse()
+        }
+
+    @Test
+    fun emitsConnected_ifTouchpadAlreadyConnectedAtTheStart() =
+        testScope.runTest {
+            fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD)
+            val initialValue = underTest.isAnyTouchpadConnected.first()
+            assertThat(initialValue).isTrue()
+        }
+
+    @Test
+    fun emitsConnected_whenNewTouchpadConnects() =
+        testScope.runTest {
+            captureDeviceListener()
+            val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+
+            fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD)
+
+            assertThat(isTouchpadConnected).isTrue()
+        }
+
+    @Test
+    fun emitsDisconnected_whenDeviceWithIdDoesNotExist() =
+        testScope.runTest {
+            captureDeviceListener()
+            val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+            whenever(fakeInputManager.inputManager.getInputDevice(eq(NULL_DEVICE_ID)))
+                .thenReturn(null)
+            fakeInputManager.addDevice(NULL_DEVICE_ID, InputDevice.SOURCE_UNKNOWN)
+            assertThat(isTouchpadConnected).isFalse()
+        }
+
+    @Test
+    fun emitsDisconnected_whenTouchpadDisconnects() =
+        testScope.runTest {
+            captureDeviceListener()
+            val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+
+            fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD)
+            assertThat(isTouchpadConnected).isTrue()
+
+            fakeInputManager.removeDevice(TOUCHPAD_ID)
+            assertThat(isTouchpadConnected).isFalse()
+        }
+
+    private suspend fun captureDeviceListener() {
+        underTest.isAnyTouchpadConnected.first()
+        Mockito.verify(fakeInputManager.inputManager)
+            .registerInputDeviceListener(deviceListenerCaptor.capture(), anyOrNull())
+        fakeInputManager.registerInputDeviceListener(deviceListenerCaptor.value)
+    }
+
+    @Test
+    fun emitsDisconnected_whenNonTouchpadConnects() =
+        testScope.runTest {
+            captureDeviceListener()
+            val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+
+            fakeInputManager.addDevice(NON_TOUCHPAD_ID, InputDevice.SOURCE_KEYBOARD)
+            assertThat(isTouchpadConnected).isFalse()
+        }
+
+    @Test
+    fun emitsDisconnected_whenTouchpadDisconnectsAndWasAlreadyConnectedAtTheStart() =
+        testScope.runTest {
+            captureDeviceListener()
+            val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+
+            fakeInputManager.removeDevice(TOUCHPAD_ID)
+            assertThat(isTouchpadConnected).isFalse()
+        }
+
+    @Test
+    fun emitsConnected_whenAnotherDeviceDisconnects() =
+        testScope.runTest {
+            captureDeviceListener()
+            val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+
+            fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD)
+            fakeInputManager.removeDevice(NON_TOUCHPAD_ID)
+
+            assertThat(isTouchpadConnected).isTrue()
+        }
+
+    @Test
+    fun emitsConnected_whenOneTouchpadDisconnectsButAnotherRemainsConnected() =
+        testScope.runTest {
+            captureDeviceListener()
+            val isTouchpadConnected by collectLastValue(underTest.isAnyTouchpadConnected)
+
+            fakeInputManager.addDevice(TOUCHPAD_ID, TOUCHPAD)
+            fakeInputManager.addDevice(ANOTHER_TOUCHPAD_ID, TOUCHPAD)
+            fakeInputManager.removeDevice(TOUCHPAD_ID)
+
+            assertThat(isTouchpadConnected).isTrue()
+        }
+
+    private companion object {
+        private const val TOUCHPAD_ID = 1
+        private const val NON_TOUCHPAD_ID = 2
+        private const val ANOTHER_TOUCHPAD_ID = 3
+        private const val NULL_DEVICE_ID = 4
+
+        private const val TOUCHPAD = InputDevice.SOURCE_TOUCHPAD
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
index cf0db7b..8875b84 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
@@ -28,6 +28,9 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NOT_STARTED
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -36,16 +39,19 @@
 @RunWith(AndroidJUnit4::class)
 class BackGestureMonitorTest : SysuiTestCase() {
 
-    private var gestureDoneWasCalled = false
-    private val gestureDoneCallback = { gestureDoneWasCalled = true }
-    private val gestureMonitor = BackGestureMonitor(SWIPE_DISTANCE.toInt(), gestureDoneCallback)
+    private var gestureState = NOT_STARTED
+    private val gestureMonitor =
+        BackGestureMonitor(
+            gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
+            gestureStateChangedCallback = { gestureState = it }
+        )
 
     companion object {
         const val SWIPE_DISTANCE = 100f
     }
 
     @Test
-    fun triggersGestureDoneForThreeFingerGestureRight() {
+    fun triggersGestureFinishedForThreeFingerGestureRight() {
         val events =
             listOf(
                 threeFingerEvent(ACTION_DOWN, x = 0f, y = 0f),
@@ -59,11 +65,11 @@
 
         events.forEach { gestureMonitor.processTouchpadEvent(it) }
 
-        assertThat(gestureDoneWasCalled).isTrue()
+        assertThat(gestureState).isEqualTo(FINISHED)
     }
 
     @Test
-    fun triggersGestureDoneForThreeFingerGestureLeft() {
+    fun triggersGestureFinishedForThreeFingerGestureLeft() {
         val events =
             listOf(
                 threeFingerEvent(ACTION_DOWN, x = SWIPE_DISTANCE, y = 0f),
@@ -77,7 +83,21 @@
 
         events.forEach { gestureMonitor.processTouchpadEvent(it) }
 
-        assertThat(gestureDoneWasCalled).isTrue()
+        assertThat(gestureState).isEqualTo(FINISHED)
+    }
+
+    @Test
+    fun triggersGestureProgressForThreeFingerGestureStarted() {
+        val events =
+            listOf(
+                threeFingerEvent(ACTION_DOWN, x = SWIPE_DISTANCE, y = 0f),
+                threeFingerEvent(ACTION_POINTER_DOWN, x = SWIPE_DISTANCE, y = 0f),
+                threeFingerEvent(ACTION_POINTER_DOWN, x = SWIPE_DISTANCE, y = 0f),
+            )
+
+        events.forEach { gestureMonitor.processTouchpadEvent(it) }
+
+        assertThat(gestureState).isEqualTo(IN_PROGRESS)
     }
 
     private fun threeFingerEvent(action: Int, x: Float, y: Float): MotionEvent {
@@ -91,7 +111,7 @@
     }
 
     @Test
-    fun doesntTriggerGestureDone_onThreeFingersSwipeUp() {
+    fun doesntTriggerGestureFinished_onThreeFingersSwipeUp() {
         val events =
             listOf(
                 threeFingerEvent(ACTION_DOWN, x = 0f, y = 0f),
@@ -105,11 +125,11 @@
 
         events.forEach { gestureMonitor.processTouchpadEvent(it) }
 
-        assertThat(gestureDoneWasCalled).isFalse()
+        assertThat(gestureState).isEqualTo(NOT_STARTED)
     }
 
     @Test
-    fun doesntTriggerGestureDone_onTwoFingersSwipe() {
+    fun doesntTriggerGestureFinished_onTwoFingersSwipe() {
         fun twoFingerEvent(action: Int, x: Float, y: Float) =
             motionEvent(
                 action = action,
@@ -127,11 +147,11 @@
 
         events.forEach { gestureMonitor.processTouchpadEvent(it) }
 
-        assertThat(gestureDoneWasCalled).isFalse()
+        assertThat(gestureState).isEqualTo(NOT_STARTED)
     }
 
     @Test
-    fun doesntTriggerGestureDone_onFourFingersSwipe() {
+    fun doesntTriggerGestureFinished_onFourFingersSwipe() {
         fun fourFingerEvent(action: Int, x: Float, y: Float) =
             motionEvent(
                 action = action,
@@ -155,6 +175,6 @@
 
         events.forEach { gestureMonitor.processTouchpadEvent(it) }
 
-        assertThat(gestureDoneWasCalled).isFalse()
+        assertThat(gestureState).isEqualTo(NOT_STARTED)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
index 769f264..dc4d5f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
@@ -32,6 +32,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NOT_STARTED
 import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGesture.BACK
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
@@ -41,8 +43,8 @@
 @RunWith(AndroidJUnit4::class)
 class TouchpadGestureHandlerTest : SysuiTestCase() {
 
-    private var gestureDone = false
-    private val handler = TouchpadGestureHandler(BACK, SWIPE_DISTANCE) { gestureDone = true }
+    private var gestureState = NOT_STARTED
+    private val handler = TouchpadGestureHandler(BACK, SWIPE_DISTANCE) { gestureState = it }
 
     companion object {
         const val SWIPE_DISTANCE = 100
@@ -84,7 +86,7 @@
     fun triggersGestureDoneForThreeFingerGesture() {
         backGestureEvents().forEach { handler.onMotionEvent(it) }
 
-        assertThat(gestureDone).isTrue()
+        assertThat(gestureState).isEqualTo(FINISHED)
     }
 
     private fun backGestureEvents(): List<MotionEvent> {
diff --git a/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt b/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt
index 6e7c05c..ee36cad 100644
--- a/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt
+++ b/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt
@@ -16,6 +16,7 @@
 
 package android.hardware.input
 
+import android.hardware.input.InputManager.InputDeviceListener
 import android.view.InputDevice
 import android.view.KeyCharacterMap
 import android.view.KeyCharacterMap.VIRTUAL_KEYBOARD
@@ -47,6 +48,8 @@
             VIRTUAL_KEYBOARD to allKeyCodes.toMutableSet()
         )
 
+    private var inputDeviceListener: InputDeviceListener? = null
+
     val inputManager =
         mock<InputManager> {
             whenever(getInputDevice(anyInt())).thenAnswer { invocation ->
@@ -84,6 +87,11 @@
         addPhysicalKeyboard(deviceId, enabled)
     }
 
+    fun registerInputDeviceListener(listener: InputDeviceListener) {
+        // TODO (b/355422259): handle this by listening to inputManager.registerInputDeviceListener
+        inputDeviceListener = listener
+    }
+
     fun addPhysicalKeyboard(id: Int, enabled: Boolean = true) {
         check(id > 0) { "Physical keyboard ids have to be > 0" }
         addKeyboard(id, enabled)
@@ -106,6 +114,16 @@
         supportedKeyCodesByDeviceId[id] = allKeyCodes.toMutableSet()
     }
 
+    fun addDevice(id: Int, sources: Int) {
+        devices[id] = InputDevice.Builder().setId(id).setSources(sources).build()
+        inputDeviceListener?.onInputDeviceAdded(id)
+    }
+
+    fun removeDevice(id: Int) {
+        devices.remove(id)
+        inputDeviceListener?.onInputDeviceRemoved(id)
+    }
+
     private fun InputDevice.copy(
         id: Int = getId(),
         type: Int = keyboardType,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
index 2ca338a..f3d5b7d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
@@ -33,7 +33,7 @@
     private var _userHandle: UserHandle = UserHandle.of(_userId),
     private var _userInfo: UserInfo = mock(),
     private var _userProfiles: List<UserInfo> = emptyList(),
-    userContentResolver: ContentResolver = MockContentResolver(),
+    userContentResolverProvider: () -> ContentResolver = { MockContentResolver() },
     userContext: Context = mock(),
     private val onCreateCurrentUserContext: (Context) -> Context = { mock() },
 ) : UserTracker {
@@ -41,14 +41,19 @@
 
     override val userId: Int
         get() = _userId
+
     override val userHandle: UserHandle
         get() = _userHandle
+
     override val userInfo: UserInfo
         get() = _userInfo
+
     override val userProfiles: List<UserInfo>
         get() = _userProfiles
 
-    override val userContentResolver: ContentResolver = userContentResolver
+    // userContentResolver is lazy because Ravenwood doesn't support MockContentResolver()
+    // and we still want to allow people use this class for tests that don't use it.
+    override val userContentResolver: ContentResolver by lazy { userContentResolverProvider() }
     override val userContext: Context = userContext
 
     override fun addCallback(callback: UserTracker.Callback, executor: Executor) {
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java
index 8fe6853..1a15d7a 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java
@@ -25,19 +25,24 @@
 import static android.os.ParcelFileDescriptor.MODE_WORLD_WRITEABLE;
 import static android.os.ParcelFileDescriptor.MODE_WRITE_ONLY;
 
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
 import com.android.internal.annotations.GuardedBy;
 import com.android.ravenwood.common.JvmWorkaround;
 
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.RandomAccessFile;
 import java.util.HashMap;
 import java.util.Map;
 
 public class ParcelFileDescriptor_host {
+    private static final String TAG = "ParcelFileDescriptor_host";
+
     /**
      * Since we don't have a great way to keep an unmanaged {@code FileDescriptor} reference
      * alive, we keep a strong reference to the {@code RandomAccessFile} we used to open it. This
@@ -98,16 +103,18 @@
         synchronized (sActive) {
             raf = sActive.remove(fd);
         }
+        int fdInt = JvmWorkaround.getInstance().getFdInt(fd);
         try {
             if (raf != null) {
                 raf.close();
             } else {
-                // Odd, we don't remember opening this ourselves, but let's release the
-                // underlying resource as requested
-                System.err.println("Closing unknown FileDescriptor: " + fd);
-                new FileOutputStream(fd).close();
+                // This FD wasn't created by native_open$ravenwood().
+                // The FD was passed to the PFD ctor. Just close it.
+                Os.close(fd);
             }
-        } catch (IOException ignored) {
+        } catch (IOException | ErrnoException e) {
+            Log.w(TAG, "Exception thrown while closing fd " + fdInt, e);
         }
     }
 }
+;
\ No newline at end of file
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java
index 22e11e1..2df93cd 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java
@@ -15,6 +15,11 @@
  */
 package com.android.platform.test.ravenwood.nativesubstitution;
 
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import java.io.FileDescriptor;
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
@@ -31,6 +36,8 @@
  * {@link ByteBuffer} wouldn't allow...)
  */
 public class Parcel_host {
+    private static final String TAG = "Parcel";
+
     private Parcel_host() {
     }
 
@@ -50,6 +57,11 @@
     // TODO Use the actual value from Parcel.java.
     private static final int OK = 0;
 
+    private final Map<Integer, FileDescriptor> mFdMap = new ConcurrentHashMap<>();
+
+    private static final int FD_PLACEHOLDER = 0xDEADBEEF;
+    private static final int FD_PAYLOAD_SIZE = 8;
+
     private void validate() {
         if (mDeleted) {
             // TODO: Put more info
@@ -67,6 +79,7 @@
         return p;
     }
 
+    /** Native method substitution */
     public static long nativeCreate() {
         final long id = sNextId.getAndIncrement();
         final Parcel_host p = new Parcel_host();
@@ -80,7 +93,8 @@
         mSize = 0;
         mPos = 0;
         mSensitive = false;
-        mAllowFds = false;
+        mAllowFds = true;
+        mFdMap.clear();
     }
 
     private void updateSize() {
@@ -89,16 +103,19 @@
         }
     }
 
+    /** Native method substitution */
     public static void nativeDestroy(long nativePtr) {
         getInstance(nativePtr).mDeleted = true;
         sInstances.remove(nativePtr);
     }
 
+    /** Native method substitution */
     public static void nativeFreeBuffer(long nativePtr) {
         getInstance(nativePtr).freeBuffer();
     }
 
-    public void freeBuffer() {
+    /** Native method substitution */
+    private void freeBuffer() {
         init();
     }
 
@@ -137,32 +154,47 @@
         }
     }
 
+    /** Native method substitution */
     public static void nativeMarkSensitive(long nativePtr) {
         getInstance(nativePtr).mSensitive = true;
     }
+
+    /** Native method substitution */
     public static int nativeDataSize(long nativePtr) {
         return getInstance(nativePtr).mSize;
     }
+
+    /** Native method substitution */
     public static int nativeDataAvail(long nativePtr) {
         var p = getInstance(nativePtr);
         return p.mSize - p.mPos;
     }
+
+    /** Native method substitution */
     public static int nativeDataPosition(long nativePtr) {
         return getInstance(nativePtr).mPos;
     }
+
+    /** Native method substitution */
     public static int nativeDataCapacity(long nativePtr) {
         return getInstance(nativePtr).mBuffer.length;
     }
+
+    /** Native method substitution */
     public static void nativeSetDataSize(long nativePtr, int size) {
         var p = getInstance(nativePtr);
         p.ensureCapacity(size);
         getInstance(nativePtr).mSize = size;
     }
+
+    /** Native method substitution */
     public static void nativeSetDataPosition(long nativePtr, int pos) {
         var p = getInstance(nativePtr);
         // TODO: Should this change the size or the capacity??
         p.mPos = pos;
     }
+
+    /** Native method substitution */
     public static void nativeSetDataCapacity(long nativePtr, int size) {
         if (size < 0) {
             throw new IllegalArgumentException("size < 0: size=" + size);
@@ -173,20 +205,25 @@
         }
     }
 
+    /** Native method substitution */
     public static boolean nativePushAllowFds(long nativePtr, boolean allowFds) {
         var p = getInstance(nativePtr);
         var prev = p.mAllowFds;
         p.mAllowFds = allowFds;
         return prev;
     }
+
+    /** Native method substitution */
     public static void nativeRestoreAllowFds(long nativePtr, boolean lastValue) {
         getInstance(nativePtr).mAllowFds = lastValue;
     }
 
+    /** Native method substitution */
     public static void nativeWriteByteArray(long nativePtr, byte[] b, int offset, int len) {
         nativeWriteBlob(nativePtr, b, offset, len);
     }
 
+    /** Native method substitution */
     public static void nativeWriteBlob(long nativePtr, byte[] b, int offset, int len) {
         var p = getInstance(nativePtr);
 
@@ -205,6 +242,7 @@
         }
     }
 
+    /** Native method substitution */
     public static int nativeWriteInt(long nativePtr, int value) {
         var p = getInstance(nativePtr);
         p.ensureMoreCapacity(Integer.BYTES);
@@ -219,14 +257,19 @@
         return OK;
     }
 
+    /** Native method substitution */
     public static int nativeWriteLong(long nativePtr, long value) {
         nativeWriteInt(nativePtr, (int) (value >>> 32));
         nativeWriteInt(nativePtr, (int) (value));
         return OK;
     }
+
+    /** Native method substitution */
     public static int nativeWriteFloat(long nativePtr, float val) {
         return nativeWriteInt(nativePtr, Float.floatToIntBits(val));
     }
+
+    /** Native method substitution */
     public static int nativeWriteDouble(long nativePtr, double val) {
         return nativeWriteLong(nativePtr, Double.doubleToLongBits(val));
     }
@@ -235,6 +278,7 @@
         return ((val + 3) / 4) * 4;
     }
 
+    /** Native method substitution */
     public static void nativeWriteString8(long nativePtr, String val) {
         if (val == null) {
             nativeWriteBlob(nativePtr, null, 0, 0);
@@ -243,15 +287,19 @@
             nativeWriteBlob(nativePtr, bytes, 0, bytes.length);
         }
     }
+
+    /** Native method substitution */
     public static void nativeWriteString16(long nativePtr, String val) {
         // Just reuse String8
         nativeWriteString8(nativePtr, val);
     }
 
+    /** Native method substitution */
     public static byte[] nativeCreateByteArray(long nativePtr) {
         return nativeReadBlob(nativePtr);
     }
 
+    /** Native method substitution */
     public static boolean nativeReadByteArray(long nativePtr, byte[] dest, int destLen) {
         if (dest == null) {
             return false;
@@ -271,6 +319,7 @@
         return true;
     }
 
+    /** Native method substitution */
     public static byte[] nativeReadBlob(long nativePtr) {
         var p = getInstance(nativePtr);
         if (p.mSize - p.mPos < 4) {
@@ -295,6 +344,8 @@
 
         return bytes;
     }
+
+    /** Native method substitution */
     public static int nativeReadInt(long nativePtr) {
         var p = getInstance(nativePtr);
 
@@ -310,19 +361,24 @@
 
         return ret;
     }
+
+    /** Native method substitution */
     public static long nativeReadLong(long nativePtr) {
         return (((long) nativeReadInt(nativePtr)) << 32)
                 | (((long) nativeReadInt(nativePtr)) & 0xffff_ffffL);
     }
 
+    /** Native method substitution */
     public static float nativeReadFloat(long nativePtr) {
         return Float.intBitsToFloat(nativeReadInt(nativePtr));
     }
 
+    /** Native method substitution */
     public static double nativeReadDouble(long nativePtr) {
         return Double.longBitsToDouble(nativeReadLong(nativePtr));
     }
 
+    /** Native method substitution */
     public static String nativeReadString8(long nativePtr) {
         final var bytes = nativeReadBlob(nativePtr);
         if (bytes == null) {
@@ -334,10 +390,13 @@
         return nativeReadString8(nativePtr);
     }
 
+    /** Native method substitution */
     public static byte[] nativeMarshall(long nativePtr) {
         var p = getInstance(nativePtr);
         return Arrays.copyOf(p.mBuffer, p.mSize);
     }
+
+    /** Native method substitution */
     public static void nativeUnmarshall(
             long nativePtr, byte[] data, int offset, int length) {
         var p = getInstance(nativePtr);
@@ -346,6 +405,8 @@
         p.mPos += length;
         p.updateSize();
     }
+
+    /** Native method substitution */
     public static int nativeCompareData(long thisNativePtr, long otherNativePtr) {
         var a = getInstance(thisNativePtr);
         var b = getInstance(otherNativePtr);
@@ -355,6 +416,8 @@
             return -1;
         }
     }
+
+    /** Native method substitution */
     public static boolean nativeCompareDataInRange(
             long ptrA, int offsetA, long ptrB, int offsetB, int length) {
         var a = getInstance(ptrA);
@@ -368,6 +431,8 @@
         return Arrays.equals(Arrays.copyOfRange(a.mBuffer, offsetA, offsetA + length),
                 Arrays.copyOfRange(b.mBuffer, offsetB, offsetB + length));
     }
+
+    /** Native method substitution */
     public static void nativeAppendFrom(
             long thisNativePtr, long otherNativePtr, int srcOffset, int length) {
         var dst = getInstance(thisNativePtr);
@@ -382,25 +447,83 @@
         // TODO: Update the other's position?
     }
 
-    public static boolean nativeHasFileDescriptors(long nativePtr) {
-        // Assume false for now, because we don't support writing FDs yet.
-        return false;
-    }
-
-    public static boolean nativeHasFileDescriptorsInRange(
-            long nativePtr, int offset, int length) {
-        // Assume false for now, because we don't support writing FDs yet.
-        return false;
-    }
-
+    /** Native method substitution */
     public static boolean nativeHasBinders(long nativePtr) {
         // Assume false for now, because we don't support adding binders.
         return false;
     }
 
+    /** Native method substitution */
     public static boolean nativeHasBindersInRange(
             long nativePtr, int offset, int length) {
         // Assume false for now, because we don't support writing FDs yet.
         return false;
     }
-}
+
+    /** Native method substitution */
+    public static void nativeWriteFileDescriptor(long nativePtr, java.io.FileDescriptor val) {
+        var p = getInstance(nativePtr);
+
+        if (!p.mAllowFds) {
+            // Simulate the FDS_NOT_ALLOWED case in frameworks/base/core/jni/android_util_Binder.cpp
+            throw new RuntimeException("Not allowed to write file descriptors here");
+        }
+
+        FileDescriptor dup = null;
+        try {
+            dup = Os.dup(val);
+        } catch (ErrnoException e) {
+            throw new RuntimeException(e);
+        }
+        p.mFdMap.put(p.mPos, dup);
+
+        // Parcel.cpp writes two int32s for a FD.
+        // Make sure FD_PAYLOAD_SIZE is in sync with this code.
+        nativeWriteInt(nativePtr, FD_PLACEHOLDER);
+        nativeWriteInt(nativePtr, FD_PLACEHOLDER);
+    }
+
+    /** Native method substitution */
+    public static java.io.FileDescriptor nativeReadFileDescriptor(long nativePtr) {
+        var p = getInstance(nativePtr);
+
+        var pos = p.mPos;
+        var fd = p.mFdMap.get(pos);
+
+        if (fd == null) {
+            Log.w(TAG, "nativeReadFileDescriptor: Not a FD at pos #" + pos);
+            return null;
+        }
+        nativeReadInt(nativePtr);
+        return fd;
+    }
+
+    /** Native method substitution */
+    public static boolean nativeHasFileDescriptors(long nativePtr) {
+        var p = getInstance(nativePtr);
+        return p.mFdMap.size() > 0;
+    }
+
+    /** Native method substitution */
+    public static boolean nativeHasFileDescriptorsInRange(long nativePtr, int offset, int length) {
+        var p = getInstance(nativePtr);
+
+        // Original code: hasFileDescriptorsInRange() in frameworks/native/libs/binder/Parcel.cpp
+        if (offset < 0 || length < 0) {
+            throw new IllegalArgumentException("Negative value not allowed: offset=" + offset
+                    + " length=" + length);
+        }
+        long limit = (long) offset + (long) length;
+        if (limit > p.mSize) {
+            throw new IllegalArgumentException("Out of range: offset=" + offset
+                    + " length=" + length + " dataSize=" + p.mSize);
+        }
+
+        for (var pos : p.mFdMap.keySet()) {
+            if (offset <= pos && (pos + FD_PAYLOAD_SIZE - 1) < (offset + length)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
index 8a1fe62..825ab72 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
@@ -53,4 +53,9 @@
     public static StructStat stat(String path) throws ErrnoException {
         return RavenwoodRuntimeNative.stat(path);
     }
+
+    /** Ravenwood version of the OS API. */
+    public static void close(FileDescriptor fd) throws ErrnoException {
+        RavenwoodRuntimeNative.close(fd);
+    }
 }
diff --git a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
index e9b305e..2bc8e71 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
@@ -48,6 +48,8 @@
 
     public static native StructStat stat(String path) throws ErrnoException;
 
+    private static native void nClose(int fd) throws ErrnoException;
+
     public static long lseek(FileDescriptor fd, long offset, int whence) throws ErrnoException {
         return nLseek(JvmWorkaround.getInstance().getFdInt(fd), offset, whence);
     }
@@ -83,4 +85,11 @@
 
         return nFstat(fdInt);
     }
+
+    /** See close(2) */
+    public static void close(FileDescriptor fd) throws ErrnoException {
+        var fdInt = JvmWorkaround.getInstance().getFdInt(fd);
+
+        nClose(fdInt);
+    }
 }
diff --git a/ravenwood/runtime-jni/ravenwood_runtime.cpp b/ravenwood/runtime-jni/ravenwood_runtime.cpp
index e0a3e1c..ee84954 100644
--- a/ravenwood/runtime-jni/ravenwood_runtime.cpp
+++ b/ravenwood/runtime-jni/ravenwood_runtime.cpp
@@ -167,6 +167,11 @@
     return doStat(env, javaPath, false);
 }
 
+static void nClose(JNIEnv* env, jclass, jint fd) {
+    // Don't use TEMP_FAILURE_RETRY() on close(): https://lkml.org/lkml/2005/9/10/129
+    throwIfMinusOne(env, "close", close(fd));
+}
+
 // ---- Registration ----
 
 static const JNINativeMethod sMethods[] =
@@ -179,6 +184,7 @@
     { "nFstat", "(I)Landroid/system/StructStat;", (void*)nFstat },
     { "lstat", "(Ljava/lang/String;)Landroid/system/StructStat;", (void*)Linux_lstat },
     { "stat", "(Ljava/lang/String;)Landroid/system/StructStat;", (void*)Linux_stat },
+    { "nClose", "(I)V", (void*)nClose },
 };
 
 extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 30c743e..9067cda 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -41,6 +41,7 @@
 import static android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED;
 import static android.view.accessibility.AccessibilityManager.FlashNotificationReason;
 
+import static com.android.hardware.input.Flags.keyboardA11yMouseKeys;
 import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME;
 import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME;
 import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
@@ -57,7 +58,6 @@
 import static com.android.internal.util.FunctionalUtils.ignoreRemoteException;
 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
 import static com.android.server.accessibility.AccessibilityUserState.doesShortcutTargetsStringContain;
-import static com.android.hardware.input.Flags.keyboardA11yMouseKeys;
 import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
 
 import android.accessibilityservice.AccessibilityGestureEvent;
@@ -825,25 +825,27 @@
     @VisibleForTesting
     boolean onPackagesForceStoppedLocked(
             String[] packages, AccessibilityUserState userState) {
-        final List<String> continuousServicePackages =
+        final Set<String> packageSet = new HashSet<>(List.of(packages));
+        final ArrayList<ComponentName> continuousServices = new ArrayList<>(
                 userState.mInstalledServices.stream().filter(service ->
                         (service.flags & FLAG_REQUEST_ACCESSIBILITY_BUTTON)
                                 == FLAG_REQUEST_ACCESSIBILITY_BUTTON
-                ).map(service -> service.getComponentName().flattenToString()).toList();
+                ).map(AccessibilityServiceInfo::getComponentName).toList());
+
+        // Filter out continuous packages that are not from the array of stopped packages.
+        continuousServices.removeIf(
+                continuousName -> !packageSet.contains(continuousName.getPackageName()));
 
         boolean enabledServicesChanged = false;
         final Iterator<ComponentName> it = userState.mEnabledServices.iterator();
         while (it.hasNext()) {
             final ComponentName comp = it.next();
             final String compPkg = comp.getPackageName();
-            for (String pkg : packages) {
-                if (compPkg.equals(pkg)) {
-                    it.remove();
-                    userState.getBindingServicesLocked().remove(comp);
-                    userState.getCrashedServicesLocked().remove(comp);
-                    enabledServicesChanged = true;
-                    break;
-                }
+            if (packageSet.contains(compPkg)) {
+                it.remove();
+                userState.getBindingServicesLocked().remove(comp);
+                userState.getCrashedServicesLocked().remove(comp);
+                enabledServicesChanged = true;
             }
         }
         if (enabledServicesChanged) {
@@ -855,8 +857,8 @@
         // Remove any button targets that match any stopped continuous services
         Set<String> buttonTargets = userState.getShortcutTargetsLocked(SOFTWARE);
         boolean buttonTargetsChanged = buttonTargets.removeIf(
-                target -> continuousServicePackages.stream().anyMatch(
-                        pkg -> Objects.equals(target, pkg)));
+                target -> continuousServices.stream().anyMatch(
+                        continuousName -> continuousName.flattenToString().equals(target)));
         if (buttonTargetsChanged) {
             userState.updateShortcutTargetsLocked(buttonTargets, SOFTWARE);
             persistColonDelimitedSetToSettingLocked(
@@ -2641,7 +2643,8 @@
         }
     }
 
-    private <T> void persistColonDelimitedSetToSettingLocked(String settingName, int userId,
+    @VisibleForTesting
+    <T> void persistColonDelimitedSetToSettingLocked(String settingName, int userId,
             Set<T> set, Function<T, String> toString) {
         persistColonDelimitedSetToSettingLocked(settingName, userId, set,
                 toString, /* defaultEmptyString= */ null);
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 7f43fae..3123268 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -174,6 +174,7 @@
         "haptics",
         "hardware_backed_security_mainline",
         "input",
+        "incremental",
         "llvm_and_toolchains",
         "lse_desktop_experience",
         "machine_learning",
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index ca69f31..8d8a54e 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -31,6 +31,7 @@
 import static android.media.audio.Flags.automaticBtDeviceType;
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+import static com.android.media.audio.Flags.asDeviceConnectionFailure;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -529,6 +530,17 @@
         }
     };
 
+    /**
+     * package-protected for unit testing only
+     * Returns the currently connected devices
+     * @return the collection of connected devices
+     */
+    /*package*/ @NonNull Collection<DeviceInfo> getConnectedDevices() {
+        synchronized (mDevicesLock) {
+            return mConnectedDevices.values();
+        }
+    }
+
     // List of devices actually connected to AudioPolicy (through AudioSystem), only one
     // by device type, which is used as the key, value is the DeviceInfo generated key.
     // For the moment only for A2DP sink devices.
@@ -598,8 +610,9 @@
     /**
      * Class to store info about connected devices.
      * Use makeDeviceListKey() to make a unique key for this list.
+     * Package-protected for unit tests
      */
-    private static class DeviceInfo {
+    /*package*/ static class DeviceInfo {
         final int mDeviceType;
         final @NonNull String mDeviceName;
         final @NonNull String mDeviceAddress;
@@ -762,13 +775,27 @@
     // Always executed on AudioDeviceBroker message queue
     /*package*/ void onRestoreDevices() {
         synchronized (mDevicesLock) {
+            int res;
+            List<DeviceInfo> failedReconnectionDeviceList = new ArrayList<>(/*initialCapacity*/ 0);
             //TODO iterate on mApmConnectedDevices instead once it handles all device types
             for (DeviceInfo di : mConnectedDevices.values()) {
-                mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes(di.mDeviceType,
+                res = mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes(
+                        di.mDeviceType,
                         di.mDeviceAddress,
                         di.mDeviceName),
                         AudioSystem.DEVICE_STATE_AVAILABLE,
                         di.mDeviceCodecFormat);
+                if (asDeviceConnectionFailure() && res != AudioSystem.AUDIO_STATUS_OK) {
+                    failedReconnectionDeviceList.add(di);
+                }
+            }
+            if (asDeviceConnectionFailure()) {
+                for (DeviceInfo di : failedReconnectionDeviceList) {
+                    AudioService.sDeviceLogger.enqueueAndSlog(
+                            "Device inventory restore failed to reconnect " + di,
+                            EventLogger.Event.ALOGE, TAG);
+                    mConnectedDevices.remove(di.getKey(), di);
+                }
             }
             mAppliedStrategyRolesInt.clear();
             mAppliedPresetRolesInt.clear();
@@ -2070,8 +2097,9 @@
                     "APM failed to make available A2DP device addr="
                             + Utils.anonymizeBluetoothAddress(address)
                             + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG));
-            // TODO: connection failed, stop here
-            // TODO: return;
+            if (asDeviceConnectionFailure()) {
+                return;
+            }
         } else {
             AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                     "A2DP sink device addr=" + Utils.anonymizeBluetoothAddress(address)
@@ -2336,8 +2364,7 @@
                     "APM failed to make unavailable A2DP device addr="
                             + Utils.anonymizeBluetoothAddress(address)
                             + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG));
-            // TODO:  failed to disconnect, stop here
-            // TODO: return;
+            // not taking further action: proceeding as if disconnection from APM worked
         } else {
             AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
                     "A2DP device addr=" + Utils.anonymizeBluetoothAddress(address)
@@ -2383,8 +2410,9 @@
                     "APM failed to make available A2DP source device addr="
                             + Utils.anonymizeBluetoothAddress(address)
                             + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG));
-            // TODO: connection failed, stop here
-            // TODO: return
+            if (asDeviceConnectionFailure()) {
+                return;
+            }
         } else {
             AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                     "A2DP source device addr=" + Utils.anonymizeBluetoothAddress(address)
@@ -2402,6 +2430,7 @@
         mAudioSystem.setDeviceConnectionState(ada,
                 AudioSystem.DEVICE_STATE_UNAVAILABLE,
                 AudioSystem.AUDIO_FORMAT_DEFAULT);
+        // always remove regardless of the result
         mConnectedDevices.remove(
                 DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address));
         mDeviceBroker.postCheckCommunicationDeviceRemoval(ada);
@@ -2418,9 +2447,18 @@
 
         AudioDeviceAttributes ada = new AudioDeviceAttributes(
                 DEVICE_OUT_HEARING_AID, address, name);
-        mAudioSystem.setDeviceConnectionState(ada,
+        final int res = mAudioSystem.setDeviceConnectionState(ada,
                 AudioSystem.DEVICE_STATE_AVAILABLE,
                 AudioSystem.AUDIO_FORMAT_DEFAULT);
+        if (asDeviceConnectionFailure() && res != AudioSystem.AUDIO_STATUS_OK) {
+            AudioService.sDeviceLogger.enqueueAndSlog(
+                    "APM failed to make available HearingAid addr=" + address
+                            + " error=" + res,
+                    EventLogger.Event.ALOGE, TAG);
+            return;
+        }
+        AudioService.sDeviceLogger.enqueueAndSlog("HearingAid made available addr=" + address,
+                EventLogger.Event.ALOGI, TAG);
         mConnectedDevices.put(
                 DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address),
                 new DeviceInfo(DEVICE_OUT_HEARING_AID, name, address));
@@ -2447,6 +2485,7 @@
         mAudioSystem.setDeviceConnectionState(ada,
                 AudioSystem.DEVICE_STATE_UNAVAILABLE,
                 AudioSystem.AUDIO_FORMAT_DEFAULT);
+        // always remove regardless of return code
         mConnectedDevices.remove(
                 DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address));
         // Remove Hearing Aid routes as well
@@ -2540,11 +2579,12 @@
             final int res = mAudioSystem.setDeviceConnectionState(ada,
                     AudioSystem.DEVICE_STATE_AVAILABLE, codec);
             if (res != AudioSystem.AUDIO_STATUS_OK) {
-                AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
+                AudioService.sDeviceLogger.enqueueAndSlog(
                         "APM failed to make available LE Audio device addr=" + address
-                                + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG));
-                // TODO: connection failed, stop here
-                // TODO: return;
+                                + " error=" + res, EventLogger.Event.ALOGE, TAG);
+                if (asDeviceConnectionFailure()) {
+                    return;
+                }
             } else {
                 AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                         "LE Audio " + (AudioSystem.isInputDevice(device) ? "source" : "sink")
@@ -2596,8 +2636,7 @@
                 AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                         "APM failed to make unavailable LE Audio device addr=" + address
                                 + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG));
-                // TODO:  failed to disconnect, stop here
-                // TODO: return;
+                // not taking further action: proceeding as if disconnection from APM worked
             } else {
                 AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                         "LE Audio device addr=" + Utils.anonymizeBluetoothAddress(address)
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 1183768..ac43e86 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -63,6 +63,7 @@
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
 import static com.android.media.audio.Flags.absVolumeIndexFix;
 import static com.android.media.audio.Flags.alarmMinVolumeZero;
+import static com.android.media.audio.Flags.asDeviceConnectionFailure;
 import static com.android.media.audio.Flags.audioserverPermissions;
 import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume;
 import static com.android.media.audio.Flags.replaceStreamBtSco;
@@ -4306,7 +4307,8 @@
         super.getVolumeGroupVolumeIndex_enforcePermission();
         synchronized (VolumeStreamState.class) {
             if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
-                throw new IllegalArgumentException("No volume group for id " + groupId);
+                Log.e(TAG, "No volume group for id " + groupId);
+                return 0;
             }
             VolumeGroupState vgs = sVolumeGroupStates.get(groupId);
             // Return 0 when muted, not min index since for e.g. Voice Call, it has a non zero
@@ -4322,7 +4324,8 @@
         super.getVolumeGroupMaxVolumeIndex_enforcePermission();
         synchronized (VolumeStreamState.class) {
             if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
-                throw new IllegalArgumentException("No volume group for id " + groupId);
+                Log.e(TAG, "No volume group for id " + groupId);
+                return 0;
             }
             VolumeGroupState vgs = sVolumeGroupStates.get(groupId);
             return vgs.getMaxIndex();
@@ -4336,7 +4339,8 @@
         super.getVolumeGroupMinVolumeIndex_enforcePermission();
         synchronized (VolumeStreamState.class) {
             if (sVolumeGroupStates.indexOfKey(groupId) < 0) {
-                throw new IllegalArgumentException("No volume group for id " + groupId);
+                Log.e(TAG, "No volume group for id " + groupId);
+                return 0;
             }
             VolumeGroupState vgs = sVolumeGroupStates.get(groupId);
             return vgs.getMinIndex();
@@ -4765,6 +4769,8 @@
     private void dumpFlags(PrintWriter pw) {
 
         pw.println("\nFun with Flags:");
+        pw.println("\tcom.android.media.audio.as_device_connection_failure:"
+                + asDeviceConnectionFailure());
         pw.println("\tandroid.media.audio.autoPublicVolumeApiHardening:"
                 + autoPublicVolumeApiHardening());
         pw.println("\tandroid.media.audio.Flags.automaticBtDeviceType:"
@@ -8259,11 +8265,21 @@
     private static final SparseArray<VolumeGroupState> sVolumeGroupStates = new SparseArray<>();
 
     private void initVolumeGroupStates() {
+        int btScoGroupId = -1;
+        VolumeGroupState voiceCallGroup = null;
         for (final AudioVolumeGroup avg : getAudioVolumeGroups()) {
             try {
-                // if no valid attributes, this volume group is not controllable
-                if (ensureValidAttributes(avg)) {
-                    sVolumeGroupStates.append(avg.getId(), new VolumeGroupState(avg));
+                if (ensureValidVolumeGroup(avg)) {
+                    final VolumeGroupState vgs = new VolumeGroupState(avg);
+                    sVolumeGroupStates.append(avg.getId(), vgs);
+                    if (vgs.isVoiceCall()) {
+                        voiceCallGroup = vgs;
+                    }
+                } else {
+                    // invalid volume group will be reported for bt sco group with no other
+                    // legacy stream type, we try to replace it in sVolumeGroupStates with the
+                    // voice call volume group
+                    btScoGroupId = avg.getId();
                 }
             } catch (IllegalArgumentException e) {
                 // Volume Groups without attributes are not controllable through set/get volume
@@ -8271,10 +8287,15 @@
                 if (DEBUG_VOL) {
                     Log.d(TAG, "volume group " + avg.name() + " for internal policy needs");
                 }
-                continue;
             }
         }
 
+        if (replaceStreamBtSco() && btScoGroupId >= 0 && voiceCallGroup != null) {
+            // the bt sco group is deprecated, storing the voice call group instead
+            // to keep the code backwards compatible when calling the volume group APIs
+            sVolumeGroupStates.append(btScoGroupId, voiceCallGroup);
+        }
+
         // need mSettingsLock for vgs.applyAllVolumes -> vss.setIndex which grabs this lock after
         // VSS.class. Locking order needs to be preserved
         synchronized (mSettingsLock) {
@@ -8285,7 +8306,15 @@
         }
     }
 
-    private boolean ensureValidAttributes(AudioVolumeGroup avg) {
+    /**
+     * Returns false if the legacy stream types only contains the deprecated
+     * {@link AudioSystem#STREAM_BLUETOOTH_SCO}.
+     *
+     * @throws IllegalArgumentException if it has more than one non-default {@link AudioAttributes}
+     *
+     * @param avg the volume group to check
+     */
+    private boolean ensureValidVolumeGroup(AudioVolumeGroup avg) {
         boolean hasAtLeastOneValidAudioAttributes = avg.getAudioAttributes().stream()
                 .anyMatch(aa -> !aa.equals(AudioProductStrategy.getDefaultAttributes()));
         if (!hasAtLeastOneValidAudioAttributes) {
@@ -8293,10 +8322,11 @@
                     + " has no valid audio attributes");
         }
         if (replaceStreamBtSco()) {
-            for (int streamType : avg.getLegacyStreamTypes()) {
-                if (streamType == AudioSystem.STREAM_BLUETOOTH_SCO) {
-                    return false;
-                }
+            // if there are multiple legacy stream types associated we can omit stream bt sco
+            // otherwise this is not a valid volume group
+            if (avg.getLegacyStreamTypes().length == 1
+                    && avg.getLegacyStreamTypes()[0] == AudioSystem.STREAM_BLUETOOTH_SCO) {
+                return false;
             }
         }
         return true;
@@ -8637,6 +8667,10 @@
             return mHasValidStreamType && mPublicStreamType == AudioSystem.STREAM_MUSIC;
         }
 
+        public boolean isVoiceCall() {
+            return mHasValidStreamType && mPublicStreamType == AudioSystem.STREAM_VOICE_CALL;
+        }
+
         public void applyAllVolumes(boolean userSwitch) {
             String caller = "from vgs";
             synchronized (AudioService.VolumeStreamState.class) {
diff --git a/services/core/java/com/android/server/crashrecovery/TEST_MAPPING b/services/core/java/com/android/server/crashrecovery/TEST_MAPPING
index 537fb325..615db34 100644
--- a/services/core/java/com/android/server/crashrecovery/TEST_MAPPING
+++ b/services/core/java/com/android/server/crashrecovery/TEST_MAPPING
@@ -1,9 +1,4 @@
 {
-  "presubmit": [
-    {
-      "name": "CrashRecoveryModuleTests"
-    }
-  ],
   "postsubmit": [
     {
       "name": "FrameworksMockingServicesTests",
@@ -12,6 +7,9 @@
           "include-filter": "com.android.server.RescuePartyTest"
         }
       ]
+    },
+    {
+      "name": "CrashRecoveryModuleTests"
     }
   ]
 }
\ No newline at end of file
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
index 13209d8..dba0465 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
@@ -75,13 +75,13 @@
     public abstract void setInteractive(boolean interactive);
 
     /**
-     * Hides the input methods for all the users, if visible.
+     * Hides the input method for the specified {@code originatingDisplayId}, if visible.
      *
      * @param reason               the reason for hiding the current input method
      * @param originatingDisplayId the display ID the request is originated
      */
     @ImfLockFree
-    public abstract void hideAllInputMethods(@SoftInputShowHideReason int reason,
+    public abstract void hideInputMethod(@SoftInputShowHideReason int reason,
             int originatingDisplayId);
 
     /**
@@ -315,7 +315,7 @@
 
                 @ImfLockFree
                 @Override
-                public void hideAllInputMethods(@SoftInputShowHideReason int reason,
+                public void hideInputMethod(@SoftInputShowHideReason int reason,
                         int originatingDisplayId) {
                 }
 
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 7ff03c2..d9bc467 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -254,7 +254,7 @@
     private @interface MultiUserUnawareField {
     }
 
-    private static final int MSG_HIDE_ALL_INPUT_METHODS = 1035;
+    private static final int MSG_HIDE_INPUT_METHOD = 1035;
     private static final int MSG_REMOVE_IME_SURFACE = 1060;
     private static final int MSG_REMOVE_IME_SURFACE_FROM_WINDOW = 1061;
 
@@ -997,8 +997,6 @@
                     Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */);
             ioThread.start();
 
-            SecureSettingsWrapper.setContentResolver(context.getContentResolver());
-
             return new InputMethodManagerService(context,
                     shouldEnableConcurrentMultiUserMode(context), thread.getLooper(),
                     Handler.createAsync(ioThread.getLooper()),
@@ -1057,7 +1055,6 @@
         public void onUserRemoved(UserInfo user) {
             // Called directly from UserManagerService. Do not block the calling thread.
             final int userId = user.id;
-            SecureSettingsWrapper.onUserRemoved(userId);
             AdditionalSubtypeMapRepository.remove(userId);
             InputMethodSettingsRepository.remove(userId);
             mService.mUserDataRepository.remove(userId);
@@ -1163,6 +1160,7 @@
             mConcurrentMultiUserModeEnabled = concurrentMultiUserModeEnabled;
             mContext = context;
             mRes = context.getResources();
+            SecureSettingsWrapper.onStart(mContext);
 
             mHandler = Handler.createAsync(uiLooper, this);
             mIoHandler = ioHandler;
@@ -1846,13 +1844,6 @@
         }
     }
 
-    @VisibleForTesting
-    void setAttachedClientForTesting(@NonNull ClientState cs) {
-        synchronized (ImfLock.class) {
-            getUserData(mCurrentUserId).mCurClient = cs;
-        }
-    }
-
     @GuardedBy("ImfLock.class")
     private boolean isShowRequestedForCurrentWindow(@UserIdInt int userId) {
         final var userData = getUserData(userId);
@@ -5034,7 +5025,7 @@
             return;
         }
 
-        if (Flags.imeSwitcherRevamp()) {
+        if (mNewInputMethodSwitcherMenuEnabled) {
             if (DEBUG) {
                 Slog.v(TAG, "Show IME switcher menu,"
                         + " showAuxSubtypes=" + showAuxSubtypes
@@ -5066,7 +5057,7 @@
     @Override
     public boolean handleMessage(Message msg) {
         switch (msg.what) {
-            case MSG_HIDE_ALL_INPUT_METHODS: {
+            case MSG_HIDE_INPUT_METHOD: {
                 @SoftInputShowHideReason final int reason = msg.arg1;
                 final int originatingDisplayId = msg.arg2;
                 synchronized (ImfLock.class) {
@@ -5802,10 +5793,10 @@
 
         @ImfLockFree
         @Override
-        public void hideAllInputMethods(@SoftInputShowHideReason int reason,
+        public void hideInputMethod(@SoftInputShowHideReason int reason,
                 int originatingDisplayId) {
-            mHandler.removeMessages(MSG_HIDE_ALL_INPUT_METHODS);
-            mHandler.obtainMessage(MSG_HIDE_ALL_INPUT_METHODS, reason, originatingDisplayId)
+            mHandler.removeMessages(MSG_HIDE_INPUT_METHOD);
+            mHandler.obtainMessage(MSG_HIDE_INPUT_METHOD, reason, originatingDisplayId)
                     .sendToTarget();
         }
 
diff --git a/services/core/java/com/android/server/inputmethod/SecureSettingsWrapper.java b/services/core/java/com/android/server/inputmethod/SecureSettingsWrapper.java
index b3500be..c208a5b 100644
--- a/services/core/java/com/android/server/inputmethod/SecureSettingsWrapper.java
+++ b/services/core/java/com/android/server/inputmethod/SecureSettingsWrapper.java
@@ -20,7 +20,10 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
+import android.app.ActivityManagerInternal;
 import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.UserInfo;
 import android.provider.Settings;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -318,13 +321,30 @@
     }
 
     /**
-     * Called when the system is starting.
+     * Called when {@link InputMethodManagerService} is starting.
      *
-     * @param contentResolver the {@link ContentResolver} to be used
+     * @param context the {@link Context} to be used.
      */
     @AnyThread
-    static void setContentResolver(@NonNull ContentResolver contentResolver) {
-        sContentResolver = contentResolver;
+    static void onStart(@NonNull Context context) {
+        sContentResolver = context.getContentResolver();
+
+        final int userId = LocalServices.getService(ActivityManagerInternal.class)
+                .getCurrentUserId();
+        final UserManagerInternal userManagerInternal =
+                LocalServices.getService(UserManagerInternal.class);
+        putOrGet(userId, createImpl(userManagerInternal, userId));
+
+        userManagerInternal.addUserLifecycleListener(
+                new UserManagerInternal.UserLifecycleListener() {
+                    @Override
+                    public void onUserRemoved(UserInfo user) {
+                        synchronized (sMutationLock) {
+                            sUserMap = sUserMap.cloneWithRemoveOrSelf(user.id);
+                        }
+                    }
+                }
+        );
     }
 
     /**
@@ -357,18 +377,6 @@
     }
 
     /**
-     * Called when a user is being removed.
-     *
-     * @param userId the ID of the user whose storage is being removed.
-     */
-    @AnyThread
-    static void onUserRemoved(@UserIdInt int userId) {
-        synchronized (sMutationLock) {
-            sUserMap = sUserMap.cloneWithRemoveOrSelf(userId);
-        }
-    }
-
-    /**
      * Put the given string {@code value} to {@code key}.
      *
      * @param key a secure settings key.
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index 1a8e44b..1fdb57c 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -950,7 +950,7 @@
                 || isPackageOrComponentAllowed(component.getPackageName(), userId))) {
             return false;
         }
-        return componentHasBindPermission(component, userId);
+        return isValidService(component, userId);
     }
 
     private boolean componentHasBindPermission(ComponentName component, int userId) {
@@ -1302,11 +1302,12 @@
                     if (TextUtils.equals(getPackageName(approvedPackageOrComponent), packageName)) {
                         final ComponentName component = ComponentName.unflattenFromString(
                                 approvedPackageOrComponent);
-                        if (component != null && !componentHasBindPermission(component, userId)) {
+                        if (component != null && !isValidService(component, userId)) {
                             approved.removeAt(j);
                             if (DEBUG) {
                                 Slog.v(TAG, "Removing " + approvedPackageOrComponent
-                                        + " from approved list; no bind permission found "
+                                        + " from approved list; no bind permission or "
+                                        + "service interface filter found "
                                         + mConfig.bindPermission);
                             }
                         }
@@ -1325,6 +1326,11 @@
         }
     }
 
+    protected boolean isValidService(ComponentName component, int userId) {
+        return componentHasBindPermission(component, userId) && queryPackageForServices(
+                component.getPackageName(), userId).contains(component);
+    }
+
     protected boolean isValidEntry(String packageOrComponent, int userId) {
         return hasMatchingServices(packageOrComponent, userId);
     }
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 7534bfe..d0706d2 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -1158,8 +1158,7 @@
                     break;
                 case SHORT_PRESS_POWER_CLOSE_IME_OR_GO_HOME: {
                     if (mDismissImeOnBackKeyPressed) {
-                        // TODO(b/308479256): Check if hiding "all" IMEs is OK or not.
-                        InputMethodManagerInternal.get().hideAllInputMethods(
+                        InputMethodManagerInternal.get().hideInputMethod(
                                 SoftInputShowHideReason.HIDE_POWER_BUTTON_GO_HOME, displayId);
                     } else {
                         shortPressPowerGoHome();
diff --git a/services/core/java/com/android/server/power/hint/TEST_MAPPING b/services/core/java/com/android/server/power/hint/TEST_MAPPING
index 9988786..5450700 100644
--- a/services/core/java/com/android/server/power/hint/TEST_MAPPING
+++ b/services/core/java/com/android/server/power/hint/TEST_MAPPING
@@ -1,5 +1,5 @@
 {
-  "postsubmit": [
+  "presubmit": [
     {
       "name": "PerformanceHintTests",
       "options": [
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 85c8900..e9423ce 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -1890,8 +1890,7 @@
         enforceStatusBarService();
         final long token = Binder.clearCallingIdentity();
         try {
-            // TODO(b/308479256): Check if hiding "all" IMEs is OK or not.
-            InputMethodManagerInternal.get().hideAllInputMethods(
+            InputMethodManagerInternal.get().hideInputMethod(
                     SoftInputShowHideReason.HIDE_BUBBLES, displayId);
         } finally {
             Binder.restoreCallingIdentity(token);
diff --git a/services/core/java/com/android/server/vibrator/HalVibration.java b/services/core/java/com/android/server/vibrator/HalVibration.java
index f9bad59..46bd7af 100644
--- a/services/core/java/com/android/server/vibrator/HalVibration.java
+++ b/services/core/java/com/android/server/vibrator/HalVibration.java
@@ -108,23 +108,6 @@
     }
 
     /**
-     * Resolves the default vibration amplitude of {@link #getEffectToPlay()} and each fallback.
-     *
-     * @param defaultAmplitude An integer in [1,255] representing the device default amplitude to
-     *                        replace the {@link VibrationEffect#DEFAULT_AMPLITUDE}.
-     */
-    public void resolveEffects(int defaultAmplitude) {
-        CombinedVibration newEffect =
-                mEffectToPlay.transform(VibrationEffect::resolve, defaultAmplitude);
-        if (!Objects.equals(mEffectToPlay, newEffect)) {
-            mEffectToPlay = newEffect;
-        }
-        for (int i = 0; i < mFallbacks.size(); i++) {
-            mFallbacks.setValueAt(i, mFallbacks.valueAt(i).resolve(defaultAmplitude));
-        }
-    }
-
-    /**
      * Scales the {@link #getEffectToPlay()} and each fallback effect based on the vibration usage.
      */
     public void scaleEffects(VibrationScaler scaler) {
diff --git a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
index 98a2ba0d..3f9da82 100644
--- a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
+++ b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
@@ -46,6 +46,8 @@
             VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK);
     private static final VibrationAttributes COMMUNICATION_REQUEST_VIBRATION_ATTRIBUTES =
             VibrationAttributes.createForUsage(VibrationAttributes.USAGE_COMMUNICATION_REQUEST);
+    private static final VibrationAttributes IME_FEEDBACK_VIBRATION_ATTRIBUTES =
+            VibrationAttributes.createForUsage(VibrationAttributes.USAGE_IME_FEEDBACK);
 
     private final VibratorInfo mVibratorInfo;
     private final boolean mHapticTextHandleEnabled;
@@ -219,8 +221,6 @@
         }
 
         int vibFlags = 0;
-        boolean fromIme =
-                (privFlags & HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS) != 0;
         boolean bypassVibrationIntensitySetting =
                 (flags & HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING) != 0;
         if (bypassVibrationIntensitySetting) {
@@ -229,9 +229,6 @@
         if (shouldBypassInterruptionPolicy(effectId)) {
             vibFlags |= VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY;
         }
-        if (shouldBypassIntensityScale(effectId, fromIme)) {
-            vibFlags |= VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE;
-        }
 
         return vibFlags == 0 ? attrs : new VibrationAttributes.Builder(attrs)
                 .setFlags(vibFlags).build();
@@ -362,22 +359,6 @@
                 /* fallbackForPredefinedEffect= */ predefinedEffectFallback);
     }
 
-    private boolean shouldBypassIntensityScale(int effectId, boolean isIme) {
-        if (!Flags.keyboardCategoryEnabled() || mKeyboardVibrationFixedAmplitude < 0 || !isIme) {
-            // Shouldn't bypass if not support keyboard category, no fixed amplitude or not an IME.
-            return false;
-        }
-        switch (effectId) {
-            case HapticFeedbackConstants.KEYBOARD_TAP:
-                return mVibratorInfo.isPrimitiveSupported(
-                        VibrationEffect.Composition.PRIMITIVE_CLICK);
-            case HapticFeedbackConstants.KEYBOARD_RELEASE:
-                return mVibratorInfo.isPrimitiveSupported(
-                        VibrationEffect.Composition.PRIMITIVE_TICK);
-        }
-        return false;
-    }
-
     private VibrationAttributes createKeyboardVibrationAttributes(
             @HapticFeedbackConstants.PrivateFlags int privFlags) {
         // Use touch attribute when the keyboard category is disable.
@@ -388,7 +369,8 @@
         if ((privFlags & HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS) == 0) {
             return TOUCH_VIBRATION_ATTRIBUTES;
         }
-        return new VibrationAttributes.Builder(TOUCH_VIBRATION_ATTRIBUTES)
+        return new VibrationAttributes.Builder(IME_FEEDBACK_VIBRATION_ATTRIBUTES)
+                // TODO(b/332661766): Remove CATEGORY_KEYBOARD logic
                 .setCategory(VibrationAttributes.CATEGORY_KEYBOARD)
                 .build();
     }
diff --git a/services/core/java/com/android/server/vibrator/VibrationSettings.java b/services/core/java/com/android/server/vibrator/VibrationSettings.java
index 0206155..fb92d60 100644
--- a/services/core/java/com/android/server/vibrator/VibrationSettings.java
+++ b/services/core/java/com/android/server/vibrator/VibrationSettings.java
@@ -21,6 +21,7 @@
 import static android.os.VibrationAttributes.USAGE_ALARM;
 import static android.os.VibrationAttributes.USAGE_COMMUNICATION_REQUEST;
 import static android.os.VibrationAttributes.USAGE_HARDWARE_FEEDBACK;
+import static android.os.VibrationAttributes.USAGE_IME_FEEDBACK;
 import static android.os.VibrationAttributes.USAGE_MEDIA;
 import static android.os.VibrationAttributes.USAGE_NOTIFICATION;
 import static android.os.VibrationAttributes.USAGE_PHYSICAL_EMULATION;
@@ -560,6 +561,7 @@
             mKeyboardVibrationOn = loadSystemSetting(
                     Settings.System.KEYBOARD_VIBRATION_ENABLED, 1, userHandle) > 0;
 
+            int keyboardIntensity = getDefaultIntensity(USAGE_IME_FEEDBACK);
             int alarmIntensity = toIntensity(
                     loadSystemSetting(Settings.System.ALARM_VIBRATION_INTENSITY, -1, userHandle),
                     getDefaultIntensity(USAGE_ALARM));
@@ -610,6 +612,12 @@
                 mCurrentVibrationIntensities.put(USAGE_TOUCH, hapticFeedbackIntensity);
             }
 
+            if (mVibrationConfig.isKeyboardVibrationSettingsSupported()) {
+                mCurrentVibrationIntensities.put(USAGE_IME_FEEDBACK, keyboardIntensity);
+            } else {
+                mCurrentVibrationIntensities.put(USAGE_IME_FEEDBACK, hapticFeedbackIntensity);
+            }
+
             // A11y is not disabled by any haptic feedback setting.
             mCurrentVibrationIntensities.put(USAGE_ACCESSIBILITY, positiveHapticFeedbackIntensity);
         }
diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
index 8c9a92d..7152844 100644
--- a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
+++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
@@ -21,7 +21,6 @@
 import android.os.Build;
 import android.os.CombinedVibration;
 import android.os.IBinder;
-import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
 import android.os.vibrator.Flags;
 import android.os.vibrator.PrebakedSegment;
@@ -177,16 +176,11 @@
             expectIsVibrationThread(true);
         }
 
-        if (!mVibration.callerInfo.attrs.isFlagSet(
-                VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)) {
-            if (Flags.adaptiveHapticsEnabled()) {
-                waitForVibrationParamsIfRequired();
-            }
-            // Scale resolves the default amplitudes from the effect before scaling them.
-            mVibration.scaleEffects(mVibrationScaler);
-        } else {
-            mVibration.resolveEffects(mVibrationScaler.getDefaultVibrationAmplitude());
+        if (Flags.adaptiveHapticsEnabled()) {
+            waitForVibrationParamsIfRequired();
         }
+        // Scale resolves the default amplitudes from the effect before scaling them.
+        mVibration.scaleEffects(mVibrationScaler);
 
         mVibration.adaptToDevice(mDeviceAdapter);
         CombinedVibration.Sequential sequentialEffect = toSequential(mVibration.getEffectToPlay());
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 48c4a68..7610d7d 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -103,8 +103,7 @@
             new VibrationAttributes.Builder().build();
     private static final int ATTRIBUTES_ALL_BYPASS_FLAGS =
             VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY
-                    | VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF
-                    | VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE;
+                    | VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF;
 
     /** Fixed large duration used to note repeating vibrations to {@link IBatteryStats}. */
     private static final long BATTERY_STATS_REPEATING_VIBRATION_DURATION = 5_000;
@@ -925,8 +924,7 @@
     private VibrationStepConductor createVibrationStepConductor(HalVibration vib) {
         CompletableFuture<Void> requestVibrationParamsFuture = null;
 
-        if (Flags.adaptiveHapticsEnabled() && !vib.callerInfo.attrs.isFlagSet(
-                VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)
+        if (Flags.adaptiveHapticsEnabled()
                 && mVibratorControlService.shouldRequestVibrationParams(
                 vib.callerInfo.attrs.getUsage())) {
             requestVibrationParamsFuture =
@@ -940,13 +938,8 @@
     }
 
     private Vibration.EndInfo startVibrationOnInputDevicesLocked(HalVibration vib) {
-        if (!vib.callerInfo.attrs.isFlagSet(
-                VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)) {
-            // Scale resolves the default amplitudes from the effect before scaling them.
-            vib.scaleEffects(mVibrationScaler);
-        } else {
-            vib.resolveEffects(mVibrationScaler.getDefaultVibrationAmplitude());
-        }
+        // Scale resolves the default amplitudes from the effect before scaling them.
+        vib.scaleEffects(mVibrationScaler);
         mInputDeviceDelegate.vibrateIfAvailable(vib.callerInfo, vib.getEffectToPlay());
 
         return new Vibration.EndInfo(Vibration.Status.FORWARDED_TO_INPUT_DEVICES);
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
index f70a3ba..d5bea4a 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
@@ -676,7 +676,12 @@
 
                     final Rect estimateCrop = new Rect(cropHint);
                     if (!multiCrop()) estimateCrop.scale(1f / options.inSampleSize);
-                    else estimateCrop.scale(1f / sampleSize);
+                    else {
+                        estimateCrop.left = (int) Math.floor(estimateCrop.left / sampleSize);
+                        estimateCrop.top = (int) Math.floor(estimateCrop.top / sampleSize);
+                        estimateCrop.right = (int) Math.ceil(estimateCrop.right / sampleSize);
+                        estimateCrop.bottom = (int) Math.ceil(estimateCrop.bottom / sampleSize);
+                    }
                     float hRatio = (float) wpData.mHeight / estimateCrop.height();
                     final int destHeight = (int) (estimateCrop.height() * hRatio);
                     final int destWidth = (int) (estimateCrop.width() * hRatio);
@@ -720,7 +725,10 @@
                         }
                         if (multiCrop()) {
                             Slog.v(TAG, "  cropHint=" + cropHint);
+                            Slog.v(TAG, "  estimateCrop=" + estimateCrop);
                             Slog.v(TAG, "  sampleSize=" + sampleSize);
+                            Slog.v(TAG, "  user defined crops: " + wallpaper.mCropHints);
+                            Slog.v(TAG, "  all crops: " + defaultCrops);
                         }
                         Slog.v(TAG, "  targetSize=" + safeWidth + "x" + safeHeight);
                         Slog.v(TAG, "  maxTextureSize=" + GLHelper.getMaxTextureSize());
diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
index 3b99954..1994174 100644
--- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
+++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
@@ -52,7 +52,7 @@
  * display change transition. In this case, we will queue all display updates until the current
  * transition's collection finishes and then apply them afterwards.
  */
-public class DeferredDisplayUpdater implements DisplayUpdater {
+class DeferredDisplayUpdater {
 
     /**
      * List of fields that could be deferred before applying to DisplayContent.
@@ -110,7 +110,7 @@
         continueScreenUnblocking();
     };
 
-    public DeferredDisplayUpdater(@NonNull DisplayContent displayContent) {
+    DeferredDisplayUpdater(@NonNull DisplayContent displayContent) {
         mDisplayContent = displayContent;
         mNonOverrideDisplayInfo.copyFrom(mDisplayContent.getDisplayInfo());
     }
@@ -122,8 +122,7 @@
      *
      * @param finishCallback is called when all pending display updates are finished
      */
-    @Override
-    public void updateDisplayInfo(@NonNull Runnable finishCallback) {
+    void updateDisplayInfo(@NonNull Runnable finishCallback) {
         // Get the latest display parameters from the DisplayManager
         final DisplayInfo displayInfo = getCurrentDisplayInfo();
 
@@ -310,9 +309,11 @@
         return !Objects.equals(first.uniqueId, second.uniqueId);
     }
 
-    @Override
-    public void onDisplayContentDisplayPropertiesPostChanged(int previousRotation, int newRotation,
-            DisplayAreaInfo newDisplayAreaInfo) {
+    /**
+     * Called after physical display has changed and after DisplayContent applied new display
+     * properties.
+     */
+    void onDisplayContentDisplayPropertiesPostChanged() {
         // Unblock immediately in case there is no transition. This is unlikely to happen.
         if (mScreenUnblocker != null && !mDisplayContent.mTransitionController.inTransition()) {
             mScreenUnblocker.sendToTarget();
@@ -320,13 +321,16 @@
         }
     }
 
-    @Override
-    public void onDisplaySwitching(boolean switching) {
+    /**
+     * Called with {@code true} when physical display is going to switch. And {@code false} when
+     * the display is turned on or the device goes to sleep.
+     */
+    void onDisplaySwitching(boolean switching) {
         mShouldWaitForTransitionWhenScreenOn = switching;
     }
 
-    @Override
-    public boolean waitForTransition(@NonNull Message screenUnblocker) {
+    /** Returns {@code true} if the transition will control when to turn on the screen. */
+    boolean waitForTransition(@NonNull Message screenUnblocker) {
         if (!Flags.waitForTransitionOnDisplaySwitch()) return false;
         if (!mShouldWaitForTransitionWhenScreenOn) {
             return false;
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 403c307..b31ae90 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -159,7 +159,6 @@
 import static com.android.server.wm.utils.DisplayInfoOverrides.copyDisplayInfoFields;
 import static com.android.server.wm.utils.RegionUtils.forEachRectReverse;
 import static com.android.server.wm.utils.RegionUtils.rectListToRegion;
-import static com.android.window.flags.Flags.deferDisplayUpdates;
 import static com.android.window.flags.Flags.explicitRefreshRateHints;
 
 import android.annotation.IntDef;
@@ -478,7 +477,7 @@
     AppCompatCameraPolicy mAppCompatCameraPolicy;
 
     DisplayFrames mDisplayFrames;
-    final DisplayUpdater mDisplayUpdater;
+    final DeferredDisplayUpdater mDisplayUpdater;
 
     private boolean mInTouchMode;
 
@@ -623,7 +622,6 @@
     @VisibleForTesting
     final DeviceStateController mDeviceStateController;
     final Consumer<DeviceStateController.DeviceState> mDeviceStateConsumer;
-    final PhysicalDisplaySwitchTransitionLauncher mDisplaySwitchTransitionLauncher;
     final RemoteDisplayChangeController mRemoteDisplayChangeController;
 
     /** Windows added since {@link #mCurrentFocus} was set to null. Used for ANR blaming. */
@@ -1140,11 +1138,7 @@
         mWallpaperController.resetLargestDisplay(display);
         display.getDisplayInfo(mDisplayInfo);
         display.getMetrics(mDisplayMetrics);
-        if (deferDisplayUpdates()) {
-            mDisplayUpdater = new DeferredDisplayUpdater(this);
-        } else {
-            mDisplayUpdater = new ImmediateDisplayUpdater(this);
-        }
+        mDisplayUpdater = new DeferredDisplayUpdater(this);
         mSystemGestureExclusionLimit = mWmService.mConstants.mSystemGestureExclusionLimitDp
                 * mDisplayMetrics.densityDpi / DENSITY_DEFAULT;
         isDefaultDisplay = mDisplayId == DEFAULT_DISPLAY;
@@ -1168,8 +1162,6 @@
         mAppTransitionController = new AppTransitionController(mWmService, this);
         mTransitionController.registerLegacyListener(mFixedRotationTransitionListener);
         mUnknownAppVisibilityController = new UnknownAppVisibilityController(mWmService, this);
-        mDisplaySwitchTransitionLauncher = new PhysicalDisplaySwitchTransitionLauncher(this,
-                mTransitionController);
         mRemoteDisplayChangeController = new RemoteDisplayChangeController(this);
 
         final InputChannel inputChannel = mWmService.mInputManager.monitorInput(
@@ -1190,7 +1182,6 @@
 
         mDeviceStateConsumer =
                 (@NonNull DeviceStateController.DeviceState newFoldState) -> {
-                    mDisplaySwitchTransitionLauncher.foldStateChanged(newFoldState);
                     mDisplayRotation.foldStateChanged(newFoldState);
                 };
         mDeviceStateController.registerDeviceStateCallback(mDeviceStateConsumer,
@@ -3094,8 +3085,6 @@
                 // metrics are updated as rotation settings might depend on them
                 mWmService.mDisplayWindowSettings.applySettingsToDisplayLocked(this,
                         /* includeRotationSettings */ false);
-                mDisplayUpdater.onDisplayContentDisplayPropertiesPreChanged(mDisplayId,
-                        mInitialDisplayWidth, mInitialDisplayHeight, newWidth, newHeight);
                 mDisplayRotation.physicalDisplayChanged();
                 mDisplayPolicy.physicalDisplayChanged();
             }
@@ -3130,8 +3119,7 @@
 
             if (physicalDisplayChanged) {
                 mDisplayPolicy.physicalDisplayUpdated();
-                mDisplayUpdater.onDisplayContentDisplayPropertiesPostChanged(currentRotation,
-                        getRotation(), getDisplayAreaInfo());
+                mDisplayUpdater.onDisplayContentDisplayPropertiesPostChanged();
             }
         }
     }
diff --git a/services/core/java/com/android/server/wm/DisplayUpdater.java b/services/core/java/com/android/server/wm/DisplayUpdater.java
deleted file mode 100644
index 918b180..0000000
--- a/services/core/java/com/android/server/wm/DisplayUpdater.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm;
-
-import android.annotation.NonNull;
-import android.os.Message;
-import android.view.Surface;
-import android.window.DisplayAreaInfo;
-
-/**
- * Interface for a helper class that manages updates of DisplayInfo coming from DisplayManager
- */
-interface DisplayUpdater {
-    /**
-     * Reads the latest display parameters from the display manager and returns them in a callback.
-     * If there are pending display updates, it will wait for them to finish first and only then it
-     * will call the callback with the latest display parameters.
-     *
-     * @param callback is called when all pending display updates are finished
-     */
-    void updateDisplayInfo(@NonNull Runnable callback);
-
-    /**
-     * Called when physical display has changed and before DisplayContent has applied new display
-     * properties
-     */
-    default void onDisplayContentDisplayPropertiesPreChanged(int displayId, int initialDisplayWidth,
-            int initialDisplayHeight, int newWidth, int newHeight) {
-    }
-
-    /**
-     * Called after physical display has changed and after DisplayContent applied new display
-     * properties
-     */
-    default void onDisplayContentDisplayPropertiesPostChanged(
-            @Surface.Rotation int previousRotation, @Surface.Rotation int newRotation,
-            @NonNull DisplayAreaInfo newDisplayAreaInfo) {
-    }
-
-    /**
-     * Called with {@code true} when physical display is going to switch. And {@code false} when
-     * the display is turned on or the device goes to sleep.
-     */
-    default void onDisplaySwitching(boolean switching) {
-    }
-
-    /** Returns {@code true} if the transition will control when to turn on the screen. */
-    default boolean waitForTransition(@NonNull Message screenUnBlocker) {
-        return false;
-    }
-}
diff --git a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
index e0d69b0..4ec318b 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
@@ -16,9 +16,12 @@
 
 package com.android.server.wm;
 
+import static android.content.pm.ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.WindowConfiguration;
+import android.companion.virtualdevice.flags.Flags;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
@@ -26,6 +29,7 @@
 import android.os.UserHandle;
 import android.util.ArraySet;
 import android.util.Slog;
+import android.view.Display;
 import android.window.DisplayWindowPolicyController;
 
 import java.io.PrintWriter;
@@ -80,6 +84,9 @@
                 if (hasDisplayCategory(activities.get(i))) {
                     return false;
                 }
+                if (!launchAllowedByDisplayPolicy(activities.get(i))) {
+                    return false;
+                }
             }
             return true;
         }
@@ -95,7 +102,13 @@
         if (mDisplayWindowPolicyController == null) {
             // Missing controller means that this display has no categories for activity launch
             // restriction.
-            return !hasDisplayCategory(activityInfo);
+            if (hasDisplayCategory(activityInfo)) {
+                return false;
+            }
+            if (!launchAllowedByDisplayPolicy(activityInfo)) {
+                return false;
+            }
+            return true;
         }
         return mDisplayWindowPolicyController.canActivityBeLaunched(activityInfo, intent,
             windowingMode, launchingFromDisplayId, isNewTask);
@@ -112,6 +125,24 @@
         return false;
     }
 
+    private boolean launchAllowedByDisplayPolicy(ActivityInfo aInfo) {
+        if (!Flags.enforceRemoteDeviceOptOutOnAllVirtualDisplays()) {
+            return true;
+        }
+        int displayType = mDisplayContent.getDisplay().getType();
+        if (displayType != Display.TYPE_VIRTUAL && displayType != Display.TYPE_WIFI) {
+            return true;
+        }
+        if ((aInfo.flags & FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES) == 0) {
+            Slog.d(TAG,
+                    String.format("Checking activity launch on display %d, activity requires"
+                                    + " android:canDisplayOnRemoteDevices=true",
+                            mDisplayContent.mDisplayId));
+            return false;
+        }
+        return true;
+    }
+
     /**
      * @see DisplayWindowPolicyController#keepActivityOnWindowFlagsChanged(ActivityInfo, int, int)
      */
diff --git a/services/core/java/com/android/server/wm/ImmediateDisplayUpdater.java b/services/core/java/com/android/server/wm/ImmediateDisplayUpdater.java
deleted file mode 100644
index 4af9013..0000000
--- a/services/core/java/com/android/server/wm/ImmediateDisplayUpdater.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm;
-
-import android.annotation.NonNull;
-import android.view.DisplayInfo;
-import android.window.DisplayAreaInfo;
-
-/**
- * DisplayUpdater that immediately applies new DisplayInfo properties
- */
-public class ImmediateDisplayUpdater implements DisplayUpdater {
-
-    private final DisplayContent mDisplayContent;
-    private final DisplayInfo mDisplayInfo = new DisplayInfo();
-
-    public ImmediateDisplayUpdater(@NonNull DisplayContent displayContent) {
-        mDisplayContent = displayContent;
-        mDisplayInfo.copyFrom(mDisplayContent.getDisplayInfo());
-    }
-
-    @Override
-    public void updateDisplayInfo(Runnable callback) {
-        mDisplayContent.mWmService.mDisplayManagerInternal.getNonOverrideDisplayInfo(
-                mDisplayContent.mDisplayId, mDisplayInfo);
-        mDisplayContent.onDisplayInfoUpdated(mDisplayInfo);
-        callback.run();
-    }
-
-    @Override
-    public void onDisplayContentDisplayPropertiesPreChanged(int displayId, int initialDisplayWidth,
-            int initialDisplayHeight, int newWidth, int newHeight) {
-        mDisplayContent.mDisplaySwitchTransitionLauncher.requestDisplaySwitchTransitionIfNeeded(
-                displayId, initialDisplayWidth, initialDisplayHeight, newWidth, newHeight);
-    }
-
-    @Override
-    public void onDisplayContentDisplayPropertiesPostChanged(int previousRotation, int newRotation,
-            DisplayAreaInfo newDisplayAreaInfo) {
-        mDisplayContent.mDisplaySwitchTransitionLauncher.onDisplayUpdated(previousRotation,
-                newRotation,
-                newDisplayAreaInfo);
-    }
-}
diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java
index b496a65..b8869f1 100644
--- a/services/core/java/com/android/server/wm/InputMonitor.java
+++ b/services/core/java/com/android/server/wm/InputMonitor.java
@@ -439,8 +439,7 @@
                         final InputMethodManagerInternal inputMethodManagerInternal =
                                 LocalServices.getService(InputMethodManagerInternal.class);
                         if (inputMethodManagerInternal != null) {
-                            // TODO(b/308479256): Check if hiding "all" IMEs is OK or not.
-                            inputMethodManagerInternal.hideAllInputMethods(
+                            inputMethodManagerInternal.hideInputMethod(
                                     SoftInputShowHideReason.HIDE_RECENTS_ANIMATION,
                                     mDisplayContent.getDisplayId());
                         }
diff --git a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
deleted file mode 100644
index 3cf301c..0000000
--- a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
+++ /dev/null
@@ -1,187 +0,0 @@
-/*
- * Copyright (C) 2022 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.wm;
-
-import static android.view.WindowManager.TRANSIT_CHANGE;
-import static android.view.WindowManager.TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH;
-
-import static com.android.internal.R.bool.config_unfoldTransitionEnabled;
-import static com.android.server.wm.ActivityTaskManagerService.POWER_MODE_REASON_CHANGE_DISPLAY;
-import static com.android.server.wm.DeviceStateController.DeviceState.FOLDED;
-import static com.android.server.wm.DeviceStateController.DeviceState.HALF_FOLDED;
-import static com.android.server.wm.DeviceStateController.DeviceState.OPEN;
-
-import android.animation.ValueAnimator;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.content.Context;
-import android.graphics.Rect;
-import android.window.DisplayAreaInfo;
-import android.window.TransitionRequestInfo;
-import android.window.WindowContainerTransaction;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.protolog.ProtoLogGroup;
-import com.android.internal.protolog.ProtoLog;
-import com.android.server.wm.DeviceStateController.DeviceState;
-
-public class PhysicalDisplaySwitchTransitionLauncher {
-
-    private final DisplayContent mDisplayContent;
-    private final ActivityTaskManagerService mAtmService;
-    private final Context mContext;
-    private final TransitionController mTransitionController;
-
-    /**
-     * If on a foldable device represents whether we need to show unfold animation when receiving
-     * a physical display switch event
-     */
-    private boolean mShouldRequestTransitionOnDisplaySwitch = false;
-    /**
-     * Current device state from {@link android.hardware.devicestate.DeviceStateManager}
-     */
-    private DeviceState mDeviceState = DeviceState.UNKNOWN;
-    private Transition mTransition;
-
-    public PhysicalDisplaySwitchTransitionLauncher(DisplayContent displayContent,
-            TransitionController transitionController) {
-        this(displayContent, displayContent.mWmService.mAtmService,
-                displayContent.mWmService.mContext, transitionController);
-    }
-
-    @VisibleForTesting
-    public PhysicalDisplaySwitchTransitionLauncher(DisplayContent displayContent,
-            ActivityTaskManagerService service, Context context,
-            TransitionController transitionController) {
-        mDisplayContent = displayContent;
-        mAtmService = service;
-        mContext = context;
-        mTransitionController = transitionController;
-    }
-
-    /**
-     * Called by the display manager just before it applied the device state, it is guaranteed
-     * that in case of physical display change the
-     * {@link PhysicalDisplaySwitchTransitionLauncher#requestDisplaySwitchTransitionIfNeeded}
-     * method will be invoked *after* this one.
-     */
-    void foldStateChanged(DeviceState newDeviceState) {
-        boolean isUnfolding = mDeviceState == FOLDED
-                && (newDeviceState == HALF_FOLDED || newDeviceState == OPEN);
-
-        if (isUnfolding) {
-            // Request transition only if we are unfolding the device
-            mShouldRequestTransitionOnDisplaySwitch = true;
-        } else if (newDeviceState != HALF_FOLDED && newDeviceState != OPEN) {
-            // Cancel the transition request in case if we are folding or switching to back
-            // to the rear display before the displays got switched
-            mShouldRequestTransitionOnDisplaySwitch = false;
-        }
-
-        mDeviceState = newDeviceState;
-    }
-
-    /**
-     * Requests to start a transition for the physical display switch
-     */
-    public void requestDisplaySwitchTransitionIfNeeded(int displayId, int oldDisplayWidth,
-            int oldDisplayHeight, int newDisplayWidth, int newDisplayHeight) {
-        if (!mShouldRequestTransitionOnDisplaySwitch) return;
-        if (!mTransitionController.isShellTransitionsEnabled()) return;
-        if (!mDisplayContent.getLastHasContent()) return;
-
-        boolean shouldRequestUnfoldTransition = mContext.getResources()
-                .getBoolean(config_unfoldTransitionEnabled) && ValueAnimator.areAnimatorsEnabled();
-
-        if (!shouldRequestUnfoldTransition) {
-            return;
-        }
-
-        mTransition = null;
-
-        if (mTransitionController.isCollecting()) {
-            // Add display container to the currently collecting transition
-            mTransition = mTransitionController.getCollectingTransition();
-            mTransition.collect(mDisplayContent);
-
-            // Make sure that transition is not ready until we finish the remote display change
-            mTransition.setReady(mDisplayContent, false);
-            mTransition.addFlag(TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH);
-
-            ProtoLog.d(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
-                    "Adding display switch to existing collecting transition");
-        } else {
-            final TransitionRequestInfo.DisplayChange displayChange =
-                    new TransitionRequestInfo.DisplayChange(displayId);
-
-            final Rect startAbsBounds = new Rect(0, 0, oldDisplayWidth, oldDisplayHeight);
-            displayChange.setStartAbsBounds(startAbsBounds);
-            final Rect endAbsBounds = new Rect(0, 0, newDisplayWidth, newDisplayHeight);
-            displayChange.setEndAbsBounds(endAbsBounds);
-            displayChange.setPhysicalDisplayChanged(true);
-
-            mTransition = mTransitionController.requestStartDisplayTransition(TRANSIT_CHANGE,
-                    0 /* flags */, mDisplayContent, null /* remoteTransition */, displayChange);
-            mTransition.collect(mDisplayContent);
-        }
-
-        if (mTransition != null) {
-            mAtmService.startPowerMode(POWER_MODE_REASON_CHANGE_DISPLAY);
-        }
-
-        mShouldRequestTransitionOnDisplaySwitch = false;
-    }
-
-    /**
-     * Called when physical display is getting updated, this could happen e.g. on foldable
-     * devices when the physical underlying display is replaced.
-     *
-     * @param fromRotation rotation before the display change
-     * @param toRotation rotation after the display change
-     * @param newDisplayAreaInfo display area info after the display change
-     */
-    public void onDisplayUpdated(int fromRotation, int toRotation,
-            @NonNull DisplayAreaInfo newDisplayAreaInfo) {
-        if (mTransition == null) return;
-
-        final boolean started = mDisplayContent.mRemoteDisplayChangeController
-                .performRemoteDisplayChange(fromRotation, toRotation, newDisplayAreaInfo,
-                        this::continueDisplayUpdate);
-
-        if (!started) {
-            markTransitionAsReady();
-        }
-    }
-
-    private void continueDisplayUpdate(@Nullable WindowContainerTransaction transaction) {
-        if (mTransition == null) return;
-
-        if (transaction != null) {
-            mAtmService.mWindowOrganizerController.applyTransaction(transaction);
-        }
-
-        markTransitionAsReady();
-    }
-
-    private void markTransitionAsReady() {
-        if (mTransition == null) return;
-
-        mTransition.setAllReady();
-        mTransition = null;
-    }
-
-}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
index efcc23f..8ae4f9a 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
@@ -41,6 +41,7 @@
 
 import static java.util.Objects.requireNonNull;
 
+import android.annotation.Nullable;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -50,6 +51,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.inputmethod.InputBindResult;
 import com.android.internal.inputmethod.SoftInputShowHideReason;
 import com.android.internal.inputmethod.StartInputFlags;
@@ -75,7 +77,7 @@
         super.setUp();
         synchronized (ImfLock.class) {
             mVisibilityApplier = mInputMethodManagerService.getVisibilityApplierLocked();
-            mInputMethodManagerService.setAttachedClientForTesting(requireNonNull(
+            setAttachedClientLocked(requireNonNull(
                     mInputMethodManagerService.getClientStateLocked(mMockInputMethodClient)));
         }
     }
@@ -168,7 +170,9 @@
         // Init a IME target client on the secondary display to show IME.
         mInputMethodManagerService.addClient(mMockInputMethodClient, mMockRemoteInputConnection,
                 10 /* selfReportedDisplayId */);
-        mInputMethodManagerService.setAttachedClientForTesting(null);
+        synchronized (ImfLock.class) {
+            setAttachedClientLocked(null);
+        }
         startInputOrWindowGainedFocus(mWindowToken, SOFT_INPUT_STATE_ALWAYS_VISIBLE);
 
         final var statsToken = ImeTracker.Token.empty();
@@ -206,7 +210,9 @@
 
     @Test
     public void testApplyImeVisibility_hideImeWhenUnbinding() {
-        mInputMethodManagerService.setAttachedClientForTesting(null);
+        synchronized (ImfLock.class) {
+            setAttachedClientLocked(null);
+        }
         startInputOrWindowGainedFocus(mWindowToken, SOFT_INPUT_STATE_ALWAYS_VISIBLE);
         ExtendedMockito.spyOn(mVisibilityApplier);
 
@@ -233,6 +239,11 @@
         }
     }
 
+    @GuardedBy("ImfLock.class")
+    private void setAttachedClientLocked(@Nullable ClientState cs) {
+        mInputMethodManagerService.getUserData(mUserId).mCurClient = cs;
+    }
+
     private InputBindResult startInputOrWindowGainedFocus(IBinder windowToken, int softInputMode) {
         return mInputMethodManagerService.startInputOrWindowGainedFocus(
                 StartInputReason.WINDOW_FOCUS_GAIN /* startInputReason */,
diff --git a/services/tests/performancehinttests/TEST_MAPPING b/services/tests/performancehinttests/TEST_MAPPING
index faffe35..fa7b897 100644
--- a/services/tests/performancehinttests/TEST_MAPPING
+++ b/services/tests/performancehinttests/TEST_MAPPING
@@ -1,4 +1,12 @@
 {
+  "presubmit": [
+    {
+      "name": "PerformanceHintTests",
+      "options": [
+        {"exclude-annotation": "org.junit.Ignore"}
+      ]
+    }
+  ],
   "ravenwood-postsubmit": [
     {
       "name": "PerformanceHintTestsRavenwood",
@@ -7,13 +15,5 @@
         {"exclude-annotation": "android.platform.test.annotations.DisabledOnRavenwood"}
       ]
     }
-  ],
-  "postsubmit": [
-    {
-      "name": "PerformanceHintTests",
-      "options": [
-        {"exclude-annotation": "org.junit.Ignore"}
-      ]
-    }
   ]
 }
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index 20b9592..1afe12f 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -840,6 +840,10 @@
         info_a.setComponentName(COMPONENT_NAME);
         final AccessibilityServiceInfo info_b = new AccessibilityServiceInfo();
         info_b.setComponentName(new ComponentName("package", "class"));
+        writeStringsToSetting(Set.of(
+                info_a.getComponentName().flattenToString(),
+                info_b.getComponentName().flattenToString()),
+                Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
 
         AccessibilityUserState userState = mA11yms.getCurrentUserState();
         userState.mInstalledServices.clear();
@@ -858,10 +862,9 @@
         userState = mA11yms.getCurrentUserState();
         assertThat(userState.mEnabledServices).containsExactly(info_b.getComponentName());
         //Assert setting change
-        final Set<ComponentName> componentsFromSetting = new ArraySet<>();
-        mA11yms.readComponentNamesFromSettingLocked(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
-                userState.mUserId, componentsFromSetting);
-        assertThat(componentsFromSetting).containsExactly(info_b.getComponentName());
+        final Set<String> enabledServices =
+                readStringsFromSetting(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
+        assertThat(enabledServices).containsExactly(info_b.getComponentName().flattenToString());
     }
 
     @Test
@@ -880,6 +883,10 @@
                         info_a.getComponentName().flattenToString(),
                         info_b.getComponentName().flattenToString()),
                 SOFTWARE);
+        writeStringsToSetting(Set.of(
+                        info_a.getComponentName().flattenToString(),
+                        info_b.getComponentName().flattenToString()),
+                ShortcutUtils.convertToKey(SOFTWARE));
 
         // despite force stopping both packages, only the first service has the relevant flag,
         // so only the first should be removed.
@@ -896,13 +903,53 @@
         assertThat(userState.getShortcutTargetsLocked(SOFTWARE)).containsExactly(
                 info_b.getComponentName().flattenToString());
         //Assert setting change
-        final Set<String> targetsFromSetting = new ArraySet<>();
-        mA11yms.readColonDelimitedSettingToSet(ShortcutUtils.convertToKey(SOFTWARE),
-                userState.mUserId, str -> str, targetsFromSetting);
+        final Set<String> targetsFromSetting = readStringsFromSetting(
+                ShortcutUtils.convertToKey(SOFTWARE));
         assertThat(targetsFromSetting).containsExactly(info_b.getComponentName().flattenToString());
     }
 
     @Test
+    public void testPackagesForceStopped_otherServiceStopped_doesNotRemoveContinuousTarget() {
+        final AccessibilityServiceInfo info_a = new AccessibilityServiceInfo();
+        info_a.setComponentName(COMPONENT_NAME);
+        info_a.flags = FLAG_REQUEST_ACCESSIBILITY_BUTTON;
+        final AccessibilityServiceInfo info_b = new AccessibilityServiceInfo();
+        info_b.setComponentName(new ComponentName("package", "class"));
+        writeStringsToSetting(Set.of(
+                        info_a.getComponentName().flattenToString(),
+                        info_b.getComponentName().flattenToString()),
+                ShortcutUtils.convertToKey(SOFTWARE));
+
+        AccessibilityUserState userState = mA11yms.getCurrentUserState();
+        userState.mInstalledServices.clear();
+        userState.mInstalledServices.add(info_a);
+        userState.mInstalledServices.add(info_b);
+        userState.updateShortcutTargetsLocked(Set.of(
+                        info_a.getComponentName().flattenToString(),
+                        info_b.getComponentName().flattenToString()),
+                SOFTWARE);
+
+        // Force stopping a service should not disable unrelated continuous services.
+        synchronized (mA11yms.getLock()) {
+            mA11yms.onPackagesForceStoppedLocked(
+                    new String[]{info_b.getComponentName().getPackageName()},
+                    userState);
+        }
+
+        //Assert user state change
+        userState = mA11yms.getCurrentUserState();
+        assertThat(userState.getShortcutTargetsLocked(SOFTWARE)).containsExactly(
+                info_a.getComponentName().flattenToString(),
+                info_b.getComponentName().flattenToString());
+        //Assert setting unchanged
+        final Set<String> targetsFromSetting = readStringsFromSetting(
+                ShortcutUtils.convertToKey(SOFTWARE));
+        assertThat(targetsFromSetting).containsExactly(
+                info_a.getComponentName().flattenToString(),
+                info_b.getComponentName().flattenToString());
+    }
+
+    @Test
     public void testPackageMonitorScanPackages_scansWithoutHoldingLock() {
         setupAccessibilityServiceConnection(0);
         final AtomicReference<Set<Boolean>> lockState = collectLockStateWhilePackageScanning();
@@ -1844,6 +1891,11 @@
         return result;
     }
 
+    private void writeStringsToSetting(Set<String> strings, String setting) {
+        mA11yms.persistColonDelimitedSetToSettingLocked(
+                setting, UserHandle.USER_SYSTEM, strings, str -> str);
+    }
+
     private void broadcastSettingRestored(String setting, String newValue) {
         Intent intent = new Intent(Intent.ACTION_SETTING_RESTORED)
                 .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java
new file mode 100644
index 0000000..b5a538f
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.audio;
+
+import static com.android.media.audio.Flags.asDeviceConnectionFailure;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioManager;
+import android.media.AudioSystem;
+import android.platform.test.annotations.Presubmit;
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Spy;
+
+@MediumTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class AudioDeviceInventoryTest {
+
+    private static final String TAG = "AudioDeviceInventoryTest";
+
+    @Mock private AudioService mMockAudioService;
+    private AudioDeviceInventory mDevInventory;
+    @Spy private AudioDeviceBroker mSpyAudioDeviceBroker;
+    @Spy private AudioSystemAdapter mSpyAudioSystem;
+
+    private SystemServerAdapter mSystemServer;
+
+    private BluetoothDevice mFakeBtDevice;
+
+    @Before
+    public void setUp() throws Exception {
+        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+        mMockAudioService = mock(AudioService.class);
+        mSpyAudioSystem = spy(new NoOpAudioSystemAdapter());
+        mDevInventory = new AudioDeviceInventory(mSpyAudioSystem);
+        mSystemServer = new NoOpSystemServerAdapter();
+        mSpyAudioDeviceBroker = spy(new AudioDeviceBroker(context, mMockAudioService, mDevInventory,
+                mSystemServer, mSpyAudioSystem));
+        mDevInventory.setDeviceBroker(mSpyAudioDeviceBroker);
+
+        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+        mFakeBtDevice = adapter.getRemoteDevice("00:01:02:03:04:05");
+    }
+
+    @After
+    public void tearDown() throws Exception { }
+
+    /**
+     * test that for DEVICE_OUT_BLUETOOTH_A2DP devices, when the device connects, it's only
+     * added to the connected devices when the connection through AudioSystem is successful
+     * @throws Exception on error
+     */
+    @Test
+    public void testSetDeviceConnectionStateA2dp() throws Exception {
+        Log.i(TAG, "starting testSetDeviceConnectionStateA2dp");
+        assertTrue("collection of connected devices not empty at start",
+                mDevInventory.getConnectedDevices().isEmpty());
+
+        final AudioDeviceAttributes ada = new AudioDeviceAttributes(
+                AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, mFakeBtDevice.getAddress());
+        AudioDeviceBroker.BtDeviceInfo btInfo =
+                new AudioDeviceBroker.BtDeviceInfo(mFakeBtDevice, BluetoothProfile.A2DP,
+                        BluetoothProfile.STATE_CONNECTED, AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
+                        AudioSystem.AUDIO_FORMAT_SBC);
+
+        // test that no device is added when AudioSystem returns AUDIO_STATUS_ERROR
+        // when setDeviceConnectionState is called for the connection
+        // NOTE: for now this is only when flag asDeviceConnectionFailure is true
+        if (asDeviceConnectionFailure()) {
+            when(mSpyAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE,
+                    AudioSystem.AUDIO_FORMAT_DEFAULT))
+                    .thenReturn(AudioSystem.AUDIO_STATUS_ERROR);
+            runWithBluetoothPrivilegedPermission(
+                    () ->  mDevInventory.onSetBtActiveDevice(/*btInfo*/ btInfo,
+                        /*codec*/ AudioSystem.AUDIO_FORMAT_DEFAULT, AudioManager.STREAM_MUSIC));
+
+            assertEquals(0, mDevInventory.getConnectedDevices().size());
+        }
+
+        // test that the device is added when AudioSystem returns AUDIO_STATUS_OK
+        // when setDeviceConnectionState is called for the connection
+        when(mSpyAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE,
+                AudioSystem.AUDIO_FORMAT_DEFAULT))
+                .thenReturn(AudioSystem.AUDIO_STATUS_OK);
+        runWithBluetoothPrivilegedPermission(
+                () ->  mDevInventory.onSetBtActiveDevice(/*btInfo*/ btInfo,
+                    /*codec*/ AudioSystem.AUDIO_FORMAT_DEFAULT, AudioManager.STREAM_MUSIC));
+        assertEquals(1, mDevInventory.getConnectedDevices().size());
+    }
+
+    // TODO add test for hearing aid
+
+    // TODO add test for BLE
+
+    /**
+     * Executes a Runnable while holding the BLUETOOTH_PRIVILEGED permission
+     * @param toRunWithPermission the runnable to run with BT privileges
+     */
+    private void runWithBluetoothPrivilegedPermission(Runnable toRunWithPermission) {
+        try {
+            InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                    .adoptShellPermissionIdentity(Manifest.permission.BLUETOOTH_PRIVILEGED);
+            toRunWithPermission.run();
+        } finally {
+            InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                    .dropShellPermissionIdentity();
+        }
+    }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
index e5c42082..fb82b872c 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
@@ -888,7 +888,7 @@
             return true;
         });
 
-        mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>());
+        mockServiceInfoWithMetaData(List.of(cn), service, pm, new ArrayMap<>());
         service.addApprovedList("a", 0, true);
 
         service.reregisterService(cn, 0);
@@ -919,7 +919,7 @@
             return true;
         });
 
-        mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>());
+        mockServiceInfoWithMetaData(List.of(cn), service, pm, new ArrayMap<>());
         service.addApprovedList("a", 0, false);
 
         service.reregisterService(cn, 0);
@@ -950,7 +950,7 @@
             return true;
         });
 
-        mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>());
+        mockServiceInfoWithMetaData(List.of(cn), service, pm, new ArrayMap<>());
         service.addApprovedList("a/a", 0, true);
 
         service.reregisterService(cn, 0);
@@ -981,7 +981,7 @@
             return true;
         });
 
-        mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>());
+        mockServiceInfoWithMetaData(List.of(cn), service, pm, new ArrayMap<>());
         service.addApprovedList("a/a", 0, false);
 
         service.reregisterService(cn, 0);
@@ -1211,6 +1211,64 @@
     }
 
     @Test
+    public void testUpgradeAppNoIntentFilterNoRebind() throws Exception {
+        Context context = spy(getContext());
+        doReturn(true).when(context).bindServiceAsUser(any(), any(), anyInt(), any());
+
+        ManagedServices service = new TestManagedServices(context, mLock, mUserProfiles,
+                mIpm, APPROVAL_BY_COMPONENT);
+
+        List<String> packages = new ArrayList<>();
+        packages.add("package");
+        addExpectedServices(service, packages, 0);
+
+        final ComponentName unapprovedComponent = ComponentName.unflattenFromString("package/C1");
+        final ComponentName approvedComponent = ComponentName.unflattenFromString("package/C2");
+
+        // Both components are approved initially
+        mExpectedPrimaryComponentNames.clear();
+        mExpectedPrimaryPackages.clear();
+        mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2");
+        mExpectedSecondaryComponentNames.clear();
+        mExpectedSecondaryPackages.clear();
+
+        loadXml(service);
+
+        //Component package/C1 loses serviceInterface intent filter
+        ManagedServices.Config config = service.getConfig();
+        when(mPm.queryIntentServicesAsUser(any(), anyInt(), anyInt())).
+            thenAnswer(new Answer<List<ResolveInfo>>() {
+                @Override
+                public List<ResolveInfo> answer(InvocationOnMock invocationOnMock)
+                    throws Throwable {
+                    Object[] args = invocationOnMock.getArguments();
+                    Intent invocationIntent = (Intent) args[0];
+                    if (invocationIntent != null) {
+                        if (invocationIntent.getAction().equals(config.serviceInterface)
+                            && packages.contains(invocationIntent.getPackage())) {
+                            List<ResolveInfo> dummyServices = new ArrayList<>();
+                            ResolveInfo resolveInfo = new ResolveInfo();
+                            ServiceInfo serviceInfo = new ServiceInfo();
+                            serviceInfo.packageName = invocationIntent.getPackage();
+                            serviceInfo.name = approvedComponent.getClassName();
+                            serviceInfo.permission = service.getConfig().bindPermission;
+                            resolveInfo.serviceInfo = serviceInfo;
+                            dummyServices.add(resolveInfo);
+                            return dummyServices;
+                        }
+                    }
+                    return new ArrayList<>();
+                }
+            });
+
+        // Trigger package update
+        service.onPackagesChanged(false, new String[]{"package"}, new int[]{0});
+
+        assertFalse(service.isComponentEnabledForCurrentProfiles(unapprovedComponent));
+        assertTrue(service.isComponentEnabledForCurrentProfiles(approvedComponent));
+    }
+
+    @Test
     public void testSetPackageOrComponentEnabled() throws Exception {
         for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) {
             ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles,
@@ -1915,7 +1973,7 @@
         metaDataAutobindAllow.putBoolean(META_DATA_DEFAULT_AUTOBIND, true);
         metaDatas.put(cn_allowed, metaDataAutobindAllow);
 
-        mockServiceInfoWithMetaData(componentNames, service, metaDatas);
+        mockServiceInfoWithMetaData(componentNames, service, pm, metaDatas);
 
         service.addApprovedList(cn_allowed.flattenToString(), 0, true);
         service.addApprovedList(cn_disallowed.flattenToString(), 0, true);
@@ -1960,7 +2018,7 @@
         metaDataAutobindDisallow.putBoolean(META_DATA_DEFAULT_AUTOBIND, false);
         metaDatas.put(cn_disallowed, metaDataAutobindDisallow);
 
-        mockServiceInfoWithMetaData(componentNames, service, metaDatas);
+        mockServiceInfoWithMetaData(componentNames, service, pm, metaDatas);
 
         service.addApprovedList(cn_disallowed.flattenToString(), 0, true);
 
@@ -1999,7 +2057,7 @@
         metaDataAutobindDisallow.putBoolean(META_DATA_DEFAULT_AUTOBIND, false);
         metaDatas.put(cn_disallowed, metaDataAutobindDisallow);
 
-        mockServiceInfoWithMetaData(componentNames, service, metaDatas);
+        mockServiceInfoWithMetaData(componentNames, service, pm, metaDatas);
 
         service.addApprovedList(cn_disallowed.flattenToString(), 0, true);
 
@@ -2070,8 +2128,8 @@
     }
 
     private void mockServiceInfoWithMetaData(List<ComponentName> componentNames,
-            ManagedServices service, ArrayMap<ComponentName, Bundle> metaDatas)
-            throws RemoteException {
+            ManagedServices service, PackageManager packageManager,
+            ArrayMap<ComponentName, Bundle> metaDatas) throws RemoteException {
         when(mIpm.getServiceInfo(any(), anyLong(), anyInt())).thenAnswer(
                 (Answer<ServiceInfo>) invocation -> {
                     ComponentName invocationCn = invocation.getArgument(0);
@@ -2086,6 +2144,39 @@
                     return null;
                 }
         );
+
+        // add components to queryIntentServicesAsUser response
+        final List<String> packages = new ArrayList<>();
+        for (ComponentName cn: componentNames) {
+            packages.add(cn.getPackageName());
+        }
+        ManagedServices.Config config = service.getConfig();
+        when(packageManager.queryIntentServicesAsUser(any(), anyInt(), anyInt())).
+                thenAnswer(new Answer<List<ResolveInfo>>() {
+                @Override
+                public List<ResolveInfo> answer(InvocationOnMock invocationOnMock)
+                    throws Throwable {
+                    Object[] args = invocationOnMock.getArguments();
+                    Intent invocationIntent = (Intent) args[0];
+                    if (invocationIntent != null) {
+                        if (invocationIntent.getAction().equals(config.serviceInterface)
+                            && packages.contains(invocationIntent.getPackage())) {
+                            List<ResolveInfo> dummyServices = new ArrayList<>();
+                            for (ComponentName cn: componentNames) {
+                                ResolveInfo resolveInfo = new ResolveInfo();
+                                ServiceInfo serviceInfo = new ServiceInfo();
+                                serviceInfo.packageName = invocationIntent.getPackage();
+                                serviceInfo.name = cn.getClassName();
+                                serviceInfo.permission = service.getConfig().bindPermission;
+                                resolveInfo.serviceInfo = serviceInfo;
+                                dummyServices.add(resolveInfo);
+                            }
+                            return dummyServices;
+                        }
+                    }
+                    return new ArrayList<>();
+                }
+            });
     }
 
     private void resetComponentsAndPackages() {
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
index 901c036..4f75931 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
@@ -20,7 +20,7 @@
 import static android.os.VibrationAttributes.CATEGORY_UNKNOWN;
 import static android.os.VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY;
 import static android.os.VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF;
-import static android.os.VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE;
+import static android.os.VibrationAttributes.USAGE_IME_FEEDBACK;
 import static android.os.VibrationAttributes.USAGE_TOUCH;
 import static android.os.VibrationEffect.Composition.PRIMITIVE_CLICK;
 import static android.os.VibrationEffect.Composition.PRIMITIVE_TICK;
@@ -346,7 +346,7 @@
     }
 
     @Test
-    public void testVibrationAttribute_keyboardCategoryOff_isIme_notUseKeyboardCategory() {
+    public void testVibrationAttribute_keyboardCategoryOff_isIme_useTouchUsage() {
         mSetFlagsRule.disableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED);
         HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
 
@@ -362,7 +362,7 @@
     }
 
     @Test
-    public void testVibrationAttribute_keyboardCategoryOn_notIme_notUseKeyboardCategory() {
+    public void testVibrationAttribute_keyboardCategoryOn_notIme_useTouchUsage() {
         mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED);
         HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
 
@@ -377,7 +377,7 @@
     }
 
     @Test
-    public void testVibrationAttribute_keyboardCategoryOn_isIme_useKeyboardCategory() {
+    public void testVibrationAttribute_keyboardCategoryOn_isIme_useImeFeedbackUsage() {
         mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED);
         HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
 
@@ -385,64 +385,14 @@
             VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
                     effectId, /* flags */ 0,
                     HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS);
-            assertWithMessage("Expected USAGE_TOUCH for effect " + effectId)
-                    .that(attrs.getUsage()).isEqualTo(USAGE_TOUCH);
+            assertWithMessage("Expected USAGE_IME_FEEDBACK for effect " + effectId)
+                    .that(attrs.getUsage()).isEqualTo(USAGE_IME_FEEDBACK);
             assertWithMessage("Expected CATEGORY_KEYBOARD for effect " + effectId)
                     .that(attrs.getCategory()).isEqualTo(CATEGORY_KEYBOARD);
         }
     }
 
     @Test
-    public void testVibrationAttribute_noFixAmplitude_notBypassIntensityScale() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED);
-        mockVibratorPrimitiveSupport(PRIMITIVE_CLICK, PRIMITIVE_TICK);
-        mockKeyboardVibrationFixedAmplitude(-1);
-        HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
-
-        for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) {
-            VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
-                    effectId, /* flags */ 0,
-                    HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS);
-            assertWithMessage("Expected no FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE for effect "
-                    + effectId)
-                    .that(attrs.isFlagSet(FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)).isFalse();
-        }
-    }
-
-    @Test
-    public void testVibrationAttribute_notIme_notBypassIntensityScale() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED);
-        mockVibratorPrimitiveSupport(PRIMITIVE_CLICK, PRIMITIVE_TICK);
-        mockKeyboardVibrationFixedAmplitude(KEYBOARD_VIBRATION_FIXED_AMPLITUDE);
-        HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
-
-        for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) {
-            VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
-                    effectId, /* flags */ 0, /* privFlags */ 0);
-            assertWithMessage("Expected no FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE for effect "
-                    + effectId)
-                    .that(attrs.isFlagSet(FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)).isFalse();
-        }
-    }
-
-    @Test
-    public void testVibrationAttribute_fixAmplitude_isIme_bypassIntensityScale() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED);
-        mockVibratorPrimitiveSupport(PRIMITIVE_CLICK, PRIMITIVE_TICK);
-        mockKeyboardVibrationFixedAmplitude(KEYBOARD_VIBRATION_FIXED_AMPLITUDE);
-        HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
-
-        for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) {
-            VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback(
-                    effectId, /* flags */ 0,
-                    HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS);
-            assertWithMessage("Expected FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE for effect "
-                    + effectId)
-                    .that(attrs.isFlagSet(FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)).isTrue();
-        }
-    }
-
-    @Test
     public void testIsRestricted_biometricConstants_returnsTrue() {
         HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations();
 
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java
index 60d8964..8d4a6aa 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java
@@ -23,6 +23,7 @@
 import static android.os.VibrationAttributes.USAGE_ALARM;
 import static android.os.VibrationAttributes.USAGE_COMMUNICATION_REQUEST;
 import static android.os.VibrationAttributes.USAGE_HARDWARE_FEEDBACK;
+import static android.os.VibrationAttributes.USAGE_IME_FEEDBACK;
 import static android.os.VibrationAttributes.USAGE_MEDIA;
 import static android.os.VibrationAttributes.USAGE_NOTIFICATION;
 import static android.os.VibrationAttributes.USAGE_PHYSICAL_EMULATION;
@@ -893,6 +894,22 @@
     }
 
     @Test
+    public void getCurrentIntensity_ImeFeedbackValueReflectsToKeyboardVibrationSettings() {
+        setDefaultIntensity(USAGE_IME_FEEDBACK, VIBRATION_INTENSITY_MEDIUM);
+        setDefaultIntensity(USAGE_TOUCH, VIBRATION_INTENSITY_HIGH);
+
+        setKeyboardVibrationSettingsSupported(false);
+        mVibrationSettings.update();
+        assertEquals(VIBRATION_INTENSITY_HIGH,
+                mVibrationSettings.getCurrentIntensity(USAGE_IME_FEEDBACK));
+
+        setKeyboardVibrationSettingsSupported(true);
+        mVibrationSettings.update();
+        assertEquals(VIBRATION_INTENSITY_MEDIUM,
+                mVibrationSettings.getCurrentIntensity(USAGE_IME_FEEDBACK));
+    }
+
+    @Test
     public void getFallbackEffect_returnsEffectsFromSettings() {
         assertNotNull(mVibrationSettings.getFallbackEffect(VibrationEffect.EFFECT_TICK));
         assertNotNull(mVibrationSettings.getFallbackEffect(VibrationEffect.EFFECT_TEXTURE_TICK));
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index bea6917..e411a17 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -154,6 +154,9 @@
     private static final VibrationAttributes RINGTONE_ATTRS =
             new VibrationAttributes.Builder().setUsage(
                     VibrationAttributes.USAGE_RINGTONE).build();
+    private static final VibrationAttributes IME_FEEDBACK_ATTRS =
+            new VibrationAttributes.Builder().setUsage(
+                    VibrationAttributes.USAGE_IME_FEEDBACK).build();
     private static final VibrationAttributes UNKNOWN_ATTRS =
             new VibrationAttributes.Builder().setUsage(VibrationAttributes.USAGE_UNKNOWN).build();
 
@@ -853,6 +856,7 @@
         vibrate(service, VibrationEffect.createOneShot(2000, 200),
                 new VibrationAttributes.Builder().setUsage(
                         VibrationAttributes.USAGE_UNKNOWN).build());
+        vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), IME_FEEDBACK_ATTRS);
 
         InOrder inOrderVerifier = inOrder(mAppOpsManagerMock);
         inOrderVerifier.verify(mAppOpsManagerMock).checkAudioOpNoThrow(eq(AppOpsManager.OP_VIBRATE),
@@ -868,6 +872,8 @@
                 anyInt(), anyString());
         inOrderVerifier.verify(mAppOpsManagerMock).checkAudioOpNoThrow(eq(AppOpsManager.OP_VIBRATE),
                 eq(AudioAttributes.USAGE_UNKNOWN), anyInt(), anyString());
+        inOrderVerifier.verify(mAppOpsManagerMock).checkAudioOpNoThrow(eq(AppOpsManager.OP_VIBRATE),
+                eq(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION), anyInt(), anyString());
     }
 
     @Test
@@ -1684,40 +1690,6 @@
     }
 
     @Test
-    public void vibrate_withBypassScaleFlag_ignoresIntensitySettingsAndResolvesAmplitude()
-            throws Exception {
-        // Permission needed for bypassing user settings
-        grantPermission(android.Manifest.permission.MODIFY_PHONE_STATE);
-
-        int defaultTouchIntensity =
-                mVibrator.getDefaultVibrationIntensity(VibrationAttributes.USAGE_TOUCH);
-        // This will scale down touch vibrations.
-        setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY,
-                defaultTouchIntensity > Vibrator.VIBRATION_INTENSITY_LOW
-                        ? defaultTouchIntensity - 1
-                        : defaultTouchIntensity);
-
-        int defaultAmplitude = mContextSpy.getResources().getInteger(
-                com.android.internal.R.integer.config_defaultVibrationAmplitude);
-
-        mockVibrators(1);
-        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1);
-        fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
-        VibratorManagerService service = createSystemReadyService();
-
-        vibrateAndWaitUntilFinished(service,
-                VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE),
-                new VibrationAttributes.Builder()
-                        .setUsage(VibrationAttributes.USAGE_TOUCH)
-                        .setFlags(VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_SCALE)
-                        .build());
-
-        assertEquals(1, fakeVibrator.getAllEffectSegments().size());
-
-        assertEquals(defaultAmplitude / 255f, fakeVibrator.getAmplitudes().get(0), 1e-5);
-    }
-
-    @Test
     @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
     public void vibrate_withAdaptiveHaptics_appliesCorrectAdaptiveScales() throws Exception {
         // Keep user settings the same as device default so only adaptive scale is applied.
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index ff1c6c8..d0080d2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -102,6 +102,7 @@
 import android.service.voice.IVoiceInteractionSession;
 import android.util.Pair;
 import android.util.Size;
+import android.view.Display;
 import android.view.Gravity;
 import android.view.RemoteAnimationAdapter;
 import android.window.TaskFragmentOrganizerToken;
@@ -941,6 +942,91 @@
                 notNull() /* options */);
     }
 
+
+    /**
+     * This test ensures that activity launch on a secondary display is allowed if the activity did
+     * not opt out from showing on remote devices.
+     */
+    @Test
+    public void testStartActivityOnVirtualDisplay() {
+        final ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_NEW_TASK,
+                false /* mockGetRootTask */);
+        starter.mRequest.activityInfo.flags |= ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES;
+
+        // Create a virtual display at bottom.
+        final TestDisplayContent secondaryDisplay =
+                new TestDisplayContent.Builder(mAtm, 1000, 1500)
+                        .setType(Display.TYPE_VIRTUAL)
+                        .setPosition(POSITION_BOTTOM).build();
+        final TaskDisplayArea secondaryTaskContainer = secondaryDisplay.getDefaultTaskDisplayArea();
+        final Task stack = secondaryTaskContainer.createRootTask(
+                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+
+        // Create an activity record on the top of secondary display.
+        final ActivityRecord topActivityOnSecondaryDisplay = createSingleTaskActivityOn(stack);
+
+        // Put an activity on default display as the top focused activity.
+        new ActivityBuilder(mAtm).setCreateTask(true).build();
+
+        // Start activity with the same intent as {@code topActivityOnSecondaryDisplay}
+        // on secondary display.
+        final ActivityOptions options = ActivityOptions.makeBasic()
+                .setLaunchDisplayId(secondaryDisplay.mDisplayId);
+        final int result = starter.setReason("testStartActivityOnVirtualDisplay")
+                .setIntent(topActivityOnSecondaryDisplay.intent)
+                .setActivityOptions(options.toBundle())
+                .execute();
+
+        // Ensure result is delivering intent to top.
+        assertEquals(START_DELIVERED_TO_TOP, result);
+
+        // Ensure secondary display only creates one stack.
+        verify(secondaryTaskContainer, times(1)).createRootTask(anyInt(), anyInt(), anyBoolean());
+    }
+
+    /**
+     * This test ensures that activity launch on a secondary display is disallowed if the activity
+     * opted out from showing on remote devices.
+     */
+    @EnableFlags(android.companion.virtualdevice.flags.Flags
+            .FLAG_ENFORCE_REMOTE_DEVICE_OPT_OUT_ON_ALL_VIRTUAL_DISPLAYS)
+    @Test
+    public void testStartOptedOutActivityOnVirtualDisplay() {
+        final ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_NEW_TASK,
+                false /* mockGetRootTask */);
+        starter.mRequest.activityInfo.flags &= ~ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES;
+
+        // Create a virtual display at bottom.
+        final TestDisplayContent secondaryDisplay =
+                new TestDisplayContent.Builder(mAtm, 1000, 1500)
+                        .setType(Display.TYPE_VIRTUAL)
+                        .setPosition(POSITION_BOTTOM).build();
+        final TaskDisplayArea secondaryTaskContainer = secondaryDisplay.getDefaultTaskDisplayArea();
+        final Task stack = secondaryTaskContainer.createRootTask(
+                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+
+        // Create an activity record on the top of secondary display.
+        final ActivityRecord topActivityOnSecondaryDisplay = createSingleTaskActivityOn(stack);
+
+        // Put an activity on default display as the top focused activity.
+        new ActivityBuilder(mAtm).setCreateTask(true).build();
+
+        // Start activity with the same intent as {@code topActivityOnSecondaryDisplay}
+        // on secondary display.
+        final ActivityOptions options = ActivityOptions.makeBasic()
+                .setLaunchDisplayId(secondaryDisplay.mDisplayId);
+        final int result = starter.setReason("testStartOptedOutActivityOnVirtualDisplay")
+                .setIntent(topActivityOnSecondaryDisplay.intent)
+                .setActivityOptions(options.toBundle())
+                .execute();
+
+        // Ensure result is canceled.
+        assertEquals(START_CANCELED, result);
+
+        // Ensure secondary display only creates one stack.
+        verify(secondaryTaskContainer, times(1)).createRootTask(anyInt(), anyInt(), anyBoolean());
+    }
+
     @Test
     public void testWasVisibleInRestartAttempt() {
         final ActivityStarter starter = prepareStarter(
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
index 57118f2..f843386 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
@@ -63,11 +63,6 @@
 
     private final Message mScreenUnblocker = mock(Message.class);
 
-    @Override
-    protected void onBeforeSystemServicesCreated() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_DEFER_DISPLAY_UPDATES);
-    }
-
     @Before
     public void before() {
         doReturn(true).when(mDisplayContent).getLastHasContent();
diff --git a/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java b/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java
deleted file mode 100644
index 78509db..0000000
--- a/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java
+++ /dev/null
@@ -1,283 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm;
-
-import static com.android.internal.R.bool.config_unfoldTransitionEnabled;
-import static com.android.server.wm.DeviceStateController.DeviceState.REAR;
-import static com.android.server.wm.DeviceStateController.DeviceState.FOLDED;
-import static com.android.server.wm.DeviceStateController.DeviceState.HALF_FOLDED;
-import static com.android.server.wm.DeviceStateController.DeviceState.OPEN;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.when;
-
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Rect;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.filters.SmallTest;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/**
- * Tests for the {@link WindowToken} class.
- *
- * Build/Install/Run:
- * atest WmTests:PhysicalDisplaySwitchTransitionLauncherTest
- */
-@SmallTest
-@Presubmit
-@RunWith(WindowTestRunner.class)
-public class PhysicalDisplaySwitchTransitionLauncherTest extends WindowTestsBase {
-
-    @Mock
-    Context mContext;
-    @Mock
-    Resources mResources;
-    @Mock
-    BLASTSyncEngine mSyncEngine;
-
-    WindowTestsBase.TestTransitionPlayer mPlayer;
-    TransitionController mTransitionController;
-    DisplayContent mDisplayContent;
-
-    private PhysicalDisplaySwitchTransitionLauncher mTarget;
-    private float mOriginalAnimationScale;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        mTransitionController = new WindowTestsBase.TestTransitionController(mAtm);
-        mTransitionController.setSyncEngine(mSyncEngine);
-        mPlayer = new WindowTestsBase.TestTransitionPlayer(
-                mTransitionController, mAtm.mWindowOrganizerController);
-        when(mContext.getResources()).thenReturn(mResources);
-        mDisplayContent = new TestDisplayContent.Builder(mAtm, 100, 150).build();
-        mTarget = new PhysicalDisplaySwitchTransitionLauncher(mDisplayContent, mAtm, mContext,
-                mTransitionController);
-        mOriginalAnimationScale = ValueAnimator.getDurationScale();
-    }
-
-    @After
-    public void after() {
-        ValueAnimator.setDurationScale(mOriginalAnimationScale);
-    }
-
-    @Test
-    public void testDisplaySwitchAfterUnfoldToOpen_animationsEnabled_requestsTransition() {
-        givenAllAnimationsEnabled();
-        mTarget.foldStateChanged(FOLDED);
-
-        mTarget.foldStateChanged(OPEN);
-        final Rect origBounds = new Rect();
-        mDisplayContent.getBounds(origBounds);
-        origBounds.offsetTo(0, 0);
-        mTarget.requestDisplaySwitchTransitionIfNeeded(
-                mDisplayContent.getDisplayId(),
-                origBounds.width(),
-                origBounds.height(),
-                /* newDisplayWidth= */ 200,
-                /* newDisplayHeight= */ 250
-        );
-
-        assertNotNull(mPlayer.mLastRequest);
-        assertEquals(mDisplayContent.getDisplayId(),
-                mPlayer.mLastRequest.getDisplayChange().getDisplayId());
-        assertEquals(origBounds, mPlayer.mLastRequest.getDisplayChange().getStartAbsBounds());
-        assertEquals(new Rect(0, 0, 200, 250),
-                mPlayer.mLastRequest.getDisplayChange().getEndAbsBounds());
-    }
-
-    @Test
-    public void testDisplaySwitchAfterFolding_animationEnabled_doesNotRequestTransition() {
-        givenAllAnimationsEnabled();
-        mTarget.foldStateChanged(OPEN);
-
-        mTarget.foldStateChanged(FOLDED);
-        requestDisplaySwitch();
-
-        assertTransitionNotRequested();
-    }
-
-    @Test
-    public void testDisplaySwitchAfterUnfoldingToHalf_animationEnabled_requestsTransition() {
-        givenAllAnimationsEnabled();
-        mTarget.foldStateChanged(FOLDED);
-
-        mTarget.foldStateChanged(HALF_FOLDED);
-        requestDisplaySwitch();
-
-        assertTransitionRequested();
-    }
-
-    @Test
-    public void testDisplaySwitchSecondTimeAfterUnfolding_animationEnabled_noTransition() {
-        givenAllAnimationsEnabled();
-        mTarget.foldStateChanged(FOLDED);
-        mTarget.foldStateChanged(OPEN);
-        requestDisplaySwitch();
-        mPlayer.mLastRequest = null;
-
-        requestDisplaySwitch();
-
-        assertTransitionNotRequested();
-    }
-
-
-    @Test
-    public void testDisplaySwitchAfterGoingToRearAndBack_animationEnabled_noTransition() {
-        givenAllAnimationsEnabled();
-        mTarget.foldStateChanged(OPEN);
-
-        mTarget.foldStateChanged(REAR);
-        mTarget.foldStateChanged(OPEN);
-        requestDisplaySwitch();
-
-        assertTransitionNotRequested();
-    }
-
-    @Test
-    public void testDisplaySwitchAfterUnfoldingAndFolding_animationEnabled_noTransition() {
-        givenAllAnimationsEnabled();
-        mTarget.foldStateChanged(FOLDED);
-        mTarget.foldStateChanged(OPEN);
-        // No request display switch event (simulate very fast fold after unfold, even before
-        // the displays switched)
-        mTarget.foldStateChanged(FOLDED);
-
-        requestDisplaySwitch();
-
-        assertTransitionNotRequested();
-    }
-
-    @Test
-    public void testDisplaySwitch_whenShellTransitionsNotEnabled_noTransition() {
-        givenAllAnimationsEnabled();
-        givenShellTransitionsEnabled(false);
-        mTarget.foldStateChanged(FOLDED);
-
-        mTarget.foldStateChanged(OPEN);
-        requestDisplaySwitch();
-
-        assertTransitionNotRequested();
-    }
-
-    @Test
-    public void testDisplaySwitch_whenAnimationsDisabled_noTransition() {
-        givenAllAnimationsEnabled();
-        givenAnimationsEnabled(false);
-        mTarget.foldStateChanged(FOLDED);
-
-        mTarget.foldStateChanged(OPEN);
-        requestDisplaySwitch();
-
-        assertTransitionNotRequested();
-    }
-
-    @Test
-    public void testDisplaySwitch_whenUnfoldAnimationDisabled_noTransition() {
-        givenAllAnimationsEnabled();
-        givenUnfoldTransitionEnabled(false);
-        mTarget.foldStateChanged(FOLDED);
-
-        mTarget.foldStateChanged(OPEN);
-        requestDisplaySwitch();
-
-        assertTransitionNotRequested();
-    }
-
-    @Test
-    public void testDisplaySwitchAfterUnfolding_otherCollectingTransition_collectsDisplaySwitch() {
-        givenAllAnimationsEnabled();
-        mTarget.foldStateChanged(FOLDED);
-
-        mTarget.foldStateChanged(OPEN);
-        requestDisplaySwitch();
-
-        // Collects to the current transition
-        assertTrue(mTransitionController.getCollectingTransition().mParticipants.contains(
-                mDisplayContent));
-    }
-
-
-    @Test
-    public void testDisplaySwitch_whenNoContentInDisplayContent_noTransition() {
-        givenAllAnimationsEnabled();
-        givenDisplayContentHasContent(false);
-        mTarget.foldStateChanged(FOLDED);
-
-        mTarget.foldStateChanged(OPEN);
-        requestDisplaySwitch();
-
-        assertTransitionNotRequested();
-    }
-
-    private void assertTransitionRequested() {
-        assertNotNull(mPlayer.mLastRequest);
-    }
-
-    private void assertTransitionNotRequested() {
-        assertNull(mPlayer.mLastRequest);
-    }
-
-    private void requestDisplaySwitch() {
-        mTarget.requestDisplaySwitchTransitionIfNeeded(
-                mDisplayContent.getDisplayId(),
-                mDisplayContent.getBounds().width(),
-                mDisplayContent.getBounds().height(),
-                /* newDisplayWidth= */ 200,
-                /* newDisplayHeight= */ 250
-        );
-    }
-
-    private void givenAllAnimationsEnabled() {
-        givenAnimationsEnabled(true);
-        givenUnfoldTransitionEnabled(true);
-        givenShellTransitionsEnabled(true);
-        givenDisplayContentHasContent(true);
-    }
-
-    private void givenUnfoldTransitionEnabled(boolean enabled) {
-        when(mResources.getBoolean(config_unfoldTransitionEnabled)).thenReturn(enabled);
-    }
-
-    private void givenAnimationsEnabled(boolean enabled) {
-        ValueAnimator.setDurationScale(enabled ? 1.0f : 0.0f);
-    }
-
-    private void givenShellTransitionsEnabled(boolean enabled) {
-        if (enabled) {
-            mTransitionController.registerTransitionPlayer(mPlayer, null /* proc */);
-        } else {
-            mTransitionController.unregisterTransitionPlayer(mPlayer);
-        }
-    }
-
-    private void givenDisplayContentHasContent(boolean hasContent) {
-        when(mDisplayContent.getLastHasContent()).thenReturn(hasContent);
-    }
-}