Merge "Revert^2 "[flexiglass] Fixes issue where user management settings didn't show"" into main
diff --git a/core/api/current.txt b/core/api/current.txt
index 836d4e99..3ffab90 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -46698,6 +46698,8 @@
     field public static final int TYPE_IMS = 64; // 0x40
     field public static final int TYPE_MCX = 1024; // 0x400
     field public static final int TYPE_MMS = 2; // 0x2
+    field @FlaggedApi("com.android.internal.telephony.flags.oem_paid_private") public static final int TYPE_OEM_PAID = 65536; // 0x10000
+    field @FlaggedApi("com.android.internal.telephony.flags.oem_paid_private") public static final int TYPE_OEM_PRIVATE = 131072; // 0x20000
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final int TYPE_RCS = 32768; // 0x8000
     field public static final int TYPE_SUPL = 4; // 0x4
     field public static final int TYPE_VSIM = 4096; // 0x1000
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index a1561c2..bc34f5b 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -15888,6 +15888,8 @@
     field public static final String TYPE_IMS_STRING = "ims";
     field public static final String TYPE_MCX_STRING = "mcx";
     field public static final String TYPE_MMS_STRING = "mms";
+    field @FlaggedApi("com.android.internal.telephony.flags.oem_paid_private") public static final String TYPE_OEM_PAID_STRING = "oem_paid";
+    field @FlaggedApi("com.android.internal.telephony.flags.oem_paid_private") public static final String TYPE_OEM_PRIVATE_STRING = "oem_private";
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String TYPE_RCS_STRING = "rcs";
     field public static final String TYPE_SUPL_STRING = "supl";
     field public static final String TYPE_VSIM_STRING = "vsim";
diff --git a/core/java/Android.bp b/core/java/Android.bp
index 92bca3c..9904632 100644
--- a/core/java/Android.bp
+++ b/core/java/Android.bp
@@ -21,14 +21,52 @@
         "**/*.aidl",
         ":framework-nfc-non-updatable-sources",
         ":messagequeue-gen",
+        ":ranging_stack_mock_initializer",
     ],
     // Exactly one MessageQueue.java will be added to srcs by messagequeue-gen
     exclude_srcs: [
         "android/os/*MessageQueue/**/*.java",
+        "android/ranging/**/*.java",
     ],
     visibility: ["//frameworks/base"],
 }
 
+//Mock to allow service registry for ranging stack.
+//TODO(b/331206299): Remove this after RELEASE_RANGING_STACK is ramped up to next.
+soong_config_module_type {
+    name: "ranging_stack_framework_mock_init",
+    module_type: "genrule",
+    config_namespace: "bootclasspath",
+    bool_variables: [
+        "release_ranging_stack",
+    ],
+    properties: [
+        "srcs",
+        "cmd",
+        "out",
+    ],
+}
+
+// The actual RangingFrameworkInitializer is present in packages/modules/Uwb/ranging/framework.
+// Mock RangingFrameworkInitializer does nothing and allows to successfully build
+// SystemServiceRegistry after registering for system service in SystemServiceRegistry both with
+// and without build flag RELEASE_RANGING_STACK enabled.
+ranging_stack_framework_mock_init {
+    name: "ranging_stack_mock_initializer",
+    soong_config_variables: {
+        release_ranging_stack: {
+            cmd: "touch $(out)",
+            // Adding an empty file as out is mandatory.
+            out: ["android/ranging/empty_ranging_fw.txt"],
+            conditions_default: {
+                srcs: ["android/ranging/mock/RangingFrameworkInitializer.java"],
+                cmd: "mkdir -p android/ranging/; cp $(in) $(out);",
+                out: ["android/ranging/RangingFrameworkInitializer.java"],
+            },
+        },
+    },
+}
+
 // Add selected MessageQueue.java implementation to srcs
 soong_config_module_type {
     name: "release_package_messagequeue_implementation_srcs",
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index c13a58f..ea4148c 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -230,6 +230,7 @@
 import android.print.PrintManager;
 import android.provider.E2eeContactKeysManager;
 import android.provider.ProviderFrameworkInitializer;
+import android.ranging.RangingFrameworkInitializer;
 import android.safetycenter.SafetyCenterFrameworkInitializer;
 import android.scheduling.SchedulingFrameworkInitializer;
 import android.security.FileIntegrityManager;
@@ -1825,6 +1826,12 @@
             if (android.webkit.Flags.updateServiceIpcWrapper()) {
                 WebViewBootstrapFrameworkInitializer.registerServiceWrappers();
             }
+            // This is guarded by aconfig flag "com.android.ranging.flags.ranging_stack_enabled"
+            // when the build flag RELEASE_RANGING_STACK is enabled. When disabled, this calls the
+            // mock RangingFrameworkInitializer#registerServiceWrappers which is no-op. As the
+            // aconfig lib for ranging module is built only if  RELEASE_RANGING_STACK is enabled,
+            // flagcannot be added here.
+            RangingFrameworkInitializer.registerServiceWrappers();
         } finally {
             // If any of the above code throws, we're in a pretty bad shape and the process
             // will likely crash, but we'll reset it just in case there's an exception handler...
diff --git a/core/java/android/ranging/mock/RangingFrameworkInitializer.java b/core/java/android/ranging/mock/RangingFrameworkInitializer.java
new file mode 100644
index 0000000..540f519
--- /dev/null
+++ b/core/java/android/ranging/mock/RangingFrameworkInitializer.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.ranging;
+
+/**
+* Mock RangingFrameworkInitializer.
+*
+* @hide
+*/
+
+// TODO(b/331206299): Remove this after RANGING_STACK_ENABLED is ramped up to next.
+public final class RangingFrameworkInitializer {
+    private RangingFrameworkInitializer() {}
+    /**
+     * @hide
+     */
+    public static void registerServiceWrappers() {
+        // No-op.
+    }
+}
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 66776ce..ac208b5 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -2064,7 +2064,11 @@
         if (mAttachInfo.mThreadedRenderer == null) return;
         if (mAttachInfo.mThreadedRenderer.setForceDark(determineForceDarkType())) {
             // TODO: Don't require regenerating all display lists to apply this setting
-            invalidateWorld(mView);
+            if (forceInvertColor()) {
+                destroyAndInvalidate();
+            } else {
+                invalidateWorld(mView);
+            }
         }
     }
 
@@ -11911,15 +11915,23 @@
         public void onHighTextContrastStateChanged(boolean enabled) {
             ThreadedRenderer.setHighContrastText(enabled);
 
-            // Destroy Displaylists so they can be recreated with high contrast recordings
-            destroyHardwareResources();
-
-            // Schedule redraw, which will rerecord + redraw all text
-            invalidate();
+            destroyAndInvalidate();
         }
     }
 
     /**
+     * Destroy Displaylists so they can be recreated with new recordings, in case you are changing
+     * the way things are rendered (e.g. high contrast, force dark), then invalidate to trigger a
+     * redraw.
+     */
+    private void destroyAndInvalidate() {
+        destroyHardwareResources();
+
+        // Schedule redraw, which will rerecord + redraw all text
+        invalidate();
+    }
+
+    /**
      * This class is an interface this ViewAncestor provides to the
      * AccessibilityManagerService to the latter can interact with
      * the view hierarchy in this ViewAncestor.
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 2f649c2..1e5c6d8 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -465,13 +465,6 @@
     private static final long USE_ASYNC_SHOW_HIDE_METHOD = 352594277L; // This is a bug id.
 
     /**
-     * Version-gating is guarded by bug-fix flag.
-     */
-    private static final boolean ASYNC_SHOW_HIDE_METHOD_ENABLED =
-            !Flags.compatchangeForZerojankproxy()
-                || CompatChanges.isChangeEnabled(USE_ASYNC_SHOW_HIDE_METHOD);
-
-    /**
      * If {@code true}, avoid calling the
      * {@link com.android.server.inputmethod.InputMethodManagerService InputMethodManagerService}
      * by skipping the call to {@link IInputMethodManager#startInputOrWindowGainedFocus}
@@ -614,6 +607,15 @@
     @UnsupportedAppUsage
     Rect mCursorRect = new Rect();
 
+    /**
+     * Version-gating is guarded by bug-fix flag.
+     */
+    // Note: this is non-static so that it only gets initialized once CompatChanges has
+    // access to the correct application context.
+    private final boolean mAsyncShowHideMethodEnabled =
+            !Flags.compatchangeForZerojankproxy()
+                    || CompatChanges.isChangeEnabled(USE_ASYNC_SHOW_HIDE_METHOD);
+
     /** Cached value for {@link #isStylusHandwritingAvailable} for userId. */
     @GuardedBy("mH")
     private PropertyInvalidatedCache<Integer, Boolean> mStylusHandwritingAvailableCache;
@@ -2419,7 +2421,7 @@
                         mCurRootView.getLastClickToolType(),
                         resultReceiver,
                         reason,
-                        ASYNC_SHOW_HIDE_METHOD_ENABLED);
+                        mAsyncShowHideMethodEnabled);
             }
         }
     }
@@ -2463,7 +2465,7 @@
                     mCurRootView.getLastClickToolType(),
                     resultReceiver,
                     reason,
-                    ASYNC_SHOW_HIDE_METHOD_ENABLED);
+                    mAsyncShowHideMethodEnabled);
         }
     }
 
@@ -2572,7 +2574,7 @@
                 return true;
             } else {
                 return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken,
-                        statsToken, flags, resultReceiver, reason, ASYNC_SHOW_HIDE_METHOD_ENABLED);
+                        statsToken, flags, resultReceiver, reason, mAsyncShowHideMethodEnabled);
             }
         }
     }
@@ -2615,7 +2617,7 @@
             ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
 
             return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, view.getWindowToken(),
-                    statsToken, flags, null, reason, ASYNC_SHOW_HIDE_METHOD_ENABLED);
+                    statsToken, flags, null, reason, mAsyncShowHideMethodEnabled);
         }
     }
 
@@ -3392,7 +3394,7 @@
                         servedInputConnection == null ? null
                                 : servedInputConnection.asIRemoteAccessibilityInputConnection(),
                         view.getContext().getApplicationInfo().targetSdkVersion, targetUserId,
-                        mImeDispatcher, ASYNC_SHOW_HIDE_METHOD_ENABLED);
+                        mImeDispatcher, mAsyncShowHideMethodEnabled);
             } else {
                 res = IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocus(
                         startInputReason, mClient, windowGainingFocus, startInputFlags,
diff --git a/graphics/java/android/graphics/PathIterator.java b/graphics/java/android/graphics/PathIterator.java
index 48b29f4..d7caabf 100644
--- a/graphics/java/android/graphics/PathIterator.java
+++ b/graphics/java/android/graphics/PathIterator.java
@@ -44,6 +44,8 @@
     private final Path mPath;
     private final int mPathGenerationId;
     private static final int POINT_ARRAY_SIZE = 8;
+    private static final boolean IS_DALVIK = "dalvik".equalsIgnoreCase(
+            System.getProperty("java.vm.name"));
 
     private static final NativeAllocationRegistry sRegistry =
             NativeAllocationRegistry.createMalloced(
@@ -80,9 +82,14 @@
         mPath = path;
         mNativeIterator = nCreate(mPath.mNativePath);
         mPathGenerationId = mPath.getGenerationId();
-        final VMRuntime runtime = VMRuntime.getRuntime();
-        mPointsArray = (float[]) runtime.newNonMovableArray(float.class, POINT_ARRAY_SIZE);
-        mPointsAddress = runtime.addressOf(mPointsArray);
+        if (IS_DALVIK) {
+            final VMRuntime runtime = VMRuntime.getRuntime();
+            mPointsArray = (float[]) runtime.newNonMovableArray(float.class, POINT_ARRAY_SIZE);
+            mPointsAddress = runtime.addressOf(mPointsArray);
+        } else {
+            mPointsArray = new float[POINT_ARRAY_SIZE];
+            mPointsAddress = 0;
+        }
         sRegistry.registerNativeAllocation(this, mNativeIterator);
     }
 
@@ -177,7 +184,8 @@
             throw new ConcurrentModificationException(
                     "Iterator cannot be used on modified Path");
         }
-        @Verb int verb = nNext(mNativeIterator, mPointsAddress);
+        @Verb int verb = IS_DALVIK
+            ? nNext(mNativeIterator, mPointsAddress) : nNextHost(mNativeIterator, mPointsArray);
         if (verb == VERB_DONE) {
             mDone = true;
         }
@@ -287,6 +295,9 @@
     private static native long nCreate(long nativePath);
     private static native long nGetFinalizer();
 
+    /* nNextHost should be used for host runtimes, e.g. LayoutLib */
+    private static native int nNextHost(long nativeIterator, float[] points);
+
     // ------------------ Critical JNI ------------------------
 
     @CriticalNative
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index 63a2880..cf0a975 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -156,6 +156,13 @@
 }
 
 flag {
+    name: "enable_flexible_two_app_split"
+    namespace: "multitasking"
+    description: "Enables only 2 app 90:10 split"
+    bug: "349828130"
+}
+
+flag {
     name: "enable_flexible_split"
     namespace: "multitasking"
     description: "Enables flexibile split feature for split screen"
diff --git a/libs/WindowManager/Shell/res/drawable/app_handle_education_tooltip_icon.xml b/libs/WindowManager/Shell/res/drawable/app_handle_education_tooltip_icon.xml
new file mode 100644
index 0000000..07e5ac1
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/app_handle_education_tooltip_icon.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?android:attr/textColorTertiary"
+    android:viewportHeight="960"
+    android:viewportWidth="960">
+    <path
+        android:fillColor="@android:color/system_on_tertiary_fixed"
+        android:pathData="M419,880Q391,880 366.5,868Q342,856 325,834L107,557L126,537Q146,516 174,512Q202,508 226,523L300,568L300,240Q300,223 311.5,211.5Q323,200 340,200Q357,200 369,211.5Q381,223 381,240L381,712L284,652L388,785Q394,792 402,796Q410,800 419,800L640,800Q673,800 696.5,776.5Q720,753 720,720L720,560Q720,543 708.5,531.5Q697,520 680,520L461,520L461,440L680,440Q730,440 765,475Q800,510 800,560L800,720Q800,786 753,833Q706,880 640,880L419,880ZM167,340Q154,318 147,292.5Q140,267 140,240Q140,157 198.5,98.5Q257,40 340,40Q423,40 481.5,98.5Q540,157 540,240Q540,267 533,292.5Q526,318 513,340L444,300Q452,286 456,271.5Q460,257 460,240Q460,190 425,155Q390,120 340,120Q290,120 255,155Q220,190 220,240Q220,257 224,271.5Q228,286 236,300L167,340ZM502,620L502,620L502,620L502,620Q502,620 502,620Q502,620 502,620L502,620Q502,620 502,620Q502,620 502,620L502,620Q502,620 502,620Q502,620 502,620L502,620L502,620Z" />
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_background.xml
new file mode 100644
index 0000000..a12a746
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_background.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape android:shape="rectangle">
+            <corners android:radius="30dp" />
+            <solid android:color="@android:color/system_tertiary_fixed" />
+        </shape>
+    </item>
+</layer-list>
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_left_arrow.xml b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_left_arrow.xml
new file mode 100644
index 0000000..aadffb5
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_left_arrow.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<!-- An arrow that points towards left. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="10dp"
+    android:height="12dp"
+    android:viewportWidth="10"
+    android:viewportHeight="12">
+  <path
+      android:pathData="M2.858,4.285C1.564,5.062 1.564,6.938 2.858,7.715L10,12L10,0L2.858,4.285Z"
+      android:fillColor="@android:color/system_tertiary_fixed"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_top_arrow.xml b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_top_arrow.xml
new file mode 100644
index 0000000..e3c9a66
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_top_arrow.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ 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.
+  -->
+
+<!-- An arrow that points upwards. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="12dp"
+    android:height="9dp"
+    android:viewportWidth="12"
+    android:viewportHeight="9">
+  <path
+      android:pathData="M7.715,1.858C6.938,0.564 5.062,0.564 4.285,1.858L0,9L12,9L7.715,1.858Z"
+      android:fillColor="@android:color/system_tertiary_fixed"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_left_arrow_tooltip.xml b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_left_arrow_tooltip.xml
new file mode 100644
index 0000000..a269b9e
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_left_arrow_tooltip.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:elevation="1dp"
+    android:orientation="horizontal">
+
+    <!-- ImageView for the arrow icon, positioned horizontally at the start of the tooltip
+    container. -->
+    <ImageView
+        android:id="@+id/arrow_icon"
+        android:layout_width="10dp"
+        android:layout_height="12dp"
+        android:layout_gravity="center_vertical"
+        android:src="@drawable/desktop_windowing_education_tooltip_left_arrow" />
+
+    <!-- Layout for the tooltip, excluding the arrow. Separating the tooltip content from the arrow
+    allows scaling of only the tooltip container when the content changes, without affecting the
+    arrow. -->
+    <include layout="@layout/desktop_windowing_education_tooltip_container" />
+</LinearLayout>
diff --git a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_tooltip_container.xml b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_tooltip_container.xml
new file mode 100644
index 0000000..bdee883
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_tooltip_container.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/tooltip_container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@drawable/desktop_windowing_education_tooltip_background"
+    android:orientation="horizontal"
+    android:padding="@dimen/desktop_windowing_education_tooltip_padding">
+
+    <ImageView
+        android:id="@+id/tooltip_icon"
+        android:layout_width="32dp"
+        android:layout_height="32dp"
+        android:layout_gravity="center_vertical"
+        android:src="@drawable/app_handle_education_tooltip_icon" />
+
+    <TextView
+        android:id="@+id/tooltip_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:layout_marginStart="2dp"
+        android:lineHeight="20dp"
+        android:maxWidth="150dp"
+        android:textColor="@android:color/system_on_tertiary_fixed"
+        android:textFontWeight="500"
+        android:textSize="14sp" />
+</LinearLayout>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_top_arrow_tooltip.xml b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_top_arrow_tooltip.xml
new file mode 100644
index 0000000..c73c1da
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_top_arrow_tooltip.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:elevation="1dp"
+    android:orientation="vertical">
+
+    <!-- ImageView for the arrow icon, positioned vertically above the tooltip container. -->
+    <ImageView
+        android:id="@+id/arrow_icon"
+        android:layout_width="12dp"
+        android:layout_height="9dp"
+        android:layout_gravity="center_horizontal"
+        android:src="@drawable/desktop_windowing_education_tooltip_top_arrow" />
+
+    <!-- Layout for the tooltip, excluding the arrow. Separating the tooltip content from the arrow
+    allows scaling of only the tooltip container when the content changes, without affecting the
+    arrow. -->
+    <include layout="@layout/desktop_windowing_education_tooltip_container" />
+</LinearLayout>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 3d87183..c7109f5 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -608,6 +608,9 @@
     <!-- The horizontal inset to apply to the close button's ripple drawable -->
     <dimen name="desktop_mode_header_close_ripple_inset_horizontal">6dp</dimen>
 
+    <!-- The padding added to all sides of windowing education tooltip -->
+    <dimen name="desktop_windowing_education_tooltip_padding">8dp</dimen>
+
     <!-- The acceptable area ratio of fg icon area/bg icon area, i.e. (72 x 72) / (108 x 108) -->
     <item type="dimen" format="float" name="splash_icon_enlarge_foreground_threshold">0.44</item>
     <!-- Scaling factor applied to splash icons without provided background i.e. (192 / 160) -->
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index 266eca8..56f25da 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -219,6 +219,15 @@
          compatibility control. [CHAR LIMIT=NONE] -->
     <string name="camera_compat_dismiss_button_description">No camera issues? Tap to dismiss.</string>
 
+    <!-- App handle education tooltip text for tooltip pointing to app handle -->
+    <string name="windowing_app_handle_education_tooltip">Tap to open the app menu</string>
+
+    <!-- App handle education tooltip text for tooltip pointing to windowing image button -->
+    <string name="windowing_desktop_mode_image_button_education_tooltip">Tap to show multiple apps together</string>
+
+    <!-- App handle education tooltip text for tooltip pointing to app chip -->
+    <string name="windowing_desktop_mode_exit_education_tooltip">Return to fullscreen from the app menu</string>
+
     <!-- The title of the letterbox education dialog. [CHAR LIMIT=NONE] -->
     <string name="letterbox_education_dialog_title">See and do more</string>
 
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/OWNERS b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/OWNERS
new file mode 100644
index 0000000..bfb6d4a
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/OWNERS
@@ -0,0 +1,4 @@
+jeremysim@google.com
+winsonc@google.com
+peanutbutter@google.com
+shuminghao@google.com
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java
index 498dc8b..7f1e4a8 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java
@@ -66,14 +66,54 @@
     public @interface SplitPosition {
     }
 
-    /** A snap target in the first half of the screen, where the split is roughly 30-70. */
-    public static final int SNAP_TO_30_70 = 0;
+    /**
+     * A snap target for two apps, where the split is 33-66. With FLAG_ENABLE_FLEXIBLE_SPLIT,
+     * only used on tablets.
+     */
+    public static final int SNAP_TO_2_33_66 = 0;
 
-    /** The 50-50 snap target */
-    public static final int SNAP_TO_50_50 = 1;
+    /** A snap target for two apps, where the split is 50-50.  */
+    public static final int SNAP_TO_2_50_50 = 1;
 
-    /** A snap target in the latter half of the screen, where the split is roughly 70-30. */
-    public static final int SNAP_TO_70_30 = 2;
+    /**
+     * A snap target for two apps, where the split is 66-33. With FLAG_ENABLE_FLEXIBLE_SPLIT,
+     * only used on tablets.
+     */
+    public static final int SNAP_TO_2_66_33 = 2;
+
+    /**
+     * A snap target for two apps, where the split is 90-10. The "10" app extends off the screen,
+     * and is actually the same size as the onscreen app, but the visible portion takes up 10% of
+     * the screen. With FLAG_ENABLE_FLEXIBLE_SPLIT, used on phones and foldables.
+     */
+    public static final int SNAP_TO_2_90_10 = 3;
+
+    /**
+     * A snap target for two apps, where the split is 10-90. The "10" app extends off the screen,
+     * and is actually the same size as the onscreen app, but the visible portion takes up 10% of
+     * the screen. With FLAG_ENABLE_FLEXIBLE_SPLIT, used on phones and foldables.
+     */
+    public static final int SNAP_TO_2_10_90 = 4;
+
+    /**
+     * A snap target for three apps, where the split is 33-33-33. With FLAG_ENABLE_FLEXIBLE_SPLIT,
+     * only used on tablets.
+     */
+    public static final int SNAP_TO_3_33_33_33 = 5;
+
+    /**
+     * A snap target for three apps, where the split is 45-45-10. The "10" app extends off the
+     * screen, and is actually the same size as the onscreen apps, but the visible portion takes
+     * up 10% of the screen. With FLAG_ENABLE_FLEXIBLE_SPLIT, only used on unfolded foldables.
+     */
+    public static final int SNAP_TO_3_45_45_10 = 6;
+
+    /**
+     * A snap target for three apps, where the split is 10-45-45. The "10" app extends off the
+     * screen, and is actually the same size as the onscreen apps, but the visible portion takes
+     * up 10% of the screen. With FLAG_ENABLE_FLEXIBLE_SPLIT, only used on unfolded foldables.
+     */
+    public static final int SNAP_TO_3_10_45_45 = 7;
 
     /**
      * These snap targets are used for split pairs in a stable, non-transient state. They may be
@@ -81,9 +121,14 @@
      * {@link SnapPosition}.
      */
     @IntDef(prefix = { "SNAP_TO_" }, value = {
-            SNAP_TO_30_70,
-            SNAP_TO_50_50,
-            SNAP_TO_70_30
+            SNAP_TO_2_33_66,
+            SNAP_TO_2_50_50,
+            SNAP_TO_2_66_33,
+            SNAP_TO_2_90_10,
+            SNAP_TO_2_10_90,
+            SNAP_TO_3_33_33_33,
+            SNAP_TO_3_45_45_10,
+            SNAP_TO_3_10_45_45,
     })
     public @interface PersistentSnapPosition {}
 
@@ -91,9 +136,14 @@
      * Checks if the snapPosition in question is a {@link PersistentSnapPosition}.
      */
     public static boolean isPersistentSnapPosition(@SnapPosition int snapPosition) {
-        return snapPosition == SNAP_TO_30_70
-                || snapPosition == SNAP_TO_50_50
-                || snapPosition == SNAP_TO_70_30;
+        return snapPosition == SNAP_TO_2_33_66
+                || snapPosition == SNAP_TO_2_50_50
+                || snapPosition == SNAP_TO_2_66_33
+                || snapPosition == SNAP_TO_2_90_10
+                || snapPosition == SNAP_TO_2_10_90
+                || snapPosition == SNAP_TO_3_33_33_33
+                || snapPosition == SNAP_TO_3_45_45_10
+                || snapPosition == SNAP_TO_3_10_45_45;
     }
 
     /** The divider doesn't snap to any target and is freely placeable. */
@@ -109,9 +159,14 @@
     public static final int SNAP_TO_MINIMIZE = 13;
 
     @IntDef(prefix = { "SNAP_TO_" }, value = {
-            SNAP_TO_30_70,
-            SNAP_TO_50_50,
-            SNAP_TO_70_30,
+            SNAP_TO_2_33_66,
+            SNAP_TO_2_50_50,
+            SNAP_TO_2_66_33,
+            SNAP_TO_2_90_10,
+            SNAP_TO_2_10_90,
+            SNAP_TO_3_33_33_33,
+            SNAP_TO_3_45_45_10,
+            SNAP_TO_3_10_45_45,
             SNAP_TO_NONE,
             SNAP_TO_START_AND_DISMISS,
             SNAP_TO_END_AND_DISMISS,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
index f7f45ae..9f100fa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
@@ -19,9 +19,9 @@
 import static android.view.WindowManager.DOCKED_LEFT;
 import static android.view.WindowManager.DOCKED_RIGHT;
 
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_30_70;
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50;
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_70_30;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_33_66;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_66_33;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_END_AND_DISMISS;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_MINIMIZE;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_NONE;
@@ -283,10 +283,10 @@
 
     private void addNonDismissingTargets(boolean isHorizontalDivision, int topPosition,
             int bottomPosition, int dividerMax) {
-        maybeAddTarget(topPosition, topPosition - getStartInset(), SNAP_TO_30_70);
+        maybeAddTarget(topPosition, topPosition - getStartInset(), SNAP_TO_2_33_66);
         addMiddleTarget(isHorizontalDivision);
         maybeAddTarget(bottomPosition,
-                dividerMax - getEndInset() - (bottomPosition + mDividerSize), SNAP_TO_70_30);
+                dividerMax - getEndInset() - (bottomPosition + mDividerSize), SNAP_TO_2_66_33);
     }
 
     private void addFixedDivisionTargets(boolean isHorizontalDivision, int dividerMax) {
@@ -332,7 +332,7 @@
     private void addMiddleTarget(boolean isHorizontalDivision) {
         int position = DockedDividerUtils.calculateMiddlePosition(isHorizontalDivision,
                 mInsets, mDisplayWidth, mDisplayHeight, mDividerSize);
-        mTargets.add(new SnapTarget(position, SNAP_TO_50_50));
+        mTargets.add(new SnapTarget(position, SNAP_TO_2_50_50));
     }
 
     private void addMinimizedTarget(boolean isHorizontalDivision, int dockedSide) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 2138acc..cbb08b8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -1344,6 +1344,9 @@
         final SurfaceControl leash = pipChange.getLeash();
         final Rect destBounds = mPipOrganizer.getCurrentOrAnimatingBounds();
         final boolean isInPip = mPipTransitionState.isInPip();
+        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                "%s: Update pip for unhandled transition, change=%s, destBounds=%s, isInPip=%b",
+                TAG, pipChange, destBounds, isInPip);
         mSurfaceTransactionHelper
                 .crop(startTransaction, leash, destBounds)
                 .round(startTransaction, leash, isInPip)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
index 226b0fb..1be26f0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
@@ -107,4 +107,27 @@
         }
         windowManagerWrapper.updateViewLayout(view, lp)
     }
+
+    class Factory {
+        fun create(
+            windowManagerWrapper: WindowManagerWrapper,
+            taskId: Int,
+            x: Int,
+            y: Int,
+            width: Int,
+            height: Int,
+            flags: Int,
+            view: View,
+        ): AdditionalSystemViewContainer =
+            AdditionalSystemViewContainer(
+                windowManagerWrapper = windowManagerWrapper,
+                taskId = taskId,
+                x = x,
+                y = y,
+                width = width,
+                height = height,
+                flags = flags,
+                view = view
+            )
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt
new file mode 100644
index 0000000..98413ee
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt
@@ -0,0 +1,249 @@
+/*
+ * 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.wm.shell.windowdecor.education
+
+import android.annotation.DimenRes
+import android.annotation.LayoutRes
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Point
+import android.util.Size
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.View.MeasureSpec.UNSPECIFIED
+import android.view.WindowManager
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.dynamicanimation.animation.DynamicAnimation
+import androidx.dynamicanimation.animation.SpringForce
+import com.android.wm.shell.R
+import com.android.wm.shell.shared.animation.PhysicsAnimator
+import com.android.wm.shell.windowdecor.WindowManagerWrapper
+import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
+
+/**
+ * Controls the lifecycle of an education tooltip, including showing and hiding it. Ensures that
+ * only one tooltip is displayed at a time.
+ */
+class DesktopWindowingEducationTooltipController(
+    private val context: Context,
+    private val additionalSystemViewContainerFactory: AdditionalSystemViewContainer.Factory,
+) {
+  // TODO: b/369384567 - Set tooltip color scheme to match LT/DT of app theme
+  private var tooltipView: View? = null
+  private var animator: PhysicsAnimator<View>? = null
+  private val springConfig by lazy {
+    PhysicsAnimator.SpringConfig(SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY)
+  }
+  private var popupWindow: AdditionalSystemViewContainer? = null
+
+  /**
+   * Shows education tooltip.
+   *
+   * @param tooltipViewConfig features of tooltip.
+   * @param taskId is used in the title of popup window created for the tooltip view.
+   */
+  fun showEducationTooltip(tooltipViewConfig: EducationViewConfig, taskId: Int) {
+    hideEducationTooltip()
+    tooltipView = createEducationTooltipView(tooltipViewConfig, taskId)
+    animator = createAnimator()
+    animateShowTooltipTransition()
+  }
+
+  /** Hide the current education view if visible */
+  private fun hideEducationTooltip() = animateHideTooltipTransition { cleanUp() }
+
+  /** Create education view by inflating layout provided. */
+  private fun createEducationTooltipView(
+      tooltipViewConfig: EducationViewConfig,
+      taskId: Int,
+  ): View {
+    val tooltipView =
+        LayoutInflater.from(context)
+            .inflate(
+                tooltipViewConfig.tooltipViewLayout, /* root= */ null, /* attachToRoot= */ false)
+            .apply {
+              alpha = 0f
+              scaleX = 0f
+              scaleY = 0f
+
+              requireViewById<TextView>(R.id.tooltip_text).apply {
+                text = tooltipViewConfig.tooltipText
+              }
+
+              setOnTouchListener { _, motionEvent ->
+                if (motionEvent.action == MotionEvent.ACTION_OUTSIDE) {
+                  hideEducationTooltip()
+                  tooltipViewConfig.onDismissAction()
+                  true
+                } else {
+                  false
+                }
+              }
+              setOnClickListener {
+                hideEducationTooltip()
+                tooltipViewConfig.onEducationClickAction()
+              }
+            }
+
+    val tooltipDimens = tooltipDimens(tooltipView = tooltipView, tooltipViewConfig.arrowDirection)
+    val tooltipViewGlobalCoordinates =
+        tooltipViewGlobalCoordinates(
+            tooltipViewGlobalCoordinates = tooltipViewConfig.tooltipViewGlobalCoordinates,
+            arrowDirection = tooltipViewConfig.arrowDirection,
+            tooltipDimen = tooltipDimens)
+    createTooltipPopupWindow(
+        taskId, tooltipViewGlobalCoordinates, tooltipDimens, tooltipView = tooltipView)
+
+    return tooltipView
+  }
+
+  /** Create animator for education transitions */
+  private fun createAnimator(): PhysicsAnimator<View>? =
+      tooltipView?.let {
+        PhysicsAnimator.getInstance(it).apply { setDefaultSpringConfig(springConfig) }
+      }
+
+  /** Animate show transition for the education view */
+  private fun animateShowTooltipTransition() {
+    animator
+        ?.spring(DynamicAnimation.ALPHA, 1f)
+        ?.spring(DynamicAnimation.SCALE_X, 1f)
+        ?.spring(DynamicAnimation.SCALE_Y, 1f)
+        ?.start()
+  }
+
+  /** Animate hide transition for the education view */
+  private fun animateHideTooltipTransition(endActions: () -> Unit) {
+    animator
+        ?.spring(DynamicAnimation.ALPHA, 0f)
+        ?.spring(DynamicAnimation.SCALE_X, 0f)
+        ?.spring(DynamicAnimation.SCALE_Y, 0f)
+        ?.start()
+    endActions()
+  }
+
+  /** Remove education tooltip and clean up all relative properties */
+  private fun cleanUp() {
+    tooltipView = null
+    animator = null
+    popupWindow?.releaseView()
+    popupWindow = null
+  }
+
+  private fun createTooltipPopupWindow(
+      taskId: Int,
+      tooltipViewGlobalCoordinates: Point,
+      tooltipDimen: Size,
+      tooltipView: View,
+  ) {
+    popupWindow =
+        additionalSystemViewContainerFactory.create(
+            windowManagerWrapper =
+                WindowManagerWrapper(context.getSystemService(WindowManager::class.java)),
+            taskId = taskId,
+            x = tooltipViewGlobalCoordinates.x,
+            y = tooltipViewGlobalCoordinates.y,
+            width = tooltipDimen.width,
+            height = tooltipDimen.height,
+            flags =
+                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
+                    WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
+            view = tooltipView)
+  }
+
+  private fun tooltipViewGlobalCoordinates(
+      tooltipViewGlobalCoordinates: Point,
+      arrowDirection: TooltipArrowDirection,
+      tooltipDimen: Size,
+  ): Point {
+    var tooltipX = tooltipViewGlobalCoordinates.x
+    var tooltipY = tooltipViewGlobalCoordinates.y
+
+    // Current values of [tooltipX]/[tooltipY] are the coordinates of tip of the arrow.
+    // Parameter x and y passed to [AdditionalSystemViewContainer] is the top left position of
+    // the window to be created. Hence we will need to move the coordinates left/up in order
+    // to position the tooltip correctly.
+    if (arrowDirection == TooltipArrowDirection.UP) {
+      // Arrow is placed at horizontal center on top edge of the tooltip. Hence decrement
+      // half of tooltip width from [tooltipX] to horizontally position the tooltip.
+      tooltipX -= tooltipDimen.width / 2
+    } else {
+      // Arrow is placed at vertical center on the left edge of the tooltip. Hence decrement
+      // half of tooltip height from [tooltipY] to vertically position the tooltip.
+      tooltipY -= tooltipDimen.height / 2
+    }
+    return Point(tooltipX, tooltipY)
+  }
+
+  private fun tooltipDimens(tooltipView: View, arrowDirection: TooltipArrowDirection): Size {
+    val tooltipBackground = tooltipView.requireViewById<LinearLayout>(R.id.tooltip_container)
+    val arrowView = tooltipView.requireViewById<ImageView>(R.id.arrow_icon)
+    tooltipBackground.measure(
+        /* widthMeasureSpec= */ UNSPECIFIED, /* heightMeasureSpec= */ UNSPECIFIED)
+    arrowView.measure(/* widthMeasureSpec= */ UNSPECIFIED, /* heightMeasureSpec= */ UNSPECIFIED)
+
+    var desiredWidth =
+        tooltipBackground.measuredWidth +
+            2 * loadDimensionPixelSize(R.dimen.desktop_windowing_education_tooltip_padding)
+    var desiredHeight =
+        tooltipBackground.measuredHeight +
+            2 * loadDimensionPixelSize(R.dimen.desktop_windowing_education_tooltip_padding)
+    if (arrowDirection == TooltipArrowDirection.UP) {
+      // desiredHeight currently does not account for the height of arrow, hence adding it.
+      desiredHeight += arrowView.height
+    } else {
+      // desiredWidth currently does not account for the width of arrow, hence adding it.
+      desiredWidth += arrowView.width
+    }
+
+    return Size(desiredWidth, desiredHeight)
+  }
+
+  private fun loadDimensionPixelSize(@DimenRes resourceId: Int): Int {
+    if (resourceId == Resources.ID_NULL) return 0
+    return context.resources.getDimensionPixelSize(resourceId)
+  }
+
+  /**
+   * The configuration for education view features:
+   *
+   * @property tooltipViewLayout Layout resource ID of the view to be used for education tooltip.
+   * @property tooltipViewGlobalCoordinates Global (screen) coordinates of the tip of the tooltip
+   *   arrow.
+   * @property tooltipText Text to be added to the TextView of tooltip.
+   * @property arrowDirection Direction of arrow of the tooltip.
+   * @property onEducationClickAction Lambda to be executed when the tooltip is clicked.
+   * @property onDismissAction Lambda to be executed when the tooltip is dismissed.
+   */
+  data class EducationViewConfig(
+      @LayoutRes val tooltipViewLayout: Int,
+      val tooltipViewGlobalCoordinates: Point,
+      val tooltipText: String,
+      val arrowDirection: TooltipArrowDirection,
+      val onEducationClickAction: () -> Unit,
+      val onDismissAction: () -> Unit,
+  )
+
+  /** Direction of arrow of the tooltip */
+  enum class TooltipArrowDirection {
+    UP,
+    LEFT,
+  }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
index 177e47a..c52d9dd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
@@ -19,7 +19,7 @@
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -136,7 +136,7 @@
     @Test
     public void testSetDivideRatio() {
         mSplitLayout.setDividerPosition(200, false /* applyLayoutChange */);
-        mSplitLayout.setDivideRatio(SNAP_TO_50_50);
+        mSplitLayout.setDivideRatio(SNAP_TO_2_50_50);
         assertThat(mSplitLayout.getDividerPosition()).isEqualTo(
                 mSplitLayout.mDividerSnapAlgorithm.getMiddleTarget().position);
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 29aea00..94e3616 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -40,6 +40,7 @@
 import android.graphics.PointF
 import android.graphics.Rect
 import android.os.Binder
+import android.os.Bundle
 import android.os.Handler
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
@@ -2865,6 +2866,108 @@
   }
 
   @Test
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+  fun newWindow_fromFullscreenOpensInSplit() {
+    setUpLandscapeDisplay()
+    val task = setUpFullscreenTask()
+    val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java)
+    runOpenNewWindow(task)
+    verify(splitScreenController)
+      .startIntent(any(), anyInt(), any(), any(),
+        optionsCaptor.capture(), anyOrNull())
+    assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
+      .isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+  fun newWindow_fromSplitOpensInSplit() {
+    setUpLandscapeDisplay()
+    val task = setUpSplitScreenTask()
+    val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java)
+    runOpenNewWindow(task)
+    verify(splitScreenController)
+      .startIntent(
+        any(), anyInt(), any(), any(),
+        optionsCaptor.capture(), anyOrNull()
+      )
+    assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
+      .isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+  fun newWindow_fromFreeformAddsNewWindow() {
+    setUpLandscapeDisplay()
+    val task = setUpFreeformTask()
+    val wctCaptor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+    runOpenNewWindow(task)
+    verify(transitions).startTransition(anyInt(), wctCaptor.capture(), anyOrNull())
+    assertThat(ActivityOptions.fromBundle(wctCaptor.value.hierarchyOps[0].launchOptions)
+      .launchWindowingMode).isEqualTo(WINDOWING_MODE_FREEFORM)
+  }
+
+  private fun runOpenNewWindow(task: RunningTaskInfo) {
+    markTaskVisible(task)
+    task.baseActivity = mock(ComponentName::class.java)
+    task.isFocused = true
+    runningTasks.add(task)
+    controller.openNewWindow(task)
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+  fun openInstance_fromFullscreenOpensInSplit() {
+    setUpLandscapeDisplay()
+    val task = setUpFullscreenTask()
+    val taskToRequest = setUpFreeformTask()
+    val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java)
+    runOpenInstance(task, taskToRequest.taskId)
+    verify(splitScreenController)
+      .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull())
+    assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
+      .isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+  fun openInstance_fromSplitOpensInSplit() {
+    setUpLandscapeDisplay()
+    val task = setUpSplitScreenTask()
+    val taskToRequest = setUpFreeformTask()
+    val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java)
+    runOpenInstance(task, taskToRequest.taskId)
+    verify(splitScreenController)
+      .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull())
+    assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
+      .isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+  fun openInstance_fromFreeformAddsNewWindow() {
+    setUpLandscapeDisplay()
+    val task = setUpFreeformTask()
+    val taskToRequest = setUpFreeformTask()
+    val wctCaptor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+    runOpenInstance(task, taskToRequest.taskId)
+    verify(transitions).startTransition(anyInt(), wctCaptor.capture(), anyOrNull())
+    assertThat(ActivityOptions.fromBundle(wctCaptor.value.hierarchyOps[0].launchOptions)
+      .launchWindowingMode).isEqualTo(WINDOWING_MODE_FREEFORM)
+  }
+
+  private fun runOpenInstance(
+    callingTask: RunningTaskInfo,
+    requestedTaskId: Int
+  ) {
+    markTaskVisible(callingTask)
+    callingTask.baseActivity = mock(ComponentName::class.java)
+    callingTask.isFocused = true
+    runningTasks.add(callingTask)
+    controller.openInstance(callingTask, requestedTaskId)
+  }
+
+  @Test
   fun toggleBounds_togglesToStableBounds() {
     val bounds = Rect(0, 0, 100, 100)
     val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt
index 0c3f98a..0c100fc 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt
@@ -30,7 +30,7 @@
 import com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_SINGLE
 import com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_SPLIT
 import com.android.wm.shell.shared.split.SplitBounds
-import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50
+import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50
 import com.google.common.truth.Correspondence
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertThrows
@@ -136,7 +136,7 @@
         assertThat(recentTaskInfoParcel.taskInfo2).isNotNull()
         assertThat(recentTaskInfoParcel.taskInfo2!!.taskId).isEqualTo(2)
         assertThat(recentTaskInfoParcel.splitBounds).isNotNull()
-        assertThat(recentTaskInfoParcel.splitBounds!!.snapPosition).isEqualTo(SNAP_TO_50_50)
+        assertThat(recentTaskInfoParcel.splitBounds!!.snapPosition).isEqualTo(SNAP_TO_2_50_50)
     }
 
     @Test
@@ -185,7 +185,7 @@
     private fun splitTasksGroupInfo(): GroupedRecentTaskInfo {
         val task1 = createTaskInfo(id = 1)
         val task2 = createTaskInfo(id = 2)
-        val splitBounds = SplitBounds(Rect(), Rect(), 1, 2, SNAP_TO_50_50)
+        val splitBounds = SplitBounds(Rect(), Rect(), 1, 2, SNAP_TO_2_50_50)
         return GroupedRecentTaskInfo.forSplitTasks(task1, task2, splitBounds)
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index 386253c..753d4cd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -22,7 +22,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -211,10 +211,10 @@
 
         // Verify only one update if the split info is the same
         SplitBounds bounds1 = new SplitBounds(new Rect(0, 0, 50, 50),
-                new Rect(50, 50, 100, 100), t1.taskId, t2.taskId, SNAP_TO_50_50);
+                new Rect(50, 50, 100, 100), t1.taskId, t2.taskId, SNAP_TO_2_50_50);
         mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, bounds1);
         SplitBounds bounds2 = new SplitBounds(new Rect(0, 0, 50, 50),
-                new Rect(50, 50, 100, 100), t1.taskId, t2.taskId, SNAP_TO_50_50);
+                new Rect(50, 50, 100, 100), t1.taskId, t2.taskId, SNAP_TO_2_50_50);
         mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, bounds2);
         verify(mRecentTasksController, times(1)).notifyRecentTasksChanged();
     }
@@ -246,9 +246,9 @@
 
         // Mark a couple pairs [t2, t4], [t3, t5]
         SplitBounds pair1Bounds =
-                new SplitBounds(new Rect(), new Rect(), 2, 4, SNAP_TO_50_50);
+                new SplitBounds(new Rect(), new Rect(), 2, 4, SNAP_TO_2_50_50);
         SplitBounds pair2Bounds =
-                new SplitBounds(new Rect(), new Rect(), 3, 5, SNAP_TO_50_50);
+                new SplitBounds(new Rect(), new Rect(), 3, 5, SNAP_TO_2_50_50);
 
         mRecentTasksController.addSplitPair(t2.taskId, t4.taskId, pair1Bounds);
         mRecentTasksController.addSplitPair(t3.taskId, t5.taskId, pair2Bounds);
@@ -277,9 +277,9 @@
 
         // Mark a couple pairs [t2, t4], [t3, t5]
         SplitBounds pair1Bounds =
-                new SplitBounds(new Rect(), new Rect(), 2, 4, SNAP_TO_50_50);
+                new SplitBounds(new Rect(), new Rect(), 2, 4, SNAP_TO_2_50_50);
         SplitBounds pair2Bounds =
-                new SplitBounds(new Rect(), new Rect(), 3, 5, SNAP_TO_50_50);
+                new SplitBounds(new Rect(), new Rect(), 3, 5, SNAP_TO_2_50_50);
 
         mRecentTasksController.addSplitPair(t2.taskId, t4.taskId, pair1Bounds);
         mRecentTasksController.addSplitPair(t3.taskId, t5.taskId, pair2Bounds);
@@ -339,7 +339,7 @@
         setRawList(t1, t2, t3, t4, t5);
 
         SplitBounds pair1Bounds =
-                new SplitBounds(new Rect(), new Rect(), 1, 2, SNAP_TO_50_50);
+                new SplitBounds(new Rect(), new Rect(), 1, 2, SNAP_TO_2_50_50);
         mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, pair1Bounds);
 
         when(mDesktopModeTaskRepository.isActiveTask(3)).thenReturn(true);
@@ -449,7 +449,7 @@
 
         // Add a pair
         SplitBounds pair1Bounds =
-                new SplitBounds(new Rect(), new Rect(), 2, 3, SNAP_TO_50_50);
+                new SplitBounds(new Rect(), new Rect(), 2, 3, SNAP_TO_2_50_50);
         mRecentTasksController.addSplitPair(t2.taskId, t3.taskId, pair1Bounds);
         reset(mRecentTasksController);
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java
index 248393c..be8e6dc 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java
@@ -1,6 +1,6 @@
 package com.android.wm.shell.recents;
 
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -46,21 +46,21 @@
     @Test
     public void testVerticalStacked() {
         SplitBounds ssb = new SplitBounds(mTopRect, mBottomRect,
-                TASK_ID_1, TASK_ID_2, SNAP_TO_50_50);
+                TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50);
         assertTrue(ssb.appsStackedVertically);
     }
 
     @Test
     public void testHorizontalStacked() {
         SplitBounds ssb = new SplitBounds(mLeftRect, mRightRect,
-                TASK_ID_1, TASK_ID_2, SNAP_TO_50_50);
+                TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50);
         assertFalse(ssb.appsStackedVertically);
     }
 
     @Test
     public void testHorizontalDividerBounds() {
         SplitBounds ssb = new SplitBounds(mTopRect, mBottomRect,
-                TASK_ID_1, TASK_ID_2, SNAP_TO_50_50);
+                TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50);
         Rect dividerBounds = ssb.visualDividerBounds;
         assertEquals(0, dividerBounds.left);
         assertEquals(DEVICE_LENGTH / 2 - DIVIDER_SIZE / 2, dividerBounds.top);
@@ -71,7 +71,7 @@
     @Test
     public void testVerticalDividerBounds() {
         SplitBounds ssb = new SplitBounds(mLeftRect, mRightRect,
-                TASK_ID_1, TASK_ID_2, SNAP_TO_50_50);
+                TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50);
         Rect dividerBounds = ssb.visualDividerBounds;
         assertEquals(DEVICE_WIDTH / 2 - DIVIDER_SIZE / 2, dividerBounds.left);
         assertEquals(0, dividerBounds.top);
@@ -82,7 +82,7 @@
     @Test
     public void testEqualVerticalTaskPercent() {
         SplitBounds ssb = new SplitBounds(mTopRect, mBottomRect,
-                TASK_ID_1, TASK_ID_2, SNAP_TO_50_50);
+                TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50);
         float topPercentSpaceTaken = (float) (DEVICE_LENGTH / 2 - DIVIDER_SIZE / 2) / DEVICE_LENGTH;
         assertEquals(topPercentSpaceTaken, ssb.topTaskPercent, 0.01);
     }
@@ -90,7 +90,7 @@
     @Test
     public void testEqualHorizontalTaskPercent() {
         SplitBounds ssb = new SplitBounds(mLeftRect, mRightRect,
-                TASK_ID_1, TASK_ID_2, SNAP_TO_50_50);
+                TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50);
         float leftPercentSpaceTaken = (float) (DEVICE_WIDTH / 2 - DIVIDER_SIZE / 2) / DEVICE_WIDTH;
         assertEquals(leftPercentSpaceTaken, ssb.leftTaskPercent, 0.01);
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/split/SplitScreenConstantsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/split/SplitScreenConstantsTest.kt
index 19c18be..ac96063 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/split/SplitScreenConstantsTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/split/SplitScreenConstantsTest.kt
@@ -42,19 +42,44 @@
             SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT,
         )
         assertEquals(
-            "the value of SNAP_TO_30_70 should be 0",
+            "the value of SNAP_TO_2_33_66 should be 0",
             0,
-            SplitScreenConstants.SNAP_TO_30_70,
+            SplitScreenConstants.SNAP_TO_2_33_66,
         )
         assertEquals(
-            "the value of SNAP_TO_50_50 should be 1",
+            "the value of SNAP_TO_2_50_50 should be 1",
             1,
-            SplitScreenConstants.SNAP_TO_50_50,
+            SplitScreenConstants.SNAP_TO_2_50_50,
         )
         assertEquals(
-            "the value of SNAP_TO_70_30 should be 2",
+            "the value of SNAP_TO_2_66_33 should be 2",
             2,
-            SplitScreenConstants.SNAP_TO_70_30,
+            SplitScreenConstants.SNAP_TO_2_66_33,
+        )
+        assertEquals(
+            "the value of SNAP_TO_2_90_10 should be 3",
+            3,
+            SplitScreenConstants.SNAP_TO_2_90_10,
+        )
+        assertEquals(
+            "the value of SNAP_TO_2_10_90 should be 4",
+            4,
+            SplitScreenConstants.SNAP_TO_2_10_90,
+        )
+        assertEquals(
+            "the value of SNAP_TO_3_33_33_33 should be 5",
+            5,
+            SplitScreenConstants.SNAP_TO_3_33_33_33,
+        )
+        assertEquals(
+            "the value of SNAP_TO_3_45_45_10 should be 6",
+            6,
+            SplitScreenConstants.SNAP_TO_3_45_45_10,
+        )
+        assertEquals(
+            "the value of SNAP_TO_3_10_45_45 should be 7",
+            7,
+            SplitScreenConstants.SNAP_TO_3_10_45_45,
         )
     }
 }
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt
new file mode 100644
index 0000000..5594981
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt
@@ -0,0 +1,237 @@
+/*
+ * 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.wm.shell.windowdecor.education
+
+import android.annotation.LayoutRes
+import android.content.Context
+import android.graphics.Point
+import android.testing.AndroidTestingRunner
+import android.testing.TestableContext
+import android.testing.TestableLooper
+import android.testing.TestableResources
+import android.view.MotionEvent
+import android.view.View
+import android.view.WindowManager
+import android.widget.TextView
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.R
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
+import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.TooltipArrowDirection
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidTestingRunner::class)
+class DesktopWindowingEducationTooltipControllerTest : ShellTestCase() {
+  @Mock private lateinit var mockWindowManager: WindowManager
+  @Mock private lateinit var mockViewContainerFactory: AdditionalSystemViewContainer.Factory
+  private lateinit var testableResources: TestableResources
+  private lateinit var testableContext: TestableContext
+  private lateinit var tooltipController: DesktopWindowingEducationTooltipController
+  private val tooltipViewArgumentCaptor = argumentCaptor<View>()
+
+  @Before
+  fun setUp() {
+    MockitoAnnotations.initMocks(this)
+    testableContext = TestableContext(mContext)
+    testableResources =
+        testableContext.orCreateTestableResources.apply {
+          addOverride(R.dimen.desktop_windowing_education_tooltip_padding, 10)
+        }
+    testableContext.addMockSystemService(
+        Context.LAYOUT_INFLATER_SERVICE, context.getSystemService(Context.LAYOUT_INFLATER_SERVICE))
+    testableContext.addMockSystemService(WindowManager::class.java, mockWindowManager)
+    tooltipController =
+        DesktopWindowingEducationTooltipController(testableContext, mockViewContainerFactory)
+  }
+
+  @Test
+  fun showEducationTooltip_createsTooltipWithCorrectText() {
+    val tooltipText = "This is a tooltip"
+    val tooltipViewConfig = createTooltipConfig(tooltipText = tooltipText)
+
+    tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123)
+
+    verify(mockViewContainerFactory, times(1))
+        .create(
+            windowManagerWrapper = any(),
+            taskId = anyInt(),
+            x = anyInt(),
+            y = anyInt(),
+            width = anyInt(),
+            height = anyInt(),
+            flags = anyInt(),
+            view = tooltipViewArgumentCaptor.capture())
+    val tooltipTextView =
+        tooltipViewArgumentCaptor.lastValue.findViewById<TextView>(R.id.tooltip_text)
+    assertThat(tooltipTextView.text).isEqualTo(tooltipText)
+  }
+
+  @Test
+  fun showEducationTooltip_usesCorrectTaskIdForWindow() {
+    val tooltipViewConfig = createTooltipConfig()
+    val taskIdArgumentCaptor = argumentCaptor<Int>()
+
+    tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123)
+
+    verify(mockViewContainerFactory, times(1))
+        .create(
+            windowManagerWrapper = any(),
+            taskId = taskIdArgumentCaptor.capture(),
+            x = anyInt(),
+            y = anyInt(),
+            width = anyInt(),
+            height = anyInt(),
+            flags = anyInt(),
+            view = anyOrNull())
+    assertThat(taskIdArgumentCaptor.lastValue).isEqualTo(123)
+  }
+
+  @Test
+  fun showEducationTooltip_tooltipPointsUpwards_horizontallyPositionTooltip() {
+    val initialTooltipX = 0
+    val initialTooltipY = 0
+    val tooltipViewConfig =
+        createTooltipConfig(
+            arrowDirection = TooltipArrowDirection.UP,
+            tooltipViewGlobalCoordinates = Point(initialTooltipX, initialTooltipY))
+    val tooltipXArgumentCaptor = argumentCaptor<Int>()
+    val tooltipWidthArgumentCaptor = argumentCaptor<Int>()
+
+    tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123)
+
+    verify(mockViewContainerFactory, times(1))
+        .create(
+            windowManagerWrapper = any(),
+            taskId = anyInt(),
+            x = tooltipXArgumentCaptor.capture(),
+            y = anyInt(),
+            width = tooltipWidthArgumentCaptor.capture(),
+            height = anyInt(),
+            flags = anyInt(),
+            view = tooltipViewArgumentCaptor.capture())
+    val expectedTooltipX = initialTooltipX - tooltipWidthArgumentCaptor.lastValue / 2
+    assertThat(tooltipXArgumentCaptor.lastValue).isEqualTo(expectedTooltipX)
+  }
+
+  @Test
+  fun showEducationTooltip_tooltipPointsLeft_verticallyPositionTooltip() {
+    val initialTooltipX = 0
+    val initialTooltipY = 0
+    val tooltipViewConfig =
+        createTooltipConfig(
+            arrowDirection = TooltipArrowDirection.LEFT,
+            tooltipViewGlobalCoordinates = Point(initialTooltipX, initialTooltipY))
+    val tooltipYArgumentCaptor = argumentCaptor<Int>()
+    val tooltipHeightArgumentCaptor = argumentCaptor<Int>()
+
+    tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123)
+
+    verify(mockViewContainerFactory, times(1))
+        .create(
+            windowManagerWrapper = any(),
+            taskId = anyInt(),
+            x = anyInt(),
+            y = tooltipYArgumentCaptor.capture(),
+            width = anyInt(),
+            height = tooltipHeightArgumentCaptor.capture(),
+            flags = anyInt(),
+            view = tooltipViewArgumentCaptor.capture())
+    val expectedTooltipY = initialTooltipY - tooltipHeightArgumentCaptor.lastValue / 2
+    assertThat(tooltipYArgumentCaptor.lastValue).isEqualTo(expectedTooltipY)
+  }
+
+  @Test
+  fun showEducationTooltip_touchEventActionOutside_dismissActionPerformed() {
+    val mockLambda: () -> Unit = mock()
+    val tooltipViewConfig = createTooltipConfig(onDismissAction = mockLambda)
+
+    tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123)
+    verify(mockViewContainerFactory, times(1))
+        .create(
+            windowManagerWrapper = any(),
+            taskId = anyInt(),
+            x = anyInt(),
+            y = anyInt(),
+            width = anyInt(),
+            height = anyInt(),
+            flags = anyInt(),
+            view = tooltipViewArgumentCaptor.capture())
+    val motionEvent =
+        MotionEvent.obtain(
+            /* downTime= */ 0L,
+            /* eventTime= */ 0L,
+            MotionEvent.ACTION_OUTSIDE,
+            /* x= */ 0f,
+            /* y= */ 0f,
+            /* metaState= */ 0)
+    tooltipViewArgumentCaptor.lastValue.dispatchTouchEvent(motionEvent)
+
+    verify(mockLambda).invoke()
+  }
+
+  @Test
+  fun showEducationTooltip_tooltipClicked_onClickActionPerformed() {
+    val mockLambda: () -> Unit = mock()
+    val tooltipViewConfig = createTooltipConfig(onEducationClickAction = mockLambda)
+
+    tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123)
+    verify(mockViewContainerFactory, times(1))
+        .create(
+            windowManagerWrapper = any(),
+            taskId = anyInt(),
+            x = anyInt(),
+            y = anyInt(),
+            width = anyInt(),
+            height = anyInt(),
+            flags = anyInt(),
+            view = tooltipViewArgumentCaptor.capture())
+    tooltipViewArgumentCaptor.lastValue.performClick()
+
+    verify(mockLambda).invoke()
+  }
+
+  private fun createTooltipConfig(
+      @LayoutRes tooltipViewLayout: Int = R.layout.desktop_windowing_education_top_arrow_tooltip,
+      tooltipViewGlobalCoordinates: Point = Point(0, 0),
+      tooltipText: String = "This is a tooltip",
+      arrowDirection: TooltipArrowDirection = TooltipArrowDirection.UP,
+      onEducationClickAction: () -> Unit = {},
+      onDismissAction: () -> Unit = {}
+  ) =
+      DesktopWindowingEducationTooltipController.EducationViewConfig(
+          tooltipViewLayout = tooltipViewLayout,
+          tooltipViewGlobalCoordinates = tooltipViewGlobalCoordinates,
+          tooltipText = tooltipText,
+          arrowDirection = arrowDirection,
+          onEducationClickAction = onEducationClickAction,
+          onDismissAction = onDismissAction,
+      )
+}
diff --git a/libs/hwui/jni/PathIterator.cpp b/libs/hwui/jni/PathIterator.cpp
index 3884342..e9de655 100644
--- a/libs/hwui/jni/PathIterator.cpp
+++ b/libs/hwui/jni/PathIterator.cpp
@@ -20,6 +20,7 @@
 #include "GraphicsJNI.h"
 #include "SkPath.h"
 #include "SkPoint.h"
+#include "graphics_jni_helpers.h"
 
 namespace android {
 
@@ -36,6 +37,18 @@
         return reinterpret_cast<jlong>(new SkPath::RawIter(*path));
     }
 
+    // A variant of 'next' (below) that is compatible with the host JVM.
+    static jint nextHost(JNIEnv* env, jclass clazz, jlong iteratorHandle, jfloatArray pointsArray) {
+        jfloat* points = env->GetFloatArrayElements(pointsArray, 0);
+#ifdef __ANDROID__
+        jint result = next(iteratorHandle, reinterpret_cast<jlong>(points));
+#else
+        jint result = next(env, clazz, iteratorHandle, reinterpret_cast<jlong>(points));
+#endif
+        env->ReleaseFloatArrayElements(pointsArray, points, 0);
+        return result;
+    }
+
     // ---------------- @CriticalNative -------------------------
 
     static jint peek(CRITICAL_JNI_PARAMS_COMMA jlong iteratorHandle) {
@@ -72,6 +85,7 @@
 
         {"nPeek", "(J)I", (void*)SkPathIteratorGlue::peek},
         {"nNext", "(JJ)I", (void*)SkPathIteratorGlue::next},
+        {"nNextHost", "(J[F)I", (void*)SkPathIteratorGlue::nextHost},
 };
 
 int register_android_graphics_PathIterator(JNIEnv* env) {
diff --git a/media/java/android/media/tv/flags/media_tv.aconfig b/media/java/android/media/tv/flags/media_tv.aconfig
index c814c95..10423b9 100644
--- a/media/java/android/media/tv/flags/media_tv.aconfig
+++ b/media/java/android/media/tv/flags/media_tv.aconfig
@@ -56,3 +56,11 @@
     description: "Enhance HDMI-CEC power state and activeness transitions"
     bug: "332780751"
 }
+
+flag {
+    name: "media_quality_fw"
+    is_exported: true
+    namespace: "media_tv"
+    description: "Media Quality V1.0 APIs for Android W"
+    bug: "348412562"
+}
diff --git a/media/jni/android_media_ImageWriter.cpp b/media/jni/android_media_ImageWriter.cpp
index 6776f61..33650d9 100644
--- a/media/jni/android_media_ImageWriter.cpp
+++ b/media/jni/android_media_ImageWriter.cpp
@@ -735,10 +735,15 @@
 }
 
 static status_t attachAndQeueuGraphicBuffer(JNIEnv* env, JNIImageWriterContext *ctx,
-        sp<Surface> surface, sp<GraphicBuffer> gb, jlong timestampNs, jint dataSpace,
+        sp<GraphicBuffer> gb, jlong timestampNs, jint dataSpace,
         jint left, jint top, jint right, jint bottom, jint transform, jint scalingMode) {
     status_t res = OK;
     // Step 1. Attach Image
+    sp<Surface> surface = ctx->getProducer();
+    if (surface == nullptr) {
+        jniThrowException(env, "java/lang/IllegalStateException",
+                "Producer surface is null, ImageWriter seems already closed");
+    }
     res = surface->attachBuffer(gb.get());
     if (res != OK) {
         ALOGE("Attach image failed: %s (%d)", strerror(-res), res);
@@ -835,7 +840,6 @@
         return -1;
     }
 
-    sp<Surface> surface = ctx->getProducer();
     if (isFormatOpaque(ctx->getBufferFormat()) != isFormatOpaque(nativeHalFormat)) {
         jniThrowException(env, "java/lang/IllegalStateException",
                 "Trying to attach an opaque image into a non-opaque ImageWriter, or vice versa");
@@ -851,7 +855,7 @@
         return -1;
     }
 
-    return attachAndQeueuGraphicBuffer(env, ctx, surface, buffer->mGraphicBuffer, timestampNs,
+    return attachAndQeueuGraphicBuffer(env, ctx, buffer->mGraphicBuffer, timestampNs,
             dataSpace, left, top, right, bottom, transform, scalingMode);
 }
 
@@ -866,7 +870,6 @@
         return -1;
     }
 
-    sp<Surface> surface = ctx->getProducer();
     if (isFormatOpaque(ctx->getBufferFormat()) != isFormatOpaque(nativeHalFormat)) {
         jniThrowException(env, "java/lang/IllegalStateException",
                 "Trying to attach an opaque image into a non-opaque ImageWriter, or vice versa");
@@ -880,7 +883,8 @@
                 "Trying to attach an invalid graphic buffer");
         return -1;
     }
-    return attachAndQeueuGraphicBuffer(env, ctx, surface, graphicBuffer, timestampNs,
+
+    return attachAndQeueuGraphicBuffer(env, ctx, graphicBuffer, timestampNs,
             dataSpace, left, top, right, bottom, transform, scalingMode);
 }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java b/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java
index ef0f6cb..13a0601 100644
--- a/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java
+++ b/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java
@@ -42,6 +42,8 @@
 import com.android.settingslib.R;
 import com.android.settingslib.Utils;
 
+import java.util.Objects;
+
 /**
  * Drawable displaying a mobile cell signal indicator.
  */
@@ -90,6 +92,10 @@
     private int mCurrentDot;
 
     public SignalDrawable(Context context) {
+        this(context, new Handler());
+    }
+
+    public SignalDrawable(@NonNull Context context, @NonNull Handler handler) {
         super(context.getDrawable(ICON_RES));
         final String attributionPathString = context.getString(
                 com.android.internal.R.string.config_signalAttributionPath);
@@ -106,7 +112,7 @@
         mIntrinsicSize = context.getResources().getDimensionPixelSize(R.dimen.signal_icon_size);
         mTransparentPaint.setColor(context.getColor(android.R.color.transparent));
         mTransparentPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
-        mHandler = new Handler();
+        mHandler = handler;
         setDarkIntensity(0);
     }
 
@@ -304,6 +310,17 @@
                 | level;
     }
 
+    @Override
+    public boolean equals(@Nullable Object other) {
+        return other instanceof SignalDrawable
+                && ((SignalDrawable) other).getLevel() == this.getLevel();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getLevel());
+    }
+
     /** Returns the state representing empty mobile signal with the given number of levels. */
     public static int getEmptyState(int numLevels) {
         return getState(0, numLevels, true);
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 88cc152..a9d4c89 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1118,6 +1118,16 @@
 }
 
 flag {
+  name: "media_controls_umo_inflation_in_background"
+  namespace: "systemui"
+  description: "Inflate UMO in background thread"
+  bug: "368514198"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
   namespace: "systemui"
   name: "enable_view_capture_tracing"
   description: "Enables view capture tracing in System UI."
@@ -1421,4 +1431,4 @@
    metadata {
        purpose: PURPOSE_BUGFIX
    }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java
new file mode 100644
index 0000000..08db95e
--- /dev/null
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java
@@ -0,0 +1,374 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.animation;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.ValueAnimator;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Rect;
+import android.hardware.display.DisplayManager;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.SurfaceControl;
+import android.window.IRemoteTransition;
+import android.window.IRemoteTransitionFinishedCallback;
+import android.window.TransitionInfo;
+import android.window.TransitionInfo.Change;
+import android.window.WindowAnimationState;
+
+import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.wm.shell.shared.TransitionUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An implementation of {@link IRemoteTransition} that accepts a {@link UIComponent} as the origin
+ * and automatically attaches it to the transition leash before the transition starts.
+ */
+public class OriginRemoteTransition extends IRemoteTransition.Stub {
+    private static final String TAG = "OriginRemoteTransition";
+
+    private final Context mContext;
+    private final boolean mIsEntry;
+    private final UIComponent mOrigin;
+    private final TransitionPlayer mPlayer;
+    private final long mDuration;
+    private final Handler mHandler;
+
+    @Nullable private SurfaceControl.Transaction mStartTransaction;
+    @Nullable private IRemoteTransitionFinishedCallback mFinishCallback;
+    @Nullable private UIComponent.Transaction mOriginTransaction;
+    @Nullable private ValueAnimator mAnimator;
+    @Nullable private SurfaceControl mOriginLeash;
+    private boolean mCancelled;
+
+    OriginRemoteTransition(
+            Context context,
+            boolean isEntry,
+            UIComponent origin,
+            TransitionPlayer player,
+            long duration,
+            Handler handler) {
+        mContext = context;
+        mIsEntry = isEntry;
+        mOrigin = origin;
+        mPlayer = player;
+        mDuration = duration;
+        mHandler = handler;
+    }
+
+    @Override
+    public void startAnimation(
+            IBinder token,
+            TransitionInfo info,
+            SurfaceControl.Transaction t,
+            IRemoteTransitionFinishedCallback finishCallback) {
+        logD("startAnimation - " + info);
+        mHandler.post(
+                () -> {
+                    mStartTransaction = t;
+                    mFinishCallback = finishCallback;
+                    startAnimationInternal(info);
+                });
+    }
+
+    @Override
+    public void mergeAnimation(
+            IBinder transition,
+            TransitionInfo info,
+            SurfaceControl.Transaction t,
+            IBinder mergeTarget,
+            IRemoteTransitionFinishedCallback finishCallback) {
+        logD("mergeAnimation - " + info);
+        mHandler.post(this::cancel);
+    }
+
+    @Override
+    public void takeOverAnimation(
+            IBinder transition,
+            TransitionInfo info,
+            SurfaceControl.Transaction t,
+            IRemoteTransitionFinishedCallback finishCallback,
+            WindowAnimationState[] states) {
+        logD("takeOverAnimation - " + info);
+    }
+
+    @Override
+    public void onTransitionConsumed(IBinder transition, boolean aborted) {
+        logD("onTransitionConsumed - aborted: " + aborted);
+        mHandler.post(this::cancel);
+    }
+
+    private void startAnimationInternal(TransitionInfo info) {
+        if (!prepareUIs(info)) {
+            logE("Unable to prepare UI!");
+            finishAnimation(/* finished= */ false);
+            return;
+        }
+        // Notify player that we are starting.
+        mPlayer.onStart(info, mStartTransaction, mOrigin, mOriginTransaction);
+
+        // Start the animator.
+        mAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
+        mAnimator.setDuration(mDuration);
+        mAnimator.addListener(
+                new AnimatorListener() {
+                    @Override
+                    public void onAnimationStart(Animator a) {}
+
+                    @Override
+                    public void onAnimationEnd(Animator a) {
+                        finishAnimation(/* finished= */ !mCancelled);
+                    }
+
+                    @Override
+                    public void onAnimationCancel(Animator a) {
+                        mCancelled = true;
+                    }
+
+                    @Override
+                    public void onAnimationRepeat(Animator a) {}
+                });
+        mAnimator.addUpdateListener(
+                a -> {
+                    mPlayer.onProgress((float) a.getAnimatedValue());
+                });
+        mAnimator.start();
+    }
+
+    private boolean prepareUIs(TransitionInfo info) {
+        if (info.getRootCount() == 0) {
+            logE("prepareUIs: no root leash!");
+            return false;
+        }
+        if (info.getRootCount() > 1) {
+            logE("prepareUIs: multi-display transition is not supported yet!");
+            return false;
+        }
+        if (info.getChanges().isEmpty()) {
+            logE("prepareUIs: no changes!");
+            return false;
+        }
+
+        SurfaceControl rootLeash = info.getRoot(0).getLeash();
+        int displayId = info.getChanges().get(0).getEndDisplayId();
+        Rect displayBounds = getDisplayBounds(displayId);
+        float windowRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext);
+        logD("prepareUIs: windowRadius=" + windowRadius + ", displayBounds=" + displayBounds);
+
+        // Create the origin leash and add to the transition root leash.
+        mOriginLeash =
+                new SurfaceControl.Builder().setName("OriginTransition-origin-leash").build();
+        mStartTransaction
+                .reparent(mOriginLeash, rootLeash)
+                .show(mOriginLeash)
+                .setCornerRadius(mOriginLeash, windowRadius)
+                .setWindowCrop(mOriginLeash, displayBounds.width(), displayBounds.height());
+
+        // Process surfaces
+        List<SurfaceControl> openingSurfaces = new ArrayList<>();
+        List<SurfaceControl> closingSurfaces = new ArrayList<>();
+        for (Change change : info.getChanges()) {
+            int mode = change.getMode();
+            SurfaceControl leash = change.getLeash();
+            // Reparent leash to the transition root.
+            mStartTransaction.reparent(leash, rootLeash);
+            if (TransitionUtil.isOpeningMode(mode)) {
+                openingSurfaces.add(change.getLeash());
+                // For opening surfaces, ending bounds are base bound. Apply corner radius if
+                // it's full screen.
+                Rect bounds = change.getEndAbsBounds();
+                if (displayBounds.equals(bounds)) {
+                    mStartTransaction
+                            .setCornerRadius(leash, windowRadius)
+                            .setWindowCrop(leash, bounds.width(), bounds.height());
+                }
+            } else if (TransitionUtil.isClosingMode(mode)) {
+                closingSurfaces.add(change.getLeash());
+                // For closing surfaces, starting bounds are base bounds. Apply corner radius if
+                // it's full screen.
+                Rect bounds = change.getStartAbsBounds();
+                if (displayBounds.equals(bounds)) {
+                    mStartTransaction
+                            .setCornerRadius(leash, windowRadius)
+                            .setWindowCrop(leash, bounds.width(), bounds.height());
+                }
+            }
+        }
+
+        // Set relative order:
+        // ----  App1  ----
+        // ---- origin ----
+        // ----  App2  ----
+        if (mIsEntry) {
+            mStartTransaction
+                    .setRelativeLayer(mOriginLeash, closingSurfaces.get(0), 1)
+                    .setRelativeLayer(
+                            openingSurfaces.get(openingSurfaces.size() - 1), mOriginLeash, 1);
+        } else {
+            mStartTransaction
+                    .setRelativeLayer(mOriginLeash, openingSurfaces.get(0), 1)
+                    .setRelativeLayer(
+                            closingSurfaces.get(closingSurfaces.size() - 1), mOriginLeash, 1);
+        }
+
+        // Attach origin UIComponent to origin leash.
+        mOriginTransaction = mOrigin.newTransaction();
+        mOriginTransaction
+                .attachToTransitionLeash(
+                        mOrigin, mOriginLeash, displayBounds.width(), displayBounds.height())
+                .commit();
+
+        // Apply all surface changes.
+        mStartTransaction.apply();
+        return true;
+    }
+
+    private Rect getDisplayBounds(int displayId) {
+        DisplayManager dm = mContext.getSystemService(DisplayManager.class);
+        DisplayMetrics metrics = new DisplayMetrics();
+        dm.getDisplay(displayId).getMetrics(metrics);
+        return new Rect(0, 0, metrics.widthPixels, metrics.heightPixels);
+    }
+
+    private void finishAnimation(boolean finished) {
+        logD("finishAnimation: finished=" + finished);
+        if (mAnimator == null) {
+            // The transition didn't start. Ensure we apply the start transaction and report
+            // finish afterwards.
+            mStartTransaction
+                    .addTransactionCommittedListener(
+                            mContext.getMainExecutor(), this::finishInternal)
+                    .apply();
+            return;
+        }
+        mAnimator = null;
+        // Notify client that we have ended.
+        mPlayer.onEnd(finished);
+        // Detach the origin from the transition leash and report finish after it's done.
+        mOriginTransaction
+                .detachFromTransitionLeash(
+                        mOrigin, mContext.getMainExecutor(), this::finishInternal)
+                .commit();
+    }
+
+    private void finishInternal() {
+        logD("finishInternal");
+        if (mOriginLeash != null) {
+            // Release origin leash.
+            mOriginLeash.release();
+            mOriginLeash = null;
+        }
+        try {
+            mFinishCallback.onTransitionFinished(null, null);
+        } catch (RemoteException e) {
+            logE("Unable to report transition finish!", e);
+        }
+        mStartTransaction = null;
+        mOriginTransaction = null;
+        mFinishCallback = null;
+    }
+
+    private void cancel() {
+        if (mAnimator != null) {
+            mAnimator.cancel();
+        }
+    }
+
+    private static void logD(String msg) {
+        if (OriginTransitionSession.DEBUG) {
+            Log.d(TAG, msg);
+        }
+    }
+
+    private static void logE(String msg) {
+        Log.e(TAG, msg);
+    }
+
+    private static void logE(String msg, Throwable e) {
+        Log.e(TAG, msg, e);
+    }
+
+    private static UIComponent wrapSurfaces(TransitionInfo info, boolean isOpening) {
+        List<SurfaceControl> surfaces = new ArrayList<>();
+        Rect maxBounds = new Rect();
+        for (Change change : info.getChanges()) {
+            int mode = change.getMode();
+            if (TransitionUtil.isOpeningMode(mode) == isOpening) {
+                surfaces.add(change.getLeash());
+                Rect bounds = isOpening ? change.getEndAbsBounds() : change.getStartAbsBounds();
+                maxBounds.union(bounds);
+            }
+        }
+        return new SurfaceUIComponent(
+                surfaces,
+                /* alpha= */ 1.0f,
+                /* visible= */ true,
+                /* bounds= */ maxBounds,
+                /* baseBounds= */ maxBounds);
+    }
+
+    /** An interface that represents an origin transitions. */
+    public interface TransitionPlayer {
+
+        /**
+         * Called when an origin transition starts. This method exposes the raw {@link
+         * TransitionInfo} so that clients can extract more information from it.
+         */
+        default void onStart(
+                TransitionInfo transitionInfo,
+                SurfaceControl.Transaction sfTransaction,
+                UIComponent origin,
+                UIComponent.Transaction uiTransaction) {
+            // Wrap transactions.
+            Transactions transactions =
+                    new Transactions()
+                            .registerTransactionForClass(origin.getClass(), uiTransaction)
+                            .registerTransactionForClass(
+                                    SurfaceUIComponent.class,
+                                    new SurfaceUIComponent.Transaction(sfTransaction));
+            // Wrap surfaces and start.
+            onStart(
+                    transactions,
+                    origin,
+                    wrapSurfaces(transitionInfo, /* isOpening= */ false),
+                    wrapSurfaces(transitionInfo, /* isOpening= */ true));
+        }
+
+        /**
+         * Called when an origin transition starts. This method exposes the opening and closing
+         * windows as wrapped {@link UIComponent} to provide simplified interface to clients.
+         */
+        void onStart(
+                UIComponent.Transaction transaction,
+                UIComponent origin,
+                UIComponent closingApp,
+                UIComponent openingApp);
+
+        /** Called to update the transition frame. */
+        void onProgress(float progress);
+
+        /** Called when the transition ended. */
+        void onEnd(boolean finished);
+    }
+}
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java
index 64bedd3..23693b6 100644
--- a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java
@@ -24,11 +24,14 @@
 import android.content.Context;
 import android.content.Intent;
 import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
 import android.os.RemoteException;
 import android.util.Log;
 import android.window.IRemoteTransition;
 import android.window.RemoteTransition;
 
+import com.android.systemui.animation.OriginRemoteTransition.TransitionPlayer;
 import com.android.systemui.animation.shared.IOriginTransitions;
 
 import java.lang.annotation.Retention;
@@ -182,6 +185,7 @@
         @Nullable private final IOriginTransitions mOriginTransitions;
         @Nullable private Supplier<IRemoteTransition> mEntryTransitionSupplier;
         @Nullable private Supplier<IRemoteTransition> mExitTransitionSupplier;
+        private Handler mHandler = new Handler(Looper.getMainLooper());
         private String mName;
         @Nullable private Predicate<RemoteTransition> mIntentStarter;
 
@@ -259,12 +263,48 @@
             return this;
         }
 
+        /** Add an origin entry transition to the builder. */
+        public Builder withEntryTransition(
+                UIComponent entryOrigin, TransitionPlayer entryPlayer, long entryDuration) {
+            mEntryTransitionSupplier =
+                    () ->
+                            new OriginRemoteTransition(
+                                    mContext,
+                                    /* isEntry= */ true,
+                                    entryOrigin,
+                                    entryPlayer,
+                                    entryDuration,
+                                    mHandler);
+            return this;
+        }
+
         /** Add an exit transition to the builder. */
         public Builder withExitTransition(IRemoteTransition transition) {
             mExitTransitionSupplier = () -> transition;
             return this;
         }
 
+        /** Add an origin exit transition to the builder. */
+        public Builder withExitTransition(
+                UIComponent exitTarget, TransitionPlayer exitPlayer, long exitDuration) {
+            mExitTransitionSupplier =
+                    () ->
+                            new OriginRemoteTransition(
+                                    mContext,
+                                    /* isEntry= */ false,
+                                    exitTarget,
+                                    exitPlayer,
+                                    exitDuration,
+                                    mHandler);
+            return this;
+        }
+
+        /** Supply a handler where transition callbacks will run. */
+        public Builder withHandler(Handler handler) {
+            mHandler = handler;
+            return this;
+        }
+
         /** Build an {@link OriginTransitionSession}. */
         public OriginTransitionSession build() {
             if (mIntentStarter == null) {
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/SurfaceUIComponent.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/SurfaceUIComponent.java
new file mode 100644
index 0000000..2438736
--- /dev/null
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/SurfaceUIComponent.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.animation;
+
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.SurfaceControl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.concurrent.Executor;
+
+/** A {@link UIComponent} representing a {@link SurfaceControl}. */
+public class SurfaceUIComponent implements UIComponent {
+    private final Collection<SurfaceControl> mSurfaces;
+    private final Rect mBaseBounds;
+    private final float[] mFloat9 = new float[9];
+
+    private float mAlpha;
+    private boolean mVisible;
+    private Rect mBounds;
+
+    public SurfaceUIComponent(
+            SurfaceControl sc, float alpha, boolean visible, Rect bounds, Rect baseBounds) {
+        this(Arrays.asList(sc), alpha, visible, bounds, baseBounds);
+    }
+
+    public SurfaceUIComponent(
+            Collection<SurfaceControl> surfaces,
+            float alpha,
+            boolean visible,
+            Rect bounds,
+            Rect baseBounds) {
+        mSurfaces = surfaces;
+        mAlpha = alpha;
+        mVisible = visible;
+        mBounds = bounds;
+        mBaseBounds = baseBounds;
+    }
+
+    @Override
+    public float getAlpha() {
+        return mAlpha;
+    }
+
+    @Override
+    public boolean isVisible() {
+        return mVisible;
+    }
+
+    @Override
+    public Rect getBounds() {
+        return mBounds;
+    }
+
+    @Override
+    public Transaction newTransaction() {
+        return new Transaction(new SurfaceControl.Transaction());
+    }
+
+    @Override
+    public String toString() {
+        return "SurfaceUIComponent{mSurfaces="
+                + mSurfaces
+                + ", mAlpha="
+                + mAlpha
+                + ", mVisible="
+                + mVisible
+                + ", mBounds="
+                + mBounds
+                + ", mBaseBounds="
+                + mBaseBounds
+                + "}";
+    }
+
+    /** A {@link Transaction} wrapping a {@link SurfaceControl.Transaction}. */
+    public static class Transaction implements UIComponent.Transaction<SurfaceUIComponent> {
+        private final SurfaceControl.Transaction mTransaction;
+        private final ArrayList<Runnable> mChanges = new ArrayList<>();
+
+        public Transaction(SurfaceControl.Transaction transaction) {
+            mTransaction = transaction;
+        }
+
+        @Override
+        public Transaction setAlpha(SurfaceUIComponent ui, float alpha) {
+            mChanges.add(
+                    () -> {
+                        ui.mAlpha = alpha;
+                        ui.mSurfaces.forEach(s -> mTransaction.setAlpha(s, alpha));
+                    });
+            return this;
+        }
+
+        @Override
+        public Transaction setVisible(SurfaceUIComponent ui, boolean visible) {
+            mChanges.add(
+                    () -> {
+                        ui.mVisible = visible;
+                        if (visible) {
+                            ui.mSurfaces.forEach(s -> mTransaction.show(s));
+                        } else {
+                            ui.mSurfaces.forEach(s -> mTransaction.hide(s));
+                        }
+                    });
+            return this;
+        }
+
+        @Override
+        public Transaction setBounds(SurfaceUIComponent ui, Rect bounds) {
+            mChanges.add(
+                    () -> {
+                        if (ui.mBounds.equals(bounds)) {
+                            return;
+                        }
+                        ui.mBounds = bounds;
+                        Matrix matrix = new Matrix();
+                        matrix.setRectToRect(
+                                new RectF(ui.mBaseBounds),
+                                new RectF(ui.mBounds),
+                                Matrix.ScaleToFit.FILL);
+                        ui.mSurfaces.forEach(s -> mTransaction.setMatrix(s, matrix, ui.mFloat9));
+                    });
+            return this;
+        }
+
+        @Override
+        public Transaction attachToTransitionLeash(
+                SurfaceUIComponent ui, SurfaceControl transitionLeash, int w, int h) {
+            mChanges.add(
+                    () -> ui.mSurfaces.forEach(s -> mTransaction.reparent(s, transitionLeash)));
+            return this;
+        }
+
+        @Override
+        public Transaction detachFromTransitionLeash(
+                SurfaceUIComponent ui, Executor executor, Runnable onDone) {
+            mChanges.add(
+                    () -> {
+                        ui.mSurfaces.forEach(s -> mTransaction.reparent(s, null));
+                        mTransaction.addTransactionCommittedListener(executor, onDone::run);
+                    });
+            return this;
+        }
+
+        @Override
+        public void commit() {
+            mChanges.forEach(Runnable::run);
+            mChanges.clear();
+            mTransaction.apply();
+        }
+    }
+}
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/Transactions.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/Transactions.java
new file mode 100644
index 0000000..5240d99
--- /dev/null
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/Transactions.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.animation;
+
+import android.annotation.FloatRange;
+import android.graphics.Rect;
+import android.util.ArrayMap;
+import android.view.SurfaceControl;
+
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * A composite {@link UIComponent.Transaction} that combines multiple other transactions for each ui
+ * type.
+ */
+public class Transactions implements UIComponent.Transaction<UIComponent> {
+    private final Map<Class, UIComponent.Transaction> mTransactions = new ArrayMap<>();
+
+    /** Register a transaction object for updating a certain {@link UIComponent} type. */
+    public <T extends UIComponent> Transactions registerTransactionForClass(
+            Class<T> clazz, UIComponent.Transaction transaction) {
+        mTransactions.put(clazz, transaction);
+        return this;
+    }
+
+    private UIComponent.Transaction getTransactionFor(UIComponent ui) {
+        UIComponent.Transaction transaction = mTransactions.get(ui.getClass());
+        if (transaction == null) {
+            transaction = ui.newTransaction();
+            mTransactions.put(ui.getClass(), transaction);
+        }
+        return transaction;
+    }
+
+    @Override
+    public Transactions setAlpha(UIComponent ui, @FloatRange(from = 0.0, to = 1.0) float alpha) {
+        getTransactionFor(ui).setAlpha(ui, alpha);
+        return this;
+    }
+
+    @Override
+    public Transactions setVisible(UIComponent ui, boolean visible) {
+        getTransactionFor(ui).setVisible(ui, visible);
+        return this;
+    }
+
+    @Override
+    public Transactions setBounds(UIComponent ui, Rect bounds) {
+        getTransactionFor(ui).setBounds(ui, bounds);
+        return this;
+    }
+
+    @Override
+    public Transactions attachToTransitionLeash(
+            UIComponent ui, SurfaceControl transitionLeash, int w, int h) {
+        getTransactionFor(ui).attachToTransitionLeash(ui, transitionLeash, w, h);
+        return this;
+    }
+
+    @Override
+    public Transactions detachFromTransitionLeash(
+            UIComponent ui, Executor executor, Runnable onDone) {
+        getTransactionFor(ui).detachFromTransitionLeash(ui, executor, onDone);
+        return this;
+    }
+
+    @Override
+    public void commit() {
+        mTransactions.values().forEach(UIComponent.Transaction::commit);
+    }
+}
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/UIComponent.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/UIComponent.java
new file mode 100644
index 0000000..747e4d1
--- /dev/null
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/UIComponent.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.animation;
+
+import android.annotation.FloatRange;
+import android.graphics.Rect;
+import android.view.SurfaceControl;
+
+import java.util.concurrent.Executor;
+
+/** An interface representing an UI component on the display. */
+public interface UIComponent {
+
+    /** Get the current alpha of this UI. */
+    float getAlpha();
+
+    /** Check if this UI is visible. */
+    boolean isVisible();
+
+    /** Get the bounds of this UI in its display. */
+    Rect getBounds();
+
+    /** Create a new {@link Transaction} that can update this UI. */
+    Transaction newTransaction();
+
+    /**
+     * A transaction class for updating {@link UIComponent}.
+     *
+     * @param <T> the subtype of {@link UIComponent} that this {@link Transaction} can handle.
+     */
+    interface Transaction<T extends UIComponent> {
+        /** Update alpha of an UI. Execution will be delayed until {@link #commit()} is called. */
+        Transaction setAlpha(T ui, @FloatRange(from = 0.0, to = 1.0) float alpha);
+
+        /**
+         * Update visibility of an UI. Execution will be delayed until {@link #commit()} is called.
+         */
+        Transaction setVisible(T ui, boolean visible);
+
+        /** Update bounds of an UI. Execution will be delayed until {@link #commit()} is called. */
+        Transaction setBounds(T ui, Rect bounds);
+
+        /**
+         * Attach a ui to the transition leash. Execution will be delayed until {@link #commit()} is
+         * called.
+         */
+        Transaction attachToTransitionLeash(T ui, SurfaceControl transitionLeash, int w, int h);
+
+        /**
+         * Detach a ui from the transition leash. Execution will be delayed until {@link #commit} is
+         * called.
+         */
+        Transaction detachFromTransitionLeash(T ui, Executor executor, Runnable onDone);
+
+        /** Commit any pending changes added to this transaction. */
+        void commit();
+    }
+}
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java
new file mode 100644
index 0000000..313789c
--- /dev/null
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.animation;
+
+import android.annotation.Nullable;
+import android.graphics.Canvas;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnDrawListener;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * A {@link UIComponent} wrapping a {@link View}. After being attached to the transition leash, this
+ * class will draw the content of the {@link View} directly into the leash, and the actual View will
+ * be changed to INVISIBLE in its view tree. This allows the {@link View} to transform in the
+ * full-screen size leash without being constrained by the view tree's boundary or inheriting its
+ * parent's alpha and transformation.
+ */
+public class ViewUIComponent implements UIComponent {
+    private static final String TAG = "ViewUIComponent";
+    private static final boolean DEBUG = Build.IS_USERDEBUG || Log.isLoggable(TAG, Log.DEBUG);
+    private final OnDrawListener mOnDrawListener = this::postDraw;
+    private final View mView;
+
+    @Nullable private SurfaceControl mSurfaceControl;
+    @Nullable private Surface mSurface;
+    @Nullable private Rect mViewBoundsOverride;
+    private boolean mVisibleOverride;
+    private boolean mDirty;
+
+    public ViewUIComponent(View view) {
+        mView = view;
+    }
+
+    @Override
+    public float getAlpha() {
+        return mView.getAlpha();
+    }
+
+    @Override
+    public boolean isVisible() {
+        return isAttachedToLeash() ? mVisibleOverride : mView.getVisibility() == View.VISIBLE;
+    }
+
+    @Override
+    public Rect getBounds() {
+        if (isAttachedToLeash() && mViewBoundsOverride != null) {
+            return mViewBoundsOverride;
+        }
+        return getRealBounds();
+    }
+
+    @Override
+    public Transaction newTransaction() {
+        return new Transaction();
+    }
+
+    private void attachToTransitionLeash(SurfaceControl transitionLeash, int w, int h) {
+        logD("attachToTransitionLeash");
+        // Remember current visibility.
+        mVisibleOverride = mView.getVisibility() == View.VISIBLE;
+
+        // Create the surface
+        mSurfaceControl =
+                new SurfaceControl.Builder().setName("ViewUIComponent").setBufferSize(w, h).build();
+        mSurface = new Surface(mSurfaceControl);
+        forceDraw();
+
+        // Attach surface to transition leash
+        SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+        t.reparent(mSurfaceControl, transitionLeash).show(mSurfaceControl);
+
+        // Make sure view draw triggers surface draw.
+        mView.getViewTreeObserver().addOnDrawListener(mOnDrawListener);
+
+        // Make the view invisible AFTER the surface is shown.
+        t.addTransactionCommittedListener(
+                        mView.getContext().getMainExecutor(),
+                        () -> mView.setVisibility(View.INVISIBLE))
+                .apply();
+    }
+
+    private void detachFromTransitionLeash(Executor executor, Runnable onDone) {
+        logD("detachFromTransitionLeash");
+        Surface s = mSurface;
+        SurfaceControl sc = mSurfaceControl;
+        mSurface = null;
+        mSurfaceControl = null;
+        mView.getViewTreeObserver().removeOnDrawListener(mOnDrawListener);
+        // Restore view visibility
+        mView.setVisibility(mVisibleOverride ? View.VISIBLE : View.INVISIBLE);
+        mView.invalidate();
+        // Clean up surfaces.
+        SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+        t.reparent(sc, null)
+                .addTransactionCommittedListener(
+                        mView.getContext().getMainExecutor(),
+                        () -> {
+                            s.release();
+                            sc.release();
+                            executor.execute(onDone);
+                        });
+        // Apply transaction AFTER the view is drawn.
+        mView.getRootSurfaceControl().applyTransactionOnDraw(t);
+    }
+
+    @Override
+    public String toString() {
+        return "ViewUIComponent{"
+                + "alpha="
+                + getAlpha()
+                + ", visible="
+                + isVisible()
+                + ", bounds="
+                + getBounds()
+                + ", attached="
+                + isAttachedToLeash()
+                + "}";
+    }
+
+    private void draw() {
+        if (!mDirty) {
+            // No need to draw. This is probably a duplicate call.
+            logD("draw: skipped - clean");
+            return;
+        }
+        mDirty = false;
+        if (!isAttachedToLeash()) {
+            // Not attached.
+            logD("draw: skipped - not attached");
+            return;
+        }
+        ViewGroup.LayoutParams params = mView.getLayoutParams();
+        if (params == null || params.width == 0 || params.height == 0) {
+            // layout pass didn't happen.
+            logD("draw: skipped - no layout");
+            return;
+        }
+        Canvas canvas = mSurface.lockHardwareCanvas();
+        // Clear the canvas first.
+        canvas.drawColor(0, PorterDuff.Mode.CLEAR);
+        if (mVisibleOverride) {
+            Rect realBounds = getRealBounds();
+            Rect renderBounds = getBounds();
+            canvas.translate(renderBounds.left, renderBounds.top);
+            canvas.scale(
+                    (float) renderBounds.width() / realBounds.width(),
+                    (float) renderBounds.height() / realBounds.height());
+            canvas.saveLayerAlpha(null, (int) (255 * mView.getAlpha()));
+            mView.draw(canvas);
+            canvas.restore();
+        }
+        mSurface.unlockCanvasAndPost(canvas);
+        logD("draw: done");
+    }
+
+    private void forceDraw() {
+        mDirty = true;
+        draw();
+    }
+
+    private Rect getRealBounds() {
+        Rect output = new Rect();
+        mView.getBoundsOnScreen(output);
+        return output;
+    }
+
+    private boolean isAttachedToLeash() {
+        return mSurfaceControl != null && mSurface != null;
+    }
+
+    private void logD(String msg) {
+        if (DEBUG) {
+            Log.d(TAG, msg);
+        }
+    }
+
+    private void setVisible(boolean visible) {
+        logD("setVisibility: " + visible);
+        if (isAttachedToLeash()) {
+            mVisibleOverride = visible;
+            postDraw();
+        } else {
+            mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+        }
+    }
+
+    private void setBounds(Rect bounds) {
+        logD("setBounds: " + bounds);
+        mViewBoundsOverride = bounds;
+        if (isAttachedToLeash()) {
+            postDraw();
+        } else {
+            Log.w(TAG, "setBounds: not attached to leash!");
+        }
+    }
+
+    private void setAlpha(float alpha) {
+        logD("setAlpha: " + alpha);
+        mView.setAlpha(alpha);
+        if (isAttachedToLeash()) {
+            postDraw();
+        }
+    }
+
+    private void postDraw() {
+        if (mDirty) {
+            return;
+        }
+        mDirty = true;
+        mView.post(this::draw);
+    }
+
+    public static class Transaction implements UIComponent.Transaction<ViewUIComponent> {
+        private final List<Runnable> mChanges = new ArrayList<>();
+
+        @Override
+        public Transaction setAlpha(ViewUIComponent ui, float alpha) {
+            mChanges.add(() -> ui.setAlpha(alpha));
+            return this;
+        }
+
+        @Override
+        public Transaction setVisible(ViewUIComponent ui, boolean visible) {
+            mChanges.add(() -> ui.setVisible(visible));
+            return this;
+        }
+
+        @Override
+        public Transaction setBounds(ViewUIComponent ui, Rect bounds) {
+            mChanges.add(() -> ui.setBounds(bounds));
+            return this;
+        }
+
+        @Override
+        public Transaction attachToTransitionLeash(
+                ViewUIComponent ui, SurfaceControl transitionLeash, int w, int h) {
+            mChanges.add(() -> ui.attachToTransitionLeash(transitionLeash, w, h));
+            return this;
+        }
+
+        @Override
+        public Transaction detachFromTransitionLeash(
+                ViewUIComponent ui, Executor executor, Runnable onDone) {
+            mChanges.add(() -> ui.detachFromTransitionLeash(executor, onDone));
+            return this;
+        }
+
+        @Override
+        public void commit() {
+            mChanges.forEach(Runnable::run);
+            mChanges.clear();
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index f4d1242..bcd3337 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -274,7 +274,7 @@
                                     if (layoutDirection == LayoutDirection.Rtl)
                                         screenWidth - offset.x
                                     else offset.x,
-                                    offset.y
+                                    offset.y,
                                 ) - contentOffset
                             val index = firstIndexAtOffset(gridState, adjustedOffset)
                             val key =
@@ -310,6 +310,9 @@
                                         it.changedToUp() || it.changedToUpIgnoreConsumed()
                                     }
                                 )
+
+                                // Reset state once touch ends.
+                                viewModel.onResetTouchState()
                             }
                         }
                     }
@@ -330,7 +333,7 @@
                                         if (layoutDirection == LayoutDirection.Rtl)
                                             screenWidth - offset.x
                                         else offset.x,
-                                        offset.y
+                                        offset.y,
                                     ) - it.positionInWindow() - contentOffset
                                 }
                             val index = adjustedOffset?.let { firstIndexAtOffset(gridState, it) }
@@ -344,14 +347,11 @@
                             }
                         }
                     }
-                },
+                }
     ) {
         AccessibilityContainer(viewModel) {
             if (!viewModel.isEditMode && isEmptyState) {
-                EmptyStateCta(
-                    contentPadding = contentPadding,
-                    viewModel = viewModel,
-                )
+                EmptyStateCta(contentPadding = contentPadding, viewModel = viewModel)
             } else {
                 val slideOffsetInPx =
                     with(LocalDensity.current) { Dimensions.SlideOffsetY.toPx().toInt() }
@@ -364,7 +364,7 @@
                         ) +
                             slideInVertically(
                                 animationSpec = tween(durationMillis = 1000, easing = Emphasized),
-                                initialOffsetY = { -slideOffsetInPx }
+                                initialOffsetY = { -slideOffsetInPx },
                             ),
                     exit =
                         fadeOut(
@@ -372,7 +372,7 @@
                         ) +
                             slideOutVertically(
                                 animationSpec = tween(durationMillis = 1000, easing = Emphasized),
-                                targetOffsetY = { -slideOffsetInPx }
+                                targetOffsetY = { -slideOffsetInPx },
                             ),
                     modifier = Modifier.fillMaxSize(),
                 ) {
@@ -389,7 +389,7 @@
                                     removeEnabled = removeButtonEnabled,
                                     offset =
                                         gridCoordinates?.let { it.positionInWindow() + offset },
-                                    containerToCheck = removeButtonCoordinates
+                                    containerToCheck = removeButtonCoordinates,
                                 )
                             },
                             gridState = gridState,
@@ -410,7 +410,7 @@
                 enter =
                     fadeIn(animationSpec = tween(durationMillis = 250, easing = LinearEasing)) +
                         slideInVertically(
-                            animationSpec = tween(durationMillis = 1000, easing = Emphasized),
+                            animationSpec = tween(durationMillis = 1000, easing = Emphasized)
                         ),
                 exit =
                     fadeOut(animationSpec = tween(durationMillis = 167, easing = LinearEasing)) +
@@ -434,7 +434,7 @@
                             viewModel.setSelectedKey(null)
                         }
                     },
-                    removeEnabled = removeButtonEnabled
+                    removeEnabled = removeButtonEnabled,
                 )
             }
         }
@@ -451,7 +451,7 @@
                 title = stringResource(id = R.string.dialog_title_to_allow_any_widget),
                 positiveButtonText = stringResource(id = R.string.button_text_to_open_settings),
                 onConfirm = viewModel::onEnableWidgetDialogConfirm,
-                onCancel = viewModel::onEnableWidgetDialogCancel
+                onCancel = viewModel::onEnableWidgetDialogCancel,
             )
 
             EnableWidgetDialog(
@@ -460,7 +460,7 @@
                 title = stringResource(id = R.string.work_mode_off_title),
                 positiveButtonText = stringResource(id = R.string.work_mode_turn_on),
                 onConfirm = viewModel::onEnableWorkProfileDialogConfirm,
-                onCancel = viewModel::onEnableWorkProfileDialogCancel
+                onCancel = viewModel::onEnableWorkProfileDialogCancel,
             )
         }
 
@@ -509,7 +509,7 @@
             imageVector = Icons.Outlined.Widgets,
             contentDescription = null,
             tint = colors.primary,
-            modifier = Modifier.size(32.dp)
+            modifier = Modifier.size(32.dp),
         )
         Spacer(modifier = Modifier.height(16.dp))
         Text(
@@ -527,7 +527,7 @@
                 Modifier.padding(horizontal = 26.dp, vertical = 16.dp)
                     .widthIn(min = 200.dp)
                     .heightIn(min = 56.dp),
-            onClick = { onButtonClicked() }
+            onClick = { onButtonClicked() },
         ) {
             Text(
                 stringResource(R.string.communal_widgets_disclaimer_button),
@@ -540,7 +540,7 @@
 @Composable
 private fun ObserveScrollEffect(
     gridState: LazyGridState,
-    communalViewModel: BaseCommunalViewModel
+    communalViewModel: BaseCommunalViewModel,
 ) {
 
     LaunchedEffect(gridState) {
@@ -667,7 +667,7 @@
             rememberGridDragDropState(
                 gridState = gridState,
                 contentListState = contentListState,
-                updateDragPositionForRemove = updateDragPositionForRemove
+                updateDragPositionForRemove = updateDragPositionForRemove,
             )
         gridModifier =
             gridModifier
@@ -677,7 +677,7 @@
                     LocalLayoutDirection.current,
                     screenWidth,
                     contentOffset,
-                    viewModel
+                    viewModel,
                 )
         // for widgets dropped from other activities
         val dragAndDropTargetState =
@@ -709,11 +709,7 @@
             contentType = { _, item -> item.key },
             span = { _, item -> GridItemSpan(item.size.span) },
         ) { index, item ->
-            val size =
-                SizeF(
-                    Dimensions.CardWidth.value,
-                    item.size.dp().value,
-                )
+            val size = SizeF(Dimensions.CardWidth.value, item.size.dp().value)
             val cardModifier = Modifier.requiredSize(width = size.width.dp, height = size.height.dp)
             if (viewModel.isEditMode && dragDropState != null) {
                 val selected = item.key == selectedKey.value
@@ -765,16 +761,13 @@
  * The empty state displays a fullscreen call-to-action (CTA) tile when no widgets are available.
  */
 @Composable
-private fun EmptyStateCta(
-    contentPadding: PaddingValues,
-    viewModel: BaseCommunalViewModel,
-) {
+private fun EmptyStateCta(contentPadding: PaddingValues, viewModel: BaseCommunalViewModel) {
     val colors = LocalAndroidColorScheme.current
     Card(
         modifier = Modifier.height(hubDimensions.GridHeight).padding(contentPadding),
         colors = CardDefaults.cardColors(containerColor = Color.Transparent),
         border = BorderStroke(3.adjustedDp, colors.secondary),
-        shape = RoundedCornerShape(size = 80.adjustedDp)
+        shape = RoundedCornerShape(size = 80.adjustedDp),
     ) {
         Column(
             modifier = Modifier.fillMaxSize().padding(horizontal = 110.adjustedDp),
@@ -788,10 +781,7 @@
                 textAlign = TextAlign.Center,
                 color = colors.secondary,
             )
-            Row(
-                modifier = Modifier.fillMaxWidth(),
-                horizontalArrangement = Arrangement.Center,
-            ) {
+            Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
                 Button(
                     modifier = Modifier.height(56.dp),
                     colors =
@@ -799,17 +789,13 @@
                             containerColor = colors.primary,
                             contentColor = colors.onPrimary,
                         ),
-                    onClick = {
-                        viewModel.onOpenWidgetEditor(
-                            shouldOpenWidgetPickerOnStart = true,
-                        )
-                    },
+                    onClick = { viewModel.onOpenWidgetEditor(shouldOpenWidgetPickerOnStart = true) },
                 ) {
                     Icon(
                         imageVector = Icons.Default.Add,
                         contentDescription =
                             stringResource(R.string.label_for_button_in_empty_state_cta),
-                        modifier = Modifier.size(24.dp)
+                        modifier = Modifier.size(24.dp),
                     )
                     Spacer(Modifier.width(ButtonDefaults.IconSpacing))
                     Text(
@@ -835,7 +821,7 @@
     setToolbarSize: (toolbarSize: IntSize) -> Unit,
     setRemoveButtonCoordinates: (coordinates: LayoutCoordinates?) -> Unit,
     onOpenWidgetPicker: () -> Unit,
-    onEditDone: () -> Unit
+    onEditDone: () -> Unit,
 ) {
     if (!removeEnabled) {
         // Clear any existing coordinates when remove is not enabled.
@@ -844,7 +830,7 @@
     val removeButtonAlpha: Float by
         animateFloatAsState(
             targetValue = if (removeEnabled) 1f else 0.5f,
-            label = "RemoveButtonAlphaAnimation"
+            label = "RemoveButtonAlphaAnimation",
         )
 
     Box(
@@ -855,7 +841,7 @@
                     start = Dimensions.ToolbarPaddingHorizontal,
                     end = Dimensions.ToolbarPaddingHorizontal,
                 )
-                .onSizeChanged { setToolbarSize(it) },
+                .onSizeChanged { setToolbarSize(it) }
     ) {
         val addWidgetText = stringResource(R.string.hub_mode_add_widget_button_text)
         ToolbarButton(
@@ -864,16 +850,14 @@
             onClick = onOpenWidgetPicker,
         ) {
             Icon(Icons.Default.Add, null)
-            Text(
-                text = addWidgetText,
-            )
+            Text(text = addWidgetText)
         }
 
         AnimatedVisibility(
             modifier = Modifier.align(Alignment.Center),
             visible = removeEnabled,
             enter = fadeIn(),
-            exit = fadeOut()
+            exit = fadeOut(),
         ) {
             Button(
                 onClick = onRemoveClicked,
@@ -887,20 +871,18 @@
                             if (removeEnabled) {
                                 setRemoveButtonCoordinates(it)
                             }
-                        }
+                        },
             ) {
                 Row(
                     horizontalArrangement =
                         Arrangement.spacedBy(
                             ButtonDefaults.IconSpacing,
-                            Alignment.CenterHorizontally
+                            Alignment.CenterHorizontally,
                         ),
-                    verticalAlignment = Alignment.CenterVertically
+                    verticalAlignment = Alignment.CenterVertically,
                 ) {
                     Icon(Icons.Default.Close, contentDescription = null)
-                    Text(
-                        text = stringResource(R.string.button_to_remove_widget),
-                    )
+                    Text(text = stringResource(R.string.button_to_remove_widget))
                 }
             }
         }
@@ -911,9 +893,7 @@
             onClick = onEditDone,
         ) {
             Icon(Icons.Default.Check, contentDescription = null)
-            Text(
-                text = stringResource(R.string.hub_mode_editing_exit_button_text),
-            )
+            Text(text = stringResource(R.string.hub_mode_editing_exit_button_text))
         }
     }
 }
@@ -926,14 +906,14 @@
     isPrimary: Boolean = true,
     onClick: () -> Unit,
     modifier: Modifier = Modifier,
-    content: @Composable RowScope.() -> Unit
+    content: @Composable RowScope.() -> Unit,
 ) {
     val colors = LocalAndroidColorScheme.current
     AnimatedVisibility(
         visible = isPrimary,
         modifier = modifier,
         enter = fadeIn(),
-        exit = fadeOut()
+        exit = fadeOut(),
     ) {
         Button(
             onClick = onClick,
@@ -943,7 +923,7 @@
             Row(
                 horizontalArrangement =
                     Arrangement.spacedBy(ButtonDefaults.IconSpacing, Alignment.CenterHorizontally),
-                verticalAlignment = Alignment.CenterVertically
+                verticalAlignment = Alignment.CenterVertically,
             ) {
                 content()
             }
@@ -954,21 +934,18 @@
         visible = !isPrimary,
         modifier = modifier,
         enter = fadeIn(),
-        exit = fadeOut()
+        exit = fadeOut(),
     ) {
         OutlinedButton(
             onClick = onClick,
-            colors =
-                ButtonDefaults.outlinedButtonColors(
-                    contentColor = colors.onPrimaryContainer,
-                ),
+            colors = ButtonDefaults.outlinedButtonColors(contentColor = colors.onPrimaryContainer),
             border = BorderStroke(width = 2.0.dp, color = colors.primary),
             contentPadding = Dimensions.ButtonPadding,
         ) {
             Row(
                 horizontalArrangement =
                     Arrangement.spacedBy(ButtonDefaults.IconSpacing, Alignment.CenterHorizontally),
-                verticalAlignment = Alignment.CenterVertically
+                verticalAlignment = Alignment.CenterVertically,
             ) {
                 content()
             }
@@ -1041,7 +1018,7 @@
                     size =
                         Size(width = size.width + padding * 2, height = size.height + padding * 2),
                     cornerRadius = CornerRadius(37.adjustedDp.toPx()),
-                    style = Stroke(width = 3.adjustedDp.toPx())
+                    style = Stroke(width = 3.adjustedDp.toPx()),
                 )
             }
     )
@@ -1061,7 +1038,7 @@
                 containerColor = colors.primary,
                 contentColor = colors.onPrimary,
             ),
-        shape = RoundedCornerShape(68.adjustedDp, 34.adjustedDp, 68.adjustedDp, 34.adjustedDp)
+        shape = RoundedCornerShape(68.adjustedDp, 34.adjustedDp, 68.adjustedDp, 34.adjustedDp),
     ) {
         Column(
             modifier =
@@ -1081,7 +1058,7 @@
                 style = MaterialTheme.typography.titleLarge,
                 fontSize = nonScalableTextSize(22.dp),
                 lineHeight = nonScalableTextSize(28.dp),
-                modifier = Modifier.verticalScroll(rememberScrollState()).weight(1F)
+                modifier = Modifier.verticalScroll(rememberScrollState()).weight(1F),
             )
             Spacer(modifier = Modifier.size(16.adjustedDp))
             Row(
@@ -1093,15 +1070,12 @@
                     LocalDensity provides
                         Density(
                             LocalDensity.current.density,
-                            LocalDensity.current.fontScale.coerceIn(0f, 1.25f)
+                            LocalDensity.current.fontScale.coerceIn(0f, 1.25f),
                         )
                 ) {
                     OutlinedButton(
                         modifier = Modifier.fillMaxHeight().weight(1F),
-                        colors =
-                            ButtonDefaults.buttonColors(
-                                contentColor = colors.onPrimary,
-                            ),
+                        colors = ButtonDefaults.buttonColors(contentColor = colors.onPrimary),
                         border = BorderStroke(width = 1.0.dp, color = colors.primaryContainer),
                         onClick = viewModel::onDismissCtaTile,
                         contentPadding = PaddingValues(0.dp, 0.dp, 0.dp, 0.dp),
@@ -1259,7 +1233,7 @@
                 visible = selected,
                 model = model,
                 widgetConfigurator = widgetConfigurator,
-                modifier = Modifier.align(Alignment.BottomEnd)
+                modifier = Modifier.align(Alignment.BottomEnd),
             )
         }
     }
@@ -1289,14 +1263,14 @@
                     containerColor = colors.primary,
                     contentColor = colors.onPrimary,
                     disabledContainerColor = Color.Transparent,
-                    disabledContentColor = Color.Transparent
+                    disabledContentColor = Color.Transparent,
                 ),
             onClick = { scope.launch { widgetConfigurator.configureWidget(model.appWidgetId) } },
         ) {
             Icon(
                 imageVector = Icons.Outlined.Edit,
                 contentDescription = stringResource(id = R.string.edit_widget),
-                modifier = Modifier.padding(12.adjustedDp)
+                modifier = Modifier.padding(12.adjustedDp),
             )
         }
     }
@@ -1323,13 +1297,13 @@
                 .background(
                     color = MaterialTheme.colorScheme.surfaceVariant,
                     shape =
-                        RoundedCornerShape(dimensionResource(system_app_widget_background_radius))
+                        RoundedCornerShape(dimensionResource(system_app_widget_background_radius)),
                 )
                 .clickable(
                     enabled = !viewModel.isEditMode,
                     interactionSource = null,
                     indication = null,
-                    onClick = viewModel::onOpenEnableWidgetDialog
+                    onClick = viewModel::onOpenEnableWidgetDialog,
                 ),
         verticalArrangement = Arrangement.Center,
         horizontalAlignment = Alignment.CenterHorizontally,
@@ -1360,7 +1334,7 @@
         modifier =
             modifier.background(
                 color = MaterialTheme.colorScheme.surfaceVariant,
-                shape = RoundedCornerShape(dimensionResource(system_app_widget_background_radius))
+                shape = RoundedCornerShape(dimensionResource(system_app_widget_background_radius)),
             ),
         verticalArrangement = Arrangement.Center,
         horizontalAlignment = Alignment.CenterHorizontally,
@@ -1418,7 +1392,7 @@
                             MotionEvent.ACTION_MOVE,
                             change.position.x,
                             change.position.y,
-                            0
+                            0,
                         )
                     viewModel.mediaHost.hostView.dispatchTouchEvent(event)
                     event.recycle()
@@ -1429,12 +1403,12 @@
                 layoutParams =
                     FrameLayout.LayoutParams(
                         FrameLayout.LayoutParams.MATCH_PARENT,
-                        FrameLayout.LayoutParams.MATCH_PARENT
+                        FrameLayout.LayoutParams.MATCH_PARENT,
                     )
             }
             viewModel.mediaHost.hostView
         },
-        onReset = {}
+        onReset = {},
     )
 }
 
@@ -1462,7 +1436,7 @@
                             ) {
                                 viewModel.changeScene(
                                     CommunalScenes.Blank,
-                                    "closed by accessibility"
+                                    "closed by accessibility",
                                 )
                                 true
                             },
@@ -1471,7 +1445,7 @@
                             ) {
                                 viewModel.onOpenWidgetEditor()
                                 true
-                            }
+                            },
                         )
                 }
             }
@@ -1514,7 +1488,7 @@
         start = Dimensions.ToolbarPaddingHorizontal,
         end = Dimensions.ToolbarPaddingHorizontal,
         top = verticalPadding + toolbarHeight,
-        bottom = verticalPadding
+        bottom = verticalPadding,
     )
 }
 
@@ -1523,7 +1497,7 @@
     return with(LocalDensity.current) {
         ContentPaddingInPx(
             start = paddingValues.calculateStartPadding(LocalLayoutDirection.current).toPx(),
-            top = paddingValues.calculateTopPadding().toPx()
+            top = paddingValues.calculateTopPadding().toPx(),
         )
     }
 }
@@ -1536,7 +1510,7 @@
 fun isPointerWithinEnabledRemoveButton(
     removeEnabled: Boolean,
     offset: Offset?,
-    containerToCheck: LayoutCoordinates?
+    containerToCheck: LayoutCoordinates?,
 ): Boolean {
     if (!removeEnabled || offset == null || containerToCheck == null) {
         return false
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index c163c6f..0490a26 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -350,7 +350,7 @@
         testScope.runTest {
             underTest.performDotFeedback(null)
 
-            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.DRAG_INDICATOR)
+            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.DRAG_INDICATOR_DISCRETE)
             assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt
index e25c1a7..d5020a5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt
@@ -109,7 +109,9 @@
         kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
         underTest.start()
         kosmos.communalSceneRepository.setTransitionState(sceneTransitions)
-        testScope.launch { keyguardTransitionRepository.emitInitialStepsFromOff(LOCKSCREEN) }
+        testScope.launch {
+            keyguardTransitionRepository.emitInitialStepsFromOff(LOCKSCREEN, testSetup = true)
+        }
     }
 
     /** Transition from blank to glanceable hub. This is the default case. */
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
index ab33269..d7fe263 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
@@ -16,10 +16,10 @@
 
 package com.android.systemui.education.domain.ui.view
 
+import android.app.Dialog
 import android.app.Notification
 import android.app.NotificationManager
 import android.content.applicationContext
-import android.widget.Toast
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -34,11 +34,13 @@
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -51,6 +53,7 @@
 import org.mockito.junit.MockitoJUnit
 import org.mockito.kotlin.any
 import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -63,10 +66,12 @@
     private val minDurationForNextEdu =
         KeyboardTouchpadEduInteractor.minIntervalBetweenEdu + 1.seconds
     private lateinit var underTest: ContextualEduUiCoordinator
-    @Mock private lateinit var toast: Toast
+    @Mock private lateinit var dialog: Dialog
     @Mock private lateinit var notificationManager: NotificationManager
+    @Mock private lateinit var accessibilityManagerWrapper: AccessibilityManagerWrapper
     @get:Rule val mockitoRule = MockitoJUnit.rule()
     private var toastContent = ""
+    private val timeoutMillis = 3500L
 
     @Before
     fun setUp() {
@@ -75,30 +80,35 @@
             interactor.updateTouchpadFirstConnectionTime()
         }
 
+        whenever(accessibilityManagerWrapper.getRecommendedTimeoutMillis(any(), any()))
+            .thenReturn(timeoutMillis.toInt())
+
         val viewModel =
             ContextualEduViewModel(
                 kosmos.applicationContext.resources,
-                kosmos.keyboardTouchpadEduInteractor
+                kosmos.keyboardTouchpadEduInteractor,
+                accessibilityManagerWrapper,
             )
+
         underTest =
             ContextualEduUiCoordinator(
                 kosmos.applicationCoroutineScope,
                 viewModel,
                 kosmos.applicationContext,
                 notificationManager
-            ) { content ->
-                toastContent = content
-                toast
+            ) { model ->
+                toastContent = model.message
+                dialog
             }
         underTest.start()
         kosmos.keyboardTouchpadEduInteractor.start()
     }
 
     @Test
-    fun showToastOnNewEdu() =
+    fun showDialogOnNewEdu() =
         testScope.runTest {
             triggerEducation(BACK)
-            verify(toast).show()
+            verify(dialog).show()
         }
 
     @Test
@@ -111,6 +121,14 @@
         }
 
     @Test
+    fun dismissDialogAfterTimeout() =
+        testScope.runTest {
+            triggerEducation(BACK)
+            advanceTimeBy(timeoutMillis + 1)
+            verify(dialog).dismiss()
+        }
+
+    @Test
     fun verifyBackEduToastContent() =
         testScope.runTest {
             triggerEducation(BACK)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt
index 129752e..aab46d8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt
@@ -22,6 +22,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.testKosmos
 import org.junit.Before
 import org.junit.Test
@@ -44,6 +45,7 @@
             KeyguardBlueprintViewModel(
                 handler = kosmos.fakeExecutorHandler,
                 keyguardBlueprintInteractor = keyguardBlueprintInteractor,
+                keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor,
             )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
index e6ea64f..d0da2e9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
@@ -89,9 +89,12 @@
     }
 
     @Test
-    fun lockscreenFadeOut() =
+    fun lockscreenFadeOut_shadeNotExpanded() =
         testScope.runTest {
             val values by collectValues(underTest.lockscreenAlpha)
+            shadeExpanded(false)
+            runCurrent()
+
             repository.sendTransitionSteps(
                 steps =
                     listOf(
@@ -104,10 +107,34 @@
                     ),
                 testScope = testScope,
             )
-            // Only 5 values should be present, since the dream overlay runs for a small fraction
-            // of the overall animation time
             assertThat(values.size).isEqualTo(5)
-            values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
+            assertThat(values[0]).isEqualTo(1f)
+            assertThat(values[1]).isEqualTo(1f)
+            assertThat(values[2]).isIn(Range.open(0f, 1f))
+            assertThat(values[3]).isIn(Range.open(0f, 1f))
+            assertThat(values[4]).isEqualTo(0f)
+        }
+
+    @Test
+    fun lockscreenFadeOut_shadeExpanded() =
+        testScope.runTest {
+            val values by collectValues(underTest.lockscreenAlpha)
+            shadeExpanded(true)
+            runCurrent()
+
+            repository.sendTransitionSteps(
+                steps =
+                    listOf(
+                        step(0f, TransitionState.STARTED), // Should start running here...
+                        step(0f),
+                        step(.1f),
+                        step(.4f),
+                        step(.7f), // ...up to here
+                        step(1f),
+                    ),
+                testScope = testScope,
+            )
+            values.forEach { assertThat(it).isEqualTo(0f) }
         }
 
     @Test
@@ -115,7 +142,7 @@
         testScope.runTest {
             configurationRepository.setDimensionPixelSize(
                 R.dimen.lockscreen_to_occluded_transition_lockscreen_translation_y,
-                100
+                100,
             )
             val values by collectValues(underTest.lockscreenTranslationY)
             repository.sendTransitionSteps(
@@ -138,7 +165,7 @@
         testScope.runTest {
             configurationRepository.setDimensionPixelSize(
                 R.dimen.lockscreen_to_occluded_transition_lockscreen_translation_y,
-                100
+                100,
             )
             val values by collectValues(underTest.lockscreenTranslationY)
             repository.sendTransitionSteps(
@@ -171,7 +198,7 @@
                     listOf(
                         step(0f, TransitionState.STARTED),
                         step(.5f),
-                        step(1f, TransitionState.FINISHED)
+                        step(1f, TransitionState.FINISHED),
                     ),
                 testScope = testScope,
             )
@@ -228,7 +255,7 @@
             to = KeyguardState.OCCLUDED,
             value = value,
             transitionState = state,
-            ownerName = "LockscreenToOccludedTransitionViewModelTest"
+            ownerName = "LockscreenToOccludedTransitionViewModelTest",
         )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModelTest.kt
new file mode 100644
index 0000000..0b7a38e
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModelTest.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import android.platform.test.flag.junit.FlagsParameterization
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.andSceneContainer
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.collect.Range
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(ParameterizedAndroidJunit4::class)
+class OffToLockscreenTransitionViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
+    val kosmos = testKosmos()
+    val testScope = kosmos.testScope
+    val repository = kosmos.fakeKeyguardTransitionRepository
+    lateinit var underTest: OffToLockscreenTransitionViewModel
+
+    companion object {
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return FlagsParameterization.allCombinationsOf().andSceneContainer()
+        }
+    }
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags)
+    }
+
+    @Before
+    fun setup() {
+        underTest = kosmos.offToLockscreenTransitionViewModel
+    }
+
+    @Test
+    fun lockscreenAlpha() =
+        testScope.runTest {
+            val alpha by collectLastValue(underTest.lockscreenAlpha)
+
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            repository.sendTransitionStep(step(0f))
+            assertThat(alpha).isEqualTo(0f)
+
+            repository.sendTransitionStep(step(0.66f))
+            assertThat(alpha).isIn(Range.open(.1f, .9f))
+
+            repository.sendTransitionStep(step(1f))
+            assertThat(alpha).isEqualTo(1f)
+        }
+
+    private fun step(
+        value: Float,
+        state: TransitionState = TransitionState.RUNNING,
+    ): TransitionStep {
+        return TransitionStep(
+            from = KeyguardState.OFF,
+            to = KeyguardState.LOCKSCREEN,
+            value = value,
+            transitionState = state,
+            ownerName = "OffToLockscreenTransitionViewModelTest",
+        )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt
index 3e3aa4f..e12c67b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt
@@ -18,7 +18,7 @@
 import com.android.wm.shell.recents.RecentTasks
 import com.android.wm.shell.shared.GroupedRecentTaskInfo
 import com.android.wm.shell.shared.split.SplitBounds
-import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50
+import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50
 import com.google.common.truth.Truth.assertThat
 import java.util.Optional
 import java.util.function.Consumer
@@ -268,7 +268,7 @@
         GroupedRecentTaskInfo.forSplitTasks(
             createTaskInfo(taskId1, userId1, isVisible),
             createTaskInfo(taskId2, userId2, isVisible),
-            SplitBounds(Rect(), Rect(), taskId1, taskId2, SNAP_TO_50_50)
+            SplitBounds(Rect(), Rect(), taskId1, taskId2, SNAP_TO_2_50_50)
         )
 
     private fun createTaskInfo(taskId: Int, userId: Int, isVisible: Boolean = false) =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
index 3388c75..ada2138 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
@@ -21,19 +21,34 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
+import com.android.systemui.authentication.domain.interactor.AuthenticationResult
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
+import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.domain.startable.sceneContainerStartable
 import com.android.systemui.scene.shared.model.Overlays
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.shade.shared.flag.DualShade
 import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayContentViewModel
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
@@ -47,18 +62,84 @@
 
     private val underTest by lazy { kosmos.notificationsShadeOverlayContentViewModel }
 
+    @Before
+    fun setUp() {
+        kosmos.sceneContainerStartable.start()
+        underTest.activateIn(testScope)
+    }
+
     @Test
     fun onScrimClicked_hidesShade() =
         testScope.runTest {
             val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
-            sceneInteractor.showOverlay(
-                overlay = Overlays.NotificationsShade,
-                loggingReason = "test",
-            )
+            sceneInteractor.showOverlay(Overlays.NotificationsShade, "test")
             assertThat(currentOverlays).contains(Overlays.NotificationsShade)
 
             underTest.onScrimClicked()
 
             assertThat(currentOverlays).doesNotContain(Overlays.NotificationsShade)
         }
+
+    @Test
+    fun deviceLocked_hidesShade() =
+        testScope.runTest {
+            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            unlockDevice()
+            sceneInteractor.showOverlay(Overlays.NotificationsShade, "test")
+            assertThat(currentOverlays).contains(Overlays.NotificationsShade)
+
+            lockDevice()
+
+            assertThat(currentOverlays).doesNotContain(Overlays.NotificationsShade)
+        }
+
+    @Test
+    fun bouncerShown_hidesShade() =
+        testScope.runTest {
+            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            lockDevice()
+            sceneInteractor.showOverlay(Overlays.NotificationsShade, "test")
+            assertThat(currentOverlays).contains(Overlays.NotificationsShade)
+
+            sceneInteractor.changeScene(Scenes.Bouncer, "test")
+            runCurrent()
+
+            assertThat(currentOverlays).doesNotContain(Overlays.NotificationsShade)
+        }
+
+    @Test
+    fun shadeNotTouchable_hidesShade() =
+        testScope.runTest {
+            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            val isShadeTouchable by collectLastValue(kosmos.shadeInteractor.isShadeTouchable)
+            assertThat(isShadeTouchable).isTrue()
+            sceneInteractor.showOverlay(Overlays.NotificationsShade, "test")
+            assertThat(currentOverlays).contains(Overlays.NotificationsShade)
+
+            lockDevice()
+            assertThat(isShadeTouchable).isFalse()
+            assertThat(currentOverlays).doesNotContain(Overlays.NotificationsShade)
+        }
+
+    private fun TestScope.lockDevice() {
+        val currentScene by collectLastValue(sceneInteractor.currentScene)
+        kosmos.powerInteractor.setAsleepForTest()
+        runCurrent()
+
+        assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+    }
+
+    private suspend fun TestScope.unlockDevice() {
+        val currentScene by collectLastValue(sceneInteractor.currentScene)
+        kosmos.powerInteractor.setAwakeForTest()
+        runCurrent()
+        assertThat(
+                kosmos.authenticationInteractor.authenticate(
+                    FakeAuthenticationRepository.DEFAULT_PIN
+                )
+            )
+            .isEqualTo(AuthenticationResult.SUCCEEDED)
+
+        assertThat(currentScene).isEqualTo(Scenes.Gone)
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java
index 2580ac2..7798f46 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java
@@ -14,6 +14,8 @@
 
 package com.android.systemui.qs.tileimpl;
 
+import static com.android.systemui.Flags.FLAG_QS_NEW_TILES;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.mockito.ArgumentMatchers.any;
@@ -21,11 +23,16 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.content.Context;
 import android.graphics.drawable.AnimatedVectorDrawable;
 import android.graphics.drawable.Drawable;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.service.quicksettings.Tile;
 import android.testing.UiThreadTest;
 import android.widget.ImageView;
@@ -47,7 +54,6 @@
 @UiThreadTest
 @SmallTest
 public class QSIconViewImplTest extends SysuiTestCase {
-
     private QSIconViewImpl mIconView;
 
     @Before
@@ -106,6 +112,34 @@
         verify(iv).setImageTintList(argThat(stateList -> stateList.getColors()[0] == desiredColor));
     }
 
+
+    @EnableFlags(FLAG_QS_NEW_TILES)
+    @Test
+    public void testIconPreloaded_withFlagOn_immediatelyLoadsAll3TintColors() {
+        Context ctx = spy(mContext);
+
+        QSIconViewImpl iconView = new QSIconViewImpl(ctx);
+
+        verify(ctx, times(3)).obtainStyledAttributes(any());
+
+        iconView.getColor(new State()); // this should not increase the call count
+
+        verify(ctx, times(3)).obtainStyledAttributes(any());
+    }
+
+    @DisableFlags(FLAG_QS_NEW_TILES)
+    @Test
+    public void testIconPreloaded_withFlagOff_loadsOneTintColorAfterIconColorIsRead() {
+        Context ctx = spy(mContext);
+        QSIconViewImpl iconView = new QSIconViewImpl(ctx);
+
+        verify(ctx, never()).obtainStyledAttributes(any()); // none of the colors are preloaded
+
+        iconView.getColor(new State());
+
+        verify(ctx, times(1)).obtainStyledAttributes(any());
+    }
+
     @Test
     public void testStateSetCorrectly_toString() {
         ImageView iv = mock(ImageView.class);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt
index 620e90d..d32ba47 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt
@@ -17,13 +17,17 @@
 package com.android.systemui.qs.tiles.impl.internet.domain
 
 import android.graphics.drawable.TestStubDrawable
+import android.os.fakeExecutorHandler
 import android.widget.Switch
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.settingslib.graph.SignalDrawable
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
+import com.android.systemui.common.shared.model.Text.Companion.loadText
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject
 import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
@@ -31,6 +35,9 @@
 import com.android.systemui.qs.tiles.viewmodel.QSTileState
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
+import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel
+import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -39,25 +46,93 @@
 class InternetTileMapperTest : SysuiTestCase() {
     private val kosmos = Kosmos()
     private val internetTileConfig = kosmos.qsInternetTileConfig
+    private val handler = kosmos.fakeExecutorHandler
     private val mapper by lazy {
         InternetTileMapper(
             context.orCreateTestableResources
                 .apply {
                     addOverride(R.drawable.ic_qs_no_internet_unavailable, TestStubDrawable())
+                    addOverride(R.drawable.ic_satellite_connected_2, TestStubDrawable())
                     addOverride(wifiRes, TestStubDrawable())
                 }
                 .resources,
             context.theme,
-            context
+            context,
+            handler,
         )
     }
 
     @Test
-    fun withActiveModel_mappedStateMatchesDataModel() {
+    fun withActiveCellularModel_mappedStateMatchesDataModel() {
         val inputModel =
             InternetTileModel.Active(
                 secondaryLabel = Text.Resource(R.string.quick_settings_networks_available),
-                iconId = wifiRes,
+                icon = InternetTileIconModel.Cellular(3),
+                stateDescription = null,
+                contentDescription =
+                    ContentDescription.Resource(R.string.quick_settings_internet_label),
+            )
+
+        val outputState = mapper.map(internetTileConfig, inputModel)
+
+        val signalDrawable = SignalDrawable(context, handler)
+        signalDrawable.setLevel(3)
+        val expectedState =
+            createInternetTileState(
+                QSTileState.ActivationState.ACTIVE,
+                context.getString(R.string.quick_settings_networks_available),
+                Icon.Loaded(signalDrawable, null),
+                null,
+                context.getString(R.string.quick_settings_internet_label),
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun withActiveSatelliteModel_mappedStateMatchesDataModel() {
+        val inputIcon =
+            SignalIconModel.Satellite(
+                3,
+                Icon.Resource(
+                    res = R.drawable.ic_satellite_connected_2,
+                    contentDescription =
+                        ContentDescription.Resource(
+                            R.string.accessibility_status_bar_satellite_good_connection
+                        ),
+                ),
+            )
+        val inputModel =
+            InternetTileModel.Active(
+                secondaryLabel = Text.Resource(R.string.quick_settings_networks_available),
+                icon = InternetTileIconModel.Satellite(inputIcon.icon),
+                stateDescription = null,
+                contentDescription =
+                    ContentDescription.Resource(
+                        R.string.accessibility_status_bar_satellite_good_connection
+                    ),
+            )
+
+        val outputState = mapper.map(internetTileConfig, inputModel)
+
+        val expectedSatIcon = SatelliteIconModel.fromSignalStrength(3)
+
+        val expectedState =
+            createInternetTileState(
+                QSTileState.ActivationState.ACTIVE,
+                inputModel.secondaryLabel.loadText(context).toString(),
+                Icon.Loaded(context.getDrawable(expectedSatIcon!!.res)!!, null),
+                expectedSatIcon.res,
+                expectedSatIcon.contentDescription.loadContentDescription(context).toString(),
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun withActiveWifiModel_mappedStateMatchesDataModel() {
+        val inputModel =
+            InternetTileModel.Active(
+                secondaryLabel = Text.Resource(R.string.quick_settings_networks_available),
+                icon = InternetTileIconModel.ResourceId(wifiRes),
                 stateDescription = null,
                 contentDescription =
                     ContentDescription.Resource(R.string.quick_settings_internet_label),
@@ -71,7 +146,7 @@
                 context.getString(R.string.quick_settings_networks_available),
                 Icon.Loaded(context.getDrawable(wifiRes)!!, contentDescription = null),
                 wifiRes,
-                context.getString(R.string.quick_settings_internet_label)
+                context.getString(R.string.quick_settings_internet_label),
             )
         QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
     }
@@ -81,7 +156,7 @@
         val inputModel =
             InternetTileModel.Inactive(
                 secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable),
-                iconId = R.drawable.ic_qs_no_internet_unavailable,
+                icon = InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_unavailable),
                 stateDescription = null,
                 contentDescription =
                     ContentDescription.Resource(R.string.quick_settings_networks_unavailable),
@@ -95,10 +170,10 @@
                 context.getString(R.string.quick_settings_networks_unavailable),
                 Icon.Loaded(
                     context.getDrawable(R.drawable.ic_qs_no_internet_unavailable)!!,
-                    contentDescription = null
+                    contentDescription = null,
                 ),
                 R.drawable.ic_qs_no_internet_unavailable,
-                context.getString(R.string.quick_settings_networks_unavailable)
+                context.getString(R.string.quick_settings_networks_unavailable),
             )
         QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
     }
@@ -107,7 +182,7 @@
         activationState: QSTileState.ActivationState,
         secondaryLabel: String,
         icon: Icon,
-        iconRes: Int,
+        iconRes: Int? = null,
         contentDescription: String,
     ): QSTileState {
         val label = context.getString(R.string.quick_settings_internet_label)
@@ -120,13 +195,13 @@
             setOf(
                 QSTileState.UserAction.CLICK,
                 QSTileState.UserAction.TOGGLE_CLICK,
-                QSTileState.UserAction.LONG_CLICK
+                QSTileState.UserAction.LONG_CLICK,
             ),
             contentDescription,
             null,
             QSTileState.SideViewIcon.Chevron,
             QSTileState.EnabledState.ENABLED,
-            Switch::class.qualifiedName
+            Switch::class.qualifiedName,
         )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt
index 5a45060..5259aa8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt
@@ -18,14 +18,12 @@
 
 import android.graphics.drawable.TestStubDrawable
 import android.os.UserHandle
-import android.testing.TestableLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.settingslib.AccessibilityContentDescriptions
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
-import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.common.shared.model.Text.Companion.loadText
 import com.android.systemui.coroutines.collectLastValue
@@ -49,6 +47,7 @@
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.shared.data.model.DefaultConnectionModel
 import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
+import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
@@ -60,9 +59,7 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
-import org.junit.Assume.assumeFalse
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -144,7 +141,6 @@
         underTest =
             InternetTileDataInteractor(
                 context,
-                testScope.coroutineContext,
                 testScope.backgroundScope,
                 airplaneModeRepository,
                 connectivityRepository,
@@ -164,9 +160,11 @@
 
             connectivityRepository.defaultConnections.value = DefaultConnectionModel()
 
+            val expectedIcon =
+                InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_unavailable)
             assertThat(latest?.secondaryLabel)
                 .isEqualTo(Text.Resource(R.string.quick_settings_networks_unavailable))
-            assertThat(latest?.iconId).isEqualTo(R.drawable.ic_qs_no_internet_unavailable)
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
         }
 
     @Test
@@ -183,11 +181,8 @@
                     underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
                 )
 
-            val networkModel =
-                WifiNetworkModel.Active.of(
-                    level = 4,
-                    ssid = "test ssid",
-                )
+            val networkModel = WifiNetworkModel.Active.of(level = 4, ssid = "test ssid")
+
             val wifiIcon =
                 WifiIcon.fromModel(model = networkModel, context = context, showHotspotInfo = true)
                     as WifiIcon.Visible
@@ -198,12 +193,9 @@
 
             assertThat(latest?.secondaryTitle).isEqualTo("test ssid")
             assertThat(latest?.secondaryLabel).isNull()
-            val expectedIcon =
-                Icon.Loaded(context.getDrawable(WifiIcons.WIFI_NO_INTERNET_ICONS[4])!!, null)
 
-            val actualIcon = latest?.icon
-            assertThat(actualIcon).isEqualTo(expectedIcon)
-            assertThat(latest?.iconId).isEqualTo(WifiIcons.WIFI_NO_INTERNET_ICONS[4])
+            val expectedIcon = InternetTileIconModel.ResourceId(WifiIcons.WIFI_NO_INTERNET_ICONS[4])
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.contentDescription.loadContentDescription(context))
                 .isEqualTo("$internet,test ssid")
             val expectedSd = wifiIcon.contentDescription
@@ -229,8 +221,7 @@
             wifiRepository.setIsWifiDefault(true)
             wifiRepository.setWifiNetwork(networkModel)
 
-            val expectedIcon =
-                Icon.Loaded(context.getDrawable(WifiIcons.WIFI_NO_INTERNET_ICONS[4])!!, null)
+            val expectedIcon = InternetTileIconModel.ResourceId(WifiIcons.WIFI_NO_INTERNET_ICONS[4])
             assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
                 .doesNotContain(
@@ -249,9 +240,8 @@
             setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.TABLET)
 
             val expectedIcon =
-                Icon.Loaded(
-                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_tablet)!!,
-                    null
+                InternetTileIconModel.ResourceId(
+                    com.android.settingslib.R.drawable.ic_hotspot_tablet
                 )
             assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
@@ -271,9 +261,8 @@
             setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.LAPTOP)
 
             val expectedIcon =
-                Icon.Loaded(
-                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_laptop)!!,
-                    null
+                InternetTileIconModel.ResourceId(
+                    com.android.settingslib.R.drawable.ic_hotspot_laptop
                 )
             assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
@@ -293,10 +282,10 @@
             setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.WATCH)
 
             val expectedIcon =
-                Icon.Loaded(
-                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_watch)!!,
-                    null
+                InternetTileIconModel.ResourceId(
+                    com.android.settingslib.R.drawable.ic_hotspot_watch
                 )
+
             assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
                 .isEqualTo(
@@ -315,10 +304,7 @@
             setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.AUTO)
 
             val expectedIcon =
-                Icon.Loaded(
-                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_auto)!!,
-                    null
-                )
+                InternetTileIconModel.ResourceId(com.android.settingslib.R.drawable.ic_hotspot_auto)
             assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
                 .isEqualTo(
@@ -336,9 +322,8 @@
             setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.PHONE)
 
             val expectedIcon =
-                Icon.Loaded(
-                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_phone)!!,
-                    null
+                InternetTileIconModel.ResourceId(
+                    com.android.settingslib.R.drawable.ic_hotspot_phone
                 )
             assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
@@ -358,9 +343,8 @@
             setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.UNKNOWN)
 
             val expectedIcon =
-                Icon.Loaded(
-                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_phone)!!,
-                    null
+                InternetTileIconModel.ResourceId(
+                    com.android.settingslib.R.drawable.ic_hotspot_phone
                 )
             assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
@@ -380,10 +364,10 @@
             setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.INVALID)
 
             val expectedIcon =
-                Icon.Loaded(
-                    context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_phone)!!,
-                    null
+                InternetTileIconModel.ResourceId(
+                    com.android.settingslib.R.drawable.ic_hotspot_phone
                 )
+
             assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
                 .isEqualTo(
@@ -426,8 +410,9 @@
             assertThat(latest?.secondaryLabel).isNull()
             assertThat(latest?.secondaryTitle)
                 .isEqualTo(context.getString(R.string.quick_settings_networks_available))
-            assertThat(latest?.icon).isNull()
-            assertThat(latest?.iconId).isEqualTo(R.drawable.ic_qs_no_internet_available)
+            val expectedIcon =
+                InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_available)
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription).isNull()
             val expectedCd =
                 "$internet,${context.getString(R.string.quick_settings_networks_available)}"
@@ -435,54 +420,19 @@
                 .isEqualTo(expectedCd)
         }
 
-    /**
-     * We expect a RuntimeException because [underTest] instantiates a SignalDrawable on the
-     * provided context, and so the SignalDrawable constructor attempts to instantiate a Handler()
-     * on the mentioned context. Since that context does not have a looper assigned to it, the
-     * handler instantiation will throw a RuntimeException.
-     *
-     * TODO(b/338068066): Robolectric behavior differs in that it does not throw the exception So
-     *   either we should make Robolectric behave similar to the device test, or change this test to
-     *   look for a different signal than the exception, when run by Robolectric. For now we just
-     *   assume the test is not Robolectric.
-     */
-    @Test(expected = java.lang.RuntimeException::class)
-    fun mobileDefault_usesNetworkNameAndIcon_throwsRunTimeException() =
-        testScope.runTest {
-            assumeFalse(isRobolectricTest())
-
-            collectLastValue(underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest)))
-
-            connectivityRepository.setMobileConnected()
-            mobileConnectionsRepository.mobileIsDefault.value = true
-            mobileConnectionRepository.apply {
-                setAllLevels(3)
-                setAllRoaming(false)
-                networkName.value = NetworkNameModel.Default("test network")
-            }
-
-            runCurrent()
-        }
-
-    /**
-     * See [mobileDefault_usesNetworkNameAndIcon_throwsRunTimeException] for description of the
-     * problem this test solves. The solution here is to assign a looper to the context via
-     * RunWithLooper. In the production code, the solution is to use a Main CoroutineContext for
-     * creating the SignalDrawable.
-     */
-    @TestableLooper.RunWithLooper
     @Test
-    fun mobileDefault_run_withLooper_usesNetworkNameAndIcon() =
+    fun mobileDefault_usesNetworkNameAndIcon() =
         testScope.runTest {
             val latest by
                 collectLastValue(
                     underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
                 )
+            val iconLevel = 3
 
             connectivityRepository.setMobileConnected()
             mobileConnectionsRepository.mobileIsDefault.value = true
             mobileConnectionRepository.apply {
-                setAllLevels(3)
+                setAllLevels(iconLevel)
                 setAllRoaming(false)
                 networkName.value = NetworkNameModel.Default("test network")
             }
@@ -491,8 +441,9 @@
             assertThat(latest?.secondaryTitle).isNotNull()
             assertThat(latest?.secondaryTitle.toString()).contains("test network")
             assertThat(latest?.secondaryLabel).isNull()
-            assertThat(latest?.icon).isInstanceOf(Icon.Loaded::class.java)
-            assertThat(latest?.iconId).isNull()
+            val expectedIcon = InternetTileIconModel.Cellular(iconLevel)
+
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription.loadContentDescription(context))
                 .isEqualTo(latest?.secondaryTitle.toString())
             assertThat(latest?.contentDescription.loadContentDescription(context))
@@ -513,8 +464,8 @@
             assertThat(latest?.secondaryLabel.loadText(context))
                 .isEqualTo(ethernetIcon!!.contentDescription.loadContentDescription(context))
             assertThat(latest?.secondaryTitle).isNull()
-            assertThat(latest?.iconId).isEqualTo(R.drawable.stat_sys_ethernet_fully)
-            assertThat(latest?.icon).isNull()
+            val expectedIcon = InternetTileIconModel.ResourceId(R.drawable.stat_sys_ethernet_fully)
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription).isNull()
             assertThat(latest?.contentDescription.loadContentDescription(context))
                 .isEqualTo(latest?.secondaryLabel.loadText(context))
@@ -534,8 +485,8 @@
             assertThat(latest?.secondaryLabel.loadText(context))
                 .isEqualTo(ethernetIcon!!.contentDescription.loadContentDescription(context))
             assertThat(latest?.secondaryTitle).isNull()
-            assertThat(latest?.iconId).isEqualTo(R.drawable.stat_sys_ethernet)
-            assertThat(latest?.icon).isNull()
+            val expectedIcon = InternetTileIconModel.ResourceId(R.drawable.stat_sys_ethernet)
+            assertThat(latest?.icon).isEqualTo(expectedIcon)
             assertThat(latest?.stateDescription).isNull()
             assertThat(latest?.contentDescription.loadContentDescription(context))
                 .isEqualTo(latest?.secondaryLabel.loadText(context))
@@ -543,11 +494,7 @@
 
     private fun setWifiNetworkWithHotspot(hotspot: WifiNetworkModel.HotspotDeviceType) {
         val networkModel =
-            WifiNetworkModel.Active.of(
-                level = 4,
-                ssid = "test ssid",
-                hotspotDeviceType = hotspot,
-            )
+            WifiNetworkModel.Active.of(level = 4, ssid = "test ssid", hotspotDeviceType = hotspot)
 
         connectivityRepository.setWifiConnected()
         wifiRepository.setIsWifiDefault(true)
@@ -560,7 +507,7 @@
         val NOT_CONNECTED_NETWORKS_UNAVAILABLE =
             InternetTileModel.Inactive(
                 secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable),
-                iconId = R.drawable.ic_qs_no_internet_unavailable,
+                icon = InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_unavailable),
                 stateDescription = null,
                 contentDescription =
                     ContentDescription.Resource(R.string.quick_settings_networks_unavailable),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt
index 8c7ec47..f32894d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt
@@ -21,18 +21,33 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
+import com.android.systemui.authentication.domain.interactor.AuthenticationResult
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
+import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.domain.startable.sceneContainerStartable
 import com.android.systemui.scene.shared.model.Overlays
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.shade.shared.flag.DualShade
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
@@ -46,18 +61,84 @@
 
     private val underTest by lazy { kosmos.quickSettingsShadeOverlayContentViewModel }
 
+    @Before
+    fun setUp() {
+        kosmos.sceneContainerStartable.start()
+        underTest.activateIn(testScope)
+    }
+
     @Test
     fun onScrimClicked_hidesShade() =
         testScope.runTest {
             val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
-            sceneInteractor.showOverlay(
-                overlay = Overlays.QuickSettingsShade,
-                loggingReason = "test",
-            )
+            sceneInteractor.showOverlay(Overlays.QuickSettingsShade, "test")
             assertThat(currentOverlays).contains(Overlays.QuickSettingsShade)
 
             underTest.onScrimClicked()
 
             assertThat(currentOverlays).doesNotContain(Overlays.QuickSettingsShade)
         }
+
+    @Test
+    fun deviceLocked_hidesShade() =
+        testScope.runTest {
+            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            unlockDevice()
+            sceneInteractor.showOverlay(Overlays.QuickSettingsShade, "test")
+            assertThat(currentOverlays).contains(Overlays.QuickSettingsShade)
+
+            lockDevice()
+
+            assertThat(currentOverlays).isEmpty()
+        }
+
+    @Test
+    fun bouncerShown_hidesShade() =
+        testScope.runTest {
+            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            lockDevice()
+            sceneInteractor.showOverlay(Overlays.QuickSettingsShade, "test")
+            assertThat(currentOverlays).contains(Overlays.QuickSettingsShade)
+
+            sceneInteractor.changeScene(Scenes.Bouncer, "test")
+            runCurrent()
+
+            assertThat(currentOverlays).doesNotContain(Overlays.QuickSettingsShade)
+        }
+
+    @Test
+    fun shadeNotTouchable_hidesShade() =
+        testScope.runTest {
+            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+            val isShadeTouchable by collectLastValue(kosmos.shadeInteractor.isShadeTouchable)
+            assertThat(isShadeTouchable).isTrue()
+            sceneInteractor.showOverlay(Overlays.QuickSettingsShade, "test")
+            assertThat(currentOverlays).contains(Overlays.QuickSettingsShade)
+
+            lockDevice()
+            assertThat(isShadeTouchable).isFalse()
+            assertThat(currentOverlays).doesNotContain(Overlays.QuickSettingsShade)
+        }
+
+    private fun TestScope.lockDevice() {
+        val currentScene by collectLastValue(sceneInteractor.currentScene)
+        kosmos.powerInteractor.setAsleepForTest()
+        runCurrent()
+
+        assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+    }
+
+    private suspend fun TestScope.unlockDevice() {
+        val currentScene by collectLastValue(sceneInteractor.currentScene)
+        kosmos.powerInteractor.setAwakeForTest()
+        runCurrent()
+        assertThat(
+                kosmos.authenticationInteractor.authenticate(
+                    FakeAuthenticationRepository.DEFAULT_PIN
+                )
+            )
+            .isEqualTo(AuthenticationResult.SUCCEEDED)
+
+        assertThat(currentScene).isEqualTo(Scenes.Gone)
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt
similarity index 88%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt
index 57cfe1b..3e5dee6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt
@@ -47,7 +47,7 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
-class IssueRecordingServiceCommandHandlerTest : SysuiTestCase() {
+class IssueRecordingServiceSessionTest : SysuiTestCase() {
 
     private val kosmos = Kosmos().also { it.testCase = this }
     private val bgExecutor = kosmos.fakeExecutor
@@ -61,13 +61,13 @@
     private val notificationManager = mock<NotificationManager>()
     private val panelInteractor = mock<PanelInteractor>()
 
-    private lateinit var underTest: IssueRecordingServiceCommandHandler
+    private lateinit var underTest: IssueRecordingServiceSession
 
     @Before
     fun setup() {
         traceurMessageSender = mock<TraceurMessageSender>()
         underTest =
-            IssueRecordingServiceCommandHandler(
+            IssueRecordingServiceSession(
                 bgExecutor,
                 dialogTransitionAnimator,
                 panelInteractor,
@@ -75,13 +75,13 @@
                 issueRecordingState,
                 iActivityManager,
                 notificationManager,
-                userContextProvider
+                userContextProvider,
             )
     }
 
     @Test
     fun startsTracing_afterReceivingActionStartCommand() {
-        underTest.handleStartCommand()
+        underTest.start()
         bgExecutor.runAllReady()
 
         Truth.assertThat(issueRecordingState.isRecording).isTrue()
@@ -90,7 +90,7 @@
 
     @Test
     fun stopsTracing_afterReceivingStopTracingCommand() {
-        underTest.handleStopCommand(mContext.contentResolver)
+        underTest.stop(mContext.contentResolver)
         bgExecutor.runAllReady()
 
         Truth.assertThat(issueRecordingState.isRecording).isFalse()
@@ -99,7 +99,7 @@
 
     @Test
     fun cancelsNotification_afterReceivingShareCommand() {
-        underTest.handleShareCommand(0, null, mContext)
+        underTest.share(0, null, mContext)
         bgExecutor.runAllReady()
 
         verify(notificationManager).cancelAsUser(isNull(), anyInt(), any<UserHandle>())
@@ -110,7 +110,7 @@
         issueRecordingState.takeBugreport = true
         val uri = mock<Uri>()
 
-        underTest.handleShareCommand(0, uri, mContext)
+        underTest.share(0, uri, mContext)
         bgExecutor.runAllReady()
 
         verify(iActivityManager).requestBugReportWithExtraAttachment(uri)
@@ -121,7 +121,7 @@
         issueRecordingState.takeBugreport = false
         val uri = mock<Uri>()
 
-        underTest.handleShareCommand(0, uri, mContext)
+        underTest.share(0, uri, mContext)
         bgExecutor.runAllReady()
 
         verify(traceurMessageSender).shareTraces(mContext, uri)
@@ -131,7 +131,7 @@
     fun closesShade_afterReceivingShareCommand() {
         val uri = mock<Uri>()
 
-        underTest.handleShareCommand(0, uri, mContext)
+        underTest.share(0, uri, mContext)
         bgExecutor.runAllReady()
 
         verify(panelInteractor).collapsePanels()
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
index be44dee..73626b4 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
@@ -184,7 +184,10 @@
             }
         }
 
-        /** Get the text for secondaryLabel. */
+        /**
+         *  If the current secondaryLabel value is not empty, ignore the given input and return
+         *  the current value. Otherwise return current value.
+         */
         public CharSequence getSecondaryLabel(CharSequence stateText) {
             // Use a local reference as the value might change from other threads
             CharSequence localSecondaryLabel = secondaryLabel;
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index e94248d..629c94f 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -2047,4 +2047,6 @@
     <!-- SliceView icon size -->
     <dimen name="abc_slice_big_pic_min_height">64dp</dimen>
     <dimen name="abc_slice_big_pic_max_height">64dp</dimen>
+
+    <dimen name="contextual_edu_dialog_bottom_margin">70dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 75389b1..c76b35f 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3818,6 +3818,8 @@
     <!-- Main text of the one line view of a redacted notification -->
     <string name="redacted_notification_single_line_text">Unlock to view</string>
 
+    <!-- Content description for contextual education dialog [CHAR LIMIT=NONE] -->
+    <string name="contextual_education_dialog_title">Contextual education</string>
     <!-- Education notification title for Back [CHAR_LIMIT=100] -->
     <string name="back_edu_notification_title">Use your touchpad to go back</string>
     <!-- Education notification text for Back [CHAR_LIMIT=100] -->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index a02c354..b34d6e4 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -1720,4 +1720,10 @@
     <style name="ShortcutHelperTheme" parent="@style/ShortcutHelperThemeCommon">
         <item name="android:windowLightNavigationBar">true</item>
     </style>
+
+    <style name="ContextualEduDialog" parent="@android:style/Theme.DeviceDefault.Dialog.NoActionBar">
+        <!-- To make the dialog wrap to content when the education text is short -->
+        <item name="windowMinWidthMajor">0%</item>
+        <item name="windowMinWidthMinor">0%</item>
+    </style>
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
index 76df9c9..fb00d6e 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
@@ -75,6 +75,9 @@
  * touches are consumed.
  */
 public class TouchMonitor {
+    // An incrementing id used to identify the touch monitor instance.
+    private static int sNextInstanceId = 0;
+
     private final Logger mLogger;
     // This executor is used to protect {@code mActiveTouchSessions} from being modified
     // concurrently. Any operation that adds or removes values should use this executor.
@@ -138,7 +141,7 @@
                     completer.set(predecessor);
                 }
 
-                if (mActiveTouchSessions.isEmpty()) {
+                if (mActiveTouchSessions.isEmpty() && mInitialized) {
                     if (mStopMonitoringPending) {
                         stopMonitoring(false);
                     } else {
@@ -271,7 +274,7 @@
 
         @Override
         public void onDestroy(LifecycleOwner owner) {
-            stopMonitoring(true);
+            destroy();
         }
     };
 
@@ -279,6 +282,11 @@
      * When invoked, instantiates a new {@link InputSession} to monitor touch events.
      */
     private void startMonitoring() {
+        if (!mInitialized) {
+            mLogger.w("attempting to startMonitoring when not initialized");
+            return;
+        }
+
         mLogger.i("startMonitoring(): monitoring started");
         stopMonitoring(true);
 
@@ -587,7 +595,7 @@
         mDisplayHelper = displayHelper;
         mWindowManagerService = windowManagerService;
         mConfigurationInteractor = configurationInteractor;
-        mLoggingName = loggingName + ":TouchMonitor";
+        mLoggingName = loggingName + ":TouchMonitor[" + sNextInstanceId++ + "]";
         mLogger = new Logger(logBuffer, mLoggingName);
     }
 
@@ -613,7 +621,8 @@
      */
     public void destroy() {
         if (!mInitialized) {
-            throw new IllegalStateException("TouchMonitor not initialized");
+            // In the case that we've already been destroyed, this is a no-op
+            return;
         }
 
         stopMonitoring(true);
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt
index b8c30fe..d6b9211 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt
@@ -69,7 +69,7 @@
                 HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING,
             )
         } else {
-            msdlPlayer.get().playToken(MSDLToken.DRAG_INDICATOR)
+            msdlPlayer.get().playToken(MSDLToken.DRAG_INDICATOR_DISCRETE)
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index cbea876..8da4d46 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -30,7 +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.tutorial.KeyboardTouchpadTutorialCoreStartable
+import com.android.systemui.haptics.msdl.MSDLCoreStartable
 import com.android.systemui.keyboard.KeyboardUI
 import com.android.systemui.keyboard.PhysicalKeyboardCoreStartable
 import com.android.systemui.keyguard.KeyguardViewConfigurator
@@ -323,4 +323,9 @@
     @IntoMap
     @ClassKey(BatteryControllerStartable::class)
     abstract fun bindsBatteryControllerStartable(impl: BatteryControllerStartable): CoreStartable
+
+    @Binds
+    @IntoMap
+    @ClassKey(MSDLCoreStartable::class)
+    abstract fun bindMSDLCoreStartable(impl: MSDLCoreStartable): CoreStartable
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 113e001..83f86a7 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -65,6 +65,7 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.navigationbar.gestural.domain.GestureInteractor;
 import com.android.systemui.navigationbar.gestural.domain.TaskMatcher;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
 import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.touch.TouchInsetManager;
 import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -499,8 +500,11 @@
 
         mDreamOverlayContainerViewController =
                 dreamOverlayComponent.getDreamOverlayContainerViewController();
-        mTouchMonitor = ambientTouchComponent.getTouchMonitor();
-        mTouchMonitor.init();
+
+        if (!SceneContainerFlag.isEnabled()) {
+            mTouchMonitor = ambientTouchComponent.getTouchMonitor();
+            mTouchMonitor.init();
+        }
 
         mStateController.setShouldShowComplications(shouldShowComplications());
 
diff --git a/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduDialog.kt b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduDialog.kt
new file mode 100644
index 0000000..287e85c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduDialog.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.systemui.education.ui.view
+
+import android.app.AlertDialog
+import android.content.Context
+import android.os.Bundle
+import android.view.Gravity
+import android.view.WindowManager
+import android.widget.ToastPresenter
+import com.android.systemui.education.ui.viewmodel.ContextualEduToastViewModel
+import com.android.systemui.res.R
+
+class ContextualEduDialog(context: Context, private val model: ContextualEduToastViewModel) :
+    AlertDialog(context, R.style.ContextualEduDialog) {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        setUpWindowProperties()
+        setWindowPosition()
+        // title is used for a11y announcement
+        window?.setTitle(context.getString(R.string.contextual_education_dialog_title))
+        // TODO: b/369791926 - replace the below toast view with a custom dialog view
+        val toastView = ToastPresenter.getTextToastView(context, model.message)
+        setView(toastView)
+        super.onCreate(savedInstanceState)
+    }
+
+    private fun setUpWindowProperties() {
+        window?.apply {
+            setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG)
+            clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
+        }
+        setCanceledOnTouchOutside(false)
+    }
+
+    private fun setWindowPosition() {
+        window?.apply {
+            setGravity(Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL)
+            this.attributes =
+                WindowManager.LayoutParams().apply {
+                    width = WindowManager.LayoutParams.WRAP_CONTENT
+                    height = WindowManager.LayoutParams.WRAP_CONTENT
+                    copyFrom(attributes)
+                    y =
+                        context.resources.getDimensionPixelSize(
+                            R.dimen.contextual_edu_dialog_bottom_margin
+                        )
+                }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt
index e62b26b..913ecdd 100644
--- a/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.education.ui.view
 
+import android.app.Dialog
 import android.app.Notification
 import android.app.NotificationChannel
 import android.app.NotificationManager
@@ -24,7 +25,6 @@
 import android.content.Intent
 import android.os.Bundle
 import android.os.UserHandle
-import android.widget.Toast
 import androidx.core.app.NotificationCompat
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
@@ -49,7 +49,7 @@
     private val viewModel: ContextualEduViewModel,
     private val context: Context,
     private val notificationManager: NotificationManager,
-    private val createToast: (String) -> Toast
+    private val createDialog: (ContextualEduToastViewModel) -> Dialog,
 ) : CoreStartable {
 
     companion object {
@@ -69,16 +69,23 @@
         viewModel,
         context,
         notificationManager,
-        createToast = { message -> Toast.makeText(context, message, Toast.LENGTH_LONG) }
+        createDialog = { model -> ContextualEduDialog(context, model) },
     )
 
+    var dialog: Dialog? = null
+
     override fun start() {
         createEduNotificationChannel()
         applicationScope.launch {
             viewModel.eduContent.collect { contentModel ->
-                when (contentModel) {
-                    is ContextualEduToastViewModel -> showToast(contentModel)
-                    is ContextualEduNotificationViewModel -> showNotification(contentModel)
+                if (contentModel != null) {
+                    when (contentModel) {
+                        is ContextualEduToastViewModel -> showDialog(contentModel)
+                        is ContextualEduNotificationViewModel -> showNotification(contentModel)
+                    }
+                } else {
+                    dialog?.dismiss()
+                    dialog = null
                 }
             }
         }
@@ -95,9 +102,9 @@
         notificationManager.createNotificationChannel(channel)
     }
 
-    private fun showToast(model: ContextualEduToastViewModel) {
-        val toast = createToast(model.message)
-        toast.show()
+    private fun showDialog(model: ContextualEduToastViewModel) {
+        dialog = createDialog(model)
+        dialog?.show()
     }
 
     private fun showNotification(model: ContextualEduNotificationViewModel) {
diff --git a/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt
index cd4a8ad..32e7f41 100644
--- a/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.education.ui.viewmodel
 
 import android.content.res.Resources
+import android.view.accessibility.AccessibilityManager
 import com.android.systemui.contextualeducation.GestureType.ALL_APPS
 import com.android.systemui.contextualeducation.GestureType.BACK
 import com.android.systemui.contextualeducation.GestureType.HOME
@@ -27,23 +28,63 @@
 import com.android.systemui.education.shared.model.EducationInfo
 import com.android.systemui.education.shared.model.EducationUiType
 import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
 import javax.inject.Inject
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.map
 
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class ContextualEduViewModel
 @Inject
-constructor(@Main private val resources: Resources, interactor: KeyboardTouchpadEduInteractor) {
-    val eduContent: Flow<ContextualEduContentViewModel> =
-        interactor.educationTriggered.filterNotNull().map {
-            if (it.educationUiType == EducationUiType.Notification) {
-                ContextualEduNotificationViewModel(getEduTitle(it), getEduContent(it), it.userId)
-            } else {
-                ContextualEduToastViewModel(getEduContent(it), it.userId)
+constructor(
+    @Main private val resources: Resources,
+    interactor: KeyboardTouchpadEduInteractor,
+    private val accessibilityManagerWrapper: AccessibilityManagerWrapper,
+) {
+
+    companion object {
+        const val DEFAULT_DIALOG_TIMEOUT_MILLIS = 3500
+    }
+
+    private val timeoutMillis: Long
+        get() =
+            accessibilityManagerWrapper
+                .getRecommendedTimeoutMillis(
+                    DEFAULT_DIALOG_TIMEOUT_MILLIS,
+                    AccessibilityManager.FLAG_CONTENT_TEXT,
+                )
+                .toLong()
+
+    val eduContent: Flow<ContextualEduContentViewModel?> =
+        interactor.educationTriggered
+            .filterNotNull()
+            .map {
+                if (it.educationUiType == EducationUiType.Notification) {
+                    ContextualEduNotificationViewModel(
+                        getEduTitle(it),
+                        getEduContent(it),
+                        it.userId,
+                    )
+                } else {
+                    ContextualEduToastViewModel(getEduContent(it), it.userId)
+                }
+            }
+            .timeout(timeoutMillis, emitAfterTimeout = null)
+
+    private fun <T> Flow<T>.timeout(timeoutMillis: Long, emitAfterTimeout: T): Flow<T> {
+        return flatMapLatest {
+            flow {
+                emit(it)
+                delay(timeoutMillis)
+                emit(emitAfterTimeout)
             }
         }
+    }
 
     private fun getEduContent(educationInfo: EducationInfo): String {
         val resourceId =
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/msdl/MSDLCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/haptics/msdl/MSDLCoreStartable.kt
new file mode 100644
index 0000000..58736c60
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/haptics/msdl/MSDLCoreStartable.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.haptics.msdl
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.google.android.msdl.domain.MSDLPlayer
+import com.google.android.msdl.logging.MSDLHistoryLogger
+import java.io.PrintWriter
+import javax.inject.Inject
+
+@SysUISingleton
+class MSDLCoreStartable @Inject constructor(private val msdlPlayer: MSDLPlayer) : CoreStartable {
+    override fun start() {}
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println("MSDLPlayer history of the last ${MSDLHistoryLogger.HISTORY_SIZE} events:")
+        msdlPlayer.getHistory().forEach { event -> pw.println("$event") }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialMetricsLogger.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialMetricsLogger.kt
new file mode 100644
index 0000000..144c5ead
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialMetricsLogger.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.tutorial
+
+import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_ENTRY_POINT_CONTEXTUAL_EDU
+import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_ENTRY_POINT_SCHEDULER
+import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEYBOARD
+import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_TOUCHPAD
+import com.android.systemui.shared.system.SysUiStatsLog
+import javax.inject.Inject
+
+class KeyboardTouchpadTutorialMetricsLogger @Inject constructor() {
+
+    fun logPeripheralTutorialLaunched(entryPointExtra: String?, tutorialTypeExtra: String?) {
+        val entryPoint =
+            when (entryPointExtra) {
+                INTENT_TUTORIAL_ENTRY_POINT_SCHEDULER ->
+                    SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED__ENTRY_POINT__SCHEDULED
+                INTENT_TUTORIAL_ENTRY_POINT_CONTEXTUAL_EDU ->
+                    SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED__ENTRY_POINT__CONTEXTUAL_EDU
+                else -> SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED__ENTRY_POINT__APP
+            }
+
+        val tutorialType =
+            when (tutorialTypeExtra) {
+                INTENT_TUTORIAL_TYPE_KEYBOARD ->
+                    SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED__TUTORIAL_TYPE__KEYBOARD
+                INTENT_TUTORIAL_TYPE_TOUCHPAD ->
+                    SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED__TUTORIAL_TYPE__TOUCHPAD
+                else -> SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED__TUTORIAL_TYPE__BOTH
+            }
+
+        SysUiStatsLog.write(SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED, entryPoint, tutorialType)
+    }
+
+    fun logPeripheralTutorialLaunchedFromSettings() {
+        val entryPoint = SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED__ENTRY_POINT__SETTINGS
+        val tutorialType = SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED__TUTORIAL_TYPE__TOUCHPAD
+        SysUiStatsLog.write(SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED, entryPoint, tutorialType)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/TutorialNotificationCoordinator.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/TutorialNotificationCoordinator.kt
index 5d9dda3..f2afaee 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/TutorialNotificationCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/TutorialNotificationCoordinator.kt
@@ -31,6 +31,8 @@
 import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSchedulerInteractor.Companion.TAG
 import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSchedulerInteractor.TutorialType
 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity
+import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_ENTRY_POINT_KEY
+import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_ENTRY_POINT_SCHEDULER
 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_BOTH
 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEY
 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEYBOARD
@@ -48,7 +50,7 @@
     @Background private val backgroundScope: CoroutineScope,
     @Application private val context: Context,
     private val tutorialSchedulerInteractor: TutorialSchedulerInteractor,
-    private val notificationManager: NotificationManager
+    private val notificationManager: NotificationManager,
 ) {
     fun start() {
         backgroundScope.launch {
@@ -68,7 +70,7 @@
         val extras = Bundle()
         extras.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 info = getNotificationInfo(tutorialType)!!
@@ -91,7 +93,7 @@
             NotificationChannel(
                 CHANNEL_ID,
                 context.getString(com.android.internal.R.string.android_system_label),
-                NotificationManager.IMPORTANCE_DEFAULT
+                NotificationManager.IMPORTANCE_DEFAULT,
             )
         notificationManager.createNotificationChannel(channel)
     }
@@ -100,13 +102,14 @@
         val intent =
             Intent(context, KeyboardTouchpadTutorialActivity::class.java).apply {
                 putExtra(INTENT_TUTORIAL_TYPE_KEY, tutorialType)
+                putExtra(INTENT_TUTORIAL_ENTRY_POINT_KEY, INTENT_TUTORIAL_ENTRY_POINT_SCHEDULER)
                 flags = Intent.FLAG_ACTIVITY_NEW_TASK
             }
         return PendingIntent.getActivity(
             context,
             /* requestCode= */ 0,
             intent,
-            PendingIntent.FLAG_IMMUTABLE
+            PendingIntent.FLAG_IMMUTABLE,
         )
     }
 
@@ -118,13 +121,13 @@
                 NotificationInfo(
                     context.getString(R.string.launch_keyboard_tutorial_notification_title),
                     context.getString(R.string.launch_keyboard_tutorial_notification_content),
-                    INTENT_TUTORIAL_TYPE_KEYBOARD
+                    INTENT_TUTORIAL_TYPE_KEYBOARD,
                 )
             TutorialType.TOUCHPAD ->
                 NotificationInfo(
                     context.getString(R.string.launch_touchpad_tutorial_notification_title),
                     context.getString(R.string.launch_touchpad_tutorial_notification_content),
-                    INTENT_TUTORIAL_TYPE_TOUCHPAD
+                    INTENT_TUTORIAL_TYPE_TOUCHPAD,
                 )
             TutorialType.BOTH ->
                 NotificationInfo(
@@ -134,7 +137,7 @@
                     context.getString(
                         R.string.launch_keyboard_touchpad_tutorial_notification_content
                     ),
-                    INTENT_TUTORIAL_TYPE_BOTH
+                    INTENT_TUTORIAL_TYPE_BOTH,
                 )
             TutorialType.NONE -> null
         }
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt
index c130c6c..29febd3 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt
@@ -30,6 +30,7 @@
 import com.android.compose.theme.PlatformTheme
 import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger
 import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger.TutorialContext
+import com.android.systemui.inputdevice.tutorial.KeyboardTouchpadTutorialMetricsLogger
 import com.android.systemui.inputdevice.tutorial.TouchpadTutorialScreensProvider
 import com.android.systemui.inputdevice.tutorial.ui.composable.ActionKeyTutorialScreen
 import com.android.systemui.inputdevice.tutorial.ui.viewmodel.KeyboardTouchpadTutorialViewModel
@@ -51,6 +52,7 @@
     private val viewModelFactoryAssistedProvider: ViewModelFactoryAssistedProvider,
     private val touchpadTutorialScreensProvider: Optional<TouchpadTutorialScreensProvider>,
     private val logger: InputDeviceTutorialLogger,
+    private val metricsLogger: KeyboardTouchpadTutorialMetricsLogger,
 ) : ComponentActivity() {
 
     companion object {
@@ -58,6 +60,9 @@
         const val INTENT_TUTORIAL_TYPE_TOUCHPAD = "touchpad"
         const val INTENT_TUTORIAL_TYPE_KEYBOARD = "keyboard"
         const val INTENT_TUTORIAL_TYPE_BOTH = "both"
+        const val INTENT_TUTORIAL_ENTRY_POINT_KEY = "entry_point"
+        const val INTENT_TUTORIAL_ENTRY_POINT_SCHEDULER = "scheduler"
+        const val INTENT_TUTORIAL_ENTRY_POINT_CONTEXTUAL_EDU = "contextual_edu"
     }
 
     private val vm by
@@ -86,6 +91,10 @@
             PlatformTheme { KeyboardTouchpadTutorialContainer(vm, touchpadTutorialScreensProvider) }
         }
         if (savedInstanceState == null) {
+            metricsLogger.logPeripheralTutorialLaunched(
+                intent.getStringExtra(INTENT_TUTORIAL_ENTRY_POINT_KEY),
+                intent.getStringExtra(INTENT_TUTORIAL_TYPE_KEY),
+            )
             logger.logOpenTutorial(TutorialContext.KEYBOARD_TOUCHPAD_TUTORIAL)
         }
     }
@@ -109,7 +118,7 @@
         ACTION_KEY ->
             ActionKeyTutorialScreen(
                 onDoneButtonClicked = vm::onDoneButtonClicked,
-                onBack = vm::onBack
+                onBack = vm::onBack,
             )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
index b9a16c4..52263ce 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
@@ -18,6 +18,7 @@
 
 import android.content.ActivityNotFoundException
 import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
 import android.content.res.Configuration
 import android.os.Bundle
 import android.provider.Settings
@@ -125,7 +126,7 @@
     private fun onKeyboardSettingsClicked() {
         try {
             startActivityAsUser(
-                Intent(Settings.ACTION_HARD_KEYBOARD_SETTINGS),
+                Intent(Settings.ACTION_HARD_KEYBOARD_SETTINGS).addFlags(FLAG_ACTIVITY_NEW_TASK),
                 userTracker.userHandle,
             )
         } catch (e: ActivityNotFoundException) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 0a38ce0..9c7cc81 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -147,6 +147,7 @@
 import com.android.systemui.flags.SystemPropertiesHelper;
 import com.android.systemui.keyguard.dagger.KeyguardModule;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionBootInteractor;
 import com.android.systemui.keyguard.shared.model.TransitionStep;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.navigationbar.NavigationModeController;
@@ -265,6 +266,7 @@
     private static final int NOTIFY_STARTED_GOING_TO_SLEEP = 17;
     private static final int SYSTEM_READY = 18;
     private static final int CANCEL_KEYGUARD_EXIT_ANIM = 19;
+    private static final int BOOT_INTERACTOR = 20;
 
     /** Enum for reasons behind updating wakeAndUnlock state. */
     @Retention(RetentionPolicy.SOURCE)
@@ -1390,6 +1392,7 @@
     private final DozeParameters mDozeParameters;
     private final SelectedUserInteractor mSelectedUserInteractor;
     private final KeyguardInteractor mKeyguardInteractor;
+    private final KeyguardTransitionBootInteractor mTransitionBootInteractor;
     @VisibleForTesting
     protected FoldGracePeriodProvider mFoldGracePeriodProvider =
             new FoldGracePeriodProvider();
@@ -1484,6 +1487,7 @@
             Lazy<WindowManagerLockscreenVisibilityManager> wmLockscreenVisibilityManager,
             SelectedUserInteractor selectedUserInteractor,
             KeyguardInteractor keyguardInteractor,
+            KeyguardTransitionBootInteractor transitionBootInteractor,
             WindowManagerOcclusionManager wmOcclusionManager) {
         mContext = context;
         mUserTracker = userTracker;
@@ -1524,6 +1528,7 @@
         mDozeParameters = dozeParameters;
         mSelectedUserInteractor = selectedUserInteractor;
         mKeyguardInteractor = keyguardInteractor;
+        mTransitionBootInteractor = transitionBootInteractor;
 
         mStatusBarStateController = statusBarStateController;
         statusBarStateController.addCallback(this);
@@ -1678,6 +1683,8 @@
             adjustStatusBarLocked();
             mDreamOverlayStateController.addCallback(mDreamOverlayStateCallback);
 
+            mHandler.obtainMessage(BOOT_INTERACTOR).sendToTarget();
+
             final DreamViewModel dreamViewModel = mDreamViewModel.get();
             final CommunalTransitionViewModel communalViewModel =
                     mCommunalTransitionViewModel.get();
@@ -2705,11 +2712,19 @@
                     message = "SYSTEM_READY";
                     handleSystemReady();
                     break;
+                case BOOT_INTERACTOR:
+                    message = "BOOT_INTERACTOR";
+                    handleBootInteractor();
+                    break;
             }
             Log.d(TAG, "KeyguardViewMediator queue processing message: " + message);
         }
     };
 
+    private void handleBootInteractor() {
+        mTransitionBootInteractor.start();
+    }
+
     private void tryKeyguardDone() {
         if (DEBUG) {
             Log.d(TAG, "tryKeyguardDone: pending - " + mKeyguardDonePending + ", animRan - "
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 8a3d017..d0a40ec 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -59,6 +59,7 @@
 import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthModule;
 import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionBootInteractor;
 import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule;
 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger;
 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLoggerImpl;
@@ -175,6 +176,7 @@
             Lazy<WindowManagerLockscreenVisibilityManager> wmLockscreenVisibilityManager,
             SelectedUserInteractor selectedUserInteractor,
             KeyguardInteractor keyguardInteractor,
+            KeyguardTransitionBootInteractor transitionBootInteractor,
             WindowManagerOcclusionManager windowManagerOcclusionManager) {
         return new KeyguardViewMediator(
                 context,
@@ -225,6 +227,7 @@
                 wmLockscreenVisibilityManager,
                 selectedUserInteractor,
                 keyguardInteractor,
+                transitionBootInteractor,
                 windowManagerOcclusionManager);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index 797a4ec..690ae71 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -23,6 +23,7 @@
 import android.annotation.SuppressLint
 import android.os.Trace
 import android.util.Log
+import com.android.app.animation.Interpolators
 import com.android.app.tracing.coroutines.withContext
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
@@ -95,7 +96,7 @@
      * Emits STARTED and FINISHED transition steps to the given state. This is used during boot to
      * seed the repository with the appropriate initial state.
      */
-    suspend fun emitInitialStepsFromOff(to: KeyguardState)
+    suspend fun emitInitialStepsFromOff(to: KeyguardState, testSetup: Boolean = false)
 
     /**
      * Allows manual control of a transition. When calling [startTransition], the consumer must pass
@@ -108,16 +109,14 @@
     suspend fun updateTransition(
         transitionId: UUID,
         @FloatRange(from = 0.0, to = 1.0) value: Float,
-        state: TransitionState
+        state: TransitionState,
     )
 }
 
 @SysUISingleton
 class KeyguardTransitionRepositoryImpl
 @Inject
-constructor(
-    @Main val mainDispatcher: CoroutineDispatcher,
-) : KeyguardTransitionRepository {
+constructor(@Main val mainDispatcher: CoroutineDispatcher) : KeyguardTransitionRepository {
     /**
      * Each transition between [KeyguardState]s will have an associated Flow. In order to collect
      * these events, clients should call [transition].
@@ -140,7 +139,7 @@
                 ownerName = "",
                 from = KeyguardState.OFF,
                 to = KeyguardState.OFF,
-                animator = null
+                animator = null,
             )
         )
     override var currentTransitionInfoInternal = _currentTransitionInfo.asStateFlow()
@@ -159,12 +158,7 @@
         // to either GONE or LOCKSCREEN once we're booted up and can determine which state we should
         // start in.
         emitTransition(
-            TransitionStep(
-                KeyguardState.OFF,
-                KeyguardState.OFF,
-                1f,
-                TransitionState.FINISHED,
-            )
+            TransitionStep(KeyguardState.OFF, KeyguardState.OFF, 1f, TransitionState.FINISHED)
         )
     }
 
@@ -217,7 +211,7 @@
                         TransitionStep(
                             info,
                             (animation.animatedValue as Float),
-                            TransitionState.RUNNING
+                            TransitionState.RUNNING,
                         )
                     )
                 }
@@ -266,7 +260,7 @@
     override suspend fun updateTransition(
         transitionId: UUID,
         @FloatRange(from = 0.0, to = 1.0) value: Float,
-        state: TransitionState
+        state: TransitionState,
     ) {
         // There is no fairness guarantee with 'withContext', which means that transitions could
         // be processed out of order. Use a Mutex to guarantee ordering. [startTransition]
@@ -282,7 +276,7 @@
     private suspend fun updateTransitionInternal(
         transitionId: UUID,
         @FloatRange(from = 0.0, to = 1.0) value: Float,
-        state: TransitionState
+        state: TransitionState,
     ) {
         if (updateTransitionId != transitionId) {
             Log.e(TAG, "Attempting to update with old/invalid transitionId: $transitionId")
@@ -303,34 +297,51 @@
         lastStep = nextStep
     }
 
-    override suspend fun emitInitialStepsFromOff(to: KeyguardState) {
-        _currentTransitionInfo.value =
-            TransitionInfo(
-                ownerName = "KeyguardTransitionRepository(boot)",
-                from = KeyguardState.OFF,
-                to = to,
-                animator = null
+    override suspend fun emitInitialStepsFromOff(to: KeyguardState, testSetup: Boolean) {
+        val ownerName = "KeyguardTransitionRepository(boot)"
+        // Tests runs on testDispatcher, which is not the main thread, causing the animator thread
+        // check to fail
+        if (testSetup) {
+            _currentTransitionInfo.value =
+                TransitionInfo(
+                    ownerName = ownerName,
+                    from = KeyguardState.OFF,
+                    to = to,
+                    animator = null,
+                )
+            emitTransition(
+                TransitionStep(
+                    KeyguardState.OFF,
+                    to,
+                    0f,
+                    TransitionState.STARTED,
+                    ownerName = ownerName,
+                )
             )
 
-        emitTransition(
-            TransitionStep(
-                KeyguardState.OFF,
-                to,
-                0f,
-                TransitionState.STARTED,
-                ownerName = "KeyguardTransitionRepository(boot)",
+            emitTransition(
+                TransitionStep(
+                    KeyguardState.OFF,
+                    to,
+                    1f,
+                    TransitionState.FINISHED,
+                    ownerName = ownerName,
+                )
             )
-        )
-
-        emitTransition(
-            TransitionStep(
-                KeyguardState.OFF,
-                to,
-                1f,
-                TransitionState.FINISHED,
-                ownerName = "KeyguardTransitionRepository(boot)",
-            ),
-        )
+        } else {
+            startTransition(
+                TransitionInfo(
+                    ownerName = ownerName,
+                    from = KeyguardState.OFF,
+                    to = to,
+                    animator =
+                        ValueAnimator().apply {
+                            interpolator = Interpolators.LINEAR
+                            duration = 933L
+                        },
+                )
+            )
+        }
     }
 
     private fun logAndTrace(step: TransitionStep, isManual: Boolean) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
index 0343786..840bc0f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
@@ -106,7 +106,7 @@
                         startTransitionToLockscreenOrHub(
                             isIdleOnCommunal,
                             showCommunalFromOccluded,
-                            dreamFromOccluded
+                            dreamFromOccluded,
                         )
                     }
             }
@@ -127,7 +127,7 @@
                         startTransitionToLockscreenOrHub(
                             isIdleOnCommunal,
                             showCommunalFromOccluded,
-                            dreamFromOccluded
+                            dreamFromOccluded,
                         )
                     }
             }
@@ -147,7 +147,7 @@
                 communalSceneInteractor.changeScene(
                     newScene = CommunalScenes.Communal,
                     loggingReason = "occluded to hub",
-                    transitionKey = CommunalTransitionKeys.SimpleFade
+                    transitionKey = CommunalTransitionKeys.SimpleFade,
                 )
             } else {
                 startTransitionTo(KeyguardState.GLANCEABLE_HUB)
@@ -210,8 +210,9 @@
 
             duration =
                 when (toState) {
-                    KeyguardState.LOCKSCREEN -> TO_LOCKSCREEN_DURATION
+                    KeyguardState.ALTERNATE_BOUNCER -> TO_ALTERNATE_BOUNCER_DURATION
                     KeyguardState.GLANCEABLE_HUB -> TO_GLANCEABLE_HUB_DURATION
+                    KeyguardState.LOCKSCREEN -> TO_LOCKSCREEN_DURATION
                     else -> DEFAULT_DURATION
                 }.inWholeMilliseconds
         }
@@ -220,9 +221,10 @@
     companion object {
         const val TAG = "FromOccludedTransitionInteractor"
         private val DEFAULT_DURATION = 500.milliseconds
-        val TO_LOCKSCREEN_DURATION = 933.milliseconds
-        val TO_GLANCEABLE_HUB_DURATION = 250.milliseconds
+        val TO_ALTERNATE_BOUNCER_DURATION = DEFAULT_DURATION
         val TO_AOD_DURATION = DEFAULT_DURATION
         val TO_DOZING_DURATION = DEFAULT_DURATION
+        val TO_GLANCEABLE_HUB_DURATION = 250.milliseconds
+        val TO_LOCKSCREEN_DURATION = 933.milliseconds
     }
 }
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 b218300..89f636d 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
@@ -17,7 +17,6 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.util.Log
-import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
@@ -46,7 +45,7 @@
     val keyguardTransitionInteractor: KeyguardTransitionInteractor,
     val internalTransitionInteractor: InternalKeyguardTransitionInteractor,
     val repository: KeyguardTransitionRepository,
-) : CoreStartable {
+) {
 
     /**
      * Whether the lockscreen should be showing when the device starts up for the first time. If not
@@ -60,14 +59,14 @@
         }
     }
 
-    override fun start() {
+    fun start() {
         scope.launch {
             if (internalTransitionInteractor.currentTransitionInfoInternal.value.from != OFF) {
                 Log.e(
                     "KeyguardTransitionInteractor",
                     "showLockscreenOnBoot emitted, but we've already " +
                         "transitioned to a state other than OFF. We'll respect that " +
-                        "transition, but this should not happen."
+                        "transition, but this should not happen.",
                 )
             } else {
                 if (SceneContainerFlag.isEnabled) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
index 25b8fd3..b715333 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
@@ -27,7 +27,6 @@
 constructor(
     private val interactors: Set<TransitionInteractor>,
     private val auditLogger: KeyguardTransitionAuditLogger,
-    private val bootInteractor: KeyguardTransitionBootInteractor,
     private val statusBarDisableFlagsInteractor: StatusBarDisableFlagsInteractor,
     private val keyguardStateCallbackInteractor: KeyguardStateCallbackInteractor,
 ) : CoreStartable {
@@ -54,7 +53,6 @@
             it.start()
         }
         auditLogger.start()
-        bootInteractor.start()
         statusBarDisableFlagsInteractor.start()
         keyguardStateCallbackInteractor.start()
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
index f1b9cba..00aa44f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
@@ -47,56 +47,53 @@
         constraintLayout.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.CREATED) {
                 launch("$TAG#viewModel.blueprint") {
-                    viewModel.blueprint
-                        .pairwise(
-                            null as KeyguardBlueprint?,
-                        )
-                        .collect { (prevBlueprint, blueprint) ->
-                            val config = Config.DEFAULT
-                            val transition =
-                                if (
-                                    !KeyguardBottomAreaRefactor.isEnabled &&
-                                        prevBlueprint != null &&
-                                        prevBlueprint != blueprint
-                                ) {
-                                    BaseBlueprintTransition(clockViewModel)
-                                        .addTransition(
-                                            IntraBlueprintTransition(
-                                                config,
-                                                clockViewModel,
-                                                smartspaceViewModel
-                                            )
+                    viewModel.blueprint.pairwise(null as KeyguardBlueprint?).collect {
+                        (prevBlueprint, blueprint) ->
+                        val config = Config.DEFAULT
+                        val transition =
+                            if (
+                                !KeyguardBottomAreaRefactor.isEnabled &&
+                                    prevBlueprint != null &&
+                                    prevBlueprint != blueprint
+                            ) {
+                                BaseBlueprintTransition(clockViewModel)
+                                    .addTransition(
+                                        IntraBlueprintTransition(
+                                            config,
+                                            clockViewModel,
+                                            smartspaceViewModel,
                                         )
-                                } else {
-                                    IntraBlueprintTransition(
-                                        config,
-                                        clockViewModel,
-                                        smartspaceViewModel
                                     )
+                            } else {
+                                IntraBlueprintTransition(
+                                    config,
+                                    clockViewModel,
+                                    smartspaceViewModel,
+                                )
+                            }
+
+                        viewModel.runTransition(constraintLayout, transition, config) {
+                            // Replace sections from the previous blueprint with the new ones
+                            blueprint.replaceViews(
+                                constraintLayout,
+                                prevBlueprint,
+                                config.rebuildSections,
+                            )
+
+                            val cs =
+                                ConstraintSet().apply {
+                                    clone(constraintLayout)
+                                    val emptyLayout = ConstraintSet.Layout()
+                                    knownIds.forEach {
+                                        getConstraint(it).layout.copyFrom(emptyLayout)
+                                    }
+                                    blueprint.applyConstraints(this)
                                 }
 
-                            viewModel.runTransition(constraintLayout, transition, config) {
-                                // Replace sections from the previous blueprint with the new ones
-                                blueprint.replaceViews(
-                                    constraintLayout,
-                                    prevBlueprint,
-                                    config.rebuildSections
-                                )
-
-                                val cs =
-                                    ConstraintSet().apply {
-                                        clone(constraintLayout)
-                                        val emptyLayout = ConstraintSet.Layout()
-                                        knownIds.forEach {
-                                            getConstraint(it).layout.copyFrom(emptyLayout)
-                                        }
-                                        blueprint.applyConstraints(this)
-                                    }
-
-                                logAlphaVisibilityScaleOfAppliedConstraintSet(cs, clockViewModel)
-                                cs.applyTo(constraintLayout)
-                            }
+                            logAlphaVisibilityScaleOfAppliedConstraintSet(cs, clockViewModel)
+                            cs.applyTo(constraintLayout)
                         }
+                    }
                 }
 
                 launch("$TAG#viewModel.refreshTransition") {
@@ -105,7 +102,8 @@
 
                         viewModel.runTransition(
                             constraintLayout,
-                            IntraBlueprintTransition(config, clockViewModel, smartspaceViewModel),
+                            clockViewModel,
+                            smartspaceViewModel,
                             config,
                         ) {
                             blueprint.rebuildViews(constraintLayout, config.rebuildSections)
@@ -126,7 +124,7 @@
 
     private fun logAlphaVisibilityScaleOfAppliedConstraintSet(
         cs: ConstraintSet,
-        viewModel: KeyguardClockViewModel
+        viewModel: KeyguardClockViewModel,
     ) {
         val currentClock = viewModel.currentClock.value
         if (!DEBUG || currentClock == null) return
@@ -137,19 +135,19 @@
             TAG,
             "applyCsToSmallClock: vis=${cs.getVisibility(smallClockViewId)} " +
                 "alpha=${cs.getConstraint(smallClockViewId).propertySet.alpha} " +
-                "scale=${cs.getConstraint(smallClockViewId).transform.scaleX} "
+                "scale=${cs.getConstraint(smallClockViewId).transform.scaleX} ",
         )
         Log.i(
             TAG,
             "applyCsToLargeClock: vis=${cs.getVisibility(largeClockViewId)} " +
                 "alpha=${cs.getConstraint(largeClockViewId).propertySet.alpha} " +
                 "scale=${cs.getConstraint(largeClockViewId).transform.scaleX} " +
-                "pivotX=${cs.getConstraint(largeClockViewId).transform.transformPivotX} "
+                "pivotX=${cs.getConstraint(largeClockViewId).transform.transformPivotX} ",
         )
         Log.i(
             TAG,
             "applyCsToSmartspaceDate: vis=${cs.getVisibility(smartspaceDateId)} " +
-                "alpha=${cs.getConstraint(smartspaceDateId).propertySet.alpha}"
+                "alpha=${cs.getConstraint(smartspaceDateId).propertySet.alpha}",
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
index aa0a9d9..9a55f7b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
@@ -29,18 +29,18 @@
     smartspaceViewModel: KeyguardSmartspaceViewModel,
 ) : TransitionSet() {
 
-    enum class Type(
-        val priority: Int,
-        val animateNotifChanges: Boolean,
-    ) {
+    enum class Type(val priority: Int, val animateNotifChanges: Boolean) {
         ClockSize(100, true),
         ClockCenter(99, false),
         DefaultClockStepping(98, false),
-        SmartspaceVisibility(2, true),
-        DefaultTransition(1, false),
+        SmartspaceVisibility(3, true),
+        DefaultTransition(2, false),
         // When transition between blueprint, we don't need any duration or interpolator but we need
         // all elements go to correct state
-        NoTransition(0, false),
+        NoTransition(1, false),
+        // Similar to NoTransition, except also does not explicitly update any alpha. Used in
+        // OFF->LOCKSCREEN transition
+        Init(0, false),
     }
 
     data class Config(
@@ -57,6 +57,7 @@
     init {
         ordering = ORDERING_TOGETHER
         when (config.type) {
+            Type.Init -> {}
             Type.NoTransition -> {}
             Type.DefaultClockStepping ->
                 addTransition(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
index ff84826..a1c963b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
@@ -53,14 +53,11 @@
 internal fun ConstraintSet.setVisibility(views: Iterable<View>, visibility: Int) =
     views.forEach { view -> this.setVisibility(view.id, visibility) }
 
-internal fun ConstraintSet.setAlpha(views: Iterable<View>, alpha: Float) =
-    views.forEach { view -> this.setAlpha(view.id, alpha) }
+internal fun ConstraintSet.setScaleX(views: Iterable<View>, scaleX: Float) =
+    views.forEach { view -> this.setScaleX(view.id, scaleX) }
 
-internal fun ConstraintSet.setScaleX(views: Iterable<View>, alpha: Float) =
-    views.forEach { view -> this.setScaleX(view.id, alpha) }
-
-internal fun ConstraintSet.setScaleY(views: Iterable<View>, alpha: Float) =
-    views.forEach { view -> this.setScaleY(view.id, alpha) }
+internal fun ConstraintSet.setScaleY(views: Iterable<View>, scaleY: Float) =
+    views.forEach { view -> this.setScaleY(view.id, scaleY) }
 
 @SysUISingleton
 class ClockSection
@@ -126,8 +123,6 @@
         return constraintSet.apply {
             setVisibility(getTargetClockFace(clock).views, VISIBLE)
             setVisibility(getNonTargetClockFace(clock).views, GONE)
-            setAlpha(getTargetClockFace(clock).views, 1F)
-            setAlpha(getNonTargetClockFace(clock).views, 0F)
             if (!keyguardClockViewModel.isLargeClockVisible.value) {
                 connect(sharedR.id.bc_smartspace_view, TOP, sharedR.id.date_smartspace_view, BOTTOM)
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToOccludedTransitionViewModel.kt
index 3f2ef29..c49e783 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToOccludedTransitionViewModel.kt
@@ -28,22 +28,22 @@
 import kotlinx.coroutines.flow.Flow
 
 /**
- * Breaks down ALTERNATE_BOUNCER->GONE transition into discrete steps for corresponding views to
+ * Breaks down ALTERNATE_BOUNCER->OCCLUDED transition into discrete steps for corresponding views to
  * consume.
  */
 @OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class AlternateBouncerToOccludedTransitionViewModel
 @Inject
-constructor(
-    animationFlow: KeyguardTransitionAnimationFlow,
-) : DeviceEntryIconTransition {
+constructor(animationFlow: KeyguardTransitionAnimationFlow) : DeviceEntryIconTransition {
     private val transitionAnimation =
         animationFlow.setup(
             duration = TO_OCCLUDED_DURATION,
             edge = Edge.create(from = ALTERNATE_BOUNCER, to = OCCLUDED),
         )
 
+    val lockscreenAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f)
+
     override val deviceEntryParentViewAlpha: Flow<Float> =
         transitionAnimation.immediatelyTransitionTo(0f)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
index a021de4..ca1a800 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
@@ -56,6 +56,7 @@
     occludedToAodTransitionViewModel: OccludedToAodTransitionViewModel,
     occludedToDozingTransitionViewModel: OccludedToDozingTransitionViewModel,
     occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
+    offToLockscreenTransitionViewModel: OffToLockscreenTransitionViewModel,
     primaryBouncerToAodTransitionViewModel: PrimaryBouncerToAodTransitionViewModel,
     primaryBouncerToDozingTransitionViewModel: PrimaryBouncerToDozingTransitionViewModel,
     primaryBouncerToLockscreenTransitionViewModel: PrimaryBouncerToLockscreenTransitionViewModel,
@@ -67,14 +68,14 @@
                     .map {
                         Utils.getColorAttrDefaultColor(
                             context,
-                            com.android.internal.R.attr.colorSurface
+                            com.android.internal.R.attr.colorSurface,
                         )
                     }
                     .onStart {
                         emit(
                             Utils.getColorAttrDefaultColor(
                                 context,
-                                com.android.internal.R.attr.colorSurface
+                                com.android.internal.R.attr.colorSurface,
                             )
                         )
                     }
@@ -86,23 +87,23 @@
         deviceEntryIconViewModel.useBackgroundProtection.flatMapLatest { useBackground ->
             if (useBackground) {
                 setOf(
-                        lockscreenToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        aodToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        goneToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        primaryBouncerToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        occludedToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        occludedToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        dreamingToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
                         alternateBouncerToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        goneToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        goneToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        primaryBouncerToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        dozingToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
                         alternateBouncerToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        aodToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        dozingToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
                         dreamingToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
-                        primaryBouncerToLockscreenTransitionViewModel
-                            .deviceEntryBackgroundViewAlpha,
+                        dreamingToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        goneToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        goneToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        goneToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        lockscreenToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        occludedToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
                         occludedToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        occludedToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        offToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        primaryBouncerToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        primaryBouncerToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha,
+                        primaryBouncerToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
                     )
                     .merge()
                     .onStart {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
index 4cf3c4e..1289036 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
@@ -24,6 +24,9 @@
 import androidx.constraintlayout.widget.ConstraintLayout
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type
 import javax.inject.Inject
@@ -37,6 +40,7 @@
 constructor(
     @Main private val handler: Handler,
     private val keyguardBlueprintInteractor: KeyguardBlueprintInteractor,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
 ) {
     val blueprint = keyguardBlueprintInteractor.blueprint
     val blueprintId = keyguardBlueprintInteractor.blueprintId
@@ -49,12 +53,12 @@
     private val transitionListener =
         object : Transition.TransitionListener {
             override fun onTransitionCancel(transition: Transition) {
-                if (DEBUG) Log.e(TAG, "onTransitionCancel: ${transition::class.simpleName}")
+                if (DEBUG) Log.w(TAG, "onTransitionCancel: ${transition::class.simpleName}")
                 updateTransitions(null) { remove(transition) }
             }
 
             override fun onTransitionEnd(transition: Transition) {
-                if (DEBUG) Log.e(TAG, "onTransitionEnd: ${transition::class.simpleName}")
+                if (DEBUG) Log.i(TAG, "onTransitionEnd: ${transition::class.simpleName}")
                 updateTransitions(null) { remove(transition) }
             }
 
@@ -86,6 +90,28 @@
 
     fun runTransition(
         constraintLayout: ConstraintLayout,
+        clockViewModel: KeyguardClockViewModel,
+        smartspaceViewModel: KeyguardSmartspaceViewModel,
+        config: Config,
+        apply: () -> Unit,
+    ) {
+        val newConfig =
+            if (keyguardTransitionInteractor.getCurrentState() == KeyguardState.OFF) {
+                config.copy(type = Type.Init)
+            } else {
+                config
+            }
+
+        runTransition(
+            constraintLayout,
+            IntraBlueprintTransition(newConfig, clockViewModel, smartspaceViewModel),
+            config,
+            apply,
+        )
+    }
+
+    fun runTransition(
+        constraintLayout: ConstraintLayout,
         transition: Transition,
         config: Config,
         apply: () -> Unit,
@@ -103,21 +129,29 @@
             return
         }
 
+        // Don't allow transitions with animations while in OFF state
+        val newConfig =
+            if (keyguardTransitionInteractor.getCurrentState() == KeyguardState.OFF) {
+                config.copy(type = Type.Init)
+            } else {
+                config
+            }
+
         if (DEBUG) {
             Log.i(
                 TAG,
                 "runTransition: running ${transition::class.simpleName}: " +
-                    "currentPriority=$currentPriority; config=$config",
+                    "currentPriority=$currentPriority; config=$newConfig",
             )
         }
 
         // beginDelayedTransition makes a copy, so we temporarially add the uncopied transition to
         // the running set until the copy is started by the handler.
-        updateTransitions(TransitionData(config)) { add(transition) }
+        updateTransitions(TransitionData(newConfig)) { add(transition) }
         transition.addListener(transitionListener)
 
         handler.post {
-            if (config.terminatePrevious) {
+            if (newConfig.terminatePrevious) {
                 TransitionManager.endTransitions(constraintLayout)
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 10a2e5c..3705c2c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -20,7 +20,6 @@
 import android.graphics.Point
 import android.util.MathUtils
 import android.view.View.VISIBLE
-import com.android.app.tracing.coroutines.launch
 import com.android.systemui.common.shared.model.NotificationContainerBounds
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.dagger.SysUISingleton
@@ -35,6 +34,7 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
 import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
+import com.android.systemui.keyguard.shared.model.KeyguardState.OFF
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
 import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
@@ -88,6 +88,8 @@
         AlternateBouncerToGoneTransitionViewModel,
     private val alternateBouncerToLockscreenTransitionViewModel:
         AlternateBouncerToLockscreenTransitionViewModel,
+    private val alternateBouncerToOccludedTransitionViewModel:
+        AlternateBouncerToOccludedTransitionViewModel,
     private val aodToGoneTransitionViewModel: AodToGoneTransitionViewModel,
     private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
     private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel,
@@ -112,9 +114,12 @@
     private val lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel,
     private val lockscreenToPrimaryBouncerTransitionViewModel:
         LockscreenToPrimaryBouncerTransitionViewModel,
+    private val occludedToAlternateBouncerTransitionViewModel:
+        OccludedToAlternateBouncerTransitionViewModel,
     private val occludedToAodTransitionViewModel: OccludedToAodTransitionViewModel,
     private val occludedToDozingTransitionViewModel: OccludedToDozingTransitionViewModel,
     private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
+    private val offToLockscreenTransitionViewModel: OffToLockscreenTransitionViewModel,
     private val primaryBouncerToAodTransitionViewModel: PrimaryBouncerToAodTransitionViewModel,
     private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
     private val primaryBouncerToLockscreenTransitionViewModel:
@@ -201,6 +206,10 @@
             notificationShadeWindowModel.isKeyguardOccluded,
             communalInteractor.isIdleOnCommunal,
             keyguardTransitionInteractor
+                .transitionValue(OFF)
+                .map { it > 1f - offToLockscreenTransitionViewModel.alphaStartAt }
+                .onStart { emit(false) },
+            keyguardTransitionInteractor
                 .transitionValue(scene = Scenes.Gone, stateWithoutSceneContainer = GONE)
                 .map { it == 1f }
                 .onStart { emit(false) },
@@ -227,6 +236,7 @@
                         alternateBouncerToAodTransitionViewModel.lockscreenAlpha(viewState),
                         alternateBouncerToGoneTransitionViewModel.lockscreenAlpha(viewState),
                         alternateBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
+                        alternateBouncerToOccludedTransitionViewModel.lockscreenAlpha,
                         aodToGoneTransitionViewModel.lockscreenAlpha(viewState),
                         aodToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
                         aodToOccludedTransitionViewModel.lockscreenAlpha(viewState),
@@ -249,14 +259,16 @@
                         lockscreenToGoneTransitionViewModel.lockscreenAlpha(viewState),
                         lockscreenToOccludedTransitionViewModel.lockscreenAlpha,
                         lockscreenToPrimaryBouncerTransitionViewModel.lockscreenAlpha,
+                        occludedToAlternateBouncerTransitionViewModel.lockscreenAlpha,
                         occludedToAodTransitionViewModel.lockscreenAlpha,
                         occludedToDozingTransitionViewModel.lockscreenAlpha,
                         occludedToLockscreenTransitionViewModel.lockscreenAlpha,
+                        offToLockscreenTransitionViewModel.lockscreenAlpha,
                         primaryBouncerToAodTransitionViewModel.lockscreenAlpha,
                         primaryBouncerToGoneTransitionViewModel.lockscreenAlpha,
                         primaryBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
                     )
-                    .onStart { emit(1f) },
+                    .onStart { emit(0f) },
             ) { hideKeyguard, alpha ->
                 if (hideKeyguard) {
                     0f
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
index 8d9ccef..88e8968 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt
@@ -52,18 +52,26 @@
 
     /** Lockscreen views alpha */
     val lockscreenAlpha: Flow<Float> =
-        transitionAnimation.sharedFlow(
-            duration = 250.milliseconds,
-            onStep = { 1f - it },
-            name = "LOCKSCREEN->OCCLUDED: lockscreenAlpha",
+        shadeDependentFlows.transitionFlow(
+            flowWhenShadeIsNotExpanded =
+                transitionAnimation.sharedFlow(
+                    duration = 250.milliseconds,
+                    onStep = { 1f - it },
+                    name = "LOCKSCREEN->OCCLUDED: lockscreenAlpha",
+                ),
+            flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(0f),
         )
 
     val shortcutsAlpha: Flow<Float> =
-        transitionAnimation.sharedFlow(
-            duration = 250.milliseconds,
-            onStep = { 1 - it },
-            onFinish = { 0f },
-            onCancel = { 1f },
+        shadeDependentFlows.transitionFlow(
+            flowWhenShadeIsNotExpanded =
+                transitionAnimation.sharedFlow(
+                    duration = 250.milliseconds,
+                    onStep = { 1f - it },
+                    onFinish = { 0f },
+                    onCancel = { 1f },
+                ),
+            flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(0f),
         )
 
     /** Lockscreen views y-translation */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAlternateBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAlternateBouncerTransitionViewModel.kt
new file mode 100644
index 0000000..5bfcccb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAlternateBouncerTransitionViewModel.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_ALTERNATE_BOUNCER_DURATION
+import com.android.systemui.keyguard.shared.model.Edge
+import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
+import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Breaks down OCCLUDED->ALTERNATE_BOUNCER transition into discrete steps for corresponding views to
+ * consume.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class OccludedToAlternateBouncerTransitionViewModel
+@Inject
+constructor(animationFlow: KeyguardTransitionAnimationFlow) : DeviceEntryIconTransition {
+    private val transitionAnimation =
+        animationFlow.setup(
+            duration = TO_ALTERNATE_BOUNCER_DURATION,
+            edge = Edge.create(from = OCCLUDED, to = ALTERNATE_BOUNCER),
+        )
+
+    val lockscreenAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f)
+
+    override val deviceEntryParentViewAlpha: Flow<Float> =
+        transitionAnimation.immediatelyTransitionTo(0f)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt
index 1eecbd5..b4acce6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.shared.model.Edge
 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
@@ -29,23 +30,29 @@
 @SysUISingleton
 class OffToLockscreenTransitionViewModel
 @Inject
-constructor(
-    animationFlow: KeyguardTransitionAnimationFlow,
-) : DeviceEntryIconTransition {
+constructor(animationFlow: KeyguardTransitionAnimationFlow) : DeviceEntryIconTransition {
+
+    private val startTime = 300.milliseconds
+    private val alphaDuration = 633.milliseconds
+    val alphaStartAt = startTime / (alphaDuration + startTime)
 
     private val transitionAnimation =
         animationFlow.setup(
-            duration = 250.milliseconds,
+            duration = startTime + alphaDuration,
             edge = Edge.create(from = OFF, to = LOCKSCREEN),
         )
 
-    val shortcutsAlpha: Flow<Float> =
+    val lockscreenAlpha: Flow<Float> =
         transitionAnimation.sharedFlow(
-            duration = 250.milliseconds,
+            startTime = startTime,
+            duration = alphaDuration,
+            interpolator = EMPHASIZED_ACCELERATE,
             onStep = { it },
-            onCancel = { 0f },
         )
 
-    override val deviceEntryParentViewAlpha: Flow<Float> =
-        transitionAnimation.immediatelyTransitionTo(1f)
+    val shortcutsAlpha: Flow<Float> = lockscreenAlpha
+
+    override val deviceEntryParentViewAlpha: Flow<Float> = lockscreenAlpha
+
+    val deviceEntryBackgroundViewAlpha: Flow<Float> = lockscreenAlpha
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
index 84aae65..222d783 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
@@ -111,7 +111,7 @@
     arrayOf(
         MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
         MediaMetadata.METADATA_KEY_ART_URI,
-        MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
+        MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI,
     )
 
 private const val TAG = "MediaDataManager"
@@ -136,7 +136,7 @@
         active = true,
         resumeAction = null,
         instanceId = InstanceId.fakeInstanceId(-1),
-        appUid = Process.INVALID_UID
+        appUid = Process.INVALID_UID,
     )
 
 internal val EMPTY_SMARTSPACE_MEDIA_DATA =
@@ -163,7 +163,7 @@
         Settings.Secure.getInt(
             context.contentResolver,
             Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
-            1
+            1,
         )
     return Utils.useQsMediaPlayer(context) && flag > 0
 }
@@ -217,7 +217,7 @@
     private val themeText =
         com.android.settingslib.Utils.getColorAttr(
                 context,
-                com.android.internal.R.attr.textColorPrimary
+                com.android.internal.R.attr.textColorPrimary,
             )
             .defaultColor
 
@@ -387,7 +387,7 @@
                 uiExecutor,
                 SmartspaceSession.OnTargetsAvailableListener { targets ->
                     smartspaceMediaDataProvider.onTargetsAvailable(targets)
-                }
+                },
             )
         }
         smartspaceSession?.let { it.requestSmartspaceUpdate() }
@@ -398,12 +398,12 @@
                     if (!allowMediaRecommendations) {
                         dismissSmartspaceRecommendation(
                             key = smartspaceMediaData.targetId,
-                            delay = 0L
+                            delay = 0L,
                         )
                     }
                 }
             },
-            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION
+            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
         )
     }
 
@@ -461,7 +461,7 @@
         token: MediaSession.Token,
         appName: String,
         appIntent: PendingIntent,
-        packageName: String
+        packageName: String,
     ) {
         // Resume controls don't have a notification key, so store by package name instead
         if (!mediaEntries.containsKey(packageName)) {
@@ -497,7 +497,7 @@
                     token,
                     appName,
                     appIntent,
-                    packageName
+                    packageName,
                 )
             }
         } else {
@@ -509,7 +509,7 @@
                     token,
                     appName,
                     appIntent,
-                    packageName
+                    packageName,
                 )
             }
         }
@@ -609,14 +609,14 @@
                     result.appUid,
                     sbn.packageName,
                     instanceId,
-                    result.playbackLocation
+                    result.playbackLocation,
                 )
             } else if (result.playbackLocation != currentEntry?.playbackLocation) {
                 logger.logPlaybackLocationChange(
                     result.appUid,
                     sbn.packageName,
                     instanceId,
-                    result.playbackLocation
+                    result.playbackLocation,
                 )
             }
 
@@ -722,30 +722,32 @@
     /** Called when the player's [PlaybackState] has been updated with new actions and/or state */
     private fun updateState(key: String, state: PlaybackState) {
         mediaEntries.get(key)?.let {
-            val token = it.token
-            if (token == null) {
-                if (DEBUG) Log.d(TAG, "State updated, but token was null")
-                return
-            }
-            val actions =
-                createActionsFromState(
-                    it.packageName,
-                    mediaControllerFactory.create(it.token),
-                    UserHandle(it.userId)
-                )
-
-            // Control buttons
-            // If flag is enabled and controller has a PlaybackState,
-            // create actions from session info
-            // otherwise, no need to update semantic actions.
-            val data =
-                if (actions != null) {
-                    it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state))
-                } else {
-                    it.copy(isPlaying = isPlayingState(state.state))
+            backgroundExecutor.execute {
+                val token = it.token
+                if (token == null) {
+                    if (DEBUG) Log.d(TAG, "State updated, but token was null")
+                    return@execute
                 }
-            if (DEBUG) Log.d(TAG, "State updated outside of notification")
-            onMediaDataLoaded(key, key, data)
+                val actions =
+                    createActionsFromState(
+                        it.packageName,
+                        mediaControllerFactory.create(it.token),
+                        UserHandle(it.userId),
+                    )
+
+                // Control buttons
+                // If flag is enabled and controller has a PlaybackState,
+                // create actions from session info
+                // otherwise, no need to update semantic actions.
+                val data =
+                    if (actions != null) {
+                        it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state))
+                    } else {
+                        it.copy(isPlaying = isPlayingState(state.state))
+                    }
+                if (DEBUG) Log.d(TAG, "State updated outside of notification")
+                foregroundExecutor.execute { onMediaDataLoaded(key, key, data) }
+            }
         }
     }
 
@@ -773,7 +775,7 @@
         }
         foregroundExecutor.executeDelayed(
             { removeEntry(key = key, userInitiated = userInitiated) },
-            delay
+            delay,
         )
         return existed
     }
@@ -793,12 +795,12 @@
             smartspaceMediaData =
                 EMPTY_SMARTSPACE_MEDIA_DATA.copy(
                     targetId = smartspaceMediaData.targetId,
-                    instanceId = smartspaceMediaData.instanceId
+                    instanceId = smartspaceMediaData.instanceId,
                 )
         }
         foregroundExecutor.executeDelayed(
             { notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = true) },
-            delay
+            delay,
         )
     }
 
@@ -826,7 +828,7 @@
         token: MediaSession.Token,
         appName: String,
         appIntent: PendingIntent,
-        packageName: String
+        packageName: String,
     ) =
         withContext(backgroundDispatcher) {
             val lastActive = systemClock.elapsedRealtime()
@@ -843,7 +845,7 @@
                         token,
                         appName,
                         appIntent,
-                        packageName
+                        packageName,
                     )
             if (result == null || desc.title.isNullOrBlank()) {
                 Log.d(TAG, "No MediaData result for resumption")
@@ -882,7 +884,7 @@
                         appUid = result.appUid,
                         isExplicit = result.isExplicit,
                         resumeProgress = result.resumeProgress,
-                    )
+                    ),
                 )
             }
         }
@@ -895,7 +897,7 @@
         token: MediaSession.Token,
         appName: String,
         appIntent: PendingIntent,
-        packageName: String
+        packageName: String,
     ) {
         if (desc.title.isNullOrBlank()) {
             Log.e(TAG, "Description incomplete")
@@ -966,7 +968,7 @@
                     appUid = appUid,
                     isExplicit = isExplicit,
                     resumeProgress = progress,
-                )
+                ),
             )
         }
     }
@@ -981,7 +983,7 @@
         val token =
             sbn.notification.extras.getParcelable(
                 Notification.EXTRA_MEDIA_SESSION,
-                MediaSession.Token::class.java
+                MediaSession.Token::class.java,
             )
         if (token == null) {
             return
@@ -993,7 +995,7 @@
         val appInfo =
             notif.extras.getParcelable(
                 Notification.EXTRA_BUILDER_APPLICATION_INFO,
-                ApplicationInfo::class.java
+                ApplicationInfo::class.java,
             ) ?: getAppInfoFromPackage(sbn.packageName)
 
         // App name
@@ -1057,7 +1059,7 @@
             val deviceIntent =
                 extras.getParcelable(
                     Notification.EXTRA_MEDIA_REMOTE_INTENT,
-                    PendingIntent::class.java
+                    PendingIntent::class.java,
                 )
             Log.d(TAG, "$key is RCN for $deviceName")
 
@@ -1073,7 +1075,7 @@
                         deviceDrawable,
                         deviceName,
                         deviceIntent,
-                        showBroadcastButton = false
+                        showBroadcastButton = false,
                     )
             }
         }
@@ -1160,7 +1162,7 @@
                 mediaData.copy(
                     resumeAction = oldResumeAction,
                     hasCheckedForResume = oldHasCheckedForResume,
-                    active = oldActive
+                    active = oldActive,
                 )
             onMediaDataLoaded(key, oldKey, mediaData)
         }
@@ -1169,7 +1171,7 @@
     private fun logSingleVsMultipleMediaAdded(
         appUid: Int,
         packageName: String,
-        instanceId: InstanceId
+        instanceId: InstanceId,
     ) {
         if (mediaEntries.size == 1) {
             logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId)
@@ -1207,7 +1209,7 @@
     private fun createActionsFromState(
         packageName: String,
         controller: MediaController,
-        user: UserHandle
+        user: UserHandle,
     ): MediaButton? {
         if (!mediaFlags.areMediaSessionActionsEnabled(packageName, user)) {
             return null
@@ -1245,7 +1247,7 @@
                 packageName,
                 ContentProvider.getUriWithoutUserId(uri),
                 Intent.FLAG_GRANT_READ_URI_PERMISSION,
-                ContentProvider.getUserIdFromUri(uri, userId)
+                ContentProvider.getUserIdFromUri(uri, userId),
             )
             return loadBitmapFromUri(uri)
         } catch (e: SecurityException) {
@@ -1282,7 +1284,7 @@
                 val scale =
                     MediaDataUtils.getScaleFactor(
                         APair(width, height),
-                        APair(artworkWidth, artworkHeight)
+                        APair(artworkWidth, artworkHeight),
                     )
 
                 // Downscale if needed
@@ -1307,7 +1309,7 @@
                 .loadDrawable(context),
             action,
             context.getString(R.string.controls_media_resume),
-            context.getDrawable(R.drawable.ic_media_play_container)
+            context.getDrawable(R.drawable.ic_media_play_container),
         )
     }
 
@@ -1371,10 +1373,7 @@
                 // There should NOT be more than 1 Smartspace media update. When it happens, it
                 // indicates a bad state or an error. Reset the status accordingly.
                 Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...")
-                notifySmartspaceMediaDataRemoved(
-                    smartspaceMediaData.targetId,
-                    immediately = false,
-                )
+                notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false)
                 smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
             }
         }
@@ -1420,7 +1419,7 @@
     private fun handlePossibleRemoval(
         key: String,
         removed: MediaData,
-        notificationRemoved: Boolean = false
+        notificationRemoved: Boolean = false,
     ) {
         val hasSession = removed.token != null
         if (hasSession && removed.semanticActions != null) {
@@ -1445,7 +1444,7 @@
                 Log.d(
                     TAG,
                     "Notification ($notificationRemoved) and/or session " +
-                        "($hasSession) gone for inactive player $key"
+                        "($hasSession) gone for inactive player $key",
                 )
             }
             convertToResumePlayer(key, removed)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
index f2825d0..4f97913 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.media.controls.domain.pipeline
 
+import android.annotation.WorkerThread
 import android.app.ActivityOptions
 import android.app.BroadcastOptions
 import android.app.Notification
@@ -50,6 +51,7 @@
  * @return a Pair consisting of a list of media actions, and a list of ints representing which of
  *   those actions should be shown in the compact player
  */
+@WorkerThread
 fun createActionsFromState(
     context: Context,
     packageName: String,
@@ -69,7 +71,7 @@
                 context.getString(R.string.controls_media_button_connecting),
                 context.getDrawable(R.drawable.ic_media_connecting_container),
                 // Specify a rebind id to prevent the spinner from restarting on later binds.
-                com.android.internal.R.drawable.progress_small_material
+                com.android.internal.R.drawable.progress_small_material,
             )
         } else if (isPlayingState(state.state)) {
             getStandardAction(context, controller, state.actions, PlaybackState.ACTION_PAUSE)
@@ -128,7 +130,7 @@
         nextCustomAction(),
         nextCustomAction(),
         reserveNext,
-        reservePrev
+        reservePrev,
     )
 }
 
@@ -146,7 +148,7 @@
     context: Context,
     controller: MediaController,
     stateActions: Long,
-    @PlaybackState.Actions action: Long
+    @PlaybackState.Actions action: Long,
 ): MediaAction? {
     if (!includesAction(stateActions, action)) {
         return null
@@ -158,7 +160,7 @@
                 context.getDrawable(R.drawable.ic_media_play),
                 { controller.transportControls.play() },
                 context.getString(R.string.controls_media_button_play),
-                context.getDrawable(R.drawable.ic_media_play_container)
+                context.getDrawable(R.drawable.ic_media_play_container),
             )
         }
         PlaybackState.ACTION_PAUSE -> {
@@ -166,7 +168,7 @@
                 context.getDrawable(R.drawable.ic_media_pause),
                 { controller.transportControls.pause() },
                 context.getString(R.string.controls_media_button_pause),
-                context.getDrawable(R.drawable.ic_media_pause_container)
+                context.getDrawable(R.drawable.ic_media_pause_container),
             )
         }
         PlaybackState.ACTION_SKIP_TO_PREVIOUS -> {
@@ -174,7 +176,7 @@
                 MediaControlDrawables.getPrevIcon(context),
                 { controller.transportControls.skipToPrevious() },
                 context.getString(R.string.controls_media_button_prev),
-                null
+                null,
             )
         }
         PlaybackState.ACTION_SKIP_TO_NEXT -> {
@@ -182,7 +184,7 @@
                 MediaControlDrawables.getNextIcon(context),
                 { controller.transportControls.skipToNext() },
                 context.getString(R.string.controls_media_button_next),
-                null
+                null,
             )
         }
         else -> null
@@ -194,13 +196,13 @@
     context: Context,
     packageName: String,
     controller: MediaController,
-    customAction: PlaybackState.CustomAction
+    customAction: PlaybackState.CustomAction,
 ): MediaAction {
     return MediaAction(
         Icon.createWithResource(packageName, customAction.icon).loadDrawable(context),
         { controller.transportControls.sendCustomAction(customAction, customAction.extras) },
         customAction.name,
-        null
+        null,
     )
 }
 
@@ -218,7 +220,7 @@
 /** Generate action buttons based on notification actions */
 fun createActionsFromNotification(
     context: Context,
-    sbn: StatusBarNotification
+    sbn: StatusBarNotification,
 ): Pair<List<MediaNotificationAction>, List<Int>> {
     val notif = sbn.notification
     val actionIcons: MutableList<MediaNotificationAction> = ArrayList()
@@ -229,7 +231,7 @@
     if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) {
         Log.e(
             TAG,
-            "Too many compact actions for ${sbn.key}, limiting to first $MAX_COMPACT_ACTIONS"
+            "Too many compact actions for ${sbn.key}, limiting to first $MAX_COMPACT_ACTIONS",
         )
         actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS)
     }
@@ -239,7 +241,7 @@
             Log.w(
                 TAG,
                 "Too many notification actions for ${sbn.key}, " +
-                    "limiting to first $MAX_NOTIFICATION_ACTIONS"
+                    "limiting to first $MAX_NOTIFICATION_ACTIONS",
             )
         }
 
@@ -253,7 +255,7 @@
             val themeText =
                 com.android.settingslib.Utils.getColorAttr(
                         context,
-                        com.android.internal.R.attr.textColorPrimary
+                        com.android.internal.R.attr.textColorPrimary,
                     )
                     .defaultColor
 
@@ -271,7 +273,7 @@
                     action.isAuthenticationRequired,
                     action.actionIntent,
                     mediaActionIcon,
-                    action.title
+                    action.title,
                 )
             actionIcons.add(mediaAction)
         }
@@ -288,7 +290,7 @@
  */
 fun getNotificationActions(
     actions: List<MediaNotificationAction>,
-    activityStarter: ActivityStarter
+    activityStarter: ActivityStarter,
 ): List<MediaAction> {
     return actions.map { action ->
         val runnable =
@@ -303,7 +305,7 @@
                             activityStarter.dismissKeyguardThenExecute(
                                 { sendPendingIntent(action.actionIntent) },
                                 {},
-                                true
+                                true,
                             )
                         else -> sendPendingIntent(actionIntent)
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
index 5f0a9f8..fd7b6dc 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
@@ -119,7 +119,7 @@
     arrayOf(
         MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
         MediaMetadata.METADATA_KEY_ART_URI,
-        MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
+        MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI,
     )
 
 private const val TAG = "MediaDataProcessor"
@@ -177,7 +177,7 @@
     private val themeText =
         com.android.settingslib.Utils.getColorAttr(
                 context,
-                com.android.internal.R.attr.textColorPrimary
+                com.android.internal.R.attr.textColorPrimary,
             )
             .defaultColor
 
@@ -365,7 +365,7 @@
                 secureSettings.getBoolForUser(
                     Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
                     true,
-                    UserHandle.USER_CURRENT
+                    UserHandle.USER_CURRENT,
                 )
 
             useQsMediaPlayer && flag
@@ -386,7 +386,7 @@
                 if (!allowMediaRecommendations) {
                     dismissSmartspaceRecommendation(
                         key = mediaDataRepository.smartspaceMediaData.value.targetId,
-                        delay = 0L
+                        delay = 0L,
                     )
                 }
             }
@@ -413,7 +413,7 @@
         token: MediaSession.Token,
         appName: String,
         appIntent: PendingIntent,
-        packageName: String
+        packageName: String,
     ) {
         // Resume controls don't have a notification key, so store by package name instead
         if (!mediaDataRepository.mediaEntries.value.containsKey(packageName)) {
@@ -449,7 +449,7 @@
                     token,
                     appName,
                     appIntent,
-                    packageName
+                    packageName,
                 )
             }
         } else {
@@ -461,7 +461,7 @@
                     token,
                     appName,
                     appIntent,
-                    packageName
+                    packageName,
                 )
             }
         }
@@ -582,30 +582,37 @@
     /** Called when the player's [PlaybackState] has been updated with new actions and/or state */
     internal fun updateState(key: String, state: PlaybackState) {
         mediaDataRepository.mediaEntries.value.get(key)?.let {
-            val token = it.token
-            if (token == null) {
-                if (DEBUG) Log.d(TAG, "State updated, but token was null")
-                return
-            }
-            val actions =
-                createActionsFromState(
-                    it.packageName,
-                    mediaControllerFactory.create(it.token),
-                    UserHandle(it.userId)
-                )
+            applicationScope.launch {
+                withContext(backgroundDispatcher) {
+                    val token = it.token
+                    if (token == null) {
+                        if (DEBUG) Log.d(TAG, "State updated, but token was null")
+                        return@withContext
+                    }
+                    val actions =
+                        createActionsFromState(
+                            it.packageName,
+                            mediaControllerFactory.create(it.token),
+                            UserHandle(it.userId),
+                        )
 
-            // Control buttons
-            // If flag is enabled and controller has a PlaybackState,
-            // create actions from session info
-            // otherwise, no need to update semantic actions.
-            val data =
-                if (actions != null) {
-                    it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state))
-                } else {
-                    it.copy(isPlaying = isPlayingState(state.state))
+                    // Control buttons
+                    // If flag is enabled and controller has a PlaybackState,
+                    // create actions from session info
+                    // otherwise, no need to update semantic actions.
+                    val data =
+                        if (actions != null) {
+                            it.copy(
+                                semanticActions = actions,
+                                isPlaying = isPlayingState(state.state),
+                            )
+                        } else {
+                            it.copy(isPlaying = isPlayingState(state.state))
+                        }
+                    if (DEBUG) Log.d(TAG, "State updated outside of notification")
+                    withContext(mainDispatcher) { onMediaDataLoaded(key, key, data) }
                 }
-            if (DEBUG) Log.d(TAG, "State updated outside of notification")
-            onMediaDataLoaded(key, key, data)
+            }
         }
     }
 
@@ -633,7 +640,7 @@
         }
         foregroundExecutor.executeDelayed(
             { removeEntry(key, userInitiated = userInitiated) },
-            delayMs
+            delayMs,
         )
         return existed
     }
@@ -657,7 +664,7 @@
         if (mediaDataRepository.dismissSmartspaceRecommendation(key)) {
             foregroundExecutor.executeDelayed(
                 { notifySmartspaceMediaDataRemoved(key, immediately = true) },
-                delay
+                delay,
             )
         }
     }
@@ -677,7 +684,7 @@
         token: MediaSession.Token,
         appName: String,
         appIntent: PendingIntent,
-        packageName: String
+        packageName: String,
     ) =
         withContext(backgroundDispatcher) {
             val lastActive = systemClock.elapsedRealtime()
@@ -694,7 +701,7 @@
                         token,
                         appName,
                         appIntent,
-                        packageName
+                        packageName,
                     )
             if (result == null || desc.title.isNullOrBlank()) {
                 Log.d(TAG, "No MediaData result for resumption")
@@ -733,7 +740,7 @@
                         appUid = result.appUid,
                         isExplicit = result.isExplicit,
                         resumeProgress = result.resumeProgress,
-                    )
+                    ),
                 )
             }
         }
@@ -746,7 +753,7 @@
         token: MediaSession.Token,
         appName: String,
         appIntent: PendingIntent,
-        packageName: String
+        packageName: String,
     ) {
         if (desc.title.isNullOrBlank()) {
             Log.e(TAG, "Description incomplete")
@@ -818,7 +825,7 @@
                     isExplicit = isExplicit,
                     resumeProgress = progress,
                     smartspaceId = SmallHash.hash(appUid + systemClock.currentTimeMillis().toInt()),
-                )
+                ),
             )
         }
     }
@@ -887,14 +894,14 @@
                     result.appUid,
                     sbn.packageName,
                     instanceId,
-                    result.playbackLocation
+                    result.playbackLocation,
                 )
             } else if (result.playbackLocation != oldEntry?.playbackLocation) {
                 logger.logPlaybackLocationChange(
                     result.appUid,
                     sbn.packageName,
                     instanceId,
-                    result.playbackLocation
+                    result.playbackLocation,
                 )
             }
 
@@ -911,7 +918,7 @@
         val token =
             sbn.notification.extras.getParcelable(
                 Notification.EXTRA_MEDIA_SESSION,
-                MediaSession.Token::class.java
+                MediaSession.Token::class.java,
             )
         if (token == null) {
             return
@@ -923,7 +930,7 @@
         val appInfo =
             notif.extras.getParcelable(
                 Notification.EXTRA_BUILDER_APPLICATION_INFO,
-                ApplicationInfo::class.java
+                ApplicationInfo::class.java,
             ) ?: getAppInfoFromPackage(sbn.packageName)
 
         // App name
@@ -987,7 +994,7 @@
             val deviceIntent =
                 extras.getParcelable(
                     Notification.EXTRA_MEDIA_REMOTE_INTENT,
-                    PendingIntent::class.java
+                    PendingIntent::class.java,
                 )
             Log.d(TAG, "$key is RCN for $deviceName")
 
@@ -1003,7 +1010,7 @@
                         deviceDrawable,
                         deviceName,
                         deviceIntent,
-                        showBroadcastButton = false
+                        showBroadcastButton = false,
                     )
             }
         }
@@ -1093,7 +1100,7 @@
                 mediaData.copy(
                     resumeAction = oldResumeAction,
                     hasCheckedForResume = oldHasCheckedForResume,
-                    active = oldActive
+                    active = oldActive,
                 )
             onMediaDataLoaded(key, oldKey, mediaData)
         }
@@ -1102,7 +1109,7 @@
     private fun logSingleVsMultipleMediaAdded(
         appUid: Int,
         packageName: String,
-        instanceId: InstanceId
+        instanceId: InstanceId,
     ) {
         if (mediaDataRepository.mediaEntries.value.size == 1) {
             logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId)
@@ -1151,7 +1158,7 @@
     private fun createActionsFromState(
         packageName: String,
         controller: MediaController,
-        user: UserHandle
+        user: UserHandle,
     ): MediaButton? {
         if (!mediaFlags.areMediaSessionActionsEnabled(packageName, user)) {
             return null
@@ -1189,7 +1196,7 @@
                 packageName,
                 ContentProvider.getUriWithoutUserId(uri),
                 Intent.FLAG_GRANT_READ_URI_PERMISSION,
-                ContentProvider.getUserIdFromUri(uri, userId)
+                ContentProvider.getUserIdFromUri(uri, userId),
             )
             return loadBitmapFromUri(uri)
         } catch (e: SecurityException) {
@@ -1226,7 +1233,7 @@
                 val scale =
                     MediaDataUtils.getScaleFactor(
                         APair(width, height),
-                        APair(artworkWidth, artworkHeight)
+                        APair(artworkWidth, artworkHeight),
                     )
 
                 // Downscale if needed
@@ -1251,7 +1258,7 @@
                 .loadDrawable(context),
             action,
             context.getString(R.string.controls_media_resume),
-            context.getDrawable(R.drawable.ic_media_play_container)
+            context.getDrawable(R.drawable.ic_media_play_container),
         )
     }
 
@@ -1291,7 +1298,7 @@
                 } else {
                     notifySmartspaceMediaDataRemoved(
                         smartspaceMediaData.targetId,
-                        immediately = false
+                        immediately = false,
                     )
                     mediaDataRepository.setRecommendation(
                         SmartspaceMediaData(
@@ -1362,7 +1369,7 @@
     private fun handlePossibleRemoval(
         key: String,
         removed: MediaData,
-        notificationRemoved: Boolean = false
+        notificationRemoved: Boolean = false,
     ) {
         val hasSession = removed.token != null
         if (hasSession && removed.semanticActions != null) {
@@ -1387,7 +1394,7 @@
                 Log.d(
                     TAG,
                     "Notification ($notificationRemoved) and/or session " +
-                        "($hasSession) gone for inactive player $key"
+                        "($hasSession) gone for inactive player $key",
                 )
             }
             convertToResumePlayer(key, removed)
@@ -1513,7 +1520,7 @@
             data: MediaData,
             immediately: Boolean = true,
             receivedSmartspaceCardLatency: Int = 0,
-            isSsReactivated: Boolean = false
+            isSsReactivated: Boolean = false,
         ) {}
 
         /**
@@ -1526,7 +1533,7 @@
         fun onSmartspaceMediaDataLoaded(
             key: String,
             data: SmartspaceMediaData,
-            shouldPrioritize: Boolean = false
+            shouldPrioritize: Boolean = false,
         ) {}
 
         /** Called whenever a previously existing Media notification was removed. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
index a0fb0bf2..72650ea 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.media.controls.ui.controller
 
+import android.annotation.WorkerThread
 import android.app.PendingIntent
 import android.content.Context
 import android.content.Intent
@@ -41,6 +42,7 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.Dumpable
+import com.android.systemui.Flags.mediaControlsUmoInflationInBackground
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
@@ -137,7 +139,7 @@
     private val activityStarter: ActivityStarter,
     private val systemClock: SystemClock,
     @Main private val mainDispatcher: CoroutineDispatcher,
-    @Main executor: DelayableExecutor,
+    @Main private val uiExecutor: DelayableExecutor,
     @Background private val bgExecutor: Executor,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
     private val mediaManager: MediaDataManager,
@@ -227,7 +229,7 @@
     private var carouselLocale: Locale? = null
 
     private val animationScaleObserver: ContentObserver =
-        object : ContentObserver(executor, 0) {
+        object : ContentObserver(uiExecutor, 0) {
             override fun onChange(selfChange: Boolean) {
                 if (!SceneContainerFlag.isEnabled) {
                     MediaPlayerData.players().forEach { it.updateAnimatorDurationScale() }
@@ -350,7 +352,7 @@
             MediaCarouselScrollHandler(
                 mediaCarousel,
                 pageIndicator,
-                executor,
+                uiExecutor,
                 this::onSwipeToDismiss,
                 this::updatePageIndicatorLocation,
                 this::updateSeekbarListening,
@@ -458,7 +460,17 @@
                     isSsReactivated: Boolean,
                 ) {
                     debugLogger.logMediaLoaded(key, data.active)
-                    if (addOrUpdatePlayer(key, oldKey, data, isSsReactivated)) {
+                    val onUiExecutionEnd =
+                        if (mediaControlsUmoInflationInBackground()) {
+                            Runnable {
+                                if (immediately) {
+                                    updateHostVisibility()
+                                }
+                            }
+                        } else {
+                            null
+                        }
+                    if (addOrUpdatePlayer(key, oldKey, data, isSsReactivated, onUiExecutionEnd)) {
                         // Log card received if a new resumable media card is added
                         MediaPlayerData.getMediaPlayer(key)?.let {
                             logSmartspaceCardReported(
@@ -980,6 +992,7 @@
         oldKey: String?,
         data: MediaData,
         isSsReactivated: Boolean,
+        onUiExecutionEnd: Runnable? = null,
     ): Boolean =
         traceSection("MediaCarouselController#addOrUpdatePlayer") {
             MediaPlayerData.moveIfExists(oldKey, key)
@@ -987,76 +1000,119 @@
             val curVisibleMediaKey =
                 MediaPlayerData.visiblePlayerKeys()
                     .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
-            if (existingPlayer == null) {
-                val newPlayer = mediaControlPanelFactory.get()
-                if (SceneContainerFlag.isEnabled) {
-                    newPlayer.mediaViewController.widthInSceneContainerPx = widthInSceneContainerPx
-                    newPlayer.mediaViewController.heightInSceneContainerPx =
-                        heightInSceneContainerPx
-                }
-                newPlayer.attachPlayer(
-                    MediaViewHolder.create(LayoutInflater.from(context), mediaContent)
-                )
-                newPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
-                val lp =
-                    LinearLayout.LayoutParams(
-                        ViewGroup.LayoutParams.MATCH_PARENT,
-                        ViewGroup.LayoutParams.WRAP_CONTENT,
-                    )
-                newPlayer.mediaViewHolder?.player?.setLayoutParams(lp)
-                newPlayer.bindPlayer(data, key)
-                newPlayer.setListening(
-                    mediaCarouselScrollHandler.visibleToUser && currentlyExpanded
-                )
-                MediaPlayerData.addMediaPlayer(
-                    key,
-                    data,
-                    newPlayer,
-                    systemClock,
-                    isSsReactivated,
-                    debugLogger,
-                )
-                updateViewControllerToState(newPlayer.mediaViewController, noAnimation = true)
-                // Media data added from a recommendation card should starts playing.
-                if (
-                    (shouldScrollToKey && data.isPlaying == true) ||
-                        (!shouldScrollToKey && data.active)
-                ) {
-                    reorderAllPlayers(curVisibleMediaKey, key)
+            if (mediaControlsUmoInflationInBackground()) {
+                if (existingPlayer == null) {
+                    bgExecutor.execute {
+                        val mediaViewHolder = createMediaViewHolderInBg()
+                        // Add the new player in the main thread.
+                        uiExecutor.execute {
+                            setupNewPlayer(
+                                key,
+                                data,
+                                isSsReactivated,
+                                curVisibleMediaKey,
+                                mediaViewHolder,
+                            )
+                            updatePageIndicator()
+                            mediaCarouselScrollHandler.onPlayersChanged()
+                            mediaFrame.requiresRemeasuring = true
+                            onUiExecutionEnd?.run()
+                        }
+                    }
                 } else {
-                    needsReordering = true
+                    updatePlayer(key, data, isSsReactivated, curVisibleMediaKey, existingPlayer)
+                    updatePageIndicator()
+                    mediaCarouselScrollHandler.onPlayersChanged()
+                    mediaFrame.requiresRemeasuring = true
+                    onUiExecutionEnd?.run()
                 }
             } else {
-                existingPlayer.bindPlayer(data, key)
-                MediaPlayerData.addMediaPlayer(
-                    key,
-                    data,
-                    existingPlayer,
-                    systemClock,
-                    isSsReactivated,
-                    debugLogger,
-                )
-                val packageName = MediaPlayerData.smartspaceMediaData?.packageName ?: String()
-                // In case of recommendations hits.
-                // Check the playing status of media player and the package name.
-                // To make sure we scroll to the right app's media player.
-                if (
-                    isReorderingAllowed ||
-                        shouldScrollToKey &&
-                            data.isPlaying == true &&
-                            packageName == data.packageName
-                ) {
-                    reorderAllPlayers(curVisibleMediaKey, key)
+                if (existingPlayer == null) {
+                    val mediaViewHolder =
+                        MediaViewHolder.create(LayoutInflater.from(context), mediaContent)
+                    setupNewPlayer(key, data, isSsReactivated, curVisibleMediaKey, mediaViewHolder)
                 } else {
-                    needsReordering = true
+                    updatePlayer(key, data, isSsReactivated, curVisibleMediaKey, existingPlayer)
                 }
+                updatePageIndicator()
+                mediaCarouselScrollHandler.onPlayersChanged()
+                mediaFrame.requiresRemeasuring = true
+                onUiExecutionEnd?.run()
             }
-            updatePageIndicator()
-            mediaCarouselScrollHandler.onPlayersChanged()
-            mediaFrame.requiresRemeasuring = true
             return existingPlayer == null
         }
 
+    private fun updatePlayer(
+        key: String,
+        data: MediaData,
+        isSsReactivated: Boolean,
+        curVisibleMediaKey: MediaPlayerData.MediaSortKey?,
+        existingPlayer: MediaControlPanel,
+    ) {
+        existingPlayer.bindPlayer(data, key)
+        MediaPlayerData.addMediaPlayer(
+            key,
+            data,
+            existingPlayer,
+            systemClock,
+            isSsReactivated,
+            debugLogger,
+        )
+        val packageName = MediaPlayerData.smartspaceMediaData?.packageName ?: String()
+        // In case of recommendations hits.
+        // Check the playing status of media player and the package name.
+        // To make sure we scroll to the right app's media player.
+        if (
+            isReorderingAllowed ||
+                shouldScrollToKey && data.isPlaying == true && packageName == data.packageName
+        ) {
+            reorderAllPlayers(curVisibleMediaKey, key)
+        } else {
+            needsReordering = true
+        }
+    }
+
+    private fun setupNewPlayer(
+        key: String,
+        data: MediaData,
+        isSsReactivated: Boolean,
+        curVisibleMediaKey: MediaPlayerData.MediaSortKey?,
+        mediaViewHolder: MediaViewHolder,
+    ) {
+        val newPlayer = mediaControlPanelFactory.get()
+        newPlayer.attachPlayer(mediaViewHolder)
+        newPlayer.mediaViewController.sizeChangedListener =
+            this@MediaCarouselController::updateCarouselDimensions
+        val lp =
+            LinearLayout.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT,
+            )
+        newPlayer.mediaViewHolder?.player?.setLayoutParams(lp)
+        newPlayer.bindPlayer(data, key)
+        newPlayer.setListening(mediaCarouselScrollHandler.visibleToUser && currentlyExpanded)
+        MediaPlayerData.addMediaPlayer(
+            key,
+            data,
+            newPlayer,
+            systemClock,
+            isSsReactivated,
+            debugLogger,
+        )
+        updateViewControllerToState(newPlayer.mediaViewController, noAnimation = true)
+        // Media data added from a recommendation card should starts playing.
+        if ((shouldScrollToKey && data.isPlaying == true) || (!shouldScrollToKey && data.active)) {
+            reorderAllPlayers(curVisibleMediaKey, key)
+        } else {
+            needsReordering = true
+        }
+    }
+
+    @WorkerThread
+    private fun createMediaViewHolderInBg(): MediaViewHolder {
+        return MediaViewHolder.create(LayoutInflater.from(context), mediaContent)
+    }
+
     private fun addSmartspaceMediaRecommendations(
         key: String,
         data: SmartspaceMediaData,
@@ -1173,8 +1229,16 @@
         val previousVisibleKey =
             MediaPlayerData.visiblePlayerKeys()
                 .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
+        val onUiExecutionEnd = Runnable {
+            if (recreateMedia) {
+                reorderAllPlayers(previousVisibleKey)
+            }
+        }
 
-        MediaPlayerData.mediaData().forEach { (key, data, isSsMediaRec) ->
+        val mediaDataList = MediaPlayerData.mediaData()
+        // Do not loop through the original list of media data because the re-addition of media data
+        // is being executed in background thread.
+        mediaDataList.forEach { (key, data, isSsMediaRec) ->
             if (isSsMediaRec) {
                 val smartspaceMediaData = MediaPlayerData.smartspaceMediaData
                 removePlayer(key, dismissMediaData = false, dismissRecommendation = false)
@@ -1185,6 +1249,7 @@
                         MediaPlayerData.shouldPrioritizeSs,
                     )
                 }
+                onUiExecutionEnd.run()
             } else {
                 val isSsReactivated = MediaPlayerData.isSsReactivated(key)
                 if (recreateMedia) {
@@ -1195,11 +1260,9 @@
                     oldKey = null,
                     data = data,
                     isSsReactivated = isSsReactivated,
+                    onUiExecutionEnd = onUiExecutionEnd,
                 )
             }
-            if (recreateMedia) {
-                reorderAllPlayers(previousVisibleKey)
-            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHostStatesManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHostStatesManager.kt
index 8660d12..782da4b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHostStatesManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHostStatesManager.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.media.controls.ui.controller
 
 import com.android.app.tracing.traceSection
+import com.android.systemui.Flags.mediaControlsUmoInflationInBackground
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.media.controls.ui.view.MediaHostState
 import com.android.systemui.util.animation.MeasurementOutput
@@ -71,23 +72,34 @@
      */
     fun updateCarouselDimensions(
         @MediaLocation location: Int,
-        hostState: MediaHostState
+        hostState: MediaHostState,
     ): MeasurementOutput =
         traceSection("MediaHostStatesManager#updateCarouselDimensions") {
             val result = MeasurementOutput(0, 0)
+            var changed = false
             for (controller in controllers) {
                 val measurement = controller.getMeasurementsForState(hostState)
                 measurement?.let {
                     if (it.measuredHeight > result.measuredHeight) {
                         result.measuredHeight = it.measuredHeight
+                        changed = true
                     }
                     if (it.measuredWidth > result.measuredWidth) {
                         result.measuredWidth = it.measuredWidth
+                        changed = true
                     }
                 }
             }
-            carouselSizes[location] = result
-            return result
+            if (mediaControlsUmoInflationInBackground()) {
+                // Set carousel size if result measurements changed. This avoids setting carousel
+                // size when this method gets called before the addition of media view controllers
+                if (!carouselSizes.contains(location) || changed) {
+                    carouselSizes[location] = result
+                }
+            } else {
+                carouselSizes[location] = result
+            }
+            return carouselSizes[location] ?: result
         }
 
     /** Add a callback to be called when a MediaState has updated */
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt
index 09a6181..5ddc347 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt
@@ -20,6 +20,7 @@
 import android.util.ArraySet
 import android.view.View
 import android.view.View.OnAttachStateChangeListener
+import com.android.systemui.Flags.mediaControlsUmoInflationInBackground
 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
 import com.android.systemui.media.controls.shared.model.MediaData
 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
@@ -91,8 +92,10 @@
                 data: MediaData,
                 immediately: Boolean,
                 receivedSmartspaceCardLatency: Int,
-                isSsReactivated: Boolean
+                isSsReactivated: Boolean,
             ) {
+                if (mediaControlsUmoInflationInBackground()) return
+
                 if (immediately) {
                     updateViewVisibility()
                 }
@@ -101,7 +104,7 @@
             override fun onSmartspaceMediaDataLoaded(
                 key: String,
                 data: SmartspaceMediaData,
-                shouldPrioritize: Boolean
+                shouldPrioritize: Boolean,
             ) {
                 updateViewVisibility()
             }
@@ -171,7 +174,7 @@
                         input.widthMeasureSpec =
                             View.MeasureSpec.makeMeasureSpec(
                                 View.MeasureSpec.getSize(input.widthMeasureSpec),
-                                View.MeasureSpec.EXACTLY
+                                View.MeasureSpec.EXACTLY,
                             )
                     }
                     // This will trigger a state change that ensures that we now have a state
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
index 8351597..c3729c0 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
@@ -68,12 +68,12 @@
 import com.android.systemui.statusbar.phone.AlertDialogWithDelegate;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 
+import dagger.Lazy;
+
 import java.util.function.Consumer;
 
 import javax.inject.Inject;
 
-import dagger.Lazy;
-
 public class MediaProjectionPermissionActivity extends Activity {
     private static final String TAG = "MediaProjectionPermissionActivity";
     private static final float MAX_APP_NAME_SIZE_PX = 500f;
@@ -132,8 +132,7 @@
                 mPackageName = launchingIntent.getStringExtra(
                         EXTRA_PACKAGE_REUSING_GRANTED_CONSENT);
             } else {
-                setResult(RESULT_CANCELED);
-                finish(RECORD_CANCEL, /* projection= */ null);
+                finishAsCancelled();
                 return;
             }
         }
@@ -145,8 +144,7 @@
             mUid = aInfo.uid;
         } catch (PackageManager.NameNotFoundException e) {
             Log.e(TAG, "Unable to look up package name", e);
-            setResult(RESULT_CANCELED);
-            finish(RECORD_CANCEL, /* projection= */ null);
+            finishAsCancelled();
             return;
         }
 
@@ -176,15 +174,13 @@
             }
         } catch (RemoteException e) {
             Log.e(TAG, "Error checking projection permissions", e);
-            setResult(RESULT_CANCELED);
-            finish(RECORD_CANCEL, /* projection= */ null);
+            finishAsCancelled();
             return;
         }
 
         if (mFeatureFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)) {
             if (showScreenCaptureDisabledDialogIfNeeded()) {
-                setResult(RESULT_CANCELED);
-                finish(RECORD_CANCEL, /* projection= */ null);
+                finishAsCancelled();
                 return;
             }
         }
@@ -346,6 +342,21 @@
     private void requestDeviceUnlock() {
         mKeyguardManager.requestDismissKeyguard(this,
                 new KeyguardManager.KeyguardDismissCallback() {
+
+                    @Override
+                    public void onDismissError() {
+                        if (com.android.systemui.Flags.mediaProjectionDialogBehindLockscreen()) {
+                            finishAsCancelled();
+                        }
+                    }
+
+                    @Override
+                    public void onDismissCancelled() {
+                        if (com.android.systemui.Flags.mediaProjectionDialogBehindLockscreen()) {
+                            finishAsCancelled();
+                        }
+                    }
+
                     @Override
                     public void onDismissSucceeded() {
                         mDialog.show();
@@ -386,8 +397,7 @@
             }
         } catch (RemoteException e) {
             Log.e(TAG, "Error granting projection permission", e);
-            setResult(RESULT_CANCELED);
-            finish(RECORD_CANCEL, /* projection= */ null);
+            finishAsCancelled();
         } finally {
             if (mDialog != null) {
                 mDialog.dismiss();
@@ -436,6 +446,14 @@
         }
     }
 
+    /**
+     * Finishes this activity and cancel the projection request.
+     */
+    private void finishAsCancelled() {
+        setResult(RESULT_CANCELED);
+        finish(RECORD_CANCEL, /* projection= */ null);
+    }
+
     @Nullable
     private MediaProjectionConfig getMediaProjectionConfig() {
         Intent intent = getIntent();
diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt
index 219e45c..0e54041 100644
--- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt
@@ -16,11 +16,19 @@
 
 package com.android.systemui.notifications.ui.viewmodel
 
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.launch
 
 /**
  * Models UI state used to render the content of the notifications shade overlay.
@@ -33,10 +41,40 @@
 constructor(
     val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory,
     val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory,
+    val sceneInteractor: SceneInteractor,
     private val shadeInteractor: ShadeInteractor,
-) {
+) : ExclusiveActivatable() {
+
+    override suspend fun onActivated(): Nothing {
+        coroutineScope {
+            launch {
+                sceneInteractor.currentScene.collect { currentScene ->
+                    when (currentScene) {
+                        // TODO(b/369513770): The ShadeSession should be preserved in this scenario.
+                        Scenes.Bouncer ->
+                            shadeInteractor.collapseNotificationsShade(
+                                loggingReason = "bouncer shown while shade is open"
+                            )
+                    }
+                }
+            }
+
+            launch {
+                shadeInteractor.isShadeTouchable
+                    .distinctUntilChanged()
+                    .filter { !it }
+                    .collect {
+                        shadeInteractor.collapseNotificationsShade(
+                            loggingReason = "device became non-interactive"
+                        )
+                    }
+            }
+        }
+        awaitCancellation()
+    }
+
     fun onScrimClicked() {
-        shadeInteractor.collapseNotificationsShade(loggingReason = "Shade scrim clicked")
+        shadeInteractor.collapseNotificationsShade(loggingReason = "shade scrim clicked")
     }
 
     @AssistedFactory
diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
index 278352c..ead38f3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
@@ -33,6 +33,7 @@
 import com.android.systemui.log.dagger.QSConfigLog
 import com.android.systemui.log.dagger.QSLog
 import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.plugins.qs.QSTile.State
 import com.android.systemui.statusbar.StatusBarState
 import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
@@ -57,6 +58,7 @@
     fun d(@CompileTimeConstant msg: String, arg: Any) {
         buffer.log(TAG, DEBUG, { str1 = arg.toString() }, { "$msg: $str1" })
     }
+
     fun i(@CompileTimeConstant msg: String, arg: Any) {
         buffer.log(TAG, INFO, { str1 = arg.toString() }, { "$msg: $str1" })
     }
@@ -73,7 +75,19 @@
                 str1 = tileSpec
                 str2 = reason
             },
-            { "[$str1] Tile destroyed. Reason: $str2" }
+            { "[$str1] Tile destroyed. Reason: $str2" },
+        )
+    }
+
+    fun logStateChanged(tileSpec: String, state: State) {
+        buffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = tileSpec
+                str2 = state.toString()
+            },
+            { "[$str1] Tile state=$str2" },
         )
     }
 
@@ -85,7 +99,7 @@
                 bool1 = listening
                 str1 = tileSpec
             },
-            { "[$str1] Tile listening=$bool1" }
+            { "[$str1] Tile listening=$bool1" },
         )
     }
 
@@ -98,7 +112,7 @@
                 str1 = containerName
                 str2 = allSpecs
             },
-            { "Tiles listening=$bool1 in $str1. $str2" }
+            { "Tiles listening=$bool1 in $str1. $str2" },
         )
     }
 
@@ -112,7 +126,7 @@
                 str2 = StatusBarState.toString(statusBarState)
                 str3 = toStateString(state)
             },
-            { "[$str1][$int1] Tile clicked. StatusBarState=$str2. TileState=$str3" }
+            { "[$str1][$int1] Tile clicked. StatusBarState=$str2. TileState=$str3" },
         )
     }
 
@@ -124,7 +138,7 @@
                 str1 = tileSpec
                 int1 = eventId
             },
-            { "[$str1][$int1] Tile handling click." }
+            { "[$str1][$int1] Tile handling click." },
         )
     }
 
@@ -138,7 +152,7 @@
                 str2 = StatusBarState.toString(statusBarState)
                 str3 = toStateString(state)
             },
-            { "[$str1][$int1] Tile secondary clicked. StatusBarState=$str2. TileState=$str3" }
+            { "[$str1][$int1] Tile secondary clicked. StatusBarState=$str2. TileState=$str3" },
         )
     }
 
@@ -150,7 +164,7 @@
                 str1 = tileSpec
                 int1 = eventId
             },
-            { "[$str1][$int1] Tile handling secondary click." }
+            { "[$str1][$int1] Tile handling secondary click." },
         )
     }
 
@@ -164,7 +178,7 @@
                 str2 = StatusBarState.toString(statusBarState)
                 str3 = toStateString(state)
             },
-            { "[$str1][$int1] Tile long clicked. StatusBarState=$str2. TileState=$str3" }
+            { "[$str1][$int1] Tile long clicked. StatusBarState=$str2. TileState=$str3" },
         )
     }
 
@@ -176,7 +190,7 @@
                 str1 = tileSpec
                 int1 = eventId
             },
-            { "[$str1][$int1] Tile handling long click." }
+            { "[$str1][$int1] Tile handling long click." },
         )
     }
 
@@ -189,7 +203,7 @@
                 int1 = lastType
                 str2 = callback
             },
-            { "[$str1] mLastTileState=$int1, Callback=$str2." }
+            { "[$str1] mLastTileState=$int1, Callback=$str2." },
         )
     }
 
@@ -198,7 +212,7 @@
         tileSpec: String,
         state: Int,
         disabledByPolicy: Boolean,
-        color: Int
+        color: Int,
     ) {
         // This method is added to further debug b/250618218 which has only been observed from the
         // InternetTile, so we are only logging the background color change for the InternetTile
@@ -215,7 +229,7 @@
                 bool1 = disabledByPolicy
                 int2 = color
             },
-            { "[$str1] state=$int1, disabledByPolicy=$bool1, color=$int2." }
+            { "[$str1] state=$int1, disabledByPolicy=$bool1, color=$int2." },
         )
     }
 
@@ -229,7 +243,7 @@
                 str3 = state.icon?.toString()
                 int1 = state.state
             },
-            { "[$str1] Tile updated. Label=$str2. State=$int1. Icon=$str3." }
+            { "[$str1] Tile updated. Label=$str2. State=$int1. Icon=$str3." },
         )
     }
 
@@ -241,7 +255,7 @@
                 str1 = containerName
                 bool1 = expanded
             },
-            { "$str1 expanded=$bool1" }
+            { "$str1 expanded=$bool1" },
         )
     }
 
@@ -253,7 +267,7 @@
                 str1 = containerName
                 int1 = orientation
             },
-            { "onViewAttached: $str1 orientation $int1" }
+            { "onViewAttached: $str1 orientation $int1" },
         )
     }
 
@@ -265,7 +279,7 @@
                 str1 = containerName
                 int1 = orientation
             },
-            { "onViewDetached: $str1 orientation $int1" }
+            { "onViewDetached: $str1 orientation $int1" },
         )
     }
 
@@ -276,7 +290,7 @@
         newShouldUseSplitShade: Boolean,
         oldScreenLayout: Int,
         newScreenLayout: Int,
-        containerName: String
+        containerName: String,
     ) {
         configChangedBuffer.log(
             TAG,
@@ -297,7 +311,7 @@
                     "screen layout=${toScreenLayoutString(long1.toInt())} " +
                     "(was ${toScreenLayoutString(long2.toInt())}), " +
                     "splitShade=$bool2 (was $bool1)"
-            }
+            },
         )
     }
 
@@ -305,7 +319,7 @@
         after: Boolean,
         before: Boolean,
         force: Boolean,
-        containerName: String
+        containerName: String,
     ) {
         buffer.log(
             TAG,
@@ -316,7 +330,7 @@
                 bool2 = before
                 bool3 = force
             },
-            { "change tile layout: $str1 horizontal=$bool1 (was $bool2), force? $bool3" }
+            { "change tile layout: $str1 horizontal=$bool1 (was $bool2), force? $bool3" },
         )
     }
 
@@ -328,7 +342,7 @@
                 int1 = tilesPerPageCount
                 int2 = totalTilesCount
             },
-            { "Distributing tiles: [tilesPerPageCount=$int1] [totalTilesCount=$int2]" }
+            { "Distributing tiles: [tilesPerPageCount=$int1] [totalTilesCount=$int2]" },
         )
     }
 
@@ -340,7 +354,7 @@
                 str1 = tileName
                 int1 = pageIndex
             },
-            { "Adding $str1 to page number $int1" }
+            { "Adding $str1 to page number $int1" },
         )
     }
 
@@ -361,7 +375,7 @@
                 str1 = viewName
                 str2 = toVisibilityString(visibility)
             },
-            { "$str1 visibility: $str2" }
+            { "$str1 visibility: $str2" },
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
index 5ea8c21..a4f3c7a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
@@ -14,6 +14,7 @@
 
 package com.android.systemui.qs.tileimpl;
 
+import static com.android.systemui.Flags.qsNewTiles;
 import static com.android.systemui.Flags.removeUpdateListenerInQsIconViewImpl;
 
 import android.animation.Animator;
@@ -66,12 +67,22 @@
 
     private ValueAnimator mColorAnimator = new ValueAnimator();
 
+    private int mColorUnavailable;
+    private int mColorInactive;
+    private int mColorActive;
+
     public QSIconViewImpl(Context context) {
         super(context);
 
         final Resources res = context.getResources();
         mIconSizePx = res.getDimensionPixelSize(R.dimen.qs_icon_size);
 
+        if (qsNewTiles()) { // pre-load icon tint colors
+            mColorUnavailable = Utils.getColorAttrDefaultColor(context, R.attr.outline);
+            mColorInactive = Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactiveVariant);
+            mColorActive = Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive);
+        }
+
         mIcon = createIcon();
         addView(mIcon);
         mColorAnimator.setDuration(QS_ANIM_LENGTH);
@@ -195,7 +206,11 @@
     }
 
     protected int getColor(QSTile.State state) {
-        return getIconColorForState(getContext(), state);
+        if (qsNewTiles()) {
+            return getCachedIconColorForState(state);
+        } else {
+            return getIconColorForState(getContext(), state);
+        }
     }
 
     private void animateGrayScale(int fromColor, int toColor, ImageView iv,
@@ -267,6 +282,19 @@
         }
     }
 
+    private int getCachedIconColorForState(QSTile.State state) {
+        if (state.disabledByPolicy || state.state == Tile.STATE_UNAVAILABLE) {
+            return mColorUnavailable;
+        } else if (state.state == Tile.STATE_INACTIVE) {
+            return mColorInactive;
+        } else if (state.state == Tile.STATE_ACTIVE) {
+            return mColorActive;
+        } else {
+            Log.e("QSIconView", "Invalid state " + state);
+            return 0;
+        }
+    }
+
     private static class EndRunnableAnimatorListener extends AnimatorListenerAdapter {
         private Runnable mRunnable;
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index 4f3ea83..18b1f07 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -643,7 +643,6 @@
     }
 
     // HANDLE STATE CHANGES RELATED METHODS
-
     protected open fun handleStateChanged(state: QSTile.State) {
         val allowAnimations = animationsEnabled()
         isClickable = state.state != Tile.STATE_UNAVAILABLE
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt
index 8965ef2..bb0b9b7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt
@@ -18,7 +18,9 @@
 
 import android.content.Context
 import android.content.res.Resources
+import android.os.Handler
 import android.widget.Switch
+import com.android.settingslib.graph.SignalDrawable
 import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text.Companion.loadText
@@ -28,6 +30,7 @@
 import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
 import com.android.systemui.qs.tiles.viewmodel.QSTileState
 import com.android.systemui.res.R
+import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel
 import javax.inject.Inject
 
 /** Maps [InternetTileModel] to [QSTileState]. */
@@ -37,6 +40,7 @@
     @Main private val resources: Resources,
     private val theme: Resources.Theme,
     private val context: Context,
+    @Main private val handler: Handler,
 ) : QSTileDataToStateMapper<InternetTileModel> {
 
     override fun map(config: QSTileConfig, data: InternetTileModel): QSTileState =
@@ -44,25 +48,42 @@
             label = resources.getString(R.string.quick_settings_internet_label)
             expandedAccessibilityClass = Switch::class
 
-            if (data.secondaryLabel != null) {
-                secondaryLabel = data.secondaryLabel.loadText(context)
-            } else {
-                secondaryLabel = data.secondaryTitle
-            }
+            secondaryLabel =
+                if (data.secondaryLabel != null) {
+                    data.secondaryLabel.loadText(context)
+                } else {
+                    data.secondaryTitle
+                }
 
             stateDescription = data.stateDescription.loadContentDescription(context)
             contentDescription = data.contentDescription.loadContentDescription(context)
 
-            iconRes = data.iconId
-            if (data.icon != null) {
-                this.icon = { data.icon }
-            } else if (data.iconId != null) {
-                val loadedIcon =
-                    Icon.Loaded(
-                        resources.getDrawable(data.iconId!!, theme),
-                        contentDescription = null
-                    )
-                this.icon = { loadedIcon }
+            when (val dataIcon = data.icon) {
+                is InternetTileIconModel.ResourceId -> {
+                    iconRes = dataIcon.resId
+                    icon = {
+                        Icon.Loaded(
+                            resources.getDrawable(dataIcon.resId, theme),
+                            contentDescription = null,
+                        )
+                    }
+                }
+
+                is InternetTileIconModel.Cellular -> {
+                    val signalDrawable = SignalDrawable(context, handler)
+                    signalDrawable.setLevel(dataIcon.level)
+                    icon = { Icon.Loaded(signalDrawable, contentDescription = null) }
+                }
+
+                is InternetTileIconModel.Satellite -> {
+                    iconRes = dataIcon.resourceIcon.res // level is inferred from res
+                    icon = {
+                        Icon.Loaded(
+                            resources.getDrawable(dataIcon.resourceIcon.res, theme),
+                            contentDescription = null,
+                        )
+                    }
+                }
             }
 
             sideViewIcon = QSTileState.SideViewIcon.Chevron
@@ -75,7 +96,7 @@
                 setOf(
                     QSTileState.UserAction.CLICK,
                     QSTileState.UserAction.TOGGLE_CLICK,
-                    QSTileState.UserAction.LONG_CLICK
+                    QSTileState.UserAction.LONG_CLICK,
                 )
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt
index 204ead3..6fe3979 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt
@@ -20,13 +20,10 @@
 import android.content.Context
 import android.os.UserHandle
 import android.text.Html
-import com.android.settingslib.graph.SignalDrawable
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
-import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
 import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
 import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
@@ -36,12 +33,12 @@
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
 import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
+import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
 import com.android.systemui.utils.coroutines.flow.mapLatestConflated
 import javax.inject.Inject
-import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
@@ -51,7 +48,6 @@
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.withContext
 
 @OptIn(ExperimentalCoroutinesApi::class)
 /** Observes internet state changes providing the [InternetTileModel]. */
@@ -59,7 +55,6 @@
 @Inject
 constructor(
     private val context: Context,
-    @Main private val mainCoroutineContext: CoroutineContext,
     @Application private val scope: CoroutineScope,
     airplaneModeRepository: AirplaneModeRepository,
     private val connectivityRepository: ConnectivityRepository,
@@ -79,8 +74,7 @@
                 flowOf(
                     InternetTileModel.Active(
                         secondaryTitle = secondary,
-                        iconId = wifiIcon.icon.res,
-                        icon = Icon.Loaded(context.getDrawable(wifiIcon.icon.res)!!, null),
+                        icon = InternetTileIconModel.ResourceId(wifiIcon.icon.res),
                         stateDescription = wifiIcon.contentDescription,
                         contentDescription = ContentDescription.Loaded("$internetLabel,$secondary"),
                     )
@@ -116,11 +110,10 @@
             if (it == null) {
                 notConnectedFlow
             } else {
-                combine(
-                        it.networkName,
-                        it.signalLevelIcon,
-                        mobileDataContentName,
-                    ) { networkNameModel, signalIcon, dataContentDescription ->
+                combine(it.networkName, it.signalLevelIcon, mobileDataContentName) {
+                        networkNameModel,
+                        signalIcon,
+                        dataContentDescription ->
                         Triple(networkNameModel, signalIcon, dataContentDescription)
                     }
                     .mapLatestConflated { (networkNameModel, signalIcon, dataContentDescription) ->
@@ -129,17 +122,12 @@
                                 val secondary =
                                     mobileDataContentConcat(
                                         networkNameModel.name,
-                                        dataContentDescription
+                                        dataContentDescription,
                                     )
 
-                                val drawable =
-                                    withContext(mainCoroutineContext) { SignalDrawable(context) }
-                                drawable.setLevel(signalIcon.level)
-                                val loadedIcon = Icon.Loaded(drawable, null)
-
                                 InternetTileModel.Active(
                                     secondaryTitle = secondary,
-                                    icon = loadedIcon,
+                                    icon = InternetTileIconModel.Cellular(signalIcon.level),
                                     stateDescription =
                                         ContentDescription.Loaded(secondary.toString()),
                                     contentDescription = ContentDescription.Loaded(internetLabel),
@@ -150,9 +138,10 @@
                                     signalIcon.icon.contentDescription.loadContentDescription(
                                         context
                                     )
+
                                 InternetTileModel.Active(
                                     secondaryTitle = secondary,
-                                    iconId = signalIcon.icon.res,
+                                    icon = InternetTileIconModel.Satellite(signalIcon.icon),
                                     stateDescription = ContentDescription.Loaded(secondary),
                                     contentDescription = ContentDescription.Loaded(internetLabel),
                                 )
@@ -164,7 +153,7 @@
 
     private fun mobileDataContentConcat(
         networkName: String?,
-        dataContentDescription: CharSequence?
+        dataContentDescription: CharSequence?,
     ): CharSequence {
         if (dataContentDescription == null) {
             return networkName ?: ""
@@ -177,9 +166,9 @@
             context.getString(
                 R.string.mobile_carrier_text_format,
                 networkName,
-                dataContentDescription
+                dataContentDescription,
             ),
-            0
+            0,
         )
     }
 
@@ -199,7 +188,7 @@
                 flowOf(
                     InternetTileModel.Active(
                         secondaryLabel = secondary?.toText(),
-                        iconId = it.res,
+                        icon = InternetTileIconModel.ResourceId(it.res),
                         stateDescription = null,
                         contentDescription = secondary,
                     )
@@ -208,16 +197,18 @@
         }
 
     private val notConnectedFlow: StateFlow<InternetTileModel> =
-        combine(
-                wifiInteractor.areNetworksAvailable,
-                airplaneModeRepository.isAirplaneMode,
-            ) { networksAvailable, isAirplaneMode ->
+        combine(wifiInteractor.areNetworksAvailable, airplaneModeRepository.isAirplaneMode) {
+                networksAvailable,
+                isAirplaneMode ->
                 when {
                     isAirplaneMode -> {
                         val secondary = context.getString(R.string.status_bar_airplane)
                         InternetTileModel.Inactive(
                             secondaryTitle = secondary,
-                            iconId = R.drawable.ic_qs_no_internet_unavailable,
+                            icon =
+                                InternetTileIconModel.ResourceId(
+                                    R.drawable.ic_qs_no_internet_unavailable
+                                ),
                             stateDescription = null,
                             contentDescription = ContentDescription.Loaded(secondary),
                         )
@@ -227,10 +218,13 @@
                             context.getString(R.string.quick_settings_networks_available)
                         InternetTileModel.Inactive(
                             secondaryTitle = secondary,
-                            iconId = R.drawable.ic_qs_no_internet_available,
+                            icon =
+                                InternetTileIconModel.ResourceId(
+                                    R.drawable.ic_qs_no_internet_available
+                                ),
                             stateDescription = null,
                             contentDescription =
-                                ContentDescription.Loaded("$internetLabel,$secondary")
+                                ContentDescription.Loaded("$internetLabel,$secondary"),
                         )
                     }
                     else -> {
@@ -248,7 +242,7 @@
      */
     override fun tileData(
         user: UserHandle,
-        triggers: Flow<DataUpdateTrigger>
+        triggers: Flow<DataUpdateTrigger>,
     ): Flow<InternetTileModel> =
         connectivityRepository.defaultConnections.flatMapLatest {
             when {
@@ -265,7 +259,7 @@
         val NOT_CONNECTED_NETWORKS_UNAVAILABLE =
             InternetTileModel.Inactive(
                 secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable),
-                iconId = R.drawable.ic_qs_no_internet_unavailable,
+                icon = InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_unavailable),
                 stateDescription = null,
                 contentDescription =
                     ContentDescription.Resource(R.string.quick_settings_networks_unavailable),
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt
index ece90461..15b4e47 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt
@@ -17,23 +17,21 @@
 package com.android.systemui.qs.tiles.impl.internet.domain.model
 
 import com.android.systemui.common.shared.model.ContentDescription
-import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
+import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel
 
 /** Model describing the state that the QS Internet tile should be in. */
 sealed interface InternetTileModel {
     val secondaryTitle: CharSequence?
     val secondaryLabel: Text?
-    val iconId: Int?
-    val icon: Icon?
+    val icon: InternetTileIconModel
     val stateDescription: ContentDescription?
     val contentDescription: ContentDescription?
 
     data class Active(
         override val secondaryTitle: CharSequence? = null,
         override val secondaryLabel: Text? = null,
-        override val iconId: Int? = null,
-        override val icon: Icon? = null,
+        override val icon: InternetTileIconModel = InternetTileIconModel.Cellular(1),
         override val stateDescription: ContentDescription? = null,
         override val contentDescription: ContentDescription? = null,
     ) : InternetTileModel
@@ -41,8 +39,7 @@
     data class Inactive(
         override val secondaryTitle: CharSequence? = null,
         override val secondaryLabel: Text? = null,
-        override val iconId: Int? = null,
-        override val icon: Icon? = null,
+        override val icon: InternetTileIconModel = InternetTileIconModel.Cellular(1),
         override val stateDescription: ContentDescription? = null,
         override val contentDescription: ContentDescription? = null,
     ) : InternetTileModel
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt
index 7c8fbea..afb9a78 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt
@@ -16,10 +16,18 @@
 
 package com.android.systemui.qs.ui.viewmodel
 
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.launch
 
 /**
  * Models UI state used to render the content of the quick settings shade overlay.
@@ -31,11 +39,42 @@
 @AssistedInject
 constructor(
     val shadeInteractor: ShadeInteractor,
+    val sceneInteractor: SceneInteractor,
     val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory,
     val quickSettingsContainerViewModel: QuickSettingsContainerViewModel,
-) {
+) : ExclusiveActivatable() {
+
+    override suspend fun onActivated(): Nothing {
+        coroutineScope {
+            launch {
+                sceneInteractor.currentScene.collect { currentScene ->
+                    when (currentScene) {
+                        // TODO(b/369513770): The ShadeSession should be preserved in this scenario.
+                        Scenes.Bouncer ->
+                            shadeInteractor.collapseQuickSettingsShade(
+                                loggingReason = "bouncer shown while shade is open"
+                            )
+                    }
+                }
+            }
+
+            launch {
+                shadeInteractor.isShadeTouchable
+                    .distinctUntilChanged()
+                    .filter { !it }
+                    .collect {
+                        shadeInteractor.collapseQuickSettingsShade(
+                            loggingReason = "device became non-interactive"
+                        )
+                    }
+            }
+        }
+
+        awaitCancellation()
+    }
+
     fun onScrimClicked() {
-        shadeInteractor.collapseQuickSettingsShade(loggingReason = "Shade scrim clicked")
+        shadeInteractor.collapseQuickSettingsShade(loggingReason = "shade scrim clicked")
     }
 
     @AssistedFactory
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
index a5f4a89..4d2bc91 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
@@ -61,11 +61,11 @@
         uiEventLogger,
         notificationManager,
         userContextProvider,
-        keyguardDismissUtil
+        keyguardDismissUtil,
     ) {
 
-    private val commandHandler =
-        IssueRecordingServiceCommandHandler(
+    private val session =
+        IssueRecordingServiceSession(
             bgExecutor,
             dialogTransitionAnimator,
             panelInteractor,
@@ -86,7 +86,7 @@
         Log.d(getTag(), "handling action: ${intent?.action}")
         when (intent?.action) {
             ACTION_START -> {
-                commandHandler.handleStartCommand()
+                session.start()
                 if (!issueRecordingState.recordScreen) {
                     // If we don't want to record the screen, the ACTION_SHOW_START_NOTIF action
                     // will circumvent the RecordingService's screen recording start code.
@@ -94,12 +94,12 @@
                 }
             }
             ACTION_STOP,
-            ACTION_STOP_NOTIF -> commandHandler.handleStopCommand(contentResolver)
+            ACTION_STOP_NOTIF -> session.stop(contentResolver)
             ACTION_SHARE -> {
-                commandHandler.handleShareCommand(
+                session.share(
                     intent.getIntExtra(EXTRA_NOTIFICATION_ID, mNotificationId),
                     intent.getParcelableExtra(EXTRA_PATH, Uri::class.java),
-                    this
+                    this,
                 )
                 // Unlike all other actions, action_share has different behavior for the screen
                 // recording qs tile than it does for the record issue qs tile. Return sticky to
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandler.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceSession.kt
similarity index 88%
rename from packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandler.kt
rename to packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceSession.kt
index 32de0f3..e4d3e6c 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceSession.kt
@@ -34,9 +34,11 @@
 /**
  * This class exists to unit test the business logic encapsulated in IssueRecordingService. Android
  * specifically calls out that there is no supported way to test IntentServices here:
- * https://developer.android.com/training/testing/other-components/services
+ * https://developer.android.com/training/testing/other-components/services, and mentions that the
+ * best way to add unit tests, is to introduce a separate class containing the business logic of
+ * that service, and test the functionality via that class.
  */
-class IssueRecordingServiceCommandHandler(
+class IssueRecordingServiceSession(
     private val bgExecutor: Executor,
     private val dialogTransitionAnimator: DialogTransitionAnimator,
     private val panelInteractor: PanelInteractor,
@@ -47,12 +49,12 @@
     private val userContextProvider: UserContextProvider,
 ) {
 
-    fun handleStartCommand() {
+    fun start() {
         bgExecutor.execute { traceurMessageSender.startTracing(issueRecordingState.traceConfig) }
         issueRecordingState.isRecording = true
     }
 
-    fun handleStopCommand(contentResolver: ContentResolver) {
+    fun stop(contentResolver: ContentResolver) {
         bgExecutor.execute {
             if (issueRecordingState.traceConfig.longTrace) {
                 Settings.Global.putInt(contentResolver, NOTIFY_SESSION_ENDED_SETTING, DISABLED)
@@ -62,12 +64,12 @@
         issueRecordingState.isRecording = false
     }
 
-    fun handleShareCommand(notificationId: Int, screenRecording: Uri?, context: Context) {
+    fun share(notificationId: Int, screenRecording: Uri?, context: Context) {
         bgExecutor.execute {
             notificationManager.cancelAsUser(
                 null,
                 notificationId,
-                UserHandle(userContextProvider.userContext.userId)
+                UserHandle(userContextProvider.userContext.userId),
             )
 
             if (issueRecordingState.takeBugreport) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 57be629..0ad22e0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -61,6 +61,7 @@
 import com.android.systemui.keyguard.ui.viewmodel.OccludedToAodTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.OccludedToGoneTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.OffToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
@@ -132,6 +133,7 @@
     private val occludedToAodTransitionViewModel: OccludedToAodTransitionViewModel,
     private val occludedToGoneTransitionViewModel: OccludedToGoneTransitionViewModel,
     private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
+    private val offToLockscreenTransitionViewModel: OffToLockscreenTransitionViewModel,
     private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
     private val primaryBouncerToLockscreenTransitionViewModel:
         PrimaryBouncerToLockscreenTransitionViewModel,
@@ -444,6 +446,7 @@
             occludedToAodTransitionViewModel.lockscreenAlpha,
             occludedToGoneTransitionViewModel.notificationAlpha(viewState),
             occludedToLockscreenTransitionViewModel.lockscreenAlpha,
+            offToLockscreenTransitionViewModel.lockscreenAlpha,
             primaryBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
             glanceableHubToLockscreenTransitionViewModel.keyguardAlpha,
             lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/model/InternetTileIconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/model/InternetTileIconModel.kt
new file mode 100644
index 0000000..f8958e0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/model/InternetTileIconModel.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.shared.ui.model
+
+import com.android.systemui.common.shared.model.Icon
+
+sealed interface InternetTileIconModel {
+    data class ResourceId(val resId: Int) : InternetTileIconModel
+
+    data class Cellular(val level: Int) : InternetTileIconModel
+
+    data class Satellite(val resourceIcon: Icon.Resource) : InternetTileIconModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
index 78edd39..a1d5cbe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
@@ -26,6 +26,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.paneTitle
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.testTagsAsResourceId
 import androidx.lifecycle.DefaultLifecycleObserver
@@ -102,7 +103,13 @@
         val cachedDarkTheme = remember { isCurrentlyInDarkTheme }
         PlatformTheme(isDarkTheme = cachedDarkTheme) {
             AlertDialogContent(
-                modifier = Modifier.semantics { testTagsAsResourceId = true },
+                modifier =
+                    Modifier.semantics {
+                        testTagsAsResourceId = true
+                        paneTitle = dialog.context.getString(
+                            R.string.accessibility_desc_quick_settings
+                        )
+                    },
                 title = {
                     Text(
                         modifier = Modifier.testTag("modes_title"),
@@ -137,7 +144,7 @@
         }
         activityStarter.startActivity(
             ZEN_MODE_SETTINGS_INTENT,
-            true /* dismissShade */,
+            /* dismissShade= */ true,
             animationController,
         )
     }
@@ -181,7 +188,7 @@
         if (animationController == null) {
             currentDialog?.dismiss()
         }
-        activityStarter.startActivity(intent, true, /* dismissShade */ animationController)
+        activityStarter.startActivity(intent, /* dismissShade= */ true, animationController)
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt
index d03b2e7..e1f7bd5 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt
@@ -29,6 +29,7 @@
 import com.android.compose.theme.PlatformTheme
 import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger
 import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger.TutorialContext
+import com.android.systemui.inputdevice.tutorial.KeyboardTouchpadTutorialMetricsLogger
 import com.android.systemui.touchpad.tutorial.ui.composable.BackGestureTutorialScreen
 import com.android.systemui.touchpad.tutorial.ui.composable.HomeGestureTutorialScreen
 import com.android.systemui.touchpad.tutorial.ui.composable.RecentAppsGestureTutorialScreen
@@ -45,6 +46,7 @@
 constructor(
     private val viewModelFactory: TouchpadTutorialViewModel.Factory,
     private val logger: InputDeviceTutorialLogger,
+    private val metricsLogger: KeyboardTouchpadTutorialMetricsLogger,
 ) : ComponentActivity() {
 
     private val vm by viewModels<TouchpadTutorialViewModel>(factoryProducer = { viewModelFactory })
@@ -57,6 +59,7 @@
         }
         // required to handle 3+ fingers on touchpad
         window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY)
+        metricsLogger.logPeripheralTutorialLaunchedFromSettings()
         logger.logOpenTutorial(TutorialContext.TOUCHPAD_TUTORIAL)
     }
 
@@ -85,7 +88,7 @@
                 onBackTutorialClicked = { vm.goTo(BACK_GESTURE) },
                 onHomeTutorialClicked = { vm.goTo(HOME_GESTURE) },
                 onRecentAppsTutorialClicked = { vm.goTo(RECENT_APPS_GESTURE) },
-                onDoneButtonClicked = closeTutorial
+                onDoneButtonClicked = closeTutorial,
             )
         BACK_GESTURE ->
             BackGestureTutorialScreen(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java
index aa8c6b7..e160ff1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java
@@ -28,6 +28,7 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 import android.content.res.Configuration;
@@ -643,6 +644,46 @@
         environment.verifyInputSessionDispose();
     }
 
+    @Test
+    public void testSessionPopAfterDestroy() {
+        final TouchHandler touchHandler = createTouchHandler();
+
+        final Environment environment = new Environment(Stream.of(touchHandler)
+                .collect(Collectors.toCollection(HashSet::new)), mKosmos);
+
+        final InputEvent initialEvent = Mockito.mock(InputEvent.class);
+        environment.publishInputEvent(initialEvent);
+
+        // Ensure session started
+        final InputChannelCompat.InputEventListener eventListener =
+                registerInputEventListener(touchHandler);
+
+        // First event will be missed since we register after the execution loop,
+        final InputEvent event = Mockito.mock(InputEvent.class);
+        environment.publishInputEvent(event);
+        verify(eventListener).onInputEvent(eq(event));
+
+        final ArgumentCaptor<TouchHandler.TouchSession> touchSessionArgumentCaptor =
+                ArgumentCaptor.forClass(TouchHandler.TouchSession.class);
+
+        verify(touchHandler).onSessionStart(touchSessionArgumentCaptor.capture());
+
+        environment.updateLifecycle(Lifecycle.State.DESTROYED);
+
+        // Check to make sure the input session is now disposed.
+        environment.verifyInputSessionDispose();
+
+        clearInvocations(environment.mInputFactory);
+
+        // Pop the session
+        touchSessionArgumentCaptor.getValue().pop();
+
+        environment.executeAll();
+
+        // Ensure no input sessions were created due to the session reset.
+        verifyNoMoreInteractions(environment.mInputFactory);
+    }
+
 
     @Test
     public void testPilfering() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index b0810a9..6608542 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -100,6 +100,7 @@
 import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.flags.SystemPropertiesHelper;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionBootInteractor;
 import com.android.systemui.kosmos.KosmosJavaAdapter;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.navigationbar.NavigationModeController;
@@ -199,6 +200,7 @@
     private @Mock ShadeWindowLogger mShadeWindowLogger;
     private @Mock SelectedUserInteractor mSelectedUserInteractor;
     private @Mock KeyguardInteractor mKeyguardInteractor;
+    private @Mock KeyguardTransitionBootInteractor mKeyguardTransitionBootInteractor;
     private @Captor ArgumentCaptor<KeyguardStateController.Callback>
             mKeyguardStateControllerCallback;
     private @Captor ArgumentCaptor<KeyguardUpdateMonitorCallback>
@@ -1294,6 +1296,7 @@
                 () -> mock(WindowManagerLockscreenVisibilityManager.class),
                 mSelectedUserInteractor,
                 mKeyguardInteractor,
+                mKeyguardTransitionBootInteractor,
                 mock(WindowManagerOcclusionManager.class));
         mViewMediator.start();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt
index 8a5af09..ad5eeab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt
@@ -65,7 +65,7 @@
             flowOf(Scenes.Lockscreen),
             progress,
             false,
-            flowOf(false)
+            flowOf(false),
         )
 
     private val goneToLs =
@@ -75,7 +75,7 @@
             flowOf(Scenes.Lockscreen),
             progress,
             false,
-            flowOf(false)
+            flowOf(false),
         )
 
     @Before
@@ -84,7 +84,8 @@
         kosmos.sceneContainerRepository.setTransitionState(sceneTransitions)
         testScope.launch {
             kosmos.realKeyguardTransitionRepository.emitInitialStepsFromOff(
-                KeyguardState.LOCKSCREEN
+                KeyguardState.LOCKSCREEN,
+                testSetup = true,
             )
         }
     }
@@ -105,11 +106,7 @@
             )
 
             progress.value = 0.4f
-            assertTransition(
-                step = currentStep!!,
-                state = TransitionState.RUNNING,
-                progress = 0.4f,
-            )
+            assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f)
 
             sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Gone)
             assertTransition(
@@ -142,11 +139,7 @@
             )
 
             progress.value = 0.4f
-            assertTransition(
-                step = currentStep!!,
-                state = TransitionState.RUNNING,
-                progress = 0.4f,
-            )
+            assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f)
 
             sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen)
 
@@ -191,7 +184,7 @@
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.AOD,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
             sceneTransitions.value = lsToGone
@@ -205,11 +198,7 @@
             )
 
             progress.value = 0.4f
-            assertTransition(
-                step = currentStep!!,
-                state = TransitionState.RUNNING,
-                progress = 0.4f,
-            )
+            assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f)
 
             sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen)
 
@@ -257,11 +246,7 @@
             )
 
             progress.value = 0.4f
-            assertTransition(
-                step = currentStep!!,
-                state = TransitionState.RUNNING,
-                progress = 0.4f,
-            )
+            assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f)
 
             sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen)
 
@@ -297,7 +282,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -330,11 +315,7 @@
             )
 
             progress.value = 0.4f
-            assertTransition(
-                step = currentStep!!,
-                state = TransitionState.RUNNING,
-                progress = 0.4f,
-            )
+            assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f)
 
             sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Gone)
             val stepM3 = allSteps[allSteps.size - 3]
@@ -393,7 +374,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -466,7 +447,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -523,7 +504,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -577,11 +558,7 @@
             )
 
             progress.value = 0.4f
-            assertTransition(
-                step = currentStep!!,
-                state = TransitionState.RUNNING,
-                progress = 0.4f,
-            )
+            assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f)
 
             kosmos.realKeyguardTransitionRepository.startTransition(
                 TransitionInfo(
@@ -589,7 +566,7 @@
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.AOD,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
 
@@ -641,11 +618,7 @@
             )
 
             progress.value = 0.4f
-            assertTransition(
-                step = currentStep!!,
-                state = TransitionState.RUNNING,
-                progress = 0.4f,
-            )
+            assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f)
 
             kosmos.realKeyguardTransitionRepository.startTransition(
                 TransitionInfo(
@@ -653,7 +626,7 @@
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.AOD,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
 
@@ -702,11 +675,7 @@
             )
 
             progress.value = 0.4f
-            assertTransition(
-                step = currentStep!!,
-                state = TransitionState.RUNNING,
-                progress = 0.4f,
-            )
+            assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f)
 
             kosmos.realKeyguardTransitionRepository.startTransition(
                 TransitionInfo(
@@ -714,7 +683,7 @@
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.AOD,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
 
@@ -736,7 +705,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -777,7 +746,7 @@
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.AOD,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
 
@@ -799,7 +768,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
             allSteps[allSteps.size - 3]
 
@@ -858,7 +827,7 @@
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.AOD,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
 
@@ -880,7 +849,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -959,7 +928,7 @@
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.AOD,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
 
@@ -977,7 +946,7 @@
                     from = KeyguardState.AOD,
                     to = KeyguardState.DOZING,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
 
@@ -995,7 +964,7 @@
                     from = KeyguardState.DOZING,
                     to = KeyguardState.OCCLUDED,
                     animator = null,
-                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                    modeOnCanceled = TransitionModeOnCanceled.RESET,
                 )
             )
 
@@ -1017,7 +986,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -1077,7 +1046,7 @@
                         from = KeyguardState.LOCKSCREEN,
                         to = KeyguardState.AOD,
                         animator = null,
-                        modeOnCanceled = TransitionModeOnCanceled.RESET
+                        modeOnCanceled = TransitionModeOnCanceled.RESET,
                     )
                 )
 
@@ -1092,7 +1061,7 @@
             kosmos.realKeyguardTransitionRepository.updateTransition(
                 ktfUuid!!,
                 1f,
-                TransitionState.FINISHED
+                TransitionState.FINISHED,
             )
 
             assertTransition(
@@ -1110,7 +1079,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -1171,7 +1140,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -1235,7 +1204,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -1291,7 +1260,7 @@
                     flowOf(Scenes.Lockscreen),
                     progress,
                     false,
-                    flowOf(false)
+                    flowOf(false),
                 )
 
             assertTransition(
@@ -1308,7 +1277,7 @@
         from: KeyguardState? = null,
         to: KeyguardState? = null,
         state: TransitionState? = null,
-        progress: Float? = null
+        progress: Float? = null,
     ) {
         if (from != null) assertThat(step.from).isEqualTo(from)
         if (to != null) assertThat(step.to).isEqualTo(to)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
index d32d8cc..fb376ce 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
@@ -1890,7 +1890,7 @@
 
         // Callback gets an updated state
         val state = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
-        stateCallbackCaptor.value.invoke(KEY, state)
+        onStateUpdated(KEY, state)
 
         // Listener is notified of updated state
         verify(listener)
@@ -1911,7 +1911,7 @@
 
         // No media added with this key
 
-        stateCallbackCaptor.value.invoke(KEY, state)
+        onStateUpdated(KEY, state)
         verify(listener, never())
             .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
@@ -1928,7 +1928,7 @@
         val state = PlaybackState.Builder().build()
 
         // Then no changes are made
-        stateCallbackCaptor.value.invoke(KEY, state)
+        onStateUpdated(KEY, state)
         verify(listener, never())
             .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
@@ -1939,7 +1939,7 @@
         whenever(controller.playbackState).thenReturn(state)
 
         addNotificationAndLoad()
-        stateCallbackCaptor.value.invoke(KEY, state)
+        onStateUpdated(KEY, state)
 
         verify(listener)
             .onMediaDataLoaded(
@@ -1983,7 +1983,7 @@
             backgroundExecutor.runAllReady()
             foregroundExecutor.runAllReady()
 
-            stateCallbackCaptor.value.invoke(PACKAGE_NAME, state)
+            onStateUpdated(PACKAGE_NAME, state)
 
             verify(listener)
                 .onMediaDataLoaded(
@@ -2008,7 +2008,7 @@
                 .build()
 
         addNotificationAndLoad()
-        stateCallbackCaptor.value.invoke(KEY, state)
+        onStateUpdated(KEY, state)
 
         verify(listener)
             .onMediaDataLoaded(
@@ -2518,4 +2518,10 @@
                 eq(false),
             )
     }
+
+    private fun onStateUpdated(key: String, state: PlaybackState) {
+        stateCallbackCaptor.value.invoke(key, state)
+        backgroundExecutor.runAllReady()
+        foregroundExecutor.runAllReady()
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
index 90af932..7d364bd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
@@ -1967,7 +1967,7 @@
 
         // Callback gets an updated state
         val state = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
-        stateCallbackCaptor.value.invoke(KEY, state)
+        testScope.onStateUpdated(KEY, state)
 
         // Listener is notified of updated state
         verify(listener)
@@ -1988,7 +1988,7 @@
 
         // No media added with this key
 
-        stateCallbackCaptor.value.invoke(KEY, state)
+        testScope.onStateUpdated(KEY, state)
         verify(listener, never())
             .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
@@ -2005,7 +2005,7 @@
         val state = PlaybackState.Builder().build()
 
         // Then no changes are made
-        stateCallbackCaptor.value.invoke(KEY, state)
+        testScope.onStateUpdated(KEY, state)
         verify(listener, never())
             .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
@@ -2016,7 +2016,7 @@
         whenever(controller.playbackState).thenReturn(state)
 
         addNotificationAndLoad()
-        stateCallbackCaptor.value.invoke(KEY, state)
+        testScope.onStateUpdated(KEY, state)
 
         verify(listener)
             .onMediaDataLoaded(
@@ -2059,7 +2059,7 @@
         backgroundExecutor.runAllReady()
         foregroundExecutor.runAllReady()
 
-        stateCallbackCaptor.value.invoke(PACKAGE_NAME, state)
+        testScope.onStateUpdated(PACKAGE_NAME, state)
 
         verify(listener)
             .onMediaDataLoaded(
@@ -2084,7 +2084,7 @@
                 .build()
 
         addNotificationAndLoad()
-        stateCallbackCaptor.value.invoke(KEY, state)
+        testScope.onStateUpdated(KEY, state)
 
         verify(listener)
             .onMediaDataLoaded(
@@ -2603,4 +2603,11 @@
                 eq(false),
             )
     }
+
+    /** Helper function to update state and run executors */
+    private fun TestScope.onStateUpdated(key: String, state: PlaybackState) {
+        stateCallbackCaptor.value.invoke(key, state)
+        runCurrent()
+        advanceUntilIdle()
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
index 03667cf..570c640 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
@@ -21,19 +21,19 @@
 import android.content.res.Configuration
 import android.database.ContentObserver
 import android.os.LocaleList
+import android.platform.test.flag.junit.FlagsParameterization
 import android.provider.Settings
 import android.testing.TestableLooper
 import android.util.MathUtils.abs
 import android.view.View
-import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.SceneKey
 import com.android.internal.logging.InstanceId
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.systemui.Flags.mediaControlsUmoInflationInBackground
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.DisableSceneContainer
@@ -71,7 +71,6 @@
 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.testKosmos
-import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.settings.GlobalSettings
 import com.android.systemui.util.settings.unconfinedDispatcherFakeSettings
@@ -106,6 +105,8 @@
 import org.mockito.kotlin.any
 import org.mockito.kotlin.capture
 import org.mockito.kotlin.eq
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 private val DATA = MediaTestUtils.emptyMediaData
 
@@ -116,8 +117,8 @@
 @ExperimentalCoroutinesApi
 @SmallTest
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
-@RunWith(AndroidJUnit4::class)
-class MediaCarouselControllerTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class MediaCarouselControllerTest(flags: FlagsParameterization) : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testDispatcher = kosmos.unconfinedTestDispatcher
     private val secureSettings = kosmos.unconfinedDispatcherFakeSettings
@@ -129,7 +130,6 @@
     @Mock lateinit var mediaHostStatesManager: MediaHostStatesManager
     @Mock lateinit var mediaHostState: MediaHostState
     @Mock lateinit var activityStarter: ActivityStarter
-    @Mock @Main private lateinit var executor: DelayableExecutor
     @Mock lateinit var mediaDataManager: MediaDataManager
     @Mock lateinit var configurationController: ConfigurationController
     @Mock lateinit var falsingManager: FalsingManager
@@ -153,16 +153,33 @@
 
     private val clock = FakeSystemClock()
     private lateinit var bgExecutor: FakeExecutor
+    private lateinit var uiExecutor: FakeExecutor
     private lateinit var mediaCarouselController: MediaCarouselController
 
     private var originalResumeSetting =
         Settings.Secure.getInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 1)
 
+    companion object {
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return FlagsParameterization.progressionOf(
+                com.android.systemui.Flags.FLAG_MEDIA_CONTROLS_UMO_INFLATION_IN_BACKGROUND
+            )
+        }
+    }
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags)
+    }
+
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
         context.resources.configuration.setLocales(LocaleList(Locale.US, Locale.UK))
         bgExecutor = FakeExecutor(clock)
+        uiExecutor = FakeExecutor(clock)
+
         mediaCarouselController =
             MediaCarouselController(
                 applicationScope = kosmos.applicationCoroutineScope,
@@ -173,7 +190,7 @@
                 activityStarter = activityStarter,
                 systemClock = clock,
                 mainDispatcher = kosmos.testDispatcher,
-                executor = executor,
+                uiExecutor = uiExecutor,
                 bgExecutor = bgExecutor,
                 backgroundDispatcher = testDispatcher,
                 mediaManager = mediaDataManager,
@@ -201,10 +218,11 @@
         whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
         MediaPlayerData.clear()
         FakeExecutor.exhaustExecutors(bgExecutor)
+        FakeExecutor.exhaustExecutors(uiExecutor)
         verify(globalSettings)
             .registerContentObserverSync(
                 eq(Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE)),
-                capture(settingsObserverCaptor)
+                capture(settingsObserverCaptor),
             )
     }
 
@@ -213,7 +231,7 @@
         Settings.Secure.putInt(
             context.contentResolver,
             Settings.Secure.MEDIA_CONTROLS_RESUME,
-            originalResumeSetting
+            originalResumeSetting,
         )
     }
 
@@ -227,9 +245,9 @@
                     active = true,
                     isPlaying = true,
                     playbackLocation = MediaData.PLAYBACK_LOCAL,
-                    resumption = false
+                    resumption = false,
                 ),
-                4500L
+                4500L,
             )
 
         val playingCast =
@@ -239,9 +257,9 @@
                     active = true,
                     isPlaying = true,
                     playbackLocation = MediaData.PLAYBACK_CAST_LOCAL,
-                    resumption = false
+                    resumption = false,
                 ),
-                5000L
+                5000L,
             )
 
         val pausedLocal =
@@ -251,9 +269,9 @@
                     active = true,
                     isPlaying = false,
                     playbackLocation = MediaData.PLAYBACK_LOCAL,
-                    resumption = false
+                    resumption = false,
                 ),
-                1000L
+                1000L,
             )
 
         val pausedCast =
@@ -263,9 +281,9 @@
                     active = true,
                     isPlaying = false,
                     playbackLocation = MediaData.PLAYBACK_CAST_LOCAL,
-                    resumption = false
+                    resumption = false,
                 ),
-                2000L
+                2000L,
             )
 
         val playingRcn =
@@ -275,9 +293,9 @@
                     active = true,
                     isPlaying = true,
                     playbackLocation = MediaData.PLAYBACK_CAST_REMOTE,
-                    resumption = false
+                    resumption = false,
                 ),
-                5000L
+                5000L,
             )
 
         val pausedRcn =
@@ -287,9 +305,9 @@
                     active = true,
                     isPlaying = false,
                     playbackLocation = MediaData.PLAYBACK_CAST_REMOTE,
-                    resumption = false
+                    resumption = false,
                 ),
-                5000L
+                5000L,
             )
 
         val active =
@@ -299,9 +317,9 @@
                     active = true,
                     isPlaying = false,
                     playbackLocation = MediaData.PLAYBACK_LOCAL,
-                    resumption = true
+                    resumption = true,
                 ),
-                250L
+                250L,
             )
 
         val resume1 =
@@ -311,9 +329,9 @@
                     active = false,
                     isPlaying = false,
                     playbackLocation = MediaData.PLAYBACK_LOCAL,
-                    resumption = true
+                    resumption = true,
                 ),
-                500L
+                500L,
             )
 
         val resume2 =
@@ -323,9 +341,9 @@
                     active = false,
                     isPlaying = false,
                     playbackLocation = MediaData.PLAYBACK_LOCAL,
-                    resumption = true
+                    resumption = true,
                 ),
-                1000L
+                1000L,
             )
 
         val activeMoreRecent =
@@ -336,9 +354,9 @@
                     isPlaying = false,
                     playbackLocation = MediaData.PLAYBACK_LOCAL,
                     resumption = true,
-                    lastActive = 2L
+                    lastActive = 2L,
                 ),
-                1000L
+                1000L,
             )
 
         val activeLessRecent =
@@ -349,9 +367,9 @@
                     isPlaying = false,
                     playbackLocation = MediaData.PLAYBACK_LOCAL,
                     resumption = true,
-                    lastActive = 1L
+                    lastActive = 1L,
                 ),
-                1000L
+                1000L,
             )
         // Expected ordering for media players:
         // Actively playing local sessions
@@ -370,7 +388,7 @@
                 pausedRcn,
                 active,
                 resume2,
-                resume1
+                resume1,
             )
 
         expected.forEach {
@@ -380,7 +398,7 @@
                 it.second.copy(notificationKey = it.first),
                 panel,
                 clock,
-                isSsReactivated = false
+                isSsReactivated = false,
             )
         }
 
@@ -403,7 +421,7 @@
             EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
             panel,
             true,
-            clock
+            clock,
         )
 
         // Then it should be shown immediately after any actively playing controls
@@ -421,7 +439,7 @@
         listener.value.onSmartspaceMediaDataLoaded(
             SMARTSPACE_KEY,
             EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
-            true
+            true,
         )
 
         // Then it should be shown immediately after any actively playing controls
@@ -439,7 +457,7 @@
             EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
             panel,
             false,
-            clock
+            clock,
         )
 
         // Then it should be shown at the end of the carousel's active entries
@@ -461,8 +479,8 @@
                 active = true,
                 isPlaying = true,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = false
-            )
+                resumption = false,
+            ),
         )
         listener.value.onMediaDataLoaded(
             PLAYING_LOCAL,
@@ -471,19 +489,20 @@
                 active = true,
                 isPlaying = false,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = true
-            )
+                resumption = true,
+            ),
         )
+        runAllReady()
 
         assertEquals(
             MediaPlayerData.getMediaPlayerIndex(PAUSED_LOCAL),
-            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
+            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex,
         )
         // paused player order should stays the same in visibleMediaPLayer map.
         // paused player order should be first in mediaPlayer map.
         assertEquals(
             MediaPlayerData.visiblePlayerKeys().elementAt(3),
-            MediaPlayerData.playerKeys().elementAt(0)
+            MediaPlayerData.playerKeys().elementAt(0),
         )
     }
 
@@ -506,7 +525,7 @@
         mediaCarouselController.onDesiredLocationChanged(
             LOCATION_QS,
             mediaHostState,
-            animate = false
+            animate = false,
         )
         bgExecutor.runAllReady()
         verify(logger).logCarouselPosition(LOCATION_QS)
@@ -517,7 +536,7 @@
         mediaCarouselController.onDesiredLocationChanged(
             MediaHierarchyManager.LOCATION_QQS,
             mediaHostState,
-            animate = false
+            animate = false,
         )
         bgExecutor.runAllReady()
         verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QQS)
@@ -528,7 +547,7 @@
         mediaCarouselController.onDesiredLocationChanged(
             MediaHierarchyManager.LOCATION_LOCKSCREEN,
             mediaHostState,
-            animate = false
+            animate = false,
         )
         bgExecutor.runAllReady()
         verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_LOCKSCREEN)
@@ -539,7 +558,7 @@
         mediaCarouselController.onDesiredLocationChanged(
             MediaHierarchyManager.LOCATION_DREAM_OVERLAY,
             mediaHostState,
-            animate = false
+            animate = false,
         )
         bgExecutor.runAllReady()
         verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_DREAM_OVERLAY)
@@ -570,8 +589,8 @@
                 active = true,
                 isPlaying = true,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = false
-            )
+                resumption = false,
+            ),
         )
         listener.value.onMediaDataLoaded(
             PAUSED_LOCAL,
@@ -580,14 +599,15 @@
                 active = true,
                 isPlaying = false,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = false
-            )
+                resumption = false,
+            ),
         )
+        runAllReady()
         // adding a media recommendation card.
         listener.value.onSmartspaceMediaDataLoaded(
             SMARTSPACE_KEY,
             EMPTY_SMARTSPACE_MEDIA_DATA,
-            false
+            false,
         )
         mediaCarouselController.shouldScrollToKey = true
         // switching between media players.
@@ -598,8 +618,8 @@
                 active = true,
                 isPlaying = false,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = true
-            )
+                resumption = true,
+            ),
         )
         listener.value.onMediaDataLoaded(
             PAUSED_LOCAL,
@@ -608,13 +628,14 @@
                 active = true,
                 isPlaying = true,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = false
-            )
+                resumption = false,
+            ),
         )
+        runAllReady()
 
         assertEquals(
             MediaPlayerData.getMediaPlayerIndex(PAUSED_LOCAL),
-            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
+            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex,
         )
     }
 
@@ -626,7 +647,7 @@
         listener.value.onSmartspaceMediaDataLoaded(
             SMARTSPACE_KEY,
             EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = "PACKAGE_NAME", isActive = true),
-            false
+            false,
         )
         listener.value.onMediaDataLoaded(
             PLAYING_LOCAL,
@@ -635,14 +656,15 @@
                 active = true,
                 isPlaying = true,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = false
-            )
+                resumption = false,
+            ),
         )
+        runAllReady()
 
         var playerIndex = MediaPlayerData.getMediaPlayerIndex(PLAYING_LOCAL)
         assertEquals(
             playerIndex,
-            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
+            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex,
         )
         assertEquals(playerIndex, 0)
 
@@ -657,9 +679,10 @@
                 isPlaying = true,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
                 resumption = false,
-                packageName = "PACKAGE_NAME"
-            )
+                packageName = "PACKAGE_NAME",
+            ),
         )
+        runAllReady()
         playerIndex = MediaPlayerData.getMediaPlayerIndex(PLAYING_LOCAL)
         assertEquals(playerIndex, 0)
     }
@@ -704,7 +727,7 @@
             player1.second.copy(notificationKey = player1.first),
             panel,
             clock,
-            isSsReactivated = false
+            isSsReactivated = false,
         )
 
         assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent1)
@@ -717,7 +740,7 @@
             player2.second.copy(notificationKey = player2.first),
             panel,
             clock,
-            isSsReactivated = false
+            isSsReactivated = false,
         )
 
         // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
@@ -732,7 +755,7 @@
             player3.second.copy(notificationKey = player3.first),
             panel,
             clock,
-            isSsReactivated = false
+            isSsReactivated = false,
         )
 
         // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
@@ -822,7 +845,7 @@
         listener.value.onSmartspaceMediaDataLoaded(
             SMARTSPACE_KEY,
             EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
-            true
+            true,
         )
 
         // Then the carousel is updated
@@ -841,7 +864,7 @@
         listener.value.onSmartspaceMediaDataLoaded(
             SMARTSPACE_KEY,
             EMPTY_SMARTSPACE_MEDIA_DATA,
-            false
+            false,
         )
 
         // Then it is added to the carousel with correct state
@@ -886,7 +909,7 @@
             transitionRepository.sendTransitionSteps(
                 from = KeyguardState.LOCKSCREEN,
                 to = KeyguardState.GONE,
-                this
+                this,
             )
 
             verify(mediaCarousel).visibility = View.VISIBLE
@@ -932,7 +955,7 @@
             transitionRepository.sendTransitionSteps(
                 from = KeyguardState.GONE,
                 to = KeyguardState.LOCKSCREEN,
-                this
+                this,
             )
 
             assertEquals(true, updatedVisibility)
@@ -961,7 +984,7 @@
             transitionRepository.sendTransitionSteps(
                 from = KeyguardState.GONE,
                 to = KeyguardState.LOCKSCREEN,
-                this
+                this,
             )
 
             assertEquals(true, updatedVisibility)
@@ -1125,12 +1148,14 @@
         Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
         val pausedMedia = DATA.copy(isPlaying = false)
         listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, pausedMedia)
+        runAllReady()
         mediaCarouselController.onSwipeToDismiss()
 
         // When it can be removed immediately on update
         whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(true)
         val inactiveMedia = pausedMedia.copy(active = false)
         listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, inactiveMedia)
+        runAllReady()
 
         // This is processed as a user-initiated dismissal
         verify(debugLogger).logMediaRemoved(eq(PAUSED_LOCAL), eq(true))
@@ -1148,12 +1173,14 @@
 
         val pausedMedia = DATA.copy(isPlaying = false)
         listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, pausedMedia)
+        runAllReady()
         mediaCarouselController.onSwipeToDismiss()
 
         // When it can't be removed immediately on update
         whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(false)
         val inactiveMedia = pausedMedia.copy(active = false)
         listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, inactiveMedia)
+        runAllReady()
         visualStabilityCallback.value.onReorderingAllowed()
 
         // This is processed as a user-initiated dismissal
@@ -1175,8 +1202,8 @@
                 active = true,
                 isPlaying = true,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = false
-            )
+                resumption = false,
+            ),
         )
         listener.value.onMediaDataLoaded(
             PAUSED_LOCAL,
@@ -1185,18 +1212,20 @@
                 active = true,
                 isPlaying = false,
                 playbackLocation = MediaData.PLAYBACK_LOCAL,
-                resumption = false
-            )
+                resumption = false,
+            ),
         )
+        runAllReady()
 
         val playersSize = MediaPlayerData.players().size
         reset(pageIndicator)
         function()
+        runAllReady()
 
         assertEquals(playersSize, MediaPlayerData.players().size)
         assertEquals(
             MediaPlayerData.getMediaPlayerIndex(PLAYING_LOCAL),
-            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
+            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex,
         )
     }
 
@@ -1225,4 +1254,11 @@
         )
         runCurrent()
     }
+
+    private fun runAllReady() {
+        if (mediaControlsUmoInflationInBackground()) {
+            bgExecutor.runAllReady()
+            uiExecutor.runAllReady()
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
index cea8857..7d5278e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
@@ -332,7 +332,10 @@
             // WHEN a row is pinned
             headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true))
             // AND the lock screen is shown
-            keyguardTransitionRepository.emitInitialStepsFromOff(to = KeyguardState.LOCKSCREEN)
+            keyguardTransitionRepository.emitInitialStepsFromOff(
+                to = KeyguardState.LOCKSCREEN,
+                testSetup = true,
+            )
 
             assertThat(showHeadsUpStatusBar).isFalse()
         }
@@ -345,7 +348,10 @@
             // WHEN a row is pinned
             headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true))
             // AND the lock screen is shown
-            keyguardTransitionRepository.emitInitialStepsFromOff(to = KeyguardState.LOCKSCREEN)
+            keyguardTransitionRepository.emitInitialStepsFromOff(
+                to = KeyguardState.LOCKSCREEN,
+                testSetup = true,
+            )
             // AND bypass is enabled
             faceAuthRepository.isBypassEnabled.value = true
 
@@ -359,7 +365,10 @@
 
             // WHEN no pinned rows
             // AND the lock screen is shown
-            keyguardTransitionRepository.emitInitialStepsFromOff(to = KeyguardState.LOCKSCREEN)
+            keyguardTransitionRepository.emitInitialStepsFromOff(
+                to = KeyguardState.LOCKSCREEN,
+                testSetup = true,
+            )
             // AND bypass is enabled
             faceAuthRepository.isBypassEnabled.value = true
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
index e396b56..0598b87 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
@@ -133,7 +133,10 @@
 
             // WHEN HUN displayed on the bypass lock screen
             headsUpRepository.setNotifications(FakeHeadsUpRowRepository("key 0", isPinned = true))
-            keyguardTransitionRepository.emitInitialStepsFromOff(KeyguardState.LOCKSCREEN)
+            keyguardTransitionRepository.emitInitialStepsFromOff(
+                KeyguardState.LOCKSCREEN,
+                testSetup = true,
+            )
             kosmos.sceneContainerRepository.snapToScene(Scenes.Lockscreen)
             faceAuthRepository.isBypassEnabled.value = true
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index a73c184..4d0e603 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -48,9 +48,8 @@
  * with OFF -> GONE. Construct with initInLockscreen = false if your test requires this behavior.
  */
 @SysUISingleton
-class FakeKeyguardTransitionRepository(
-    private val initInLockscreen: Boolean = true,
-) : KeyguardTransitionRepository {
+class FakeKeyguardTransitionRepository(private val initInLockscreen: Boolean = true) :
+    KeyguardTransitionRepository {
     private val _transitions =
         MutableSharedFlow<TransitionStep>(replay = 3, onBufferOverflow = BufferOverflow.DROP_OLDEST)
     override val transitions: SharedFlow<TransitionStep> = _transitions
@@ -63,7 +62,7 @@
                 ownerName = "",
                 from = KeyguardState.OFF,
                 to = KeyguardState.LOCKSCREEN,
-                animator = null
+                animator = null,
             )
         )
     override var currentTransitionInfoInternal = _currentTransitionInfo.asStateFlow()
@@ -71,12 +70,7 @@
     init {
         // Seed with a FINISHED transition in OFF, same as the real repository.
         _transitions.tryEmit(
-            TransitionStep(
-                KeyguardState.OFF,
-                KeyguardState.OFF,
-                1f,
-                TransitionState.FINISHED,
-            )
+            TransitionStep(KeyguardState.OFF, KeyguardState.OFF, 1f, TransitionState.FINISHED)
         )
 
         if (initInLockscreen) {
@@ -173,7 +167,7 @@
                         transitionState = TransitionState.RUNNING,
                         from = from,
                         to = to,
-                        value = 0.5f
+                        value = 0.5f,
                     )
             )
             testScheduler.runCurrent()
@@ -184,7 +178,7 @@
                         transitionState = TransitionState.RUNNING,
                         from = from,
                         to = to,
-                        value = 1f
+                        value = 1f,
                     )
             )
             testScheduler.runCurrent()
@@ -208,7 +202,7 @@
         this.sendTransitionStep(
             step = step,
             validateStep = validateStep,
-            ownerName = step.ownerName
+            ownerName = step.ownerName,
         )
     }
 
@@ -240,9 +234,9 @@
                 to = to,
                 value = value,
                 transitionState = transitionState,
-                ownerName = ownerName
+                ownerName = ownerName,
             ),
-        validateStep: Boolean = true
+        validateStep: Boolean = true,
     ) {
         if (step.transitionState == TransitionState.STARTED) {
             _currentTransitionInfo.value =
@@ -273,7 +267,7 @@
     fun sendTransitionStepJava(
         coroutineScope: CoroutineScope,
         step: TransitionStep,
-        validateStep: Boolean = true
+        validateStep: Boolean = true,
     ): Job {
         return coroutineScope.launch {
             sendTransitionStep(step = step, validateStep = validateStep)
@@ -283,7 +277,7 @@
     suspend fun sendTransitionSteps(
         steps: List<TransitionStep>,
         testScope: TestScope,
-        validateSteps: Boolean = true
+        validateSteps: Boolean = true,
     ) {
         steps.forEach {
             sendTransitionStep(step = it, validateStep = validateSteps)
@@ -296,7 +290,7 @@
         return if (info.animator == null) UUID.randomUUID() else null
     }
 
-    override suspend fun emitInitialStepsFromOff(to: KeyguardState) {
+    override suspend fun emitInitialStepsFromOff(to: KeyguardState, testSetup: Boolean) {
         tryEmitInitialStepsFromOff(to)
     }
 
@@ -318,14 +312,14 @@
                 1f,
                 TransitionState.FINISHED,
                 ownerName = "KeyguardTransitionRepository(boot)",
-            ),
+            )
         )
     }
 
     override suspend fun updateTransition(
         transitionId: UUID,
         @FloatRange(from = 0.0, to = 1.0) value: Float,
-        state: TransitionState
+        state: TransitionState,
     ) = Unit
 }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelKosmos.kt
index 0c538ff..ab7ccb3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelKosmos.kt
@@ -18,6 +18,7 @@
 
 import android.os.fakeExecutorHandler
 import com.android.systemui.keyguard.domain.interactor.keyguardBlueprintInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.kosmos.Kosmos
 
 val Kosmos.keyguardBlueprintViewModel by
@@ -25,5 +26,6 @@
         KeyguardBlueprintViewModel(
             fakeExecutorHandler,
             keyguardBlueprintInteractor,
+            keyguardTransitionInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index 38626a5..3c87106 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -47,6 +47,8 @@
         alternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel,
         alternateBouncerToLockscreenTransitionViewModel =
             alternateBouncerToLockscreenTransitionViewModel,
+        alternateBouncerToOccludedTransitionViewModel =
+            alternateBouncerToOccludedTransitionViewModel,
         aodToGoneTransitionViewModel = aodToGoneTransitionViewModel,
         aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel,
         aodToOccludedTransitionViewModel = aodToOccludedTransitionViewModel,
@@ -69,9 +71,12 @@
         lockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel,
         lockscreenToPrimaryBouncerTransitionViewModel =
             lockscreenToPrimaryBouncerTransitionViewModel,
+        occludedToAlternateBouncerTransitionViewModel =
+            occludedToAlternateBouncerTransitionViewModel,
         occludedToAodTransitionViewModel = occludedToAodTransitionViewModel,
         occludedToDozingTransitionViewModel = occludedToDozingTransitionViewModel,
         occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel,
+        offToLockscreenTransitionViewModel = offToLockscreenTransitionViewModel,
         primaryBouncerToAodTransitionViewModel = primaryBouncerToAodTransitionViewModel,
         primaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel,
         primaryBouncerToLockscreenTransitionViewModel =
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAlternateBouncerTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAlternateBouncerTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..2acd1b4
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAlternateBouncerTransitionViewModelKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+val Kosmos.occludedToAlternateBouncerTransitionViewModel by Fixture {
+    OccludedToAlternateBouncerTransitionViewModel(animationFlow = keyguardTransitionAnimationFlow)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..5d62a0f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModelKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+val Kosmos.offToLockscreenTransitionViewModel by Fixture {
+    OffToLockscreenTransitionViewModel(animationFlow = keyguardTransitionAnimationFlow)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt
index a80a409..6540ed6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.qs.ui.viewmodel
 
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.shade.ui.viewmodel.shadeHeaderViewModelFactory
 
@@ -24,6 +25,7 @@
     Kosmos.Fixture {
         QuickSettingsShadeOverlayContentViewModel(
             shadeInteractor = shadeInteractor,
+            sceneInteractor = sceneInteractor,
             shadeHeaderViewModelFactory = shadeHeaderViewModelFactory,
             quickSettingsContainerViewModel = quickSettingsContainerViewModel,
         )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt
index 7a15fdf..718347f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt
@@ -19,6 +19,7 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel
+import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModelFactory
 
@@ -27,6 +28,7 @@
     NotificationsShadeOverlayContentViewModel(
         shadeHeaderViewModelFactory = shadeHeaderViewModelFactory,
         notificationsPlaceholderViewModelFactory = notificationsPlaceholderViewModelFactory,
+        sceneInteractor = sceneInteractor,
         shadeInteractor = shadeInteractor,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
index a9e117a..237f7e4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
@@ -42,6 +42,7 @@
 import com.android.systemui.keyguard.ui.viewmodel.occludedToAodTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.occludedToGoneTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.occludedToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.offToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.primaryBouncerToGoneTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.primaryBouncerToLockscreenTransitionViewModel
 import com.android.systemui.kosmos.Kosmos
@@ -85,6 +86,7 @@
         occludedToAodTransitionViewModel = occludedToAodTransitionViewModel,
         occludedToGoneTransitionViewModel = occludedToGoneTransitionViewModel,
         occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel,
+        offToLockscreenTransitionViewModel = offToLockscreenTransitionViewModel,
         primaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel,
         primaryBouncerToLockscreenTransitionViewModel =
             primaryBouncerToLockscreenTransitionViewModel,
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java
index 428eb57..b4b8715 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java
@@ -84,10 +84,10 @@
         try {
             mOutputWriter = new PrintWriter(mOutputFile);
         } catch (IOException e) {
-            throw new RuntimeException("Failed to crete logfile. File=" + mOutputFile, e);
+            throw new RuntimeException("Failed to create logfile. File=" + mOutputFile, e);
         }
 
-        // Crete the "latest" symlink.
+        // Create the "latest" symlink.
         Path symlink = Paths.get(tmpdir, basename + "latest.csv");
         try {
             if (Files.exists(symlink)) {
@@ -96,7 +96,7 @@
             Files.createSymbolicLink(symlink, Paths.get(mOutputFile.getName()));
 
         } catch (IOException e) {
-            throw new RuntimeException("Failed to crete logfile. File=" + mOutputFile, e);
+            throw new RuntimeException("Failed to create logfile. File=" + mOutputFile, e);
         }
 
         Log.i(TAG, "Test result stats file: " + mOutputFile);
diff --git a/ravenwood/runtime-jni/ravenwood_sysprop.cpp b/ravenwood/runtime-jni/ravenwood_sysprop.cpp
index 4fb61b6..aafc426 100644
--- a/ravenwood/runtime-jni/ravenwood_sysprop.cpp
+++ b/ravenwood/runtime-jni/ravenwood_sysprop.cpp
@@ -56,7 +56,7 @@
     if (key == nullptr || *key == '\0') return false;
     if (value == nullptr) value = "";
     bool read_only = !strncmp(key, "ro.", 3);
-    if (!read_only && strlen(value) >= PROP_VALUE_MAX) return -1;
+    if (!read_only && strlen(value) >= PROP_VALUE_MAX) return false;
 
     std::lock_guard lock(g_properties_lock);
     auto [it, success] = g_properties.emplace(key, value);
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index a13ce65..bae9a67 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -40,6 +40,7 @@
 import android.aconfigd.Aconfigd.StorageReturnMessage;
 import android.aconfigd.Aconfigd.StorageReturnMessages;
 import static com.android.aconfig_new_storage.Flags.enableAconfigStorageDaemon;
+import static com.android.aconfig_new_storage.Flags.supportImmediateLocalOverrides;
 
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
@@ -491,14 +492,18 @@
     static void writeFlagOverrideRequest(
         ProtoOutputStream proto, String packageName, String flagName, String flagValue,
         boolean isLocal) {
+      int localOverrideTag = supportImmediateLocalOverrides()
+          ? StorageRequestMessage.LOCAL_IMMEDIATE
+          : StorageRequestMessage.LOCAL_ON_REBOOT;
+
       long msgsToken = proto.start(StorageRequestMessages.MSGS);
       long msgToken = proto.start(StorageRequestMessage.FLAG_OVERRIDE_MESSAGE);
       proto.write(StorageRequestMessage.FlagOverrideMessage.PACKAGE_NAME, packageName);
       proto.write(StorageRequestMessage.FlagOverrideMessage.FLAG_NAME, flagName);
       proto.write(StorageRequestMessage.FlagOverrideMessage.FLAG_VALUE, flagValue);
       proto.write(StorageRequestMessage.FlagOverrideMessage.OVERRIDE_TYPE, isLocal
-                ? StorageRequestMessage.LOCAL_ON_REBOOT
-                : StorageRequestMessage.SERVER_ON_REBOOT);
+            ? localOverrideTag
+            : StorageRequestMessage.SERVER_ON_REBOOT);
       proto.end(msgToken);
       proto.end(msgsToken);
     }
diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig
index 9b51b6a..4f6da3b 100644
--- a/services/core/java/com/android/server/am/flags.aconfig
+++ b/services/core/java/com/android/server/am/flags.aconfig
@@ -205,4 +205,12 @@
     metadata {
         purpose: PURPOSE_BUGFIX
     }
-}
\ No newline at end of file
+}
+
+flag {
+    name: "defer_display_events_when_frozen"
+    namespace: "system_performance"
+    is_fixed_read_only: true
+    description: "Defer submitting display events to frozen processes."
+    bug: "326315985"
+}
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index feef540..4c91789 100644
--- a/services/core/java/com/android/server/biometrics/BiometricService.java
+++ b/services/core/java/com/android/server/biometrics/BiometricService.java
@@ -725,7 +725,7 @@
                 return -1;
             }
 
-            if (!Utils.isValidAuthenticatorConfig(promptInfo)) {
+            if (!Utils.isValidAuthenticatorConfig(getContext(), promptInfo)) {
                 throw new SecurityException("Invalid authenticator configuration");
             }
 
@@ -763,7 +763,7 @@
                     + ", Caller=" + callingUserId
                     + ", Authenticators=" + authenticators);
 
-            if (!Utils.isValidAuthenticatorConfig(authenticators)) {
+            if (!Utils.isValidAuthenticatorConfig(getContext(), authenticators)) {
                 throw new SecurityException("Invalid authenticator configuration");
             }
 
@@ -1038,7 +1038,7 @@
                     + ", Caller=" + callingUserId
                     + ", Authenticators=" + authenticators);
 
-            if (!Utils.isValidAuthenticatorConfig(authenticators)) {
+            if (!Utils.isValidAuthenticatorConfig(getContext(), authenticators)) {
                 throw new SecurityException("Invalid authenticator configuration");
             }
 
@@ -1060,7 +1060,7 @@
 
             Slog.d(TAG, "getSupportedModalities: Authenticators=" + authenticators);
 
-            if (!Utils.isValidAuthenticatorConfig(authenticators)) {
+            if (!Utils.isValidAuthenticatorConfig(getContext(), authenticators)) {
                 throw new SecurityException("Invalid authenticator configuration");
             }
 
diff --git a/services/core/java/com/android/server/biometrics/Utils.java b/services/core/java/com/android/server/biometrics/Utils.java
index 407ef1e..de7bce7 100644
--- a/services/core/java/com/android/server/biometrics/Utils.java
+++ b/services/core/java/com/android/server/biometrics/Utils.java
@@ -16,6 +16,7 @@
 
 package com.android.server.biometrics;
 
+import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED;
 import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL;
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
 import static android.hardware.biometrics.BiometricManager.Authenticators;
@@ -233,17 +234,18 @@
      * @param promptInfo
      * @return
      */
-    static boolean isValidAuthenticatorConfig(PromptInfo promptInfo) {
+    static boolean isValidAuthenticatorConfig(Context context, PromptInfo promptInfo) {
         final int authenticators = promptInfo.getAuthenticators();
-        return isValidAuthenticatorConfig(authenticators);
+        return isValidAuthenticatorConfig(context, authenticators);
     }
 
     /**
-     * Checks if the authenticator configuration is a valid combination of the public APIs
-     * @param authenticators
-     * @return
+     * Checks if the authenticator configuration is a valid combination of the public APIs.
+     *
+     * throws {@link SecurityException} if the caller requests for mandatory biometrics without
+     * {@link SET_BIOMETRIC_DIALOG_ADVANCED} permission
      */
-    static boolean isValidAuthenticatorConfig(int authenticators) {
+    static boolean isValidAuthenticatorConfig(Context context, int authenticators) {
         // The caller is not required to set the authenticators. But if they do, check the below.
         if (authenticators == 0) {
             return true;
@@ -271,6 +273,9 @@
         } else if (biometricBits == Authenticators.BIOMETRIC_WEAK) {
             return true;
         } else if (isMandatoryBiometricsRequested(authenticators)) {
+            //TODO(b/347123256): Update CTS test
+            context.enforceCallingOrSelfPermission(SET_BIOMETRIC_DIALOG_ADVANCED,
+                    "Must have SET_BIOMETRIC_DIALOG_ADVANCED permission");
             return true;
         }
 
diff --git a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
index 41313fa..ef1220f 100644
--- a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
+++ b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
@@ -33,9 +33,6 @@
     @GuardedBy("ImfLock.class")
     private final ArrayList<InputMethodSubtypeHandle> mSubtypeHandles = new ArrayList<>();
 
-    HardwareKeyboardShortcutController() {
-    }
-
     @GuardedBy("ImfLock.class")
     void update(@NonNull InputMethodSettings settings) {
         mSubtypeHandles.clear();
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java b/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java
index 6cd2493..fc4c0fc 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java
@@ -40,6 +40,7 @@
                 if (KEY_HIDE_IME_WHEN_NO_EDITOR_FOCUS.equals(name)) {
                     mHideImeWhenNoEditorFocus = properties.getBoolean(name,
                             true /* defaultValue */);
+                    break;
                 }
             }
         };
diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
index 214aa1d..49d4332 100644
--- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
+++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
@@ -394,6 +394,7 @@
                                     flags),
                     this::offload).get();
         } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
             throw new RuntimeException(e);
         } catch (ExecutionException e) {
             throw new RuntimeException(e);
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 79633f1..655f2e4 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -6248,6 +6248,7 @@
             int callingUid = Binder.getCallingUid();
             @ZenModeConfig.ConfigOrigin int origin = computeZenOrigin(fromUser);
 
+            boolean isSystemCaller = isCallerSystemOrSystemUiOrShell();
             boolean shouldApplyAsImplicitRule = android.app.Flags.modesApi()
                     && !canManageGlobalZenPolicy(pkg, callingUid);
 
@@ -6284,11 +6285,33 @@
                             policy.priorityCallSenders, policy.priorityMessageSenders,
                             policy.suppressedVisualEffects, currPolicy.priorityConversationSenders);
                 }
+
                 int newVisualEffects = calculateSuppressedVisualEffects(
                             policy, currPolicy, applicationInfo.targetSdkVersion);
-                policy = new Policy(policy.priorityCategories,
-                        policy.priorityCallSenders, policy.priorityMessageSenders,
-                        newVisualEffects, policy.priorityConversationSenders);
+
+                if (android.app.Flags.modesUi()) {
+                    // 1. Callers should not modify STATE_CHANNELS_BYPASSING_DND, which is
+                    // internally calculated and only indicates whether channels that want to bypass
+                    // DND _exist_.
+                    // 2. Only system callers should modify STATE_PRIORITY_CHANNELS_BLOCKED because
+                    // it is @hide.
+                    // 3. If the policy has been modified by the targetSdkVersion checks above then
+                    // it has lost its state flags and that's fine (STATE_PRIORITY_CHANNELS_BLOCKED
+                    // didn't exist until V).
+                    int newState = Policy.STATE_UNSET;
+                    if (isSystemCaller && policy.state != Policy.STATE_UNSET) {
+                        newState = Policy.policyState(
+                                currPolicy.hasPriorityChannels(),
+                                policy.allowPriorityChannels());
+                    }
+                    policy = new Policy(policy.priorityCategories,
+                            policy.priorityCallSenders, policy.priorityMessageSenders,
+                            newVisualEffects, newState, policy.priorityConversationSenders);
+                } else {
+                    policy = new Policy(policy.priorityCategories,
+                            policy.priorityCallSenders, policy.priorityMessageSenders,
+                            newVisualEffects, policy.priorityConversationSenders);
+                }
 
                 if (shouldApplyAsImplicitRule) {
                     mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, callingUid, policy);
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 4754ffb..946b61a 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -1468,7 +1468,7 @@
                         || change == PACKAGE_TEMPORARY_CHANGE) {
                     changed = true;
                     if (doit) {
-                        Slog.w(TAG, "Wallpaper uninstalled, removing: "
+                        Slog.e(TAG, "Wallpaper uninstalled, removing: "
                                 + wallpaper.getComponent());
                         clearWallpaperLocked(wallpaper.mWhich, wallpaper.userId, false, null);
                     }
@@ -1491,7 +1491,7 @@
                             PackageManager.MATCH_DIRECT_BOOT_AWARE
                                     | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
                 } catch (NameNotFoundException e) {
-                    Slog.w(TAG, "Wallpaper component gone, removing: "
+                    Slog.e(TAG, "Wallpaper component gone, removing: "
                             + wallpaper.getComponent());
                     clearWallpaperLocked(wallpaper.mWhich, wallpaper.userId, false, null);
                 }
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index d29ff54..4092a0b 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -100,6 +100,7 @@
 import android.app.WaitResult;
 import android.app.WindowConfiguration;
 import android.compat.annotation.ChangeId;
+import android.compat.annotation.Disabled;
 import android.compat.annotation.EnabledSince;
 import android.content.IIntentSender;
 import android.content.Intent;
@@ -182,7 +183,7 @@
      * Feature flag for go/activity-security rules
      */
     @ChangeId
-    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @Disabled
     static final long ASM_RESTRICTIONS = 230590090L;
 
     private final ActivityTaskManagerService mService;
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 86bb75a..14f034b 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -66,6 +66,7 @@
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_LOCKTASK;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_TASKS;
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS_MIN;
 import static com.android.server.wm.ActivityRecord.State.PAUSED;
 import static com.android.server.wm.ActivityRecord.State.PAUSING;
 import static com.android.server.wm.ActivityRecord.State.RESUMED;
@@ -6177,6 +6178,8 @@
 
     void maybeApplyLastRecentsAnimationTransaction() {
         if (mLastRecentsAnimationTransaction != null) {
+            ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS_MIN,
+                    "Applying last recents animation transaction.");
             final SurfaceControl.Transaction tx = getPendingTransaction();
             if (mLastRecentsAnimationOverlay != null) {
                 tx.reparent(mLastRecentsAnimationOverlay, mSurfaceControl);
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java b/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java
index 14cb22d..efc2d97 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java
@@ -16,12 +16,20 @@
 
 package com.android.server.biometrics;
 
+import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED;
 import static android.hardware.biometrics.BiometricManager.Authenticators;
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+
+import android.content.Context;
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricManager;
@@ -36,8 +44,12 @@
 
 import androidx.test.filters.SmallTest;
 
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 
 @Presubmit
 @SmallTest
@@ -45,6 +57,17 @@
     @Rule
     public final CheckFlagsRule mCheckFlagsRule =
             DeviceFlagsValueProvider.createCheckFlagsRule();
+    @Rule
+    public MockitoRule mockitorule = MockitoJUnit.rule();
+
+    @Mock
+    private Context mContext;
+
+    @Before
+    public void setUp() {
+        doThrow(SecurityException.class).when(mContext).enforceCallingOrSelfPermission(
+                eq(SET_BIOMETRIC_DIALOG_ADVANCED), any());
+    }
 
     @Test
     public void testCombineAuthenticatorBundles_withKeyDeviceCredential_andKeyAuthenticators() {
@@ -162,28 +185,39 @@
 
     @Test
     public void testIsValidAuthenticatorConfig() {
-        assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.EMPTY_SET));
+        assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.EMPTY_SET));
 
-        assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_STRONG));
+        assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.BIOMETRIC_STRONG));
 
-        assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_WEAK));
+        assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.BIOMETRIC_WEAK));
 
-        assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.DEVICE_CREDENTIAL));
+        assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.DEVICE_CREDENTIAL));
 
-        assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.DEVICE_CREDENTIAL
+        assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.DEVICE_CREDENTIAL
                 | Authenticators.BIOMETRIC_STRONG));
 
-        assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.DEVICE_CREDENTIAL
+        assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.DEVICE_CREDENTIAL
                 | Authenticators.BIOMETRIC_WEAK));
 
-        assertFalse(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_CONVENIENCE));
+        assertFalse(Utils.isValidAuthenticatorConfig(
+                mContext, Authenticators.BIOMETRIC_CONVENIENCE));
 
-        assertFalse(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_CONVENIENCE
+        assertFalse(Utils.isValidAuthenticatorConfig(mContext, Authenticators.BIOMETRIC_CONVENIENCE
                 | Authenticators.DEVICE_CREDENTIAL));
 
-        assertFalse(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_MAX_STRENGTH));
+        assertFalse(Utils.isValidAuthenticatorConfig(
+                mContext, Authenticators.BIOMETRIC_MAX_STRENGTH));
 
-        assertFalse(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_MIN_STRENGTH));
+        assertFalse(Utils.isValidAuthenticatorConfig(
+                mContext, Authenticators.BIOMETRIC_MIN_STRENGTH));
+
+        assertThrows(SecurityException.class, () -> Utils.isValidAuthenticatorConfig(
+                        mContext, Authenticators.MANDATORY_BIOMETRICS));
+
+        doNothing().when(mContext).enforceCallingOrSelfPermission(
+                eq(SET_BIOMETRIC_DIALOG_ADVANCED), any());
+
+        assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.MANDATORY_BIOMETRICS));
 
         // The rest of the bits are not allowed to integrate with the public APIs
         for (int i = 8; i < 32; i++) {
@@ -192,7 +226,7 @@
                     || authenticator == Authenticators.MANDATORY_BIOMETRICS) {
                 continue;
             }
-            assertFalse(Utils.isValidAuthenticatorConfig(1 << i));
+            assertFalse(Utils.isValidAuthenticatorConfig(mContext, 1 << i));
         }
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java
index d071c15..ae781dc 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java
@@ -60,6 +60,7 @@
 import android.os.ServiceSpecificException;
 import android.os.UserManager;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsDisabled;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -130,6 +131,7 @@
     private SecretKey mAesKey;
     private MockInjector mMockInjector;
     private Handler mHandler;
+    private Network mNetwork;
 
     public interface MockableRebootEscrowInjected {
         int getBootCount();
@@ -342,6 +344,7 @@
         when(mCallbacks.isUserSecure(NONSECURE_SECONDARY_USER_ID)).thenReturn(false);
         when(mCallbacks.isUserSecure(SECURE_SECONDARY_USER_ID)).thenReturn(true);
         mInjected = mock(MockableRebootEscrowInjected.class);
+        mNetwork = mock(Network.class);
         mMockInjector =
                 new MockInjector(
                         mContext,
@@ -351,6 +354,10 @@
                         mKeyStoreManager,
                         mStorage,
                         mInjected);
+        mMockInjector.mNetworkConsumer =
+                (callback) -> {
+                    callback.onAvailable(mNetwork);
+                };
         HandlerThread thread = new HandlerThread("RebootEscrowManagerTest");
         thread.start();
         mHandler = new Handler(thread.getLooper());
@@ -367,6 +374,10 @@
                         mKeyStoreManager,
                         mStorage,
                         mInjected);
+        mMockInjector.mNetworkConsumer =
+                (callback) -> {
+                    callback.onAvailable(mNetwork);
+                };
         mService = new RebootEscrowManager(mMockInjector, mCallbacks, mStorage, mHandler);
     }
 
@@ -621,7 +632,7 @@
         // pretend reboot happens here
         when(mInjected.getBootCount()).thenReturn(1);
 
-        mService.loadRebootEscrowDataIfAvailable(null);
+        mService.loadRebootEscrowDataIfAvailable(mHandler);
         verify(mServiceConnection, never()).unwrap(any(), anyLong());
         verify(mCallbacks, never()).onRebootEscrowRestored(anyByte(), any(), anyInt());
     }
@@ -678,7 +689,7 @@
         when(mServiceConnection.unwrap(any(), anyLong()))
                 .thenAnswer(invocation -> invocation.getArgument(0));
 
-        mService.loadRebootEscrowDataIfAvailable(null);
+        mService.loadRebootEscrowDataIfAvailable(mHandler);
 
         verify(mServiceConnection).unwrap(any(), anyLong());
         verify(mCallbacks).onRebootEscrowRestored(anyByte(), any(), eq(PRIMARY_USER_ID));
@@ -734,7 +745,7 @@
         when(mServiceConnection.unwrap(any(), anyLong()))
                 .thenAnswer(invocation -> invocation.getArgument(0));
 
-        mService.loadRebootEscrowDataIfAvailable(null);
+        mService.loadRebootEscrowDataIfAvailable(mHandler);
 
         verify(mServiceConnection).unwrap(any(), anyLong());
         verify(mCallbacks).onRebootEscrowRestored(anyByte(), any(), eq(PRIMARY_USER_ID));
@@ -783,7 +794,7 @@
 
         when(mServiceConnection.unwrap(any(), anyLong()))
                 .thenAnswer(invocation -> invocation.getArgument(0));
-        mService.loadRebootEscrowDataIfAvailable(null);
+        mService.loadRebootEscrowDataIfAvailable(mHandler);
         verify(mServiceConnection).unwrap(any(), anyLong());
         assertTrue(metricsSuccessCaptor.getValue());
         verify(mKeyStoreManager).clearKeyStoreEncryptionKey();
@@ -827,7 +838,7 @@
                         anyInt());
 
         when(mServiceConnection.unwrap(any(), anyLong())).thenThrow(RemoteException.class);
-        mService.loadRebootEscrowDataIfAvailable(null);
+        mService.loadRebootEscrowDataIfAvailable(mHandler);
         verify(mServiceConnection).unwrap(any(), anyLong());
         assertFalse(metricsSuccessCaptor.getValue());
         assertEquals(
@@ -836,6 +847,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_WAIT_FOR_INTERNET_ROR)
     public void loadRebootEscrowDataIfAvailable_ServerBasedIoError_RetryFailure() throws Exception {
         setServerBasedRebootEscrowProvider();
 
@@ -930,114 +942,6 @@
 
     @Test
     @RequiresFlagsEnabled(Flags.FLAG_WAIT_FOR_INTERNET_ROR)
-    public void loadRebootEscrowDataIfAvailable_serverBasedWaitForInternet_success()
-            throws Exception {
-        setServerBasedRebootEscrowProvider();
-
-        when(mInjected.getBootCount()).thenReturn(0);
-        RebootEscrowListener mockListener = mock(RebootEscrowListener.class);
-        mService.setRebootEscrowListener(mockListener);
-        mService.prepareRebootEscrow();
-
-        clearInvocations(mServiceConnection);
-        callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
-        verify(mockListener).onPreparedForReboot(eq(true));
-        verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
-
-        // Use x -> x for both wrap & unwrap functions.
-        when(mServiceConnection.wrapBlob(any(), anyLong(), anyLong()))
-                .thenAnswer(invocation -> invocation.getArgument(0));
-        assertEquals(ARM_REBOOT_ERROR_NONE, mService.armRebootEscrowIfNeeded());
-        verify(mServiceConnection).wrapBlob(any(), anyLong(), anyLong());
-        assertTrue(mStorage.hasRebootEscrowServerBlob());
-
-        // pretend reboot happens here
-        when(mInjected.getBootCount()).thenReturn(1);
-        ArgumentCaptor<Boolean> metricsSuccessCaptor = ArgumentCaptor.forClass(Boolean.class);
-        doNothing()
-                .when(mInjected)
-                .reportMetric(
-                        metricsSuccessCaptor.capture(),
-                        eq(0) /* error code */,
-                        eq(2) /* Server based */,
-                        eq(1) /* attempt count */,
-                        anyInt(),
-                        eq(0) /* vbmeta status */,
-                        anyInt());
-
-        // load escrow data
-        when(mServiceConnection.unwrap(any(), anyLong()))
-                .thenAnswer(invocation -> invocation.getArgument(0));
-        Network mockNetwork = mock(Network.class);
-        mMockInjector.mNetworkConsumer =
-                (callback) -> {
-                    callback.onAvailable(mockNetwork);
-                };
-
-        mService.loadRebootEscrowDataIfAvailable(mHandler);
-        verify(mServiceConnection).unwrap(any(), anyLong());
-        assertTrue(metricsSuccessCaptor.getValue());
-        verify(mKeyStoreManager).clearKeyStoreEncryptionKey();
-        assertNull(mMockInjector.mNetworkCallback);
-    }
-
-    @Test
-    @RequiresFlagsEnabled(Flags.FLAG_WAIT_FOR_INTERNET_ROR)
-    public void loadRebootEscrowDataIfAvailable_serverBasedWaitForInternetRemoteException_Failure()
-            throws Exception {
-        setServerBasedRebootEscrowProvider();
-
-        when(mInjected.getBootCount()).thenReturn(0);
-        RebootEscrowListener mockListener = mock(RebootEscrowListener.class);
-        mService.setRebootEscrowListener(mockListener);
-        mService.prepareRebootEscrow();
-
-        clearInvocations(mServiceConnection);
-        callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
-        verify(mockListener).onPreparedForReboot(eq(true));
-        verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
-
-        // Use x -> x for both wrap & unwrap functions.
-        when(mServiceConnection.wrapBlob(any(), anyLong(), anyLong()))
-                .thenAnswer(invocation -> invocation.getArgument(0));
-        assertEquals(ARM_REBOOT_ERROR_NONE, mService.armRebootEscrowIfNeeded());
-        verify(mServiceConnection).wrapBlob(any(), anyLong(), anyLong());
-        assertTrue(mStorage.hasRebootEscrowServerBlob());
-
-        // pretend reboot happens here
-        when(mInjected.getBootCount()).thenReturn(1);
-        ArgumentCaptor<Boolean> metricsSuccessCaptor = ArgumentCaptor.forClass(Boolean.class);
-        ArgumentCaptor<Integer> metricsErrorCodeCaptor = ArgumentCaptor.forClass(Integer.class);
-        doNothing()
-                .when(mInjected)
-                .reportMetric(
-                        metricsSuccessCaptor.capture(),
-                        metricsErrorCodeCaptor.capture(),
-                        eq(2) /* Server based */,
-                        eq(1) /* attempt count */,
-                        anyInt(),
-                        eq(0) /* vbmeta status */,
-                        anyInt());
-
-        // load escrow data
-        when(mServiceConnection.unwrap(any(), anyLong())).thenThrow(RemoteException.class);
-        Network mockNetwork = mock(Network.class);
-        mMockInjector.mNetworkConsumer =
-                (callback) -> {
-                    callback.onAvailable(mockNetwork);
-                };
-
-        mService.loadRebootEscrowDataIfAvailable(mHandler);
-        verify(mServiceConnection).unwrap(any(), anyLong());
-        assertFalse(metricsSuccessCaptor.getValue());
-        assertEquals(
-                Integer.valueOf(RebootEscrowManager.ERROR_LOAD_ESCROW_KEY),
-                metricsErrorCodeCaptor.getValue());
-        assertNull(mMockInjector.mNetworkCallback);
-    }
-
-    @Test
-    @RequiresFlagsEnabled(Flags.FLAG_WAIT_FOR_INTERNET_ROR)
     public void loadRebootEscrowDataIfAvailable_waitForInternet_networkUnavailable()
             throws Exception {
         setServerBasedRebootEscrowProvider();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 44770d2..bbf2cbd 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -193,6 +193,7 @@
 import android.app.NotificationChannel;
 import android.app.NotificationChannelGroup;
 import android.app.NotificationManager;
+import android.app.NotificationManager.Policy;
 import android.app.PendingIntent;
 import android.app.Person;
 import android.app.RemoteInput;
@@ -655,7 +656,8 @@
         when(mAtm.getTaskToShowPermissionDialogOn(anyString(), anyInt()))
                 .thenReturn(INVALID_TASK_ID);
         mContext.addMockSystemService(AppOpsManager.class, mock(AppOpsManager.class));
-        when(mUm.getProfileIds(eq(mUserId), eq(false))).thenReturn(new int[] { mUserId });
+        when(mUm.getProfileIds(eq(mUserId), anyBoolean())).thenReturn(new int[]{mUserId});
+        when(mUmInternal.getProfileIds(eq(mUserId), anyBoolean())).thenReturn(new int[]{mUserId});
         when(mAmi.getCurrentUserId()).thenReturn(mUserId);
 
         when(mPackageManagerClient.hasSystemFeature(FEATURE_TELECOM)).thenReturn(true);
@@ -15971,6 +15973,57 @@
         assertThat(updatedRule.getValue().isEnabled()).isFalse();
     }
 
+    @Test
+    @EnableFlags({android.app.Flags.FLAG_MODES_API, android.app.Flags.FLAG_MODES_UI})
+    public void setNotificationPolicy_fromSystemApp_appliesPriorityChannelsAllowed()
+            throws Exception {
+        setUpRealZenTest();
+        // Start with hasPriorityChannels=true, allowPriorityChannels=true ("default").
+        mService.mZenModeHelper.setNotificationPolicy(new Policy(0, 0, 0, 0,
+                        Policy.policyState(true, true), 0),
+                ZenModeConfig.ORIGIN_SYSTEM, Process.SYSTEM_UID);
+
+        // The caller will supply states with "wrong" hasPriorityChannels.
+        int stateBlockingPriorityChannels = Policy.policyState(false, false);
+        mBinderService.setNotificationPolicy(mPkg,
+                new Policy(1, 0, 0, 0, stateBlockingPriorityChannels, 0), false);
+
+        // hasPriorityChannels is untouched and allowPriorityChannels was updated.
+        assertThat(mBinderService.getNotificationPolicy(mPkg).priorityCategories).isEqualTo(1);
+        assertThat(mBinderService.getNotificationPolicy(mPkg).state).isEqualTo(
+                Policy.policyState(true, false));
+
+        // Same but setting allowPriorityChannels to true.
+        int stateAllowingPriorityChannels = Policy.policyState(false, true);
+        mBinderService.setNotificationPolicy(mPkg,
+                new Policy(2, 0, 0, 0, stateAllowingPriorityChannels, 0), false);
+
+        assertThat(mBinderService.getNotificationPolicy(mPkg).priorityCategories).isEqualTo(2);
+        assertThat(mBinderService.getNotificationPolicy(mPkg).state).isEqualTo(
+                Policy.policyState(true, true));
+    }
+
+    @Test
+    @EnableFlags({android.app.Flags.FLAG_MODES_API, android.app.Flags.FLAG_MODES_UI})
+    @DisableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setNotificationPolicy_fromRegularAppThatCanModifyPolicy_ignoresState()
+            throws Exception {
+        setUpRealZenTest();
+        // Start with hasPriorityChannels=true, allowPriorityChannels=true ("default").
+        mService.mZenModeHelper.setNotificationPolicy(new Policy(0, 0, 0, 0,
+                        Policy.policyState(true, true), 0),
+                ZenModeConfig.ORIGIN_SYSTEM, Process.SYSTEM_UID);
+        mService.setCallerIsNormalPackage();
+
+        mBinderService.setNotificationPolicy(mPkg,
+                new Policy(1, 0, 0, 0, Policy.policyState(false, false), 0), false);
+
+        // Policy was updated but the attempt to change state was ignored (it's a @hide API).
+        assertThat(mBinderService.getNotificationPolicy(mPkg).priorityCategories).isEqualTo(1);
+        assertThat(mBinderService.getNotificationPolicy(mPkg).state).isEqualTo(
+                Policy.policyState(true, true));
+    }
+
     /** Prepares for a zen-related test that uses the real {@link ZenModeHelper}. */
     private void setUpRealZenTest() throws Exception {
         when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 3e226cc..92effe0 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -19426,4 +19426,48 @@
                 return "UNKNOWN(" + state + ")";
         }
     }
+
+    /**
+     * This API can be used by only CTS to override the Euicc UI component.
+     *
+     * @param componentName ui component to be launched for testing. {@code null} to reset.
+     *
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+    public void setTestEuiccUiComponent(@Nullable ComponentName componentName) {
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony == null) {
+                Rlog.e(TAG, "setTestEuiccUiComponent(): ITelephony instance is NULL");
+                throw new IllegalStateException("Telephony service not available.");
+            }
+            telephony.setTestEuiccUiComponent(componentName);
+        } catch (RemoteException ex) {
+            Rlog.e(TAG, "setTestEuiccUiComponent() RemoteException : " + ex);
+            throw ex.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
+     * This API can be used by only CTS to retrieve the Euicc UI component.
+     *
+     * @return The Euicc UI component for testing. {@code null} if not available.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+    @Nullable
+    public ComponentName getTestEuiccUiComponent() {
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony == null) {
+                Rlog.e(TAG, "getTestEuiccUiComponent(): ITelephony instance is NULL");
+                throw new IllegalStateException("Telephony service not available.");
+            }
+            return telephony.getTestEuiccUiComponent();
+        } catch (RemoteException ex) {
+            Rlog.e(TAG, "getTestEuiccUiComponent() RemoteException : " + ex);
+            throw ex.rethrowAsRuntimeException();
+        }
+    }
 }
diff --git a/telephony/java/android/telephony/data/ApnSetting.java b/telephony/java/android/telephony/data/ApnSetting.java
index 44d3fca..567314b 100644
--- a/telephony/java/android/telephony/data/ApnSetting.java
+++ b/telephony/java/android/telephony/data/ApnSetting.java
@@ -128,6 +128,12 @@
     /** APN type for RCS (Rich Communication Services). */
     @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
     public static final int TYPE_RCS = ApnTypes.RCS;
+    /** APN type for OEM_PAID networks (Automotive PANS) */
+    @FlaggedApi(Flags.FLAG_OEM_PAID_PRIVATE)
+    public static final int TYPE_OEM_PAID = 1 << 16; // TODO(b/366194627): ApnTypes.OEM_PAID;
+    /** APN type for OEM_PRIVATE networks (Automotive PANS) */
+    @FlaggedApi(Flags.FLAG_OEM_PAID_PRIVATE)
+    public static final int TYPE_OEM_PRIVATE = 1 << 17; // TODO(b/366194627): ApnTypes.OEM_PRIVATE;
 
     /** @hide */
     @IntDef(flag = true, prefix = {"TYPE_"}, value = {
@@ -146,7 +152,9 @@
             TYPE_BIP,
             TYPE_VSIM,
             TYPE_ENTERPRISE,
-            TYPE_RCS
+            TYPE_RCS,
+            TYPE_OEM_PAID,
+            TYPE_OEM_PRIVATE,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ApnType {
@@ -375,6 +383,27 @@
     @SystemApi
     public static final String TYPE_RCS_STRING = "rcs";
 
+    /**
+     * APN type for OEM_PAID networks (Automotive PANS)
+     *
+     * Note: String representations of APN types are intended for system apps to communicate with
+     * modem components or carriers. Non-system apps should use the integer variants instead.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_OEM_PAID_PRIVATE)
+    @SystemApi
+    public static final String TYPE_OEM_PAID_STRING = "oem_paid";
+
+    /**
+     * APN type for OEM_PRIVATE networks (Automotive PANS)
+     *
+     * Note: String representations of APN types are intended for system apps to communicate with
+     * modem components or carriers. Non-system apps should use the integer variants instead.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_OEM_PAID_PRIVATE)
+    @SystemApi
+    public static final String TYPE_OEM_PRIVATE_STRING = "oem_private";
 
     /** @hide */
     @IntDef(prefix = { "AUTH_TYPE_" }, value = {
@@ -489,6 +518,8 @@
         APN_TYPE_STRING_MAP.put(TYPE_VSIM_STRING, TYPE_VSIM);
         APN_TYPE_STRING_MAP.put(TYPE_BIP_STRING, TYPE_BIP);
         APN_TYPE_STRING_MAP.put(TYPE_RCS_STRING, TYPE_RCS);
+        APN_TYPE_STRING_MAP.put(TYPE_OEM_PAID_STRING, TYPE_OEM_PAID);
+        APN_TYPE_STRING_MAP.put(TYPE_OEM_PRIVATE_STRING, TYPE_OEM_PRIVATE);
 
         APN_TYPE_INT_MAP = new ArrayMap<>();
         APN_TYPE_INT_MAP.put(TYPE_DEFAULT, TYPE_DEFAULT_STRING);
@@ -507,6 +538,8 @@
         APN_TYPE_INT_MAP.put(TYPE_VSIM, TYPE_VSIM_STRING);
         APN_TYPE_INT_MAP.put(TYPE_BIP, TYPE_BIP_STRING);
         APN_TYPE_INT_MAP.put(TYPE_RCS, TYPE_RCS_STRING);
+        APN_TYPE_INT_MAP.put(TYPE_OEM_PAID, TYPE_OEM_PAID_STRING);
+        APN_TYPE_INT_MAP.put(TYPE_OEM_PRIVATE, TYPE_OEM_PRIVATE_STRING);
 
         PROTOCOL_STRING_MAP = new ArrayMap<>();
         PROTOCOL_STRING_MAP.put("IP", PROTOCOL_IP);
@@ -2383,7 +2416,8 @@
         public ApnSetting build() {
             if ((mApnTypeBitmask & (TYPE_DEFAULT | TYPE_MMS | TYPE_SUPL | TYPE_DUN | TYPE_HIPRI
                     | TYPE_FOTA | TYPE_IMS | TYPE_CBS | TYPE_IA | TYPE_EMERGENCY | TYPE_MCX
-                    | TYPE_XCAP | TYPE_VSIM | TYPE_BIP | TYPE_ENTERPRISE | TYPE_RCS)) == 0
+                    | TYPE_XCAP | TYPE_VSIM | TYPE_BIP | TYPE_ENTERPRISE | TYPE_RCS | TYPE_OEM_PAID
+                    | TYPE_OEM_PRIVATE)) == 0
                 || TextUtils.isEmpty(mApnName) || TextUtils.isEmpty(mEntryName)) {
                 return null;
             }
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 4eefaac..bd5c759 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -1113,6 +1113,12 @@
      * @hide
      */
     public static final int DATAGRAM_TYPE_SMS = 6;
+    /**
+     * Datagram type indicating that the message to be sent is an SMS checking
+     * for pending incoming SMS.
+     * @hide
+     */
+    public static final int DATAGRAM_TYPE_CHECK_PENDING_INCOMING_SMS = 7;
 
     /** @hide */
     @IntDef(prefix = "DATAGRAM_TYPE_", value = {
@@ -1122,7 +1128,8 @@
             DATAGRAM_TYPE_KEEP_ALIVE,
             DATAGRAM_TYPE_LAST_SOS_MESSAGE_STILL_NEED_HELP,
             DATAGRAM_TYPE_LAST_SOS_MESSAGE_NO_HELP_NEEDED,
-            DATAGRAM_TYPE_SMS
+            DATAGRAM_TYPE_SMS,
+            DATAGRAM_TYPE_CHECK_PENDING_INCOMING_SMS
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface DatagramType {}
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index e57c207..cca0f8c 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -3409,4 +3409,20 @@
      * @hide
      */
     boolean setSatelliteSubscriberIdListChangedIntentComponent(in String name);
+
+    /**
+     * This API can be used by only CTS to override the Euicc UI component.
+     *
+     * @param componentName ui component to be launched for testing
+     * @hide
+     */
+    void setTestEuiccUiComponent(in ComponentName componentName);
+
+    /**
+     * This API can be used by only CTS to retrieve the Euicc UI component.
+     *
+     * @return The Euicc UI component for testing.
+     * @hide
+     */
+    ComponentName getTestEuiccUiComponent();
 }
diff --git a/tests/testables/Android.bp b/tests/testables/Android.bp
index 7596ee7..f211185 100644
--- a/tests/testables/Android.bp
+++ b/tests/testables/Android.bp
@@ -25,7 +25,10 @@
 
 java_library {
     name: "testables",
-    srcs: ["src/**/*.java"],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
     libs: [
         "android.test.runner.stubs.system",
         "android.test.mock.stubs.system",
diff --git a/tests/testables/src/android/testing/TestWithLooperRule.java b/tests/testables/src/android/testing/TestWithLooperRule.java
index 37b39c3..10df17f 100644
--- a/tests/testables/src/android/testing/TestWithLooperRule.java
+++ b/tests/testables/src/android/testing/TestWithLooperRule.java
@@ -34,13 +34,13 @@
  * Looper for the Statement.
  */
 public class TestWithLooperRule implements MethodRule {
-
     /*
      * This rule requires to be the inner most Rule, so the next statement is RunAfters
      * instead of another rule. You can set it by '@Rule(order = Integer.MAX_VALUE)'
      */
     @Override
     public Statement apply(Statement base, FrameworkMethod method, Object target) {
+
         // getting testRunner check, if AndroidTestingRunning then we skip this rule
         RunWith runWithAnnotation = target.getClass().getAnnotation(RunWith.class);
         if (runWithAnnotation != null) {
@@ -97,6 +97,9 @@
                     case "InvokeParameterizedMethod":
                         this.wrapFieldMethodFor(next, "frameworkMethod", method, target);
                         return;
+                    case "ExpectException":
+                        next = this.getNextStatement(next, "next");
+                        break;
                     default:
                         throw new Exception(
                                 String.format("Unexpected Statement received: [%s]",
diff --git a/tests/testables/tests/Android.bp b/tests/testables/tests/Android.bp
index 1eb36fa..c23f41a 100644
--- a/tests/testables/tests/Android.bp
+++ b/tests/testables/tests/Android.bp
@@ -34,6 +34,7 @@
         "androidx.core_core-animation",
         "androidx.core_core-ktx",
         "androidx.test.rules",
+        "androidx.test.ext.junit",
         "hamcrest-library",
         "mockito-target-inline-minus-junit4",
         "testables",
diff --git a/tests/testables/tests/src/android/testing/TestableLooperJUnit4Test.java b/tests/testables/tests/src/android/testing/TestableLooperJUnit4Test.java
new file mode 100644
index 0000000..b7d5e0e
--- /dev/null
+++ b/tests/testables/tests/src/android/testing/TestableLooperJUnit4Test.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.testing;
+
+import android.testing.TestableLooper.RunWithLooper;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test that TestableLooper now handles expected exceptions in tests
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@RunWithLooper
+public class TestableLooperJUnit4Test {
+    @Rule
+    public final TestWithLooperRule mTestWithLooperRule = new TestWithLooperRule();
+
+    @Test(expected = Exception.class)
+    public void testException() throws Exception {
+        throw new Exception("this exception is expected");
+    }
+}
+