Merge "AudioService: Fix ring over SCO sequence." into main
diff --git a/core/api/current.txt b/core/api/current.txt
index ff068f1..94481b6 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -55041,6 +55041,7 @@
     field public static final int CONTENT_CHANGE_TYPE_DRAG_STARTED = 128; // 0x80
     field public static final int CONTENT_CHANGE_TYPE_ENABLED = 4096; // 0x1000
     field public static final int CONTENT_CHANGE_TYPE_ERROR = 2048; // 0x800
+    field @FlaggedApi("android.view.accessibility.a11y_expansion_state_api") public static final int CONTENT_CHANGE_TYPE_EXPANDED = 16384; // 0x4000
     field public static final int CONTENT_CHANGE_TYPE_PANE_APPEARED = 16; // 0x10
     field public static final int CONTENT_CHANGE_TYPE_PANE_DISAPPEARED = 32; // 0x20
     field public static final int CONTENT_CHANGE_TYPE_PANE_TITLE = 8; // 0x8
@@ -55190,6 +55191,7 @@
     method public CharSequence getContentDescription();
     method public int getDrawingOrder();
     method public CharSequence getError();
+    method @FlaggedApi("android.view.accessibility.a11y_expansion_state_api") public int getExpandedState();
     method @Nullable public android.view.accessibility.AccessibilityNodeInfo.ExtraRenderingInfo getExtraRenderingInfo();
     method public android.os.Bundle getExtras();
     method public CharSequence getHintText();
@@ -55282,6 +55284,7 @@
     method public void setEditable(boolean);
     method public void setEnabled(boolean);
     method public void setError(CharSequence);
+    method @FlaggedApi("android.view.accessibility.a11y_expansion_state_api") public void setExpandedState(int);
     method public void setFocusable(boolean);
     method public void setFocused(boolean);
     method @FlaggedApi("android.view.accessibility.granular_scrolling") public void setGranularScrollingSupported(boolean);
@@ -55368,6 +55371,10 @@
     field @FlaggedApi("android.view.accessibility.tri_state_checked") public static final int CHECKED_STATE_PARTIAL = 2; // 0x2
     field @FlaggedApi("android.view.accessibility.tri_state_checked") public static final int CHECKED_STATE_TRUE = 1; // 0x1
     field @NonNull public static final android.os.Parcelable.Creator<android.view.accessibility.AccessibilityNodeInfo> CREATOR;
+    field @FlaggedApi("android.view.accessibility.a11y_expansion_state_api") public static final int EXPANDED_STATE_COLLAPSED = 1; // 0x1
+    field @FlaggedApi("android.view.accessibility.a11y_expansion_state_api") public static final int EXPANDED_STATE_FULL = 3; // 0x3
+    field @FlaggedApi("android.view.accessibility.a11y_expansion_state_api") public static final int EXPANDED_STATE_PARTIAL = 2; // 0x2
+    field @FlaggedApi("android.view.accessibility.a11y_expansion_state_api") public static final int EXPANDED_STATE_UNDEFINED = 0; // 0x0
     field public static final String EXTRA_DATA_RENDERING_INFO_KEY = "android.view.accessibility.extra.DATA_RENDERING_INFO_KEY";
     field public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH = "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH";
     field public static final int EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_MAX_LENGTH = 20000; // 0x4e20
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index f2a36e9..768b70c 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -39,6 +39,7 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.content.pm.ParceledListSlice;
 import android.content.pm.ShortcutInfo;
 import android.graphics.drawable.Icon;
@@ -1344,11 +1345,15 @@
      */
     @FlaggedApi(Flags.FLAG_MODES_API)
     public boolean areAutomaticZenRulesUserManaged() {
-        // modes ui is dependent on modes api
-        return Flags.modesApi() && Flags.modesUi();
+        if (Flags.modesApi() && Flags.modesUi()) {
+            PackageManager pm = mContext.getPackageManager();
+            return !pm.hasSystemFeature(PackageManager.FEATURE_WATCH)
+                    && !pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+        } else {
+            return false;
+        }
     }
 
-
     /**
      * Returns AutomaticZenRules owned by the caller.
      *
diff --git a/core/java/android/content/pm/IPackageInstaller.aidl b/core/java/android/content/pm/IPackageInstaller.aidl
index ecea479..4fdbf1e9e 100644
--- a/core/java/android/content/pm/IPackageInstaller.aidl
+++ b/core/java/android/content/pm/IPackageInstaller.aidl
@@ -95,8 +95,8 @@
     void reportUnarchivalStatus(int unarchiveId, int status, long requiredStorageBytes, in PendingIntent userActionIntent, in UserHandle userHandle);
 
     @EnforcePermission("VERIFICATION_AGENT")
-    int getVerificationPolicy();
+    int getVerificationPolicy(int userId);
 
     @EnforcePermission("VERIFICATION_AGENT")
-    boolean setVerificationPolicy(int policy);
+    boolean setVerificationPolicy(int policy, int userId);
 }
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index 5da1444..54c5596 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -1613,7 +1613,7 @@
     @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)
     public final @VerificationPolicy int getVerificationPolicy() {
         try {
-            return mInstaller.getVerificationPolicy();
+            return mInstaller.getVerificationPolicy(mUserId);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1631,7 +1631,7 @@
     @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)
     public final boolean setVerificationPolicy(@VerificationPolicy int policy) {
         try {
-            return mInstaller.setVerificationPolicy(policy);
+            return mInstaller.setVerificationPolicy(policy, mUserId);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig
index 6669754..1206855 100644
--- a/core/java/android/hardware/input/input_framework.aconfig
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -141,3 +141,10 @@
   description: "Adds shortcuts to toggle and control a11y features"
   bug: "373458181"
 }
+
+flag {
+  name: "override_power_key_behavior_in_focused_window"
+  namespace: "input_native"
+  description: "Allows privileged focused windows to capture power key events."
+  bug: "357144512"
+}
diff --git a/core/java/android/service/notification/SystemZenRules.java b/core/java/android/service/notification/SystemZenRules.java
index 1d18643..ebb8569 100644
--- a/core/java/android/service/notification/SystemZenRules.java
+++ b/core/java/android/service/notification/SystemZenRules.java
@@ -19,6 +19,7 @@
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.StringRes;
 import android.app.AutomaticZenRule;
 import android.app.Flags;
 import android.content.Context;
@@ -122,7 +123,7 @@
     public static String getTriggerDescriptionForScheduleTime(Context context,
             @NonNull ScheduleInfo schedule) {
         final StringBuilder sb = new StringBuilder();
-        String daysSummary = getShortDaysSummary(context, schedule);
+        String daysSummary = getDaysOfWeekShort(context, schedule);
         if (daysSummary == null) {
             // no use outputting times without dates
             return null;
@@ -135,11 +136,35 @@
     }
 
     /**
-     * Returns an ordered summarized list of the days on which this schedule applies, with
-     * adjacent days grouped together ("Sun-Wed" instead of "Sun,Mon,Tue,Wed").
+     * Returns a short, ordered summarized list of the days on which this schedule applies, using
+     * abbreviated week days, with adjacent days grouped together ("Sun-Wed" instead of
+     * "Sun,Mon,Tue,Wed").
      */
     @Nullable
-    public static String getShortDaysSummary(Context context, @NonNull ScheduleInfo schedule) {
+    public static String getDaysOfWeekShort(Context context, @NonNull ScheduleInfo schedule) {
+        return getDaysSummary(context, R.string.zen_mode_trigger_summary_range_symbol_combination,
+                new SimpleDateFormat("EEE", getLocale(context)), schedule);
+    }
+
+    /**
+     * Returns a string representing the days on which this schedule applies, using full week days,
+     * with adjacent days grouped together (e.g. "Sunday to Wednesday" instead of
+     * "Sunday,Monday,Tuesday,Wednesday").
+     */
+    @Nullable
+    public static String getDaysOfWeekFull(Context context, @NonNull ScheduleInfo schedule) {
+        return getDaysSummary(context, R.string.zen_mode_trigger_summary_range_words,
+                new SimpleDateFormat("EEEE", getLocale(context)), schedule);
+    }
+
+    /**
+     * Returns an ordered summarized list of the days on which this schedule applies, with
+     * adjacent days grouped together. The formatting of each individual day of week is done with
+     * the provided {@link SimpleDateFormat}.
+     */
+    @Nullable
+    private static String getDaysSummary(Context context, @StringRes int rangeFormatResId,
+            SimpleDateFormat dayOfWeekFormat, @NonNull ScheduleInfo schedule) {
         // Compute a list of days with contiguous days grouped together, for example: "Sun-Thu" or
         // "Sun-Mon,Wed,Fri"
         final int[] days = schedule.days;
@@ -197,19 +222,18 @@
                                 context.getString(R.string.zen_mode_trigger_summary_divider_text));
                     }
 
-                    SimpleDateFormat dayFormat = new SimpleDateFormat("EEE", getLocale(context));
                     if (startDay == lastSeenDay) {
                         // last group was only one day
                         cStart.set(Calendar.DAY_OF_WEEK, daysOfWeek[startDay]);
-                        sb.append(dayFormat.format(cStart.getTime()));
+                        sb.append(dayOfWeekFormat.format(cStart.getTime()));
                     } else {
                         // last group was a contiguous group of days, so group them together
                         cStart.set(Calendar.DAY_OF_WEEK, daysOfWeek[startDay]);
                         cEnd.set(Calendar.DAY_OF_WEEK, daysOfWeek[lastSeenDay]);
                         sb.append(context.getString(
-                                R.string.zen_mode_trigger_summary_range_symbol_combination,
-                                dayFormat.format(cStart.getTime()),
-                                dayFormat.format(cEnd.getTime())));
+                                rangeFormatResId,
+                                dayOfWeekFormat.format(cStart.getTime()),
+                                dayOfWeekFormat.format(cEnd.getTime())));
                     }
                 }
             }
diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java
index dfac244..4e9d054 100644
--- a/core/java/android/view/accessibility/AccessibilityEvent.java
+++ b/core/java/android/view/accessibility/AccessibilityEvent.java
@@ -798,6 +798,18 @@
     @FlaggedApi(Flags.FLAG_TRI_STATE_CHECKED)
     public static final int CONTENT_CHANGE_TYPE_CHECKED = 1 << 13;
 
+    /**
+     * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event: The source node changed its
+     * expanded state which is returned by {@link AccessibilityNodeInfo#getExpandedState()}. The
+     * view changing the node's expanded state should call {@link
+     * AccessibilityNodeInfo#setExpandedState(int)} and then send this event.
+     *
+     * @see AccessibilityNodeInfo#getExpandedState()
+     * @see AccessibilityNodeInfo#setExpandedState(int)
+     */
+    @FlaggedApi(Flags.FLAG_A11Y_EXPANSION_STATE_API)
+    public static final int CONTENT_CHANGE_TYPE_EXPANDED = 1 << 14;
+
     // Speech state change types.
 
     /** Change type for {@link #TYPE_SPEECH_STATE_CHANGE} event: another service is speaking. */
diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
index 60ccb77..d3e7faa 100644
--- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
@@ -756,6 +756,56 @@
     public static final String ACTION_ARGUMENT_SCROLL_AMOUNT_FLOAT =
             "android.view.accessibility.action.ARGUMENT_SCROLL_AMOUNT_FLOAT";
 
+    // Expanded state types.
+
+    /**
+     * Expanded state for a non-expandable element
+     *
+     * @see #getExpandedState()
+     * @see #setExpandedState(int)
+     */
+    @FlaggedApi(Flags.FLAG_A11Y_EXPANSION_STATE_API)
+    public static final int EXPANDED_STATE_UNDEFINED = 0;
+
+    /**
+     * Expanded state for a collapsed expandable element.
+     *
+     * @see #getExpandedState()
+     * @see #setExpandedState(int)
+     */
+    @FlaggedApi(Flags.FLAG_A11Y_EXPANSION_STATE_API)
+    public static final int EXPANDED_STATE_COLLAPSED = 1;
+
+    /**
+     * Expanded state for an expanded expandable element that can still be expanded further.
+     *
+     * @see #getExpandedState()
+     * @see #setExpandedState(int)
+     */
+    @FlaggedApi(Flags.FLAG_A11Y_EXPANSION_STATE_API)
+    public static final int EXPANDED_STATE_PARTIAL = 2;
+
+    /**
+     * Expanded state for a expanded expandable element that cannot be expanded further.
+     *
+     * @see #getExpandedState()
+     * @see #setExpandedState(int)
+     */
+    @FlaggedApi(Flags.FLAG_A11Y_EXPANSION_STATE_API)
+    public static final int EXPANDED_STATE_FULL = 3;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            prefix = "EXPANDED_STATE_",
+            value = {
+                EXPANDED_STATE_UNDEFINED,
+                EXPANDED_STATE_COLLAPSED,
+                EXPANDED_STATE_PARTIAL,
+                EXPANDED_STATE_FULL,
+            })
+    public @interface ExpandedState {}
+
     // Focus types.
 
     /**
@@ -1048,6 +1098,10 @@
     private int mMaxTextLength = -1;
     private int mMovementGranularities;
 
+    // TODO(b/362782158) Initialize mExpandedState explicitly with
+    // the EXPANDED_STATE_UNDEFINED state when flagging is removed.
+    private int mExpandedState;
+
     private int mTextSelectionStart = UNDEFINED_SELECTION_INDEX;
     private int mTextSelectionEnd = UNDEFINED_SELECTION_INDEX;
     private int mInputType = InputType.TYPE_NULL;
@@ -1931,6 +1985,47 @@
     }
 
     /**
+     * Sets the expanded state of the node.
+     *
+     * <p><strong>Note:</strong> Cannot be called from an {@link
+     * android.accessibilityservice.AccessibilityService}. This class is made immutable before being
+     * delivered to an {@link android.accessibilityservice.AccessibilityService}.
+     *
+     * @param state new expanded state of this node.
+     * @throws IllegalArgumentException If state is not one of:
+     *     <ul>
+     *       <li>{@link #EXPANDED_STATE_UNDEFINED}
+     *       <li>{@link #EXPANDED_STATE_COLLAPSED}
+     *       <li>{@link #EXPANDED_STATE_PARTIAL}
+     *       <li>{@link #EXPANDED_STATE_FULL}
+     *     </ul>
+     *
+     * @throws IllegalStateException If called from an AccessibilityService
+     */
+    @FlaggedApi(Flags.FLAG_A11Y_EXPANSION_STATE_API)
+    public void setExpandedState(@ExpandedState int state) {
+        enforceValidExpandedState(state);
+        enforceNotSealed();
+        mExpandedState = state;
+    }
+
+    /**
+     * Gets the expanded state for this node.
+     *
+     * @return The expanded state, one of:
+     *     <ul>
+     *       <li>{@link #EXPANDED_STATE_UNDEFINED}
+     *       <li>{@link #EXPANDED_STATE_COLLAPSED}
+     *       <li>{@link #EXPANDED_STATE_FULL}
+     *       <li>{@link #EXPANDED_STATE_PARTIAL}
+     *     </ul>
+     */
+    @FlaggedApi(Flags.FLAG_A11Y_EXPANSION_STATE_API)
+    public @ExpandedState int getExpandedState() {
+        return mExpandedState;
+    }
+
+    /**
      * Sets the minimum time duration between two content change events, which is used in throttling
      * content change events in accessibility services.
      *
@@ -4369,6 +4464,20 @@
         }
     }
 
+    private void enforceValidExpandedState(int state) {
+        if (Flags.a11yExpansionStateApi()) {
+            switch (state) {
+                case EXPANDED_STATE_UNDEFINED:
+                case EXPANDED_STATE_COLLAPSED:
+                case EXPANDED_STATE_PARTIAL:
+                case EXPANDED_STATE_FULL:
+                    return;
+                default:
+                    throw new IllegalArgumentException("Unknown expanded state: " + state);
+            }
+        }
+    }
+
     /**
      * Enforces that this instance is not sealed.
      *
@@ -4623,6 +4732,11 @@
         if (mChecked != DEFAULT.mChecked) {
             nonDefaultFields |= bitAt(fieldIndex);
         }
+        fieldIndex++;
+        if (mExpandedState != DEFAULT.mExpandedState) {
+            nonDefaultFields |= bitAt(fieldIndex);
+        }
+
         int totalFields = fieldIndex;
         parcel.writeLong(nonDefaultFields);
 
@@ -4794,6 +4908,9 @@
         if (isBitSet(nonDefaultFields, fieldIndex++)) {
             parcel.writeInt(mChecked);
         }
+        if (isBitSet(nonDefaultFields, fieldIndex++)) {
+            parcel.writeInt(mExpandedState);
+        }
 
         if (DEBUG) {
             fieldIndex--;
@@ -4883,6 +5000,7 @@
         mLeashedParent = other.mLeashedParent;
         mLeashedParentNodeId = other.mLeashedParentNodeId;
         mChecked = other.mChecked;
+        mExpandedState = other.mExpandedState;
     }
 
     private void initCopyInfos(AccessibilityNodeInfo other) {
@@ -5075,6 +5193,9 @@
         if (isBitSet(nonDefaultFields, fieldIndex++)) {
             mChecked = parcel.readInt();
         }
+        if (isBitSet(nonDefaultFields, fieldIndex++)) {
+            mExpandedState = parcel.readInt();
+        }
 
         mSealed = sealed;
     }
@@ -5249,6 +5370,26 @@
         }
     }
 
+    private static String getExpandedStateSymbolicName(int state) {
+        if (Flags.a11yExpansionStateApi()) {
+            switch (state) {
+                case EXPANDED_STATE_UNDEFINED:
+                    return "EXPANDED_STATE_UNDEFINED";
+                case EXPANDED_STATE_COLLAPSED:
+                    return "EXPANDED_STATE_COLLAPSED";
+                case EXPANDED_STATE_PARTIAL:
+                    return "EXPANDED_STATE_PARTIAL";
+                case EXPANDED_STATE_FULL:
+                    return "EXPANDED_STATE_FULL";
+                default:
+                    throw new IllegalArgumentException("Unknown expanded state: " + state);
+            }
+        } else {
+            // TODO(b/362782158) Remove when flag is removed.
+            return "";
+        }
+    }
+
     private static boolean canPerformRequestOverConnection(int connectionId,
             int windowId, long accessibilityNodeId) {
         final boolean hasWindowId = windowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
@@ -5346,6 +5487,7 @@
         builder.append("; containerTitle: ").append(mContainerTitle);
         builder.append("; viewIdResName: ").append(mViewIdResourceName);
         builder.append("; uniqueId: ").append(mUniqueId);
+        builder.append("; expandedState: ").append(getExpandedStateSymbolicName(mExpandedState));
 
         builder.append("; checkable: ").append(isCheckable());
         builder.append("; checked: ").append(isChecked());
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index 05dc910..22eec29 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -49,6 +49,7 @@
     ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS(
             Flags::enableCaptionCompatInsetForceConsumptionAlways, true),
     ENABLE_CASCADING_WINDOWS(Flags::enableCascadingWindows, true),
+    ENABLE_TILE_RESIZING(Flags::enableTileResizing, true),
     ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY(
             Flags::enableDesktopWindowingWallpaperActivity, true),
     ENABLE_DESKTOP_WINDOWING_MODALS_POLICY(Flags::enableDesktopWindowingModalsPolicy, true),
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 7aca535..732c316 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5348,6 +5348,8 @@
     <string name="zen_mode_trigger_summary_divider_text">,\u0020</string>
     <!-- [CHAR LIMIT=40] General template for a symbolic start - end range in a text summary, used for the trigger description of a Zen mode -->
     <string name="zen_mode_trigger_summary_range_symbol_combination"><xliff:g id="start" example="Sun">%1$s</xliff:g> - <xliff:g id="end" example="Thu">%2$s</xliff:g></string>
+    <!-- [CHAR LIMIT=40] General template for a start - end range in a text summary, used for the trigger description of a Zen mode -->
+    <string name="zen_mode_trigger_summary_range_words"><xliff:g id="start" example="Sunday">%1$s</xliff:g> to <xliff:g id="end" example="Thursday">%2$s</xliff:g></string>
     <!-- [CHAR LIMIT=40] Event-based rule calendar option value for any calendar, used for the trigger description of a Zen mode -->
     <string name="zen_mode_trigger_event_calendar_any">Any calendar</string>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index ecc3afe..4dbeb2f 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2657,6 +2657,7 @@
   <java-symbol type="string" name="zen_mode_implicit_deactivated" />
   <java-symbol type="string" name="zen_mode_trigger_summary_divider_text" />
   <java-symbol type="string" name="zen_mode_trigger_summary_range_symbol_combination" />
+  <java-symbol type="string" name="zen_mode_trigger_summary_range_words" />
   <java-symbol type="string" name="zen_mode_trigger_event_calendar_any" />
 
   <java-symbol type="string" name="display_rotation_camera_compat_toast_after_rotation" />
diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java
index 013117e..1721e1e 100644
--- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java
+++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java
@@ -34,6 +34,7 @@
 import android.os.Parcel;
 import android.os.UserHandle;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.text.InputType;
 import android.text.Spannable;
 import android.text.SpannableString;
@@ -87,6 +88,8 @@
         TEST_EDITOR_INFO.targetInputMethodUser = UserHandle.of(TEST_USER_ID);
     }
 
+    private final DeviceFlagsValueProvider mFlagsValueProvider = new DeviceFlagsValueProvider();
+
     /**
      * Makes sure that {@code null} {@link EditorInfo#targetInputMethodUser} can be copied via
      * {@link Parcel}.
@@ -522,7 +525,9 @@
         info.setSupportedHandwritingGestures(Arrays.asList(SelectGesture.class));
         info.setSupportedHandwritingGesturePreviews(
                 Stream.of(SelectGesture.class).collect(Collectors.toSet()));
-        if (Flags.editorinfoHandwritingEnabled()) {
+        final boolean isStylusHandwritingEnabled =
+                mFlagsValueProvider.getBoolean(Flags.FLAG_EDITORINFO_HANDWRITING_ENABLED);
+        if (isStylusHandwritingEnabled) {
             info.setStylusHandwritingEnabled(true);
         }
         info.packageName = "android.view.inputmethod";
@@ -548,8 +553,7 @@
                         + "prefix2: hintLocales=[en,es,zh]\n"
                         + "prefix2: supportedHandwritingGestureTypes=SELECT\n"
                         + "prefix2: supportedHandwritingGesturePreviewTypes=SELECT\n"
-                        + "prefix2: isStylusHandwritingEnabled="
-                                + Flags.editorinfoHandwritingEnabled() + "\n"
+                        + "prefix2: isStylusHandwritingEnabled=" + isStylusHandwritingEnabled + "\n"
                         + "prefix2: contentMimeTypes=[image/png]\n"
                         + "prefix2: targetInputMethodUserId=10\n");
     }
diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java
index 61bf137..44b2d90 100644
--- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java
+++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java
@@ -26,6 +26,8 @@
 import android.content.pm.ServiceInfo;
 import android.os.Bundle;
 import android.os.Parcel;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.platform.test.flag.junit.SetFlagsRule;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -43,8 +45,9 @@
 public class InputMethodInfoTest {
 
     @Rule
-    public SetFlagsRule mSetFlagsRule =
-            new SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT);
+    public SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    private final DeviceFlagsValueProvider mFlagsValueProvider = new DeviceFlagsValueProvider();
 
     @Test
     public void testEqualsAndHashCode() throws Exception {
@@ -70,7 +73,7 @@
         assertThat(imi.supportsInlineSuggestionsWithTouchExploration(), is(false));
         assertThat(imi.supportsStylusHandwriting(), is(false));
         assertThat(imi.createStylusHandwritingSettingsActivityIntent(), equalTo(null));
-        if (Flags.imeSwitcherRevampApi()) {
+        if (mFlagsValueProvider.getBoolean(Flags.FLAG_IME_SWITCHER_REVAMP_API)) {
             assertThat(imi.createImeLanguageSettingsActivityIntent(), equalTo(null));
         }
     }
@@ -121,9 +124,8 @@
     }
 
     @Test
+    @EnableFlags(android.companion.virtual.flags.Flags.FLAG_VDM_CUSTOM_IME)
     public void testIsVirtualDeviceOnly() throws Exception {
-        mSetFlagsRule.enableFlags(android.companion.virtual.flags.Flags.FLAG_VDM_CUSTOM_IME);
-
         final InputMethodInfo imi = buildInputMethodForTest(R.xml.ime_meta_virtual_device_only);
 
         assertThat(imi.isVirtualDeviceOnly(), is(true));
diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java
index da202b6..930e03d 100644
--- a/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java
+++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java
@@ -46,7 +46,7 @@
     // The number of fields tested in the corresponding CTS AccessibilityNodeInfoTest:
     // See fullyPopulateAccessibilityNodeInfo, assertEqualsAccessibilityNodeInfo,
     // and assertAccessibilityNodeInfoCleared in that class.
-    private static final int NUM_MARSHALLED_PROPERTIES = 45;
+    private static final int NUM_MARSHALLED_PROPERTIES = 46;
 
     /**
      * The number of properties that are purposely not marshalled
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp b/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp
index b6db6d9..61c09f2 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp
@@ -29,6 +29,8 @@
     static_libs: [
         "WindowManager-Shell",
         "platform-screenshot-diff-core",
+        "ScreenshotComposeUtilsLib", // ComposableScreenshotTestRule & Theme.PlatformUi.Screenshot
+        "SystemUI-res", // Theme.SystemUI (dragged in by ScreenshotComposeUtilsLib)
     ],
     asset_dirs: ["goldens/robolectric"],
     manifest: "AndroidManifestRobolectric.xml",
@@ -63,6 +65,8 @@
     ],
     static_libs: [
         "WindowManager-Shell",
+        "ScreenshotComposeUtilsLib", // ComposableScreenshotTestRule & Theme.PlatformUi.Screenshot
+        "SystemUI-res", // Theme.SystemUI (dragged in by ScreenshotComposeUtilsLib)
         "junit",
         "androidx.test.runner",
         "androidx.test.rules",
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml
index b4bdaea..72d0d5e4 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml
@@ -18,6 +18,7 @@
     <application android:debuggable="true" android:supportsRtl="true">
         <activity
             android:name="platform.test.screenshot.ScreenshotActivity"
+            android:theme="@style/Theme.PlatformUi.Screenshot"
             android:exported="true">
         </activity>
     </application>
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png
index 736bca7..5b429c0 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png
index 736bca7..6028fa2 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt
index f09969d..8cf3ce9 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt
@@ -15,10 +15,12 @@
  */
 package com.android.wm.shell.bubbles
 
+import android.graphics.Color
 import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.android.wm.shell.R
 import com.android.wm.shell.shared.bubbles.BubblePopupView
 import com.android.wm.shell.testing.goldenpathmanager.WMShellGoldenPathManager
-import com.android.wm.shell.R
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -48,6 +50,10 @@
     fun bubblesEducation() {
         screenshotRule.screenshotTest("bubbles_education") { activity ->
             activity.actionBar?.hide()
+            // Set the background color of the activity to be something not from the theme to
+            // ensure good contrast between the education view and the background
+            val rootView = activity.window.decorView.findViewById(android.R.id.content) as ViewGroup
+            rootView.setBackgroundColor(Color.RED)
             val view =
                 LayoutInflater.from(activity)
                     .inflate(R.layout.bubble_bar_stack_education, null) as BubblePopupView
diff --git a/libs/WindowManager/Shell/res/layout/tiling_split_divider.xml b/libs/WindowManager/Shell/res/layout/tiling_split_divider.xml
new file mode 100644
index 0000000..ce2e8be
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/tiling_split_divider.xml
@@ -0,0 +1,37 @@
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<com.android.wm.shell.windowdecor.tiling.TilingDividerView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_height="match_parent"
+    android:layout_width="match_parent"
+    android:id="@+id/divider_bar">
+
+        <com.android.wm.shell.common.split.DividerHandleView
+            android:id="@+id/docked_divider_handle"
+            android:layout_height="match_parent"
+            android:layout_width="match_parent"
+            android:layout_gravity="center"
+            android:contentDescription="@string/accessibility_divider"
+            android:background="@null"/>
+
+        <com.android.wm.shell.common.split.DividerRoundedCorner
+            android:id="@+id/docked_divider_rounded_corner"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"/>
+
+
+</com.android.wm.shell.windowdecor.tiling.TilingDividerView>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java
index 6c83d88..eb7ef14 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java
@@ -63,8 +63,19 @@
      * @param currentBounds {@link Rect} of the current animation bounds.
      * @param fraction progress of the animation ranged from 0f to 1f.
      */
-    public abstract void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
-            Rect currentBounds, float fraction);
+    public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+            Rect currentBounds, float fraction) {}
+
+    /**
+     * Animates the internal {@link #mLeash} by a given fraction for a config-at-end transition.
+     * @param atomicTx {@link SurfaceControl.Transaction} to operate, you should not explicitly
+     *                 call apply on this transaction, it should be applied on the caller side.
+     * @param scale scaling to apply onto the overlay.
+     * @param fraction progress of the animation ranged from 0f to 1f.
+     * @param endBounds the final bounds PiP is animating into.
+     */
+    public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+            float scale, float fraction, Rect endBounds) {}
 
     /** A {@link PipContentOverlay} uses solid color. */
     public static final class PipColorOverlay extends PipContentOverlay {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java
index bdbd4cf..6c04e2a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java
@@ -103,7 +103,8 @@
         mHoveringHeight = mHeight > mWidth ? ((int) (mHeight * 1.5f)) : mHeight;
     }
 
-    void setIsLeftRightSplit(boolean isLeftRightSplit) {
+    /** sets whether it's a left/right or top/bottom split */
+    public void setIsLeftRightSplit(boolean isLeftRightSplit) {
         mIsLeftRightSplit = isLeftRightSplit;
         updateDimens();
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java
index 834c15d..d5aaf75 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java
@@ -98,7 +98,11 @@
         return false;
     }
 
-    void setIsLeftRightSplit(boolean isLeftRightSplit) {
+    /**
+     * Set whether the rounded corner is for a left/right split.
+     * @param isLeftRightSplit whether it's a left/right split or top/bottom split.
+     */
+    public void setIsLeftRightSplit(boolean isLeftRightSplit) {
         mIsLeftRightSplit = isLeftRightSplit;
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 7f54786..0942e05 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -130,6 +130,7 @@
 import com.android.wm.shell.windowdecor.WindowDecorViewModel;
 import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer;
 import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController;
+import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel;
 
 import dagger.Binds;
 import dagger.Lazy;
@@ -649,7 +650,8 @@
             InteractionJankMonitor interactionJankMonitor,
             InputManager inputManager,
             FocusTransitionObserver focusTransitionObserver,
-            DesktopModeEventLogger desktopModeEventLogger) {
+            DesktopModeEventLogger desktopModeEventLogger,
+            DesktopTilingDecorViewModel desktopTilingDecorViewModel) {
         return new DesktopTasksController(context, shellInit, shellCommandHandler, shellController,
                 displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer,
                 dragAndDropController, transitions, keyguardManager,
@@ -661,8 +663,32 @@
                 desktopModeLoggerTransitionObserver, launchAdjacentController,
                 recentsTransitionHandler, multiInstanceHelper, mainExecutor, desktopTasksLimiter,
                 recentTasksController.orElse(null), interactionJankMonitor, mainHandler,
-                inputManager, focusTransitionObserver,
-                desktopModeEventLogger);
+                inputManager, focusTransitionObserver, desktopModeEventLogger,
+                desktopTilingDecorViewModel);
+    }
+
+    @WMSingleton
+    @Provides
+    static DesktopTilingDecorViewModel provideDesktopTilingViewModel(Context context,
+            DisplayController displayController,
+            RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
+            SyncTransactionQueue syncQueue,
+            Transitions transitions,
+            ShellTaskOrganizer shellTaskOrganizer,
+            ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler,
+            ReturnToDragStartAnimator returnToDragStartAnimator,
+            @DynamicOverride DesktopRepository desktopRepository) {
+        return new DesktopTilingDecorViewModel(
+                context,
+                displayController,
+                rootTaskDisplayAreaOrganizer,
+                syncQueue,
+                transitions,
+                shellTaskOrganizer,
+                toggleResizeDesktopTaskTransitionHandler,
+                returnToDragStartAnimator,
+                desktopRepository
+        );
     }
 
     @WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index bc78e43..eec2ba5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -118,6 +118,7 @@
 import com.android.wm.shell.transition.OneShotRemoteHandler
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.transition.Transitions.TransitionFinishCallback
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
 import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility
 import com.android.wm.shell.windowdecor.MoveToDesktopAnimator
 import com.android.wm.shell.windowdecor.OnTaskRepositionAnimationListener
@@ -125,6 +126,7 @@
 import com.android.wm.shell.windowdecor.extension.isFullscreen
 import com.android.wm.shell.windowdecor.extension.isMultiWindow
 import com.android.wm.shell.windowdecor.extension.requestingImmersive
+import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel
 import java.io.PrintWriter
 import java.util.Optional
 import java.util.concurrent.Executor
@@ -163,6 +165,7 @@
     private val inputManager: InputManager,
     private val focusTransitionObserver: FocusTransitionObserver,
     private val desktopModeEventLogger: DesktopModeEventLogger,
+    private val desktopTilingDecorViewModel: DesktopTilingDecorViewModel,
 ) :
     RemoteCallable<DesktopTasksController>,
     Transitions.TransitionHandler,
@@ -237,6 +240,7 @@
                 override fun onAnimationStateChanged(running: Boolean) {
                     logV("Recents animation state changed running=%b", running)
                     recentsAnimationRunning = running
+                    desktopTilingDecorViewModel.onOverviewAnimationStateChange(running)
                 }
             }
         )
@@ -492,6 +496,7 @@
         taskInfo: RunningTaskInfo,
     ): ((IBinder) -> Unit)? {
         val taskId = taskInfo.taskId
+        desktopTilingDecorViewModel.removeTaskIfTiled(displayId, taskId)
         if (taskRepository.isOnlyVisibleNonClosingTask(taskId)) {
             removeWallpaperActivity(wct)
         }
@@ -532,6 +537,7 @@
     /** Move a task with given `taskId` to fullscreen */
     fun moveToFullscreen(taskId: Int, transitionSource: DesktopModeTransitionSource) {
         shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task ->
+            desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, taskId)
             moveToFullscreenWithAnimation(task, task.positionInParent, transitionSource)
         }
     }
@@ -539,6 +545,7 @@
     /** Enter fullscreen by moving the focused freeform task in given `displayId` to fullscreen. */
     fun enterFullscreen(displayId: Int, transitionSource: DesktopModeTransitionSource) {
         getFocusedFreeformTask(displayId)?.let {
+            desktopTilingDecorViewModel.removeTaskIfTiled(displayId, it.taskId)
             moveToFullscreenWithAnimation(it, it.positionInParent, transitionSource)
         }
     }
@@ -546,6 +553,7 @@
     /** Move a desktop app to split screen. */
     fun moveToSplit(task: RunningTaskInfo) {
         logV( "moveToSplit taskId=%s", task.taskId)
+        desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, task.taskId)
         val wct = WindowContainerTransaction()
         wct.setBounds(task.token, Rect())
         // Rather than set windowing mode to multi-window at task level, set it to
@@ -643,6 +651,11 @@
     @JvmOverloads
     fun moveTaskToFront(taskInfo: RunningTaskInfo, remoteTransition: RemoteTransition? = null) {
         logV("moveTaskToFront taskId=%s", taskInfo.taskId)
+        // If a task is tiled, another task should be brought to foreground with it so let
+        // tiling controller handle the request.
+        if (desktopTilingDecorViewModel.moveTaskToFrontIfTiled(taskInfo)) {
+            return
+        }
         val wct = WindowContainerTransaction()
         wct.reorder(taskInfo.token, true /* onTop */, true /* includingParents */)
         val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(
@@ -804,13 +817,13 @@
         } else {
             // Save current bounds so that task can be restored back to original bounds if necessary
             // and toggle to the stable bounds.
+            desktopTilingDecorViewModel.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId)
             taskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, currentTaskBounds)
 
             destinationBounds.set(calculateMaximizeBounds(displayLayout, taskInfo))
         }
 
 
-
         val shouldRestoreToSnap =
             isMaximized && isTaskSnappedToHalfScreen(taskInfo, destinationBounds)
 
@@ -918,7 +931,20 @@
         position: SnapPosition,
         resizeTrigger: ResizeTrigger,
         motionEvent: MotionEvent?,
+        desktopWindowDecoration: DesktopModeWindowDecoration,
     ) {
+        if (DesktopModeFlags.ENABLE_TILE_RESIZING.isTrue()) {
+            val isTiled = desktopTilingDecorViewModel.snapToHalfScreen(
+                taskInfo,
+                desktopWindowDecoration,
+                position,
+                currentDragBounds,
+            )
+            if (isTiled) {
+                taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(true)
+            }
+            return
+        }
         val destinationBounds = getSnapBounds(taskInfo, position)
         desktopModeEventLogger.logTaskResizingEnded(
             resizeTrigger,
@@ -938,7 +964,7 @@
                     taskSurface,
                     startBounds = currentDragBounds,
                     endBounds = destinationBounds,
-                    isResizable = taskInfo.isResizeable
+                    isResizable = taskInfo.isResizeable,
                 )
             }
             return
@@ -958,6 +984,7 @@
         currentDragBounds: Rect,
         dragStartBounds: Rect,
         motionEvent: MotionEvent,
+        desktopModeWindowDecoration: DesktopModeWindowDecoration,
     ) {
         releaseVisualIndicator()
         if (!taskInfo.isResizeable && DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE.isTrue()) {
@@ -992,6 +1019,7 @@
                 position,
                 resizeTrigger,
                 motionEvent,
+                desktopModeWindowDecoration,
             )
         }
     }
@@ -1499,7 +1527,11 @@
             addPendingMinimizeTransition(transition, taskIdToMinimize)
             return wct
         }
-        return if (wct.isEmpty) null else wct
+        if (!wct.isEmpty) {
+            desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, task.taskId)
+            return wct
+        }
+        return null
     }
 
     private fun handleFullscreenTaskLaunch(
@@ -1565,6 +1597,7 @@
 
         if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
             taskRepository.addClosingTask(task.displayId, task.taskId)
+            desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, task.taskId)
         }
 
         taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
@@ -1812,6 +1845,7 @@
         taskBounds: Rect
     ) {
         if (taskInfo.windowingMode != WINDOWING_MODE_FREEFORM) return
+        desktopTilingDecorViewModel.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId)
         updateVisualIndicator(taskInfo, taskSurface, inputX, taskBounds.top.toFloat(),
             DragStartState.FROM_FREEFORM)
     }
@@ -1861,6 +1895,7 @@
         validDragArea: Rect,
         dragStartBounds: Rect,
         motionEvent: MotionEvent,
+        desktopModeWindowDecoration: DesktopModeWindowDecoration,
     ) {
         if (taskInfo.configuration.windowConfiguration.windowingMode != WINDOWING_MODE_FREEFORM) {
             return
@@ -1887,6 +1922,7 @@
                     currentDragBounds,
                     dragStartBounds,
                     motionEvent,
+                    desktopModeWindowDecoration,
                 )
             }
             IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> {
@@ -1897,6 +1933,7 @@
                     currentDragBounds,
                     dragStartBounds,
                     motionEvent,
+                    desktopModeWindowDecoration,
                 )
             }
             IndicatorType.NO_INDICATOR -> {
@@ -2090,6 +2127,7 @@
     // TODO(b/366397912): Support full multi-user mode in Windowing.
     override fun onUserChanged(newUserId: Int, userContext: Context) {
         userId = newUserId
+        desktopTilingDecorViewModel.onUserChange()
     }
 
     /** Called when a task's info changes. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java
index 5381a62..740b9af 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java
@@ -23,6 +23,7 @@
 import android.animation.RectEvaluator;
 import android.animation.ValueAnimator;
 import android.content.Context;
+import android.content.pm.ActivityInfo;
 import android.graphics.Matrix;
 import android.graphics.PointF;
 import android.graphics.Rect;
@@ -33,10 +34,13 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.icons.IconProvider;
 import com.android.wm.shell.R;
 import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
+import com.android.wm.shell.pip2.phone.PipAppIconOverlay;
 import com.android.wm.shell.shared.animation.Interpolators;
+import com.android.wm.shell.shared.pip.PipContentOverlay;
 
 /**
  * Animator that handles bounds animations for entering PIP.
@@ -59,6 +63,10 @@
 
     private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
             mSurfaceControlTransactionFactory;
+    Matrix mTransformTensor = new Matrix();
+    final float[] mMatrixTmp = new float[9];
+    @Nullable private PipContentOverlay mContentOverlay;
+
 
     // Internal state representing initial transform - cached to avoid recalculation.
     private final PointF mInitScale = new PointF();
@@ -67,9 +75,6 @@
     private final PointF mInitActivityScale = new PointF();
     private final PointF mInitActivityPos = new PointF();
 
-    Matrix mTransformTensor = new Matrix();
-    final float[] mMatrixTmp = new float[9];
-
     public PipEnterAnimator(Context context,
             @NonNull SurfaceControl leash,
             SurfaceControl.Transaction startTransaction,
@@ -161,10 +166,15 @@
         mRectEvaluator.evaluate(fraction, initCrop, endCrop);
         tx.setCrop(mLeash, mAnimatedRect);
 
+        mTransformTensor.reset();
         mTransformTensor.setScale(scaleX, scaleY);
         mTransformTensor.postTranslate(posX, posY);
         mTransformTensor.postRotate(degrees);
         tx.setMatrix(mLeash, mTransformTensor, mMatrixTmp);
+
+        if (mContentOverlay != null) {
+            mContentOverlay.onAnimationUpdate(tx, 1f / scaleX, fraction, mEndBounds);
+        }
     }
 
     // no-ops
@@ -200,4 +210,48 @@
         }
         PipUtils.calcStartTransform(pipChange, mInitScale, mInitPos, mInitCrop);
     }
+
+    /**
+     * Initializes and attaches an app icon overlay on top of the PiP layer.
+     */
+    public void setAppIconContentOverlay(Context context, Rect appBounds, Rect destinationBounds,
+            ActivityInfo activityInfo, int appIconSizePx) {
+        reattachAppIconOverlay(
+                new PipAppIconOverlay(context, appBounds, destinationBounds,
+                        new IconProvider(context).getIcon(activityInfo), appIconSizePx));
+    }
+
+    private void reattachAppIconOverlay(PipAppIconOverlay overlay) {
+        final SurfaceControl.Transaction tx =
+                mSurfaceControlTransactionFactory.getTransaction();
+        if (mContentOverlay != null) {
+            mContentOverlay.detach(tx);
+        }
+        mContentOverlay = overlay;
+        mContentOverlay.attach(tx, mLeash);
+    }
+
+    /**
+     * Clears the {@link #mContentOverlay}, this should be done after the content overlay is
+     * faded out.
+     */
+    public void clearAppIconOverlay() {
+        if (mContentOverlay == null) {
+            return;
+        }
+        SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction();
+        mContentOverlay.detach(tx);
+        mContentOverlay = null;
+    }
+
+    /**
+     * @return the app icon overlay leash; null if no overlay is attached.
+     */
+    @Nullable
+    public SurfaceControl getContentOverlayLeash() {
+        if (mContentOverlay == null) {
+            return null;
+        }
+        return mContentOverlay.getLeash();
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java
new file mode 100644
index 0000000..b4cf890
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java
@@ -0,0 +1,144 @@
+/*
+ * 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.pip2.phone;
+
+import static android.util.TypedValue.COMPLEX_UNIT_DIP;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.TypedValue;
+import android.view.SurfaceControl;
+
+import com.android.wm.shell.shared.pip.PipContentOverlay;
+
+/** A {@link PipContentOverlay} shows app icon on solid color background. */
+public final class PipAppIconOverlay extends PipContentOverlay {
+    private static final String TAG = PipAppIconOverlay.class.getSimpleName();
+    // The maximum size for app icon in pixel.
+    private static final int MAX_APP_ICON_SIZE_DP = 72;
+
+    private final Context mContext;
+    private final int mAppIconSizePx;
+    private final Rect mAppBounds;
+    private final int mOverlayHalfSize;
+    private final Matrix mTmpTransform = new Matrix();
+    private final float[] mTmpFloat9 = new float[9];
+
+    private Bitmap mBitmap;
+
+    public PipAppIconOverlay(Context context, Rect appBounds, Rect destinationBounds,
+            Drawable appIcon, int appIconSizePx) {
+        mContext = context;
+        final int maxAppIconSizePx = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP,
+                MAX_APP_ICON_SIZE_DP, context.getResources().getDisplayMetrics());
+        mAppIconSizePx = Math.min(maxAppIconSizePx, appIconSizePx);
+
+        final int overlaySize = getOverlaySize(appBounds, destinationBounds);
+        mOverlayHalfSize = overlaySize >> 1;
+
+        // When the activity is in the secondary split, make sure the scaling center is not
+        // offset.
+        mAppBounds = new Rect(0, 0, appBounds.width(), appBounds.height());
+
+        mBitmap = Bitmap.createBitmap(overlaySize, overlaySize, Bitmap.Config.ARGB_8888);
+        prepareAppIconOverlay(appIcon);
+        mLeash = new SurfaceControl.Builder()
+                .setCallsite(TAG)
+                .setName(LAYER_NAME)
+                .build();
+    }
+
+    /**
+     * Returns the size of the app icon overlay.
+     *
+     * In order to have the overlay always cover the pip window during the transition,
+     * the overlay will be drawn with the max size of the start and end bounds in different
+     * rotation.
+     */
+    public static int getOverlaySize(Rect appBounds, Rect destinationBounds) {
+        final int appWidth = appBounds.width();
+        final int appHeight = appBounds.height();
+
+        return Math.max(Math.max(appWidth, appHeight),
+                Math.max(destinationBounds.width(), destinationBounds.height())) + 1;
+    }
+
+    @Override
+    public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) {
+        tx.show(mLeash);
+        tx.setLayer(mLeash, Integer.MAX_VALUE);
+        tx.setBuffer(mLeash, mBitmap.getHardwareBuffer());
+        tx.setAlpha(mLeash, 0f);
+        tx.reparent(mLeash, parentLeash);
+        tx.apply();
+    }
+
+    @Override
+    public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+            float scale, float fraction, Rect endBounds) {
+        mTmpTransform.reset();
+        // Scale back the bitmap with the pivot at parent origin
+        mTmpTransform.setScale(scale, scale);
+        // We are negative-cropping away from the final bounds crop in config-at-end enter PiP;
+        // this means that the overlay shift depends on the final bounds.
+        // Note: translation is also dependent on the scaling of the parent.
+        mTmpTransform.postTranslate(endBounds.width() / 2f - mOverlayHalfSize * scale,
+                endBounds.height() / 2f - mOverlayHalfSize * scale);
+        atomicTx.setMatrix(mLeash, mTmpTransform, mTmpFloat9)
+                .setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2);
+    }
+
+
+
+    @Override
+    public void detach(SurfaceControl.Transaction tx) {
+        super.detach(tx);
+        if (mBitmap != null && !mBitmap.isRecycled()) {
+            mBitmap.recycle();
+        }
+    }
+
+    private void prepareAppIconOverlay(Drawable appIcon) {
+        final Canvas canvas = new Canvas();
+        canvas.setBitmap(mBitmap);
+        final TypedArray ta = mContext.obtainStyledAttributes(new int[] {
+                android.R.attr.colorBackground });
+        try {
+            int colorAccent = ta.getColor(0, 0);
+            canvas.drawRGB(
+                    Color.red(colorAccent),
+                    Color.green(colorAccent),
+                    Color.blue(colorAccent));
+        } finally {
+            ta.recycle();
+        }
+        final Rect appIconBounds = new Rect(
+                mOverlayHalfSize - mAppIconSizePx / 2,
+                mOverlayHalfSize - mAppIconSizePx / 2,
+                mOverlayHalfSize + mAppIconSizePx / 2,
+                mOverlayHalfSize + mAppIconSizePx / 2);
+        appIcon.setBounds(appIconBounds);
+        appIcon.draw(canvas);
+        mBitmap = mBitmap.copy(Bitmap.Config.HARDWARE, false /* mutable */);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
index 0427294..9a93371 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
@@ -456,6 +456,10 @@
         }
     }
 
+    private void setLauncherAppIconSize(int iconSizePx) {
+        mPipBoundsState.getLauncherState().setAppIconSizePx(iconSizePx);
+    }
+
     /**
      * The interface for calls from outside the Shell, within the host process.
      */
@@ -571,7 +575,10 @@
         }
 
         @Override
-        public void setLauncherAppIconSize(int iconSizePx) {}
+        public void setLauncherAppIconSize(int iconSizePx) {
+            executeRemoteCallWithTaskPermission(mController, "setLauncherAppIconSize",
+                    (controller) -> controller.setLauncherAppIconSize(iconSizePx));
+        }
 
         @Override
         public void setPipAnimationListener(IPipAnimationListener listener) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index b286211..e90b32c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -30,9 +30,6 @@
 import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP;
 import static com.android.wm.shell.transition.Transitions.TRANSIT_RESIZE_PIP;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
 import android.annotation.NonNull;
 import android.app.ActivityManager;
 import android.app.PictureInPictureParams;
@@ -362,33 +359,19 @@
         animator.setEnterStartState(pipChange, pipActivityChange);
         animator.onEnterAnimationUpdate(1.0f /* fraction */, startTransaction);
         startTransaction.apply();
+
+        if (swipePipToHomeOverlay != null) {
+            // fadeout the overlay if needed.
+            startOverlayFadeoutAnimation(swipePipToHomeOverlay, () -> {
+                SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+                tx.remove(swipePipToHomeOverlay);
+                tx.apply();
+            });
+        }
         finishInner();
         return true;
     }
 
-    private void startOverlayFadeoutAnimation() {
-        ValueAnimator animator = ValueAnimator.ofFloat(1f, 0f);
-        animator.setDuration(CONTENT_OVERLAY_FADE_OUT_DELAY_MS);
-        animator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                super.onAnimationEnd(animation);
-                SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
-                tx.remove(mPipTransitionState.getSwipePipToHomeOverlay());
-                tx.apply();
-
-                // We have fully completed enter-PiP animation after the overlay is gone.
-                mPipTransitionState.setState(PipTransitionState.ENTERED_PIP);
-            }
-        });
-        animator.addUpdateListener(animation -> {
-            float alpha = (float) animation.getAnimatedValue();
-            SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
-            tx.setAlpha(mPipTransitionState.getSwipePipToHomeOverlay(), alpha).apply();
-        });
-        animator.start();
-    }
-
     private boolean startBoundsTypeEnterAnimation(@NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
@@ -405,15 +388,18 @@
             return false;
         }
 
-        Rect endBounds = pipChange.getEndAbsBounds();
-        SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash;
-        Preconditions.checkNotNull(pipLeash, "Leash is null for bounds transition.");
+        final Rect startBounds = pipChange.getStartAbsBounds();
+        final Rect endBounds = pipChange.getEndAbsBounds();
 
-        Rect sourceRectHint = null;
-        if (pipChange.getTaskInfo() != null
-                && pipChange.getTaskInfo().pictureInPictureParams != null) {
-            sourceRectHint = pipChange.getTaskInfo().pictureInPictureParams.getSourceRectHint();
-        }
+        final PictureInPictureParams params = pipChange.getTaskInfo().pictureInPictureParams;
+        final float aspectRatio = mPipBoundsAlgorithm.getAspectRatioOrDefault(params);
+
+        final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, startBounds,
+                endBounds);
+
+        final SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash;
+        final Rect adjustedSourceRectHint = sourceRectHint != null ? new Rect(sourceRectHint)
+                : PipUtils.getEnterPipWithOverlaySrcRectHint(startBounds, aspectRatio);
 
         // For opening type transitions, if there is a change of mode TO_FRONT/OPEN,
         // make sure that change has alpha of 1f, since it's init state might be set to alpha=0f
@@ -441,14 +427,36 @@
         }
 
         PipEnterAnimator animator = new PipEnterAnimator(mContext, pipLeash,
-                startTransaction, finishTransaction, endBounds, sourceRectHint, delta);
+                startTransaction, finishTransaction, endBounds, adjustedSourceRectHint, delta);
+        if (sourceRectHint == null) {
+            // update the src-rect-hint in params in place, to set up initial animator transform.
+            params.getSourceRectHint().set(adjustedSourceRectHint);
+            animator.setAppIconContentOverlay(
+                    mContext, startBounds, endBounds, pipChange.getTaskInfo().topActivityInfo,
+                    mPipBoundsState.getLauncherState().getAppIconSizePx());
+        }
         animator.setAnimationStartCallback(() -> animator.setEnterStartState(pipChange,
                 pipActivityChange));
-        animator.setAnimationEndCallback(this::finishInner);
+        animator.setAnimationEndCallback(() -> {
+            if (animator.getContentOverlayLeash() != null) {
+                startOverlayFadeoutAnimation(animator.getContentOverlayLeash(),
+                        animator::clearAppIconOverlay);
+            }
+            finishInner();
+        });
         animator.start();
         return true;
     }
 
+    private void startOverlayFadeoutAnimation(@NonNull SurfaceControl overlayLeash,
+            @NonNull Runnable onAnimationEnd) {
+        PipAlphaAnimator animator = new PipAlphaAnimator(mContext, overlayLeash,
+                null /* startTx */, PipAlphaAnimator.FADE_OUT);
+        animator.setDuration(CONTENT_OVERLAY_FADE_OUT_DELAY_MS);
+        animator.setAnimationEndCallback(onAnimationEnd);
+        animator.start();
+    }
+
     private void handleBoundsTypeFixedRotation(TransitionInfo.Change pipTaskChange,
             TransitionInfo.Change pipActivityChange, int endRotation) {
         final Rect endBounds = pipTaskChange.getEndAbsBounds();
@@ -696,9 +704,7 @@
 
     private void finishInner() {
         finishTransition(null /* tx */);
-        if (mPipTransitionState.getSwipePipToHomeOverlay() != null) {
-            startOverlayFadeoutAnimation();
-        } else if (mPipTransitionState.getState() == PipTransitionState.ENTERING_PIP) {
+        if (mPipTransitionState.getState() == PipTransitionState.ENTERING_PIP) {
             // If we were entering PiP (i.e. playing the animation) with a valid srcRectHint,
             // and then we get a signal on client finishing its draw after the transition
             // has ended, then we have fully entered PiP.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index fde01ee..3946b61 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -290,9 +290,11 @@
 
         final Resources res = mResult.mRootView.getResources();
         mDragResizeListener.setGeometry(new DragResizeWindowGeometry(0 /* taskCornerRadius */,
-                new Size(mResult.mWidth, mResult.mHeight), getResizeEdgeHandleSize(res),
-                getResizeHandleEdgeInset(res), getFineResizeCornerSize(res),
-                getLargeResizeCornerSize(res)), touchSlop);
+                        new Size(mResult.mWidth, mResult.mHeight),
+                        getResizeEdgeHandleSize(res),
+                        getResizeHandleEdgeInset(res), getFineResizeCornerSize(res),
+                        getLargeResizeCornerSize(res), DragResizeWindowGeometry.DisabledEdge.NONE),
+                touchSlop);
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index f404326..a3324cc6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -187,7 +187,7 @@
             new ExclusionRegionListenerImpl();
 
     private final SparseArray<DesktopModeWindowDecoration> mWindowDecorByTaskId;
-    private final DragStartListenerImpl mDragStartListener = new DragStartListenerImpl();
+    private final DragEventListenerImpl mDragEventListener = new DragEventListenerImpl();
     private final InputMonitorFactory mInputMonitorFactory;
     private TaskOperations mTaskOperations;
     private final Supplier<SurfaceControl.Transaction> mTransactionFactory;
@@ -613,7 +613,8 @@
                     decoration.mTaskInfo.configuration.windowConfiguration.getBounds(),
                     left ? SnapPosition.LEFT : SnapPosition.RIGHT,
                     resizeTrigger,
-                    motionEvent);
+                    motionEvent,
+                    mWindowDecorByTaskId.get(taskId));
         }
 
         decoration.closeHandleMenu();
@@ -1072,7 +1073,8 @@
                             taskInfo, decoration.mTaskSurface, position,
                             new PointF(e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)),
                             newTaskBounds, decoration.calculateValidDragArea(),
-                            new Rect(mOnDragStartInitialBounds), e);
+                            new Rect(mOnDragStartInitialBounds), e,
+                            mWindowDecorByTaskId.get(taskInfo.taskId));
                     if (touchingButton && !mHasLongClicked) {
                         // We need the input event to not be consumed here to end the ripple
                         // effect on the touched button. We will reset drag state in the ensuing
@@ -1524,7 +1526,7 @@
                 mTaskOrganizer,
                 windowDecoration,
                 mDisplayController,
-                mDragStartListener,
+                mDragEventListener,
                 mTransitions,
                 mInteractionJankMonitor,
                 mTransactionFactory,
@@ -1667,13 +1669,18 @@
         }
     }
 
-    private class DragStartListenerImpl
-            implements DragPositioningCallbackUtility.DragStartListener {
+    private class DragEventListenerImpl
+            implements DragPositioningCallbackUtility.DragEventListener {
         @Override
         public void onDragStart(int taskId) {
             final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
             decoration.closeHandleMenu();
         }
+
+        @Override
+        public void onDragMove(int taskId) {
+
+        }
     }
 
     /**
@@ -1767,7 +1774,7 @@
                 ShellTaskOrganizer taskOrganizer,
                 DesktopModeWindowDecoration windowDecoration,
                 DisplayController displayController,
-                DragPositioningCallbackUtility.DragStartListener dragStartListener,
+                DragPositioningCallbackUtility.DragEventListener dragEventListener,
                 Transitions transitions,
                 InteractionJankMonitor interactionJankMonitor,
                 Supplier<SurfaceControl.Transaction> transactionFactory,
@@ -1777,7 +1784,7 @@
                             taskOrganizer,
                             windowDecoration,
                             displayController,
-                            dragStartListener,
+                            dragEventListener,
                             transitions,
                             interactionJankMonitor,
                             handler)
@@ -1786,7 +1793,7 @@
                             transitions,
                             windowDecoration,
                             displayController,
-                            dragStartListener,
+                            dragEventListener,
                             transactionFactory);
 
             if (DesktopModeFlags.ENABLE_WINDOWING_SCALED_RESIZING.isTrue()) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 8865112..dc27cfe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -27,14 +27,18 @@
 import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION;
 import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS;
 
+
 import static com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT;
 import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode;
 import static com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.DisabledEdge;
+import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.DisabledEdge.NONE;
 import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize;
 import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLargeResizeCornerSize;
 import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize;
 import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeHandleEdgeInset;
+import static com.android.wm.shell.windowdecor.DragPositioningCallbackUtility.DragEventListener;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -155,6 +159,8 @@
     private DragResizeInputListener mDragResizeListener;
     private Runnable mCurrentViewHostRunnable = null;
     private RelayoutParams mRelayoutParams = new RelayoutParams();
+    private DisabledEdge mDisabledResizingEdge =
+            NONE;
     private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult =
             new WindowDecoration.RelayoutResult<>();
     private final Runnable mViewHostRunnable =
@@ -329,6 +335,24 @@
         mOnToSplitscreenClickListener = listener;
     }
 
+    /**
+     * Adds a drag resize observer that gets notified on the task being drag resized.
+     *
+     * @param dragResizeListener The observing object to be added.
+     */
+    public void addDragResizeListener(DragEventListener dragResizeListener) {
+        mTaskDragResizer.addDragEventListener(dragResizeListener);
+    }
+
+    /**
+     * Removes an already existing drag resize observer.
+     *
+     * @param dragResizeListener observer to be removed.
+     */
+    public void removeDragResizeListener(DragEventListener dragResizeListener) {
+        mTaskDragResizer.removeDragEventListener(dragResizeListener);
+    }
+
     /** Registers a listener to be called when the decoration's new window action is triggered. */
     void setOnNewWindowClickListener(Function0<Unit> listener) {
         mOnNewWindowClickListener = listener;
@@ -386,6 +410,24 @@
         }
     }
 
+    /**
+     * Disables resizing for the given edge.
+     *
+     * @param disabledResizingEdge edge to disable.
+     * @param shouldDelayUpdate whether the update should be executed immediately or delayed.
+     */
+    public void updateDisabledResizingEdge(
+            DragResizeWindowGeometry.DisabledEdge disabledResizingEdge, boolean shouldDelayUpdate) {
+        mDisabledResizingEdge = disabledResizingEdge;
+        final boolean inFullImmersive = mDesktopRepository
+                .isTaskInFullImmersiveState(mTaskInfo.taskId);
+        if (shouldDelayUpdate) {
+            return;
+        }
+        updateDragResizeListener(mDecorationContainerSurface, inFullImmersive);
+    }
+
+
     void relayout(ActivityManager.RunningTaskInfo taskInfo,
             SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
             boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop,
@@ -645,7 +687,8 @@
                 new DragResizeWindowGeometry(mRelayoutParams.mCornerRadius,
                         new Size(mResult.mWidth, mResult.mHeight),
                         getResizeEdgeHandleSize(res), getResizeHandleEdgeInset(res),
-                        getFineResizeCornerSize(res), getLargeResizeCornerSize(res)), touchSlop)
+                        getFineResizeCornerSize(res), getLargeResizeCornerSize(res),
+                        mDisabledResizingEdge), touchSlop)
                 || !mTaskInfo.positionInParent.equals(mPositionInParent)) {
             updateExclusionRegion(inFullImmersive);
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java
index 01bb7f7..d36fc12 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java
@@ -42,7 +42,7 @@
  *
  * All touch events must be passed through this class to track a drag event.
  */
-class DragDetector {
+public class DragDetector {
     private final MotionEventHandler mEventHandler;
 
     private final PointF mInputDownPoint = new PointF();
@@ -55,7 +55,14 @@
 
     private boolean mResultOfDownAction;
 
-    DragDetector(@NonNull MotionEventHandler eventHandler, long holdToDragMinDurationMs,
+    /**
+     * Initialises a drag detector.
+     *
+     * @param eventHandler drag event handler.
+     * @param holdToDragMinDurationMs hold to drag duration.
+     * @param touchSlop touch slope threshold.
+     */
+    public DragDetector(@NonNull MotionEventHandler eventHandler, long holdToDragMinDurationMs,
             int touchSlop) {
         resetState();
         mEventHandler = eventHandler;
@@ -69,7 +76,7 @@
      * @return the result returned by {@link #mEventHandler}, or the result when
      * {@link #mEventHandler} handles the previous down event if the event shouldn't be passed
      */
-    boolean onMotionEvent(MotionEvent ev) {
+    public boolean onMotionEvent(MotionEvent ev) {
         return onMotionEvent(null /* view */, ev);
     }
 
@@ -79,7 +86,7 @@
      * @return the result returned by {@link #mEventHandler}, or the result when
      * {@link #mEventHandler} handles the previous down event if the event shouldn't be passed
      */
-    boolean onMotionEvent(View v, MotionEvent ev) {
+    public boolean onMotionEvent(View v, MotionEvent ev) {
         final boolean isTouchScreen =
                 (ev.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN;
         if (!isTouchScreen) {
@@ -190,7 +197,16 @@
         mDidHoldForMinDuration = false;
     }
 
-    interface MotionEventHandler {
+    /**
+     * Interface to be implemented by the class using the DragDetector for callback.
+     */
+    public interface MotionEventHandler {
+        /**
+         * Called back when drag is detected to notify the implementing class to handle drag events.
+         * @param v view on which the input arrived.
+         * @param ev motion event that resulted in drag.
+         * @return whether this was a drag event or not.
+         */
         boolean handleMotionEvent(@Nullable View v, MotionEvent ev);
     }
 }
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
index 78e7962..8eced3e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
@@ -208,7 +208,17 @@
         return result;
     }
 
-    private static boolean isExceedingWidthConstraint(int repositionedWidth, int startingWidth,
+    /**
+     * Checks whether the new task bounds exceed the allowed width.
+     *
+     * @param repositionedWidth task width after repositioning.
+     * @param startingWidth task width before repositioning.
+     * @param maxResizeBounds stable bounds for display.
+     * @param displayController display controller for the task being checked.
+     * @param windowDecoration contains decor info and helpers for the task.
+     * @return whether the task is exceeding any of the width constrains, minimum or maximum.
+     */
+    public static boolean isExceedingWidthConstraint(int repositionedWidth, int startingWidth,
             Rect maxResizeBounds, DisplayController displayController,
             WindowDecoration windowDecoration) {
         boolean isSizeIncreasing = (repositionedWidth - startingWidth) > 0;
@@ -223,7 +233,17 @@
                 && repositionedWidth > maxResizeBounds.width() && isSizeIncreasing;
     }
 
-    private static boolean isExceedingHeightConstraint(int repositionedHeight, int startingHeight,
+    /**
+     * Checks whether the new task bounds exceed the allowed height.
+     *
+     * @param repositionedHeight task's height after repositioning.
+     * @param startingHeight task's height before repositioning.
+     * @param maxResizeBounds stable bounds for display.
+     * @param displayController display controller for the task being checked.
+     * @param windowDecoration contains decor info and helpers for the task.
+     * @return whether the task is exceeding any of the height constrains, minimum or maximum.
+     */
+    public static boolean isExceedingHeightConstraint(int repositionedHeight, int startingHeight,
             Rect maxResizeBounds, DisplayController displayController,
             WindowDecoration windowDecoration) {
         boolean isSizeIncreasing = (repositionedHeight - startingHeight) > 0;
@@ -284,12 +304,19 @@
                 && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS.isTrue();
     }
 
-    interface DragStartListener {
+    public interface DragEventListener {
         /**
          * Inform the implementing class that a drag resize has started
          *
          * @param taskId id of this positioner's {@link WindowDecoration}
          */
         void onDragStart(int taskId);
+
+        /**
+         * Inform the implementing class that a drag move has started.
+         *
+         * @param taskId id of this positioner's {@link WindowDecoration}
+         */
+        void onDragMove(int taskId);
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
index 844ceb3..6f72d34 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
@@ -42,7 +42,7 @@
 /**
  * Geometry for a drag resize region for a particular window.
  */
-final class DragResizeWindowGeometry {
+public final class DragResizeWindowGeometry {
     private final int mTaskCornerRadius;
     private final Size mTaskSize;
     // The size of the handle outside the task window applied to the edges of the window, for the
@@ -58,19 +58,24 @@
     // The bounds for each edge drag region, which can resize the task in one direction.
     final @NonNull TaskEdges mTaskEdges;
 
+    private final DisabledEdge mDisabledEdge;
+
     DragResizeWindowGeometry(int taskCornerRadius, @NonNull Size taskSize,
             int resizeHandleEdgeOutset, int resizeHandleEdgeInset, int fineCornerSize,
-            int largeCornerSize) {
+            int largeCornerSize, DisabledEdge disabledEdge) {
         mTaskCornerRadius = taskCornerRadius;
         mTaskSize = taskSize;
         mResizeHandleEdgeOutset = resizeHandleEdgeOutset;
         mResizeHandleEdgeInset = resizeHandleEdgeInset;
 
-        mLargeTaskCorners = new TaskCorners(mTaskSize, largeCornerSize);
-        mFineTaskCorners = new TaskCorners(mTaskSize, fineCornerSize);
+        mDisabledEdge = disabledEdge;
+
+        mLargeTaskCorners = new TaskCorners(mTaskSize, largeCornerSize, disabledEdge);
+        mFineTaskCorners = new TaskCorners(mTaskSize, fineCornerSize, disabledEdge);
 
         // Save touch areas for each edge.
-        mTaskEdges = new TaskEdges(mTaskSize, mResizeHandleEdgeOutset, mResizeHandleEdgeInset);
+        mTaskEdges = new TaskEdges(mTaskSize, mResizeHandleEdgeOutset, mResizeHandleEdgeInset,
+                mDisabledEdge);
     }
 
     /**
@@ -170,7 +175,7 @@
                     || e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE
                     // Touchpad input
                     || (e.isFromSource(SOURCE_MOUSE)
-                        && e.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER);
+                    && e.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER);
         } else {
             return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE;
         }
@@ -187,8 +192,9 @@
     /**
      * Returns the control type for the drag-resize, based on the touch regions and this
      * MotionEvent's coordinates.
+     *
      * @param isTouchscreen Controls the size of the corner resize regions; touchscreen events
-     *                      (finger & stylus) are eligible for a larger area than cursor events
+     *                      (finger & stylus) are eligible for a larger area than cursor events.
      * @param isEdgeResizePermitted Indicates if the event is eligible for falling into an edge
      *                              resize region.
      */
@@ -252,6 +258,10 @@
     @DragPositioningCallback.CtrlType
     private int checkDistanceFromCenter(@DragPositioningCallback.CtrlType int ctrlType, float x,
             float y) {
+        if ((mDisabledEdge == DisabledEdge.RIGHT && (ctrlType & CTRL_TYPE_RIGHT) != 0)
+                || mDisabledEdge == DisabledEdge.LEFT && ((ctrlType & CTRL_TYPE_LEFT) != 0)) {
+            return CTRL_TYPE_UNDEFINED;
+        }
         final Point cornerRadiusCenter = calculateCenterForCornerRadius(ctrlType);
         double distanceFromCenter = Math.hypot(x - cornerRadiusCenter.x, y - cornerRadiusCenter.y);
 
@@ -337,29 +347,31 @@
         private final @NonNull Rect mRightTopCornerBounds;
         private final @NonNull Rect mLeftBottomCornerBounds;
         private final @NonNull Rect mRightBottomCornerBounds;
+        private final @NonNull DisabledEdge mDisabledEdge;
 
-        TaskCorners(@NonNull Size taskSize, int cornerSize) {
+        TaskCorners(@NonNull Size taskSize, int cornerSize, DisabledEdge disabledEdge) {
             mCornerSize = cornerSize;
+            mDisabledEdge = disabledEdge;
             final int cornerRadius = cornerSize / 2;
-            mLeftTopCornerBounds = new Rect(
+            mLeftTopCornerBounds = (disabledEdge == DisabledEdge.LEFT) ? new Rect() : new Rect(
                     -cornerRadius,
                     -cornerRadius,
                     cornerRadius,
                     cornerRadius);
 
-            mRightTopCornerBounds = new Rect(
+            mRightTopCornerBounds = (disabledEdge == DisabledEdge.RIGHT) ? new Rect() : new Rect(
                     taskSize.getWidth() - cornerRadius,
                     -cornerRadius,
                     taskSize.getWidth() + cornerRadius,
                     cornerRadius);
 
-            mLeftBottomCornerBounds = new Rect(
+            mLeftBottomCornerBounds = (disabledEdge == DisabledEdge.LEFT) ? new Rect() : new Rect(
                     -cornerRadius,
                     taskSize.getHeight() - cornerRadius,
                     cornerRadius,
                     taskSize.getHeight() + cornerRadius);
 
-            mRightBottomCornerBounds = new Rect(
+            mRightBottomCornerBounds = (disabledEdge == DisabledEdge.RIGHT) ? new Rect() : new Rect(
                     taskSize.getWidth() - cornerRadius,
                     taskSize.getHeight() - cornerRadius,
                     taskSize.getWidth() + cornerRadius,
@@ -370,10 +382,14 @@
          * Updates the region to include all four corners.
          */
         void union(Region region) {
-            region.union(mLeftTopCornerBounds);
-            region.union(mRightTopCornerBounds);
-            region.union(mLeftBottomCornerBounds);
-            region.union(mRightBottomCornerBounds);
+            if (mDisabledEdge != DisabledEdge.RIGHT) {
+                region.union(mRightTopCornerBounds);
+                region.union(mRightBottomCornerBounds);
+            }
+            if (mDisabledEdge != DisabledEdge.LEFT) {
+                region.union(mLeftTopCornerBounds);
+                region.union(mLeftBottomCornerBounds);
+            }
         }
 
         /**
@@ -440,9 +456,12 @@
         private final @NonNull Rect mRightEdgeBounds;
         private final @NonNull Rect mBottomEdgeBounds;
         private final @NonNull Region mRegion;
+        private final @NonNull DisabledEdge mDisabledEdge;
 
         private TaskEdges(@NonNull Size taskSize, int resizeHandleThickness,
-                int resizeHandleEdgeInset) {
+                int resizeHandleEdgeInset, DisabledEdge disabledEdge) {
+            // Save touch areas for each edge.
+            mDisabledEdge = disabledEdge;
             // Save touch areas for each edge.
             mTopEdgeBounds = new Rect(
                     -resizeHandleThickness,
@@ -466,10 +485,7 @@
                     taskSize.getHeight() + resizeHandleThickness);
 
             mRegion = new Region();
-            mRegion.union(mTopEdgeBounds);
-            mRegion.union(mLeftEdgeBounds);
-            mRegion.union(mRightEdgeBounds);
-            mRegion.union(mBottomEdgeBounds);
+            union(mRegion);
         }
 
         /**
@@ -483,9 +499,13 @@
          * Updates the region to include all four corners.
          */
         private void union(Region region) {
+            if (mDisabledEdge != DisabledEdge.RIGHT) {
+                region.union(mRightEdgeBounds);
+            }
+            if (mDisabledEdge != DisabledEdge.LEFT) {
+                region.union(mLeftEdgeBounds);
+            }
             region.union(mTopEdgeBounds);
-            region.union(mLeftEdgeBounds);
-            region.union(mRightEdgeBounds);
             region.union(mBottomEdgeBounds);
         }
 
@@ -519,4 +539,10 @@
                     mBottomEdgeBounds);
         }
     }
+
+    public enum DisabledEdge {
+        LEFT,
+        RIGHT,
+        NONE
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
index ccf329c..3efae9d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
@@ -35,6 +35,7 @@
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.transition.Transitions;
 
+import java.util.ArrayList;
 import java.util.function.Supplier;
 
 /**
@@ -55,7 +56,8 @@
     private final WindowDecoration mWindowDecoration;
     private final Supplier<SurfaceControl.Transaction> mTransactionSupplier;
     private DisplayController mDisplayController;
-    private DragPositioningCallbackUtility.DragStartListener mDragStartListener;
+    private ArrayList<DragPositioningCallbackUtility.DragEventListener> mDragEventListeners =
+            new ArrayList<>();
     private final Rect mStableBounds = new Rect();
     private final Rect mTaskBoundsAtDragStart = new Rect();
     private final PointF mRepositionStartPoint = new PointF();
@@ -69,20 +71,22 @@
     FluidResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, Transitions transitions,
             WindowDecoration windowDecoration, DisplayController displayController) {
         this(taskOrganizer, transitions, windowDecoration, displayController,
-                dragStartListener -> {}, SurfaceControl.Transaction::new);
+                null, SurfaceControl.Transaction::new);
     }
 
     FluidResizeTaskPositioner(ShellTaskOrganizer taskOrganizer,
             Transitions transitions,
             WindowDecoration windowDecoration,
             DisplayController displayController,
-            DragPositioningCallbackUtility.DragStartListener dragStartListener,
+            DragPositioningCallbackUtility.DragEventListener dragEventListener,
             Supplier<SurfaceControl.Transaction> supplier) {
         mTaskOrganizer = taskOrganizer;
         mTransitions = transitions;
         mWindowDecoration = windowDecoration;
         mDisplayController = displayController;
-        mDragStartListener = dragStartListener;
+        if (dragEventListener != null) {
+            mDragEventListeners.add(dragEventListener);
+        }
         mTransactionSupplier = supplier;
     }
 
@@ -92,7 +96,9 @@
         mTaskBoundsAtDragStart.set(
                 mWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds());
         mRepositionStartPoint.set(x, y);
-        mDragStartListener.onDragStart(mWindowDecoration.mTaskInfo.taskId);
+        for (DragPositioningCallbackUtility.DragEventListener listener : mDragEventListeners) {
+            listener.onDragStart(mWindowDecoration.mTaskInfo.taskId);
+        }
         if (mCtrlType != CTRL_TYPE_UNDEFINED && !mWindowDecoration.mHasGlobalFocus) {
             WindowContainerTransaction wct = new WindowContainerTransaction();
             wct.reorder(mWindowDecoration.mTaskInfo.token, true /* onTop */,
@@ -120,6 +126,10 @@
             // The task is being resized, send the |dragResizing| hint to core with the first
             // bounds-change wct.
             if (!mHasDragResized) {
+                for (DragPositioningCallbackUtility.DragEventListener listener :
+                        mDragEventListeners) {
+                    listener.onDragMove(mWindowDecoration.mTaskInfo.taskId);
+                }
                 // This is the first bounds change since drag resize operation started.
                 wct.setDragResizing(mWindowDecoration.mTaskInfo.token, true /* dragResizing */);
             }
@@ -216,4 +226,16 @@
     public boolean isResizingOrAnimating() {
         return mIsResizingOrAnimatingResize;
     }
+
+    @Override
+    public void addDragEventListener(
+            DragPositioningCallbackUtility.DragEventListener dragEventListener) {
+        mDragEventListeners.add(dragEventListener);
+    }
+
+    @Override
+    public void removeDragEventListener(
+            DragPositioningCallbackUtility.DragEventListener dragEventListener) {
+        mDragEventListeners.remove(dragEventListener);
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
index fb81ed4..8770d35 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
@@ -49,7 +49,7 @@
 /**
  * Creates and updates a veil that covers task contents on resize.
  */
-class ResizeVeil @JvmOverloads constructor(
+public class ResizeVeil @JvmOverloads constructor(
         private val context: Context,
         private val displayController: DisplayController,
         private val appIcon: Bitmap,
@@ -188,28 +188,16 @@
             t.apply()
             return
         }
-        isVisible = true
         val background = backgroundSurface
         val icon = iconSurface
-        val veil = veilSurface
-        if (background == null || icon == null || veil == null) return
-
-        // Parent surface can change, ensure it is up to date.
-        if (parent != parentSurface) {
-            t.reparent(veil, parent)
-            parentSurface = parent
-        }
-
-        val backgroundColor = when (decorThemeUtil.getAppTheme(taskInfo)) {
-            Theme.LIGHT -> lightColors.surfaceContainer
-            Theme.DARK -> darkColors.surfaceContainer
-        }
-        t.show(veil)
-                .setLayer(veil, VEIL_CONTAINER_LAYER)
-                .setLayer(icon, VEIL_ICON_LAYER)
-                .setLayer(background, VEIL_BACKGROUND_LAYER)
-                .setColor(background, Color.valueOf(backgroundColor.toArgb()).components)
-        relayout(taskBounds, t)
+        if (background == null || icon == null) return
+        updateTransactionWithShowVeil(
+            t,
+            parent,
+            taskBounds,
+            taskInfo,
+            fadeIn,
+        )
         if (fadeIn) {
             cancelAnimation()
             val veilAnimT = surfaceControlTransactionSupplier.get()
@@ -259,11 +247,43 @@
             iconAnimator.start()
         } else {
             // Show the veil immediately.
+            t.apply()
+        }
+    }
+
+    fun updateTransactionWithShowVeil(
+        t: SurfaceControl.Transaction,
+        parent: SurfaceControl,
+        taskBounds: Rect,
+        taskInfo: RunningTaskInfo,
+        fadeIn: Boolean = false,
+    ) {
+        if (!isReady || isVisible) return
+        isVisible = true
+        val background = backgroundSurface
+        val icon = iconSurface
+        val veil = veilSurface
+        if (background == null || icon == null || veil == null) return
+        // Parent surface can change, ensure it is up to date.
+        if (parent != parentSurface) {
+            t.reparent(veil, parent)
+            parentSurface = parent
+        }
+        val backgroundColor = when (decorThemeUtil.getAppTheme(taskInfo)) {
+            Theme.LIGHT -> lightColors.surfaceContainer
+            Theme.DARK -> darkColors.surfaceContainer
+        }
+        t.show(veil)
+            .setLayer(veil, VEIL_CONTAINER_LAYER)
+            .setLayer(icon, VEIL_ICON_LAYER)
+            .setLayer(background, VEIL_BACKGROUND_LAYER)
+            .setColor(background, Color.valueOf(backgroundColor.toArgb()).components)
+        relayout(taskBounds, t)
+        if (!fadeIn) {
             t.show(icon)
-                    .show(background)
-                    .setAlpha(icon, 1f)
-                    .setAlpha(background, 1f)
-                    .apply()
+                .show(background)
+                .setAlpha(icon, 1f)
+                .setAlpha(background, 1f)
         }
     }
 
@@ -314,8 +334,12 @@
      * @param newBounds bounds to update veil to.
      */
     fun updateResizeVeil(t: SurfaceControl.Transaction, newBounds: Rect) {
+        updateTransactionWithResizeVeil(t, newBounds)
+        t.apply()
+    }
+
+    fun updateTransactionWithResizeVeil(t: SurfaceControl.Transaction, newBounds: Rect) {
         if (!isVisible) {
-            t.apply()
             return
         }
         veilAnimator?.let { animator ->
@@ -325,7 +349,6 @@
             }
         }
         relayout(newBounds, t)
-        t.apply()
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskDragResizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskDragResizer.java
index d7ea0c3..63b288d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskDragResizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskDragResizer.java
@@ -26,4 +26,19 @@
      * a resize is complete.
      */
     boolean isResizingOrAnimating();
+
+    /**
+     * Adds a drag start listener to be notified of drag start events.
+     *
+     * @param dragEventListener Listener to be added.
+     */
+    void addDragEventListener(DragPositioningCallbackUtility.DragEventListener dragEventListener);
+
+    /**
+     * Removes a drag start listener from the listener set.
+     *
+     * @param dragEventListener Listener to be removed.
+     */
+    void removeDragEventListener(
+            DragPositioningCallbackUtility.DragEventListener dragEventListener);
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
index ff3b455..a1e329a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
@@ -43,6 +43,7 @@
 import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.transition.Transitions;
 
+import java.util.ArrayList;
 import java.util.function.Supplier;
 
 /**
@@ -56,7 +57,8 @@
     private DesktopModeWindowDecoration mDesktopWindowDecoration;
     private ShellTaskOrganizer mTaskOrganizer;
     private DisplayController mDisplayController;
-    private DragPositioningCallbackUtility.DragStartListener mDragStartListener;
+    private ArrayList<DragPositioningCallbackUtility.DragEventListener>
+            mDragEventListeners = new ArrayList<>();
     private final Transitions mTransitions;
     private final Rect mStableBounds = new Rect();
     private final Rect mTaskBoundsAtDragStart = new Rect();
@@ -73,23 +75,23 @@
     public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer,
             DesktopModeWindowDecoration windowDecoration,
             DisplayController displayController,
-            DragPositioningCallbackUtility.DragStartListener dragStartListener,
+            DragPositioningCallbackUtility.DragEventListener dragEventListener,
             Transitions transitions, InteractionJankMonitor interactionJankMonitor,
             @ShellMainThread Handler handler) {
-        this(taskOrganizer, windowDecoration, displayController, dragStartListener,
+        this(taskOrganizer, windowDecoration, displayController, dragEventListener,
                 SurfaceControl.Transaction::new, transitions, interactionJankMonitor, handler);
     }
 
     public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer,
             DesktopModeWindowDecoration windowDecoration,
             DisplayController displayController,
-            DragPositioningCallbackUtility.DragStartListener dragStartListener,
+            DragPositioningCallbackUtility.DragEventListener dragEventListener,
             Supplier<SurfaceControl.Transaction> supplier, Transitions transitions,
             InteractionJankMonitor interactionJankMonitor, @ShellMainThread Handler handler) {
         mDesktopWindowDecoration = windowDecoration;
         mTaskOrganizer = taskOrganizer;
         mDisplayController = displayController;
-        mDragStartListener = dragStartListener;
+        mDragEventListeners.add(dragEventListener);
         mTransactionSupplier = supplier;
         mTransitions = transitions;
         mInteractionJankMonitor = interactionJankMonitor;
@@ -113,7 +115,10 @@
                 mTaskOrganizer.applyTransaction(wct);
             }
         }
-        mDragStartListener.onDragStart(mDesktopWindowDecoration.mTaskInfo.taskId);
+        for (DragPositioningCallbackUtility.DragEventListener dragEventListener :
+                mDragEventListeners) {
+            dragEventListener.onDragStart(mDesktopWindowDecoration.mTaskInfo.taskId);
+        }
         mRepositionTaskBounds.set(mTaskBoundsAtDragStart);
         int rotation = mDesktopWindowDecoration
                 .mTaskInfo.configuration.windowConfiguration.getDisplayRotation();
@@ -137,6 +142,10 @@
                 mRepositionTaskBounds, mTaskBoundsAtDragStart, mStableBounds, delta,
                 mDisplayController, mDesktopWindowDecoration)) {
             if (!mIsResizingOrAnimatingResize) {
+                for (DragPositioningCallbackUtility.DragEventListener dragEventListener :
+                        mDragEventListeners) {
+                    dragEventListener.onDragMove(mDesktopWindowDecoration.mTaskInfo.taskId);
+                }
                 mDesktopWindowDecoration.showResizeVeil(mRepositionTaskBounds);
                 mIsResizingOrAnimatingResize = true;
             } else {
@@ -237,4 +246,16 @@
     public boolean isResizingOrAnimating() {
         return mIsResizingOrAnimatingResize;
     }
+
+    @Override
+    public void addDragEventListener(
+            DragPositioningCallbackUtility.DragEventListener dragEventListener) {
+        mDragEventListeners.add(dragEventListener);
+    }
+
+    @Override
+    public void removeDragEventListener(
+            DragPositioningCallbackUtility.DragEventListener dragEventListener) {
+        mDragEventListeners.remove(dragEventListener);
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 34cc098..f97dfb89 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -131,12 +131,15 @@
                 }
             };
 
-    RunningTaskInfo mTaskInfo;
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public RunningTaskInfo mTaskInfo;
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public Context mDecorWindowContext;
     int mLayoutResId;
-    final SurfaceControl mTaskSurface;
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public final SurfaceControl mTaskSurface;
 
     Display mDisplay;
-    Context mDecorWindowContext;
     SurfaceControl mDecorationContainerSurface;
 
     SurfaceControl mCaptionContainerSurface;
@@ -200,6 +203,14 @@
     }
 
     /**
+     * Gets the decoration's task leash.
+     * @return the decoration' task surface used to manipulate the task.
+     */
+    public SurfaceControl getLeash() {
+        return mTaskSurface;
+    }
+
+    /**
      * Used by {@link WindowDecoration} to trigger a new relayout because the requirements for a
      * relayout weren't satisfied are satisfied now.
      *
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt
new file mode 100644
index 0000000..ff418c6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.tiling
+
+import android.app.ActivityManager
+import android.app.ActivityManager.RunningTaskInfo
+import android.content.Context
+import android.graphics.Rect
+import android.util.SparseArray
+import androidx.core.util.valueIterator
+import com.android.internal.annotations.VisibleForTesting
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.desktopmode.DesktopRepository
+import com.android.wm.shell.desktopmode.DesktopTasksController
+import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator
+import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
+
+/** Manages tiling for each displayId/userId independently. */
+class DesktopTilingDecorViewModel(
+    private val context: Context,
+    private val displayController: DisplayController,
+    private val rootTdaOrganizer: RootTaskDisplayAreaOrganizer,
+    private val syncQueue: SyncTransactionQueue,
+    private val transitions: Transitions,
+    private val shellTaskOrganizer: ShellTaskOrganizer,
+    private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler,
+    private val returnToDragStartAnimator: ReturnToDragStartAnimator,
+    private val taskRepository: DesktopRepository,
+) {
+    @VisibleForTesting
+    var tilingTransitionHandlerByDisplayId = SparseArray<DesktopTilingWindowDecoration>()
+
+    fun snapToHalfScreen(
+        taskInfo: ActivityManager.RunningTaskInfo,
+        desktopModeWindowDecoration: DesktopModeWindowDecoration,
+        position: DesktopTasksController.SnapPosition,
+        destinationBounds: Rect,
+    ): Boolean {
+        val displayId = taskInfo.displayId
+        val handler =
+            tilingTransitionHandlerByDisplayId.get(displayId)
+                ?: run {
+                    val newHandler =
+                        DesktopTilingWindowDecoration(
+                            context,
+                            syncQueue,
+                            displayController,
+                            displayId,
+                            rootTdaOrganizer,
+                            transitions,
+                            shellTaskOrganizer,
+                            toggleResizeDesktopTaskTransitionHandler,
+                            returnToDragStartAnimator,
+                            taskRepository,
+                        )
+                    tilingTransitionHandlerByDisplayId.put(displayId, newHandler)
+                    newHandler
+                }
+        transitions.registerObserver(handler)
+        return handler.onAppTiled(
+            taskInfo,
+            desktopModeWindowDecoration,
+            position,
+            destinationBounds,
+        )
+    }
+
+    fun removeTaskIfTiled(displayId: Int, taskId: Int) {
+        tilingTransitionHandlerByDisplayId.get(displayId)?.removeTaskIfTiled(taskId)
+    }
+
+    fun moveTaskToFrontIfTiled(taskInfo: RunningTaskInfo): Boolean {
+        return tilingTransitionHandlerByDisplayId
+            .get(taskInfo.displayId)
+            ?.moveTiledPairToFront(taskInfo) ?: false
+    }
+
+    fun onOverviewAnimationStateChange(isRunning: Boolean) {
+        for (tilingHandler in tilingTransitionHandlerByDisplayId.valueIterator()) {
+            tilingHandler.onOverviewAnimationStateChange(isRunning)
+        }
+    }
+
+    fun onUserChange() {
+        for (tilingHandler in tilingTransitionHandlerByDisplayId.valueIterator()) {
+            tilingHandler.onUserChange()
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt
new file mode 100644
index 0000000..9bf1304
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt
@@ -0,0 +1,228 @@
+/*
+ * 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.tiling
+
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.PixelFormat
+import android.graphics.Rect
+import android.graphics.Region
+import android.os.Binder
+import android.view.LayoutInflater
+import android.view.SurfaceControl
+import android.view.SurfaceControlViewHost
+import android.view.View
+import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+import android.view.WindowManager.LayoutParams.FLAG_SLIPPERY
+import android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
+import android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+import android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER
+import android.view.WindowlessWindowManager
+import com.android.wm.shell.R
+import com.android.wm.shell.common.SyncTransactionQueue
+import java.util.function.Supplier
+
+/**
+ * a [WindowlessWindowManaer] responsible for hosting the [TilingDividerView] on the display root
+ * when two tasks are tiled on left and right to resize them simultaneously.
+ */
+class DesktopTilingDividerWindowManager(
+    private val config: Configuration,
+    private val windowName: String,
+    private val context: Context,
+    private val leash: SurfaceControl,
+    private val syncQueue: SyncTransactionQueue,
+    private val transitionHandler: DesktopTilingWindowDecoration,
+    private val transactionSupplier: Supplier<SurfaceControl.Transaction>,
+    private var dividerBounds: Rect,
+) : WindowlessWindowManager(config, leash, null), DividerMoveCallback, View.OnLayoutChangeListener {
+    private lateinit var viewHost: SurfaceControlViewHost
+    private var tilingDividerView: TilingDividerView? = null
+    private var dividerShown = false
+    private var handleRegionWidth: Int = -1
+    private var setTouchRegion = true
+
+    /**
+     * Gets bounds of divider window with screen based coordinate on the param Rect.
+     *
+     * @param rect bounds for the [TilingDividerView]
+     */
+    fun getDividerBounds(rect: Rect) {
+        rect.set(dividerBounds)
+    }
+
+    /** Sets the touch region for the SurfaceControlViewHost. */
+    fun setTouchRegion(region: Rect) {
+        setTouchRegion(viewHost.windowToken.asBinder(), Region(region))
+    }
+
+    /**
+     * Builds a view host upon tiling two tasks left and right, and shows the divider view in the
+     * middle of the screen between both tasks.
+     *
+     * @param relativeLeash the task leash that the TilingDividerView should be shown on top of.
+     */
+    fun generateViewHost(relativeLeash: SurfaceControl) {
+        val t = transactionSupplier.get()
+        val surfaceControlViewHost =
+            SurfaceControlViewHost(context, context.display, this, "DesktopTilingManager")
+        val dividerView =
+            LayoutInflater.from(context).inflate(R.layout.tiling_split_divider, /* root= */ null)
+                as TilingDividerView
+        val lp = getWindowManagerParams()
+        surfaceControlViewHost.setView(dividerView, lp)
+        val tmpDividerBounds = Rect()
+        getDividerBounds(tmpDividerBounds)
+        dividerView.setup(this, tmpDividerBounds)
+        t.setRelativeLayer(leash, relativeLeash, 1)
+            .setPosition(leash, dividerBounds.left.toFloat(), dividerBounds.top.toFloat())
+            .show(leash)
+        syncQueue.runInSync { transaction ->
+            transaction.merge(t)
+            t.close()
+        }
+        dividerShown = true
+        viewHost = surfaceControlViewHost
+        dividerView.addOnLayoutChangeListener(this)
+        tilingDividerView = dividerView
+        handleRegionWidth = dividerView.handleRegionWidth
+    }
+
+    /** Hides the divider bar. */
+    fun hideDividerBar() {
+        if (!dividerShown) {
+            return
+        }
+        val t = transactionSupplier.get()
+        t.hide(leash)
+        t.apply()
+        dividerShown = false
+    }
+
+    /** Shows the divider bar. */
+    fun showDividerBar() {
+        if (dividerShown) return
+        val t = transactionSupplier.get()
+        t.show(leash)
+        t.apply()
+        dividerShown = true
+    }
+
+    /**
+     * When the tiled task on top changes, the divider bar's Z access should change to be on top of
+     * the latest focused task.
+     */
+    fun onRelativeLeashChanged(relativeLeash: SurfaceControl, t: SurfaceControl.Transaction) {
+        t.setRelativeLayer(leash, relativeLeash, 1)
+    }
+
+    override fun onDividerMoveStart(pos: Int) {
+        setSlippery(false)
+    }
+
+    /**
+     * Moves the divider view to a new position after touch, gets called from the
+     * [TilingDividerView] onTouch function.
+     */
+    override fun onDividerMove(pos: Int): Boolean {
+        val t = transactionSupplier.get()
+        t.setPosition(leash, pos.toFloat(), dividerBounds.top.toFloat())
+        val dividerWidth = dividerBounds.width()
+        dividerBounds.set(pos, dividerBounds.top, pos + dividerWidth, dividerBounds.bottom)
+        return transitionHandler.onDividerHandleMoved(dividerBounds, t)
+    }
+
+    /**
+     * Notifies the transition handler of tiling operations ending, which might result in resizing
+     * WindowContainerTransactions if the sizes of the tiled tasks changed.
+     */
+    override fun onDividerMovedEnd(pos: Int) {
+        setSlippery(true)
+        val t = transactionSupplier.get()
+        t.setPosition(leash, pos.toFloat(), dividerBounds.top.toFloat())
+        val dividerWidth = dividerBounds.width()
+        dividerBounds.set(pos, dividerBounds.top, pos + dividerWidth, dividerBounds.bottom)
+        transitionHandler.onDividerHandleDragEnd(dividerBounds, t)
+    }
+
+    private fun getWindowManagerParams(): WindowManager.LayoutParams {
+        val lp =
+            WindowManager.LayoutParams(
+                dividerBounds.width(),
+                dividerBounds.height(),
+                TYPE_DOCK_DIVIDER,
+                FLAG_NOT_FOCUSABLE or
+                    FLAG_NOT_TOUCH_MODAL or
+                    FLAG_WATCH_OUTSIDE_TOUCH or
+                    FLAG_SPLIT_TOUCH or
+                    FLAG_SLIPPERY,
+                PixelFormat.TRANSLUCENT,
+            )
+        lp.token = Binder()
+        lp.title = windowName
+        lp.privateFlags =
+            lp.privateFlags or (PRIVATE_FLAG_NO_MOVE_ANIMATION or PRIVATE_FLAG_TRUSTED_OVERLAY)
+        return lp
+    }
+
+    /**
+     * Releases the surface control of the current [TilingDividerView] and tear down the view
+     * hierarchy.y.
+     */
+    fun release() {
+        tilingDividerView = null
+        viewHost.release()
+        transactionSupplier.get().hide(leash).remove(leash).apply()
+    }
+
+    override fun onLayoutChange(
+        v: View?,
+        left: Int,
+        top: Int,
+        right: Int,
+        bottom: Int,
+        oldLeft: Int,
+        oldTop: Int,
+        oldRight: Int,
+        oldBottom: Int,
+    ) {
+        if (!setTouchRegion) return
+
+        val startX = (dividerBounds.width() - handleRegionWidth) / 2
+        val startY = 0
+        val tempRect = Rect(startX, startY, startX + handleRegionWidth, dividerBounds.height())
+        setTouchRegion(tempRect)
+        setTouchRegion = false
+    }
+
+    private fun setSlippery(slippery: Boolean) {
+        val lp = tilingDividerView?.layoutParams as WindowManager.LayoutParams
+        val isSlippery = (lp.flags and FLAG_SLIPPERY) != 0
+        if (isSlippery == slippery) return
+
+        if (slippery) {
+            lp.flags = lp.flags or FLAG_SLIPPERY
+        } else {
+            lp.flags = lp.flags and FLAG_SLIPPERY.inv()
+        }
+        viewHost.relayout(lp)
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt
new file mode 100644
index 0000000..6ea1d14
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt
@@ -0,0 +1,654 @@
+/*
+ * 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.tiling
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.content.Context
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.Rect
+import android.os.IBinder
+import android.util.Slog
+import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_TO_BACK
+import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import android.window.WindowContainerTransaction
+import com.android.internal.annotations.VisibleForTesting
+import com.android.launcher3.icons.BaseIconFactory
+import com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT
+import com.android.launcher3.icons.IconProvider
+import com.android.wm.shell.R
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.desktopmode.DesktopRepository
+import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
+import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator
+import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TRANSIT_MINIMIZE
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
+import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility
+import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility.DragEventListener
+import com.android.wm.shell.windowdecor.DragResizeWindowGeometry
+import com.android.wm.shell.windowdecor.DragResizeWindowGeometry.DisabledEdge.NONE
+import com.android.wm.shell.windowdecor.ResizeVeil
+import com.android.wm.shell.windowdecor.extension.isFullscreen
+import java.util.function.Supplier
+
+class DesktopTilingWindowDecoration(
+    private var context: Context,
+    private val syncQueue: SyncTransactionQueue,
+    private val displayController: DisplayController,
+    private val displayId: Int,
+    private val rootTdaOrganizer: RootTaskDisplayAreaOrganizer,
+    private val transitions: Transitions,
+    private val shellTaskOrganizer: ShellTaskOrganizer,
+    private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler,
+    private val returnToDragStartAnimator: ReturnToDragStartAnimator,
+    private val taskRepository: DesktopRepository,
+    private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() },
+) :
+    Transitions.TransitionHandler,
+    ShellTaskOrganizer.FocusListener,
+    ShellTaskOrganizer.TaskVanishedListener,
+    DragEventListener,
+    Transitions.TransitionObserver {
+    companion object {
+        private val TAG: String = DesktopTilingWindowDecoration::class.java.simpleName
+        private const val TILING_DIVIDER_TAG = "Tiling Divider"
+    }
+
+    var leftTaskResizingHelper: AppResizingHelper? = null
+    var rightTaskResizingHelper: AppResizingHelper? = null
+    private var isTilingManagerInitialised = false
+    @VisibleForTesting
+    var desktopTilingDividerWindowManager: DesktopTilingDividerWindowManager? = null
+    private lateinit var dividerBounds: Rect
+    private var isResizing = false
+    private var isTilingFocused = false
+
+    fun onAppTiled(
+        taskInfo: RunningTaskInfo,
+        desktopModeWindowDecoration: DesktopModeWindowDecoration,
+        position: SnapPosition,
+        currentBounds: Rect,
+    ): Boolean {
+        val destinationBounds = getSnapBounds(taskInfo, position)
+        val resizeMetadata =
+            AppResizingHelper(
+                taskInfo,
+                desktopModeWindowDecoration,
+                context,
+                destinationBounds,
+                displayController,
+                transactionSupplier,
+            )
+        val isFirstTiledApp = leftTaskResizingHelper == null && rightTaskResizingHelper == null
+        val isTiled = destinationBounds != taskInfo.configuration.windowConfiguration.bounds
+
+        initTilingApps(resizeMetadata, position, taskInfo)
+        // Observe drag resizing to break tiling if a task is drag resized.
+        desktopModeWindowDecoration.addDragResizeListener(this)
+
+        if (isTiled) {
+            val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds)
+            toggleResizeDesktopTaskTransitionHandler.startTransition(wct, currentBounds)
+        } else {
+            // Handle the case where we attempt to snap resize when already snap resized: the task
+            // position won't need to change but we want to animate the surface going back to the
+            // snapped position from the "dragged-to-the-edge" position.
+            if (destinationBounds != currentBounds) {
+                returnToDragStartAnimator.start(
+                    taskInfo.taskId,
+                    resizeMetadata.getLeash(),
+                    startBounds = currentBounds,
+                    endBounds = destinationBounds,
+                    isResizable = taskInfo.isResizeable,
+                )
+            }
+        }
+        initTilingForDisplayIfNeeded(taskInfo.configuration, isFirstTiledApp)
+        return isTiled
+    }
+
+    // If a task is already tiled on the same position, release this task, otherwise if the same
+    // task is tiled on the opposite side, remove it from the opposite side so it's tiled correctly.
+    private fun initTilingApps(
+        taskResizingHelper: AppResizingHelper,
+        position: SnapPosition,
+        taskInfo: RunningTaskInfo,
+    ) {
+        when (position) {
+            SnapPosition.RIGHT -> {
+                rightTaskResizingHelper?.let { removeTaskIfTiled(it.taskInfo.taskId) }
+                if (leftTaskResizingHelper?.taskInfo?.taskId == taskInfo.taskId) {
+                    removeTaskIfTiled(taskInfo.taskId)
+                }
+                rightTaskResizingHelper = taskResizingHelper
+            }
+
+            SnapPosition.LEFT -> {
+                leftTaskResizingHelper?.let { removeTaskIfTiled(it.taskInfo.taskId) }
+                if (taskInfo.taskId == rightTaskResizingHelper?.taskInfo?.taskId) {
+                    removeTaskIfTiled(taskInfo.taskId)
+                }
+                leftTaskResizingHelper = taskResizingHelper
+            }
+        }
+    }
+
+    private fun initTilingForDisplayIfNeeded(config: Configuration, firstTiledApp: Boolean) {
+        if (leftTaskResizingHelper != null && rightTaskResizingHelper != null) {
+            if (!isTilingManagerInitialised) {
+                desktopTilingDividerWindowManager = initTilingManagerForDisplay(displayId, config)
+                isTilingManagerInitialised = true
+                shellTaskOrganizer.addFocusListener(this)
+                isTilingFocused = true
+            }
+            leftTaskResizingHelper?.initIfNeeded()
+            rightTaskResizingHelper?.initIfNeeded()
+            leftTaskResizingHelper
+                ?.desktopModeWindowDecoration
+                ?.updateDisabledResizingEdge(
+                    DragResizeWindowGeometry.DisabledEdge.RIGHT,
+                    /* shouldDelayUpdate = */ false,
+                )
+            rightTaskResizingHelper
+                ?.desktopModeWindowDecoration
+                ?.updateDisabledResizingEdge(
+                    DragResizeWindowGeometry.DisabledEdge.LEFT,
+                    /* shouldDelayUpdate = */ false,
+                )
+        } else if (firstTiledApp) {
+            shellTaskOrganizer.addTaskVanishedListener(this)
+        }
+    }
+
+    private fun initTilingManagerForDisplay(
+        displayId: Int,
+        config: Configuration,
+    ): DesktopTilingDividerWindowManager? {
+        val displayLayout = displayController.getDisplayLayout(displayId)
+        val builder = SurfaceControl.Builder()
+        rootTdaOrganizer.attachToDisplayArea(displayId, builder)
+        val leash = builder.setName(TILING_DIVIDER_TAG).setContainerLayer().build()
+        val tilingManager =
+            displayLayout?.let {
+                dividerBounds = inflateDividerBounds(it)
+                DesktopTilingDividerWindowManager(
+                    config,
+                    TAG,
+                    context,
+                    leash,
+                    syncQueue,
+                    this,
+                    transactionSupplier,
+                    dividerBounds,
+                )
+            }
+        // a leash to present the divider on top of, without re-parenting.
+        val relativeLeash =
+            leftTaskResizingHelper?.desktopModeWindowDecoration?.getLeash() ?: return tilingManager
+        tilingManager?.generateViewHost(relativeLeash)
+        return tilingManager
+    }
+
+    fun onDividerHandleMoved(dividerBounds: Rect, t: SurfaceControl.Transaction): Boolean {
+        val leftTiledTask = leftTaskResizingHelper ?: return false
+        val rightTiledTask = rightTaskResizingHelper ?: return false
+        val stableBounds = Rect()
+        val displayLayout = displayController.getDisplayLayout(displayId)
+        displayLayout?.getStableBounds(stableBounds)
+
+        if (stableBounds.isEmpty) return false
+
+        val leftBounds = leftTiledTask.bounds
+        val rightBounds = rightTiledTask.bounds
+        val newLeftBounds =
+            Rect(leftBounds.left, leftBounds.top, dividerBounds.left, leftBounds.bottom)
+        val newRightBounds =
+            Rect(dividerBounds.right, rightBounds.top, rightBounds.right, rightBounds.bottom)
+
+        // If one of the apps is getting smaller or bigger than size constraint, ignore finger move.
+        if (
+            isResizeWithinSizeConstraints(
+                newLeftBounds,
+                newRightBounds,
+                leftBounds,
+                rightBounds,
+                stableBounds,
+            )
+        ) {
+            return false
+        }
+
+        // The final new bounds for each app has to be registered to make sure a startAnimate
+        // when the new bounds are different from old bounds, otherwise hide the veil without
+        // waiting for an animation as no animation will run when no bounds are changed.
+        leftTiledTask.newBounds.set(newLeftBounds)
+        rightTiledTask.newBounds.set(newRightBounds)
+        if (!isResizing) {
+            leftTiledTask.showVeil(t)
+            rightTiledTask.showVeil(t)
+            isResizing = true
+        } else {
+            leftTiledTask.updateVeil(t)
+            rightTiledTask.updateVeil(t)
+        }
+
+        // Applies showing/updating veil for both apps and moving the divider into its new position.
+        t.apply()
+        return true
+    }
+
+    fun onDividerHandleDragEnd(dividerBounds: Rect, t: SurfaceControl.Transaction) {
+        val leftTiledTask = leftTaskResizingHelper ?: return
+        val rightTiledTask = rightTaskResizingHelper ?: return
+
+        if (leftTiledTask.newBounds == leftTiledTask.bounds) {
+            leftTiledTask.hideVeil()
+            rightTiledTask.hideVeil()
+            isResizing = false
+            return
+        }
+        leftTiledTask.bounds.set(leftTiledTask.newBounds)
+        rightTiledTask.bounds.set(rightTiledTask.newBounds)
+        onDividerHandleMoved(dividerBounds, t)
+        isResizing = false
+        val wct = WindowContainerTransaction()
+        wct.setBounds(leftTiledTask.taskInfo.token, leftTiledTask.bounds)
+        wct.setBounds(rightTiledTask.taskInfo.token, rightTiledTask.bounds)
+        transitions.startTransition(TRANSIT_CHANGE, wct, this)
+    }
+
+    override fun startAnimation(
+        transition: IBinder,
+        info: TransitionInfo,
+        startTransaction: Transaction,
+        finishTransaction: Transaction,
+        finishCallback: Transitions.TransitionFinishCallback,
+    ): Boolean {
+        val leftTiledTask = leftTaskResizingHelper ?: return false
+        val rightTiledTask = rightTaskResizingHelper ?: return false
+        for (change in info.getChanges()) {
+            val sc: SurfaceControl = change.getLeash()
+            val endBounds =
+                if (change.taskInfo?.taskId == leftTiledTask.taskInfo.taskId) {
+                    leftTiledTask.bounds
+                } else {
+                    rightTiledTask.bounds
+                }
+            startTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height())
+            finishTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height())
+        }
+
+        startTransaction.apply()
+        leftTiledTask.hideVeil()
+        rightTiledTask.hideVeil()
+        finishCallback.onTransitionFinished(null)
+        return true
+    }
+
+    // TODO(b/361505243) bring tasks to front here when the empty request info bug is fixed.
+    override fun handleRequest(
+        transition: IBinder,
+        request: TransitionRequestInfo,
+    ): WindowContainerTransaction? {
+        return null
+    }
+
+    override fun onDragStart(taskId: Int) {}
+
+    override fun onDragMove(taskId: Int) {
+        removeTaskIfTiled(taskId)
+    }
+
+    override fun onTransitionReady(
+        transition: IBinder,
+        info: TransitionInfo,
+        startTransaction: Transaction,
+        finishTransaction: Transaction,
+    ) {
+        for (change in info.changes) {
+            change.taskInfo?.let {
+                if (it.isFullscreen || isMinimized(change.mode, info.type)) {
+                    removeTaskIfTiled(it.taskId, /* taskVanished= */ false, it.isFullscreen)
+                }
+            }
+        }
+    }
+
+    private fun isMinimized(changeMode: Int, infoType: Int): Boolean {
+        return (changeMode == TRANSIT_TO_BACK &&
+            (infoType == TRANSIT_MINIMIZE || infoType == TRANSIT_TO_BACK))
+    }
+
+    class AppResizingHelper(
+        val taskInfo: RunningTaskInfo,
+        val desktopModeWindowDecoration: DesktopModeWindowDecoration,
+        val context: Context,
+        val bounds: Rect,
+        val displayController: DisplayController,
+        val transactionSupplier: Supplier<Transaction>,
+    ) {
+        var isInitialised = false
+        var newBounds = Rect(bounds)
+        private lateinit var resizeVeilBitmap: Bitmap
+        private lateinit var resizeVeil: ResizeVeil
+        private val displayContext = displayController.getDisplayContext(taskInfo.displayId)
+
+        fun initIfNeeded() {
+            if (!isInitialised) {
+                initVeil()
+                isInitialised = true
+            }
+        }
+
+        private fun initVeil() {
+            val baseActivity = taskInfo.baseActivity
+            if (baseActivity == null) {
+                Slog.e(TAG, "Base activity component not found in task")
+                return
+            }
+            val resizeVeilIconFactory =
+                displayContext?.let {
+                    createIconFactory(displayContext, R.dimen.desktop_mode_resize_veil_icon_size)
+                } ?: return
+            val pm = context.getApplicationContext().getPackageManager()
+            val activityInfo = pm.getActivityInfo(baseActivity, 0 /* flags */)
+            val provider = IconProvider(displayContext)
+            val appIconDrawable = provider.getIcon(activityInfo)
+            resizeVeilBitmap =
+                resizeVeilIconFactory.createScaledBitmap(appIconDrawable, MODE_DEFAULT)
+            resizeVeil =
+                ResizeVeil(
+                    context = displayContext,
+                    displayController = displayController,
+                    appIcon = resizeVeilBitmap,
+                    parentSurface = desktopModeWindowDecoration.getLeash(),
+                    surfaceControlTransactionSupplier = transactionSupplier,
+                    taskInfo = taskInfo,
+                )
+        }
+
+        fun showVeil(t: Transaction) =
+            resizeVeil.updateTransactionWithShowVeil(
+                t,
+                desktopModeWindowDecoration.getLeash(),
+                bounds,
+                taskInfo,
+            )
+
+        fun updateVeil(t: Transaction) = resizeVeil.updateTransactionWithResizeVeil(t, newBounds)
+
+        fun hideVeil() = resizeVeil.hideVeil()
+
+        private fun createIconFactory(context: Context, dimensions: Int): BaseIconFactory {
+            val resources: Resources = context.resources
+            val densityDpi: Int = resources.getDisplayMetrics().densityDpi
+            val iconSize: Int = resources.getDimensionPixelSize(dimensions)
+            return BaseIconFactory(context, densityDpi, iconSize)
+        }
+
+        fun getLeash(): SurfaceControl = desktopModeWindowDecoration.getLeash()
+
+        fun dispose() {
+            if (isInitialised) resizeVeil.dispose()
+        }
+    }
+
+    private fun isTilingFocusRemoved(taskInfo: RunningTaskInfo): Boolean {
+        return taskInfo.isFocused &&
+            isTilingFocused &&
+            taskInfo.taskId != leftTaskResizingHelper?.taskInfo?.taskId &&
+            taskInfo.taskId != rightTaskResizingHelper?.taskInfo?.taskId
+    }
+
+    override fun onFocusTaskChanged(taskInfo: RunningTaskInfo?) {
+        if (taskInfo != null) {
+            moveTiledPairToFront(taskInfo)
+        }
+    }
+
+    private fun isTilingRefocused(taskInfo: RunningTaskInfo): Boolean {
+        return !isTilingFocused &&
+            taskInfo.isFocused &&
+            (taskInfo.taskId == leftTaskResizingHelper?.taskInfo?.taskId ||
+                taskInfo.taskId == rightTaskResizingHelper?.taskInfo?.taskId)
+    }
+
+    private fun buildTiledTasksMoveToFront(leftOnTop: Boolean): WindowContainerTransaction {
+        val wct = WindowContainerTransaction()
+        val leftTiledTask = leftTaskResizingHelper ?: return wct
+        val rightTiledTask = rightTaskResizingHelper ?: return wct
+        if (leftOnTop) {
+            wct.reorder(rightTiledTask.taskInfo.token, true)
+            wct.reorder(leftTiledTask.taskInfo.token, true)
+        } else {
+            wct.reorder(leftTiledTask.taskInfo.token, true)
+            wct.reorder(rightTiledTask.taskInfo.token, true)
+        }
+        return wct
+    }
+
+    fun removeTaskIfTiled(
+        taskId: Int,
+        taskVanished: Boolean = false,
+        shouldDelayUpdate: Boolean = false,
+    ) {
+        if (taskId == leftTaskResizingHelper?.taskInfo?.taskId) {
+            removeTask(leftTaskResizingHelper, taskVanished, shouldDelayUpdate)
+            leftTaskResizingHelper = null
+            rightTaskResizingHelper
+                ?.desktopModeWindowDecoration
+                ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate)
+            tearDownTiling()
+            return
+        }
+
+        if (taskId == rightTaskResizingHelper?.taskInfo?.taskId) {
+            removeTask(rightTaskResizingHelper, taskVanished, shouldDelayUpdate)
+            rightTaskResizingHelper = null
+            leftTaskResizingHelper
+                ?.desktopModeWindowDecoration
+                ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate)
+            tearDownTiling()
+        }
+    }
+
+    fun onUserChange() {
+        if (leftTaskResizingHelper != null) {
+            removeTask(leftTaskResizingHelper, taskVanished = false, shouldDelayUpdate = true)
+            leftTaskResizingHelper = null
+        }
+        if (rightTaskResizingHelper != null) {
+            removeTask(rightTaskResizingHelper, taskVanished = false, shouldDelayUpdate = true)
+            rightTaskResizingHelper = null
+        }
+        tearDownTiling()
+    }
+
+    private fun removeTask(
+        appResizingHelper: AppResizingHelper?,
+        taskVanished: Boolean = false,
+        shouldDelayUpdate: Boolean,
+    ) {
+        if (appResizingHelper == null) return
+        if (!taskVanished) {
+            appResizingHelper.desktopModeWindowDecoration.removeDragResizeListener(this)
+            appResizingHelper.desktopModeWindowDecoration.updateDisabledResizingEdge(
+                NONE,
+                shouldDelayUpdate,
+            )
+        }
+        appResizingHelper.dispose()
+    }
+
+    fun onOverviewAnimationStateChange(isRunning: Boolean) {
+        if (!isTilingManagerInitialised) return
+
+        if (isRunning) {
+            desktopTilingDividerWindowManager?.hideDividerBar()
+        } else if (allTiledTasksVisible()) {
+            desktopTilingDividerWindowManager?.showDividerBar()
+        }
+    }
+
+    override fun onTaskVanished(taskInfo: RunningTaskInfo?) {
+        val taskId = taskInfo?.taskId ?: return
+        removeTaskIfTiled(taskId, taskVanished = true, shouldDelayUpdate = true)
+    }
+
+    fun moveTiledPairToFront(taskInfo: RunningTaskInfo): Boolean {
+        if (!isTilingManagerInitialised) return false
+
+        // If a task that isn't tiled is being focused, let the generic handler do the work.
+        if (isTilingFocusRemoved(taskInfo)) {
+            isTilingFocused = false
+            return false
+        }
+
+        val leftTiledTask = leftTaskResizingHelper ?: return false
+        val rightTiledTask = rightTaskResizingHelper ?: return false
+        if (!allTiledTasksVisible()) return false
+        val isLeftOnTop = taskInfo.taskId == leftTiledTask.taskInfo.taskId
+        if (isTilingRefocused(taskInfo)) {
+            val t = transactionSupplier.get()
+            isTilingFocused = true
+            if (taskInfo.taskId == leftTaskResizingHelper?.taskInfo?.taskId) {
+                desktopTilingDividerWindowManager?.onRelativeLeashChanged(
+                    leftTiledTask.getLeash(),
+                    t,
+                )
+            }
+            if (taskInfo.taskId == rightTaskResizingHelper?.taskInfo?.taskId) {
+                desktopTilingDividerWindowManager?.onRelativeLeashChanged(
+                    rightTiledTask.getLeash(),
+                    t,
+                )
+            }
+            transitions.startTransition(
+                TRANSIT_TO_FRONT,
+                buildTiledTasksMoveToFront(isLeftOnTop),
+                null,
+            )
+            t.apply()
+            return true
+        }
+        return false
+    }
+
+    private fun allTiledTasksVisible(): Boolean {
+        val leftTiledTask = leftTaskResizingHelper ?: return false
+        val rightTiledTask = rightTaskResizingHelper ?: return false
+        return taskRepository.isVisibleTask(leftTiledTask.taskInfo.taskId) &&
+            taskRepository.isVisibleTask(rightTiledTask.taskInfo.taskId)
+    }
+
+    private fun isResizeWithinSizeConstraints(
+        newLeftBounds: Rect,
+        newRightBounds: Rect,
+        leftBounds: Rect,
+        rightBounds: Rect,
+        stableBounds: Rect,
+    ): Boolean {
+        return DragPositioningCallbackUtility.isExceedingWidthConstraint(
+            newLeftBounds.width(),
+            leftBounds.width(),
+            stableBounds,
+            displayController,
+            leftTaskResizingHelper?.desktopModeWindowDecoration,
+        ) ||
+            DragPositioningCallbackUtility.isExceedingWidthConstraint(
+                newRightBounds.width(),
+                rightBounds.width(),
+                stableBounds,
+                displayController,
+                rightTaskResizingHelper?.desktopModeWindowDecoration,
+            )
+    }
+
+    private fun getSnapBounds(taskInfo: RunningTaskInfo, position: SnapPosition): Rect {
+        val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return Rect()
+
+        val stableBounds = Rect()
+        displayLayout.getStableBounds(stableBounds)
+        val leftTiledTask = leftTaskResizingHelper
+        val rightTiledTask = rightTaskResizingHelper
+        val destinationWidth = stableBounds.width() / 2
+        return when (position) {
+            SnapPosition.LEFT -> {
+                val rightBound =
+                    if (rightTiledTask == null) {
+                        stableBounds.left + destinationWidth -
+                            context.resources.getDimensionPixelSize(
+                                R.dimen.split_divider_bar_width
+                            ) / 2
+                    } else {
+                        rightTiledTask.bounds.left -
+                            context.resources.getDimensionPixelSize(R.dimen.split_divider_bar_width)
+                    }
+                Rect(stableBounds.left, stableBounds.top, rightBound, stableBounds.bottom)
+            }
+
+            SnapPosition.RIGHT -> {
+                val leftBound =
+                    if (leftTiledTask == null) {
+                        stableBounds.right - destinationWidth +
+                            context.resources.getDimensionPixelSize(
+                                R.dimen.split_divider_bar_width
+                            ) / 2
+                    } else {
+                        leftTiledTask.bounds.right +
+                            context.resources.getDimensionPixelSize(R.dimen.split_divider_bar_width)
+                    }
+                Rect(leftBound, stableBounds.top, stableBounds.right, stableBounds.bottom)
+            }
+        }
+    }
+
+    private fun inflateDividerBounds(displayLayout: DisplayLayout): Rect {
+        val stableBounds = Rect()
+        displayLayout.getStableBounds(stableBounds)
+
+        val leftDividerBounds = leftTaskResizingHelper?.bounds?.right ?: return Rect()
+        val rightDividerBounds = rightTaskResizingHelper?.bounds?.left ?: return Rect()
+
+        // Bounds should never be null here, so assertion is necessary otherwise it's illegal state.
+        return Rect(leftDividerBounds, stableBounds.top, rightDividerBounds, stableBounds.bottom)
+    }
+
+    private fun tearDownTiling() {
+        if (isTilingManagerInitialised) shellTaskOrganizer.removeFocusListener(this)
+
+        if (leftTaskResizingHelper == null && rightTaskResizingHelper == null) {
+            shellTaskOrganizer.removeTaskVanishedListener(this)
+        }
+        isTilingFocused = false
+        isTilingManagerInitialised = false
+        desktopTilingDividerWindowManager?.release()
+        desktopTilingDividerWindowManager = null
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DividerMoveCallback.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DividerMoveCallback.kt
new file mode 100644
index 0000000..b3b30ad
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DividerMoveCallback.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.
+ */
+package com.android.wm.shell.windowdecor.tiling
+
+/** Divider move callback to whichever entity that handles the moving logic. */
+interface DividerMoveCallback {
+    /** Called on the divider move start gesture. */
+    fun onDividerMoveStart(pos: Int)
+
+    /** Called on the divider moved by dragging it. */
+    fun onDividerMove(pos: Int): Boolean
+
+    /** Called on divider move gesture end. */
+    fun onDividerMovedEnd(pos: Int)
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt
new file mode 100644
index 0000000..065a5d7
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt
@@ -0,0 +1,258 @@
+/*
+ * 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.tiling
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.Rect
+import android.provider.DeviceConfig
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.view.PointerIcon
+import android.view.View
+import android.view.ViewConfiguration
+import android.widget.FrameLayout
+import com.android.internal.annotations.VisibleForTesting
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
+import com.android.wm.shell.R
+import com.android.wm.shell.common.split.DividerHandleView
+import com.android.wm.shell.common.split.DividerRoundedCorner
+import com.android.wm.shell.shared.animation.Interpolators
+import com.android.wm.shell.windowdecor.DragDetector
+
+/** Divider for tiling split screen, currently mostly a copy of [DividerView]. */
+class TilingDividerView : FrameLayout, View.OnTouchListener, DragDetector.MotionEventHandler {
+    private val paint = Paint()
+    private val backgroundRect = Rect()
+
+    private lateinit var callback: DividerMoveCallback
+    private lateinit var handle: DividerHandleView
+    private lateinit var corners: DividerRoundedCorner
+    private var touchElevation = 0
+
+    private var moving = false
+    private var startPos = 0
+    var handleRegionWidth: Int = 0
+    private var handleRegionHeight = 0
+    private var lastAcceptedPos = 0
+    @VisibleForTesting
+    var handleStartY = 0
+    @VisibleForTesting
+    var handleEndY = 0
+    private var canResize = false
+    /**
+     * Tracks divider bar visible bounds in screen-based coordination. Used to calculate with
+     * insets.
+     */
+    private val dividerBounds = Rect()
+    private var dividerBar: FrameLayout? = null
+    private lateinit var dragDetector: DragDetector
+
+    constructor(context: Context) : super(context)
+
+    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+
+    constructor(
+        context: Context,
+        attrs: AttributeSet?,
+        defStyleAttr: Int,
+    ) : super(context, attrs, defStyleAttr)
+
+    constructor(
+        context: Context,
+        attrs: AttributeSet?,
+        defStyleAttr: Int,
+        defStyleRes: Int,
+    ) : super(context, attrs, defStyleAttr, defStyleRes)
+
+    /** Sets up essential dependencies of the divider bar. */
+    fun setup(dividerMoveCallback: DividerMoveCallback, dividerBounds: Rect) {
+        callback = dividerMoveCallback
+        this.dividerBounds.set(dividerBounds)
+        handle.setIsLeftRightSplit(true)
+        corners.setIsLeftRightSplit(true)
+        handleRegionHeight =
+            resources.getDimensionPixelSize(R.dimen.split_divider_handle_region_width)
+
+        handleRegionWidth =
+            resources.getDimensionPixelSize(R.dimen.split_divider_handle_region_height)
+        initHandleYCoordinates()
+        dragDetector =
+            DragDetector(
+                this,
+                /* holdToDragMinDurationMs= */ 0,
+                ViewConfiguration.get(mContext).scaledTouchSlop,
+            )
+    }
+
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+        dividerBar = requireViewById(R.id.divider_bar)
+        handle = requireViewById(R.id.docked_divider_handle)
+        corners = requireViewById(R.id.docked_divider_rounded_corner)
+        touchElevation =
+            resources.getDimensionPixelSize(R.dimen.docked_stack_divider_lift_elevation)
+        setOnTouchListener(this)
+        setWillNotDraw(false)
+        paint.color = resources.getColor(R.color.split_divider_background, null)
+        paint.isAntiAlias = true
+        paint.style = Paint.Style.FILL
+    }
+
+    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+        super.onLayout(changed, left, top, right, bottom)
+        if (changed) {
+            val dividerSize = resources.getDimensionPixelSize(R.dimen.split_divider_bar_width)
+            val backgroundLeft = (width - dividerSize) / 2
+            val backgroundTop = 0
+            val backgroundRight = left + dividerSize
+            val backgroundBottom = height
+            backgroundRect.set(backgroundLeft, backgroundTop, backgroundRight, backgroundBottom)
+        }
+    }
+
+    override fun onResolvePointerIcon(event: MotionEvent, pointerIndex: Int): PointerIcon =
+        PointerIcon.getSystemIcon(context, PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW)
+
+    override fun onTouch(v: View, event: MotionEvent): Boolean =
+        dragDetector.onMotionEvent(v, event)
+
+    private fun setTouching() {
+        handle.setTouching(true, true)
+        // Lift handle as well so it doesn't get behind the background, even though it doesn't
+        // cast shadow.
+        handle
+            .animate()
+            .setInterpolator(Interpolators.TOUCH_RESPONSE)
+            .setDuration(TOUCH_ANIMATION_DURATION)
+            .translationZ(touchElevation.toFloat())
+            .start()
+    }
+
+    private fun releaseTouching() {
+        handle.setTouching(false, true)
+        handle
+            .animate()
+            .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+            .setDuration(TOUCH_RELEASE_ANIMATION_DURATION)
+            .translationZ(0f)
+            .start()
+    }
+
+    override fun onHoverEvent(event: MotionEvent): Boolean {
+        if (
+            !DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_SYSTEMUI,
+                SystemUiDeviceConfigFlags.CURSOR_HOVER_STATES_ENABLED,
+                /* defaultValue = */ false,
+            )
+        ) {
+            return false
+        }
+
+        if (event.action == MotionEvent.ACTION_HOVER_ENTER) {
+            setHovering()
+            return true
+        }
+        if (event.action == MotionEvent.ACTION_HOVER_EXIT) {
+            releaseHovering()
+            return true
+        }
+        return false
+    }
+
+    @VisibleForTesting
+    fun setHovering() {
+        handle.setHovering(true, true)
+        handle
+            .animate()
+            .setInterpolator(Interpolators.TOUCH_RESPONSE)
+            .setDuration(TOUCH_ANIMATION_DURATION)
+            .translationZ(touchElevation.toFloat())
+            .start()
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        canvas.drawRect(backgroundRect, paint)
+    }
+
+    @VisibleForTesting
+    fun releaseHovering() {
+        handle.setHovering(false, true)
+        handle
+            .animate()
+            .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+            .setDuration(TOUCH_RELEASE_ANIMATION_DURATION)
+            .translationZ(0f)
+            .start()
+    }
+
+    override fun handleMotionEvent(v: View?, event: MotionEvent): Boolean {
+        val touchPos = event.rawX.toInt()
+        val yTouchPosInDivider = event.y.toInt()
+        when (event.actionMasked) {
+            MotionEvent.ACTION_DOWN -> {
+                if (!isWithinHandleRegion(yTouchPosInDivider)) return true
+                callback.onDividerMoveStart(touchPos)
+                setTouching()
+                startPos = touchPos
+                canResize = true
+            }
+
+            MotionEvent.ACTION_MOVE -> {
+                if (!canResize) return true
+                if (!moving) {
+                    startPos = touchPos
+                    moving = true
+                }
+
+                val pos = dividerBounds.left + touchPos - startPos
+                if (callback.onDividerMove(pos)) {
+                    lastAcceptedPos = touchPos
+                }
+            }
+
+            MotionEvent.ACTION_CANCEL,
+            MotionEvent.ACTION_UP -> {
+                if (!canResize) return true
+                dividerBounds.left = dividerBounds.left + lastAcceptedPos - startPos
+                if (moving) {
+                    callback.onDividerMovedEnd(dividerBounds.left)
+                    moving = false
+                    canResize = false
+                }
+
+                releaseTouching()
+            }
+        }
+        return true
+    }
+
+    private fun isWithinHandleRegion(touchYPos: Int): Boolean {
+        return touchYPos in handleStartY..handleEndY
+    }
+
+    private fun initHandleYCoordinates() {
+        handleStartY = (dividerBounds.height() - handleRegionHeight) / 2
+        handleEndY = handleStartY + handleRegionHeight
+    }
+
+    companion object {
+        const val TOUCH_ANIMATION_DURATION: Long = 150
+        const val TOUCH_RELEASE_ANIMATION_DURATION: Long = 200
+    }
+}
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 bc2b36c..0a6dfbf 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
@@ -128,6 +128,8 @@
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS
 import com.android.wm.shell.transition.Transitions.TransitionHandler
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
+import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import java.util.function.Consumer
@@ -223,6 +225,10 @@
   @Mock lateinit var motionEvent: MotionEvent
 
   private lateinit var mockitoSession: StaticMockitoSession
+  @Mock
+  private lateinit var desktopTilingDecorViewModel: DesktopTilingDecorViewModel
+  @Mock
+  private lateinit var desktopWindowDecoration: DesktopModeWindowDecoration
   private lateinit var controller: DesktopTasksController
   private lateinit var shellInit: ShellInit
   private lateinit var taskRepository: DesktopRepository
@@ -336,6 +342,7 @@
         mockInputManager,
         mockFocusTransitionObserver,
         desktopModeEventLogger,
+        desktopTilingDecorViewModel,
       )
   }
 
@@ -2835,7 +2842,9 @@
         Rect(100, -100, 500, 1000), /* currentDragBounds */
         Rect(0, 50, 2000, 2000), /* validDragArea */
         Rect() /* dragStartBounds */,
-        motionEvent)
+        motionEvent,
+        desktopWindowDecoration,
+        )
     val rectAfterEnd = Rect(100, 50, 500, 1150)
     verify(transitions)
         .startTransition(
@@ -2871,7 +2880,9 @@
       currentDragBounds, /* currentDragBounds */
       Rect(0, 50, 2000, 2000) /* validDragArea */,
       Rect() /* dragStartBounds */,
-      motionEvent)
+      motionEvent,
+      desktopWindowDecoration,
+      )
 
 
     verify(transitions)
@@ -3135,6 +3146,7 @@
   }
 
   @Test
+  @DisableFlags(Flags.FLAG_ENABLE_TILE_RESIZING)
   fun snapToHalfScreen_getSnapBounds_calculatesBoundsForResizable() {
     val bounds = Rect(100, 100, 300, 300)
     val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds).apply {
@@ -3150,7 +3162,8 @@
       STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom
     )
 
-    controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, motionEvent)
+    controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT,
+      ResizeTrigger.SNAP_LEFT_MENU, motionEvent, desktopWindowDecoration)
     // Assert bounds set to stable bounds
     val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds)
     assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds)
@@ -3165,6 +3178,7 @@
   }
 
   @Test
+  @DisableFlags(Flags.FLAG_ENABLE_TILE_RESIZING)
   fun snapToHalfScreen_snapBoundsWhenAlreadySnapped_animatesSurfaceWithoutWCT() {
     // Set up task to already be in snapped-left bounds
     val bounds = Rect(
@@ -3180,8 +3194,8 @@
 
     // Attempt to snap left again
     val currentDragBounds = Rect(bounds).apply { offset(-100, 0) }
-    controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, motionEvent)
-
+    controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT,
+      ResizeTrigger.SNAP_LEFT_MENU, motionEvent, desktopWindowDecoration)
     // Assert that task is NOT updated via WCT
     verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any())
 
@@ -3204,7 +3218,7 @@
   }
 
   @Test
-  @DisableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING)
+  @DisableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING, Flags.FLAG_ENABLE_TILE_RESIZING)
   fun handleSnapResizingTask_nonResizable_snapsToHalfScreen() {
     val task = setUpFreeformTask(DEFAULT_DISPLAY, Rect(0, 0, 200, 100)).apply {
       isResizeable = false
@@ -3215,7 +3229,9 @@
       Rect(STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom)
 
     controller.handleSnapResizingTask(
-      task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds, motionEvent
+
+      task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds, motionEvent,
+      desktopWindowDecoration
     )
     val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds)
     assertThat(findBoundsChange(wct, task)).isEqualTo(
@@ -3239,7 +3255,8 @@
     val currentDragBounds = Rect(0, 100, 300, 500)
 
     controller.handleSnapResizingTask(
-      task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds, motionEvent)
+      task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds, motionEvent,
+      desktopWindowDecoration)
     verify(mReturnToDragStartAnimator).start(
       eq(task.taskId),
       eq(mockSurface),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
index f95b0d1..8dd1545 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
@@ -31,6 +31,7 @@
 
 import android.app.ActivityManager;
 import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.view.SurfaceControl;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -49,6 +50,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -65,6 +67,9 @@
 @RunWith(AndroidJUnit4.class)
 public final class FreeformTaskListenerTests extends ShellTestCase {
 
+    @Rule
+    public final SetFlagsRule setFlagsRule = new SetFlagsRule();
+
     @Mock
     private ShellTaskOrganizer mTaskOrganizer;
     @Mock
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index c5526fc..c42be7f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -666,7 +666,8 @@
             eq(currentBounds),
             eq(SnapPosition.LEFT),
             eq(ResizeTrigger.SNAP_LEFT_MENU),
-            eq(null)
+            eq(null),
+            eq(decor)
         )
         assertEquals(taskSurfaceCaptor.firstValue, decor.mTaskSurface)
     }
@@ -706,7 +707,8 @@
             eq(currentBounds),
             eq(SnapPosition.LEFT),
             eq(ResizeTrigger.SNAP_LEFT_MENU),
-            eq(null)
+            eq(null),
+            eq(decor),
         )
         assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue)
     }
@@ -727,7 +729,9 @@
         verify(mockDesktopTasksController, never())
             .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT),
                 eq(ResizeTrigger.MAXIMIZE_BUTTON),
-                eq(null))
+                eq(null),
+                eq(decor),
+            )
         verify(mockToast).show()
     }
 
@@ -750,7 +754,8 @@
             eq(currentBounds),
             eq(SnapPosition.RIGHT),
             eq(ResizeTrigger.SNAP_RIGHT_MENU),
-            eq(null)
+            eq(null),
+            eq(decor),
         )
         assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue)
     }
@@ -790,7 +795,8 @@
             eq(currentBounds),
             eq(SnapPosition.RIGHT),
             eq(ResizeTrigger.SNAP_RIGHT_MENU),
-            eq(null)
+            eq(null),
+            eq(decor),
         )
         assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue)
     }
@@ -811,7 +817,9 @@
         verify(mockDesktopTasksController, never())
             .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT),
                 eq(ResizeTrigger.MAXIMIZE_BUTTON),
-                eq(null))
+                eq(null),
+                eq(decor),
+        )
         verify(mockToast).show()
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
index 57469bf..e7d328e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
@@ -65,7 +65,7 @@
     private static final int LARGE_CORNER_SIZE = FINE_CORNER_SIZE + 10;
     private static final DragResizeWindowGeometry GEOMETRY = new DragResizeWindowGeometry(
             TASK_CORNER_RADIUS, TASK_SIZE, EDGE_RESIZE_THICKNESS, EDGE_RESIZE_HANDLE_INSET,
-            FINE_CORNER_SIZE, LARGE_CORNER_SIZE);
+            FINE_CORNER_SIZE, LARGE_CORNER_SIZE, DragResizeWindowGeometry.DisabledEdge.NONE);
     // Points in the edge resize handle. Note that coordinates start from the top left.
     private static final Point TOP_EDGE_POINT = new Point(TASK_SIZE.getWidth() / 2,
             -EDGE_RESIZE_THICKNESS / 2);
@@ -100,23 +100,25 @@
                         GEOMETRY,
                         new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
                                 EDGE_RESIZE_THICKNESS, EDGE_RESIZE_HANDLE_INSET, FINE_CORNER_SIZE,
-                                LARGE_CORNER_SIZE))
+                                LARGE_CORNER_SIZE, DragResizeWindowGeometry.DisabledEdge.NONE))
                 .addEqualityGroup(
                         new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
                                 EDGE_RESIZE_THICKNESS + 10, EDGE_RESIZE_HANDLE_INSET,
-                                FINE_CORNER_SIZE, LARGE_CORNER_SIZE),
+                                FINE_CORNER_SIZE, LARGE_CORNER_SIZE,
+                                DragResizeWindowGeometry.DisabledEdge.NONE),
                         new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
                                 EDGE_RESIZE_THICKNESS + 10, EDGE_RESIZE_HANDLE_INSET,
-                                FINE_CORNER_SIZE, LARGE_CORNER_SIZE))
+                                FINE_CORNER_SIZE, LARGE_CORNER_SIZE,
+                                DragResizeWindowGeometry.DisabledEdge.NONE))
                 .addEqualityGroup(
                         new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
                                 EDGE_RESIZE_THICKNESS + 10, EDGE_RESIZE_HANDLE_INSET,
                                 FINE_CORNER_SIZE,
-                                LARGE_CORNER_SIZE + 5),
+                                LARGE_CORNER_SIZE + 5, DragResizeWindowGeometry.DisabledEdge.NONE),
                         new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
                                 EDGE_RESIZE_THICKNESS + 10, EDGE_RESIZE_HANDLE_INSET,
                                 FINE_CORNER_SIZE,
-                                LARGE_CORNER_SIZE + 5))
+                                LARGE_CORNER_SIZE + 5, DragResizeWindowGeometry.DisabledEdge.NONE))
                 .testEquals();
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
index ca1f9ab..3b80cb4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
@@ -47,6 +47,7 @@
 import java.util.function.Supplier
 import org.mockito.Mockito.eq
 import org.mockito.Mockito.mock
+import org.mockito.kotlin.times
 import org.mockito.Mockito.`when` as whenever
 
 /**
@@ -66,7 +67,7 @@
     @Mock
     private lateinit var mockWindowDecoration: WindowDecoration<*>
     @Mock
-    private lateinit var mockDragStartListener: DragPositioningCallbackUtility.DragStartListener
+    private lateinit var mockDragEventListener: DragPositioningCallbackUtility.DragEventListener
 
     @Mock
     private lateinit var taskToken: WindowContainerToken
@@ -140,7 +141,7 @@
                 mockTransitions,
                 mockWindowDecoration,
                 mockDisplayController,
-                mockDragStartListener,
+                mockDragEventListener,
                 mockTransactionFactory
         )
     }
@@ -220,6 +221,7 @@
                         change.configuration.windowConfiguration.bounds == rectAfterMove
             }
         })
+        verify(mockDragEventListener, times(1)).onDragMove(eq(TASK_ID))
 
         taskPositioner.onDragPositioningEnd(
                 STARTING_BOUNDS.left.toFloat() + 10,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
index 1dfbd67..e7df864 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
@@ -79,7 +79,7 @@
     @Mock
     private lateinit var mockDesktopWindowDecoration: DesktopModeWindowDecoration
     @Mock
-    private lateinit var mockDragStartListener: DragPositioningCallbackUtility.DragStartListener
+    private lateinit var mockDragEventListener: DragPositioningCallbackUtility.DragEventListener
 
     @Mock
     private lateinit var taskToken: WindowContainerToken
@@ -156,7 +156,7 @@
                         mockShellTaskOrganizer,
                         mockDesktopWindowDecoration,
                         mockDisplayController,
-                        mockDragStartListener,
+                        mockDragEventListener,
                         mockTransactionFactory,
                         mockTransitions,
                         mockInteractionJankMonitor,
@@ -433,6 +433,7 @@
 
         // isResizingOrAnimating should be set to true after move during a resize
         Assert.assertTrue(taskPositioner.isResizingOrAnimating)
+        verify(mockDragEventListener, times(1)).onDragMove(eq(TASK_ID))
 
         taskPositioner.onDragPositioningEnd(
                 STARTING_BOUNDS.left.toFloat(),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt
new file mode 100644
index 0000000..0ccd424
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt
@@ -0,0 +1,164 @@
+/*
+ * 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.tiling
+
+import android.content.Context
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.desktopmode.DesktopRepository
+import com.android.wm.shell.desktopmode.DesktopTasksController
+import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
+import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator
+import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DesktopTilingDecorViewModelTest : ShellTestCase() {
+    private val contextMock: Context = mock()
+    private val displayControllerMock: DisplayController = mock()
+    private val rootTdaOrganizerMock: RootTaskDisplayAreaOrganizer = mock()
+    private val syncQueueMock: SyncTransactionQueue = mock()
+    private val transitionsMock: Transitions = mock()
+    private val shellTaskOrganizerMock: ShellTaskOrganizer = mock()
+    private val desktopRepository: DesktopRepository = mock()
+    private val toggleResizeDesktopTaskTransitionHandlerMock:
+        ToggleResizeDesktopTaskTransitionHandler =
+        mock()
+    private val returnToDragStartAnimatorMock: ReturnToDragStartAnimator = mock()
+
+    private val desktopModeWindowDecorationMock: DesktopModeWindowDecoration = mock()
+    private val desktopTilingDecoration: DesktopTilingWindowDecoration = mock()
+    private lateinit var desktopTilingDecorViewModel: DesktopTilingDecorViewModel
+
+    @Before
+    fun setUp() {
+        desktopTilingDecorViewModel =
+            DesktopTilingDecorViewModel(
+                contextMock,
+                displayControllerMock,
+                rootTdaOrganizerMock,
+                syncQueueMock,
+                transitionsMock,
+                shellTaskOrganizerMock,
+                toggleResizeDesktopTaskTransitionHandlerMock,
+                returnToDragStartAnimatorMock,
+                desktopRepository,
+            )
+    }
+
+    @Test
+    fun testTiling_shouldCreate_newTilingDecoration() {
+        val task1 = createFreeformTask()
+        val task2 = createFreeformTask()
+        task1.displayId = 1
+        task2.displayId = 2
+
+        desktopTilingDecorViewModel.snapToHalfScreen(
+            task1,
+            desktopModeWindowDecorationMock,
+            DesktopTasksController.SnapPosition.LEFT,
+            BOUNDS,
+        )
+        assertThat(desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.size())
+            .isEqualTo(1)
+        desktopTilingDecorViewModel.snapToHalfScreen(
+            task2,
+            desktopModeWindowDecorationMock,
+            DesktopTasksController.SnapPosition.LEFT,
+            BOUNDS,
+        )
+        assertThat(desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.size())
+            .isEqualTo(2)
+    }
+
+    @Test
+    fun removeTile_shouldCreate_newTilingDecoration() {
+        val task1 = createFreeformTask()
+        task1.displayId = 1
+        desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.put(
+            1,
+            desktopTilingDecoration,
+        )
+        desktopTilingDecorViewModel.removeTaskIfTiled(task1.displayId, task1.taskId)
+
+        verify(desktopTilingDecoration, times(1)).removeTaskIfTiled(any(), any(), any())
+    }
+
+    @Test
+    fun moveTaskToFront_shouldRoute_toCorrectTilingDecoration() {
+
+        val task1 = createFreeformTask()
+        task1.displayId = 1
+        desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.put(
+            1,
+            desktopTilingDecoration,
+        )
+        desktopTilingDecorViewModel.moveTaskToFrontIfTiled(task1)
+
+        verify(desktopTilingDecoration, times(1)).moveTiledPairToFront(any())
+    }
+
+    @Test
+    fun overviewAnimation_starting_ShouldNotifyAllDecorations() {
+        desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.put(
+            1,
+            desktopTilingDecoration,
+        )
+        desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.put(
+            2,
+            desktopTilingDecoration,
+        )
+        desktopTilingDecorViewModel.onOverviewAnimationStateChange(true)
+
+        verify(desktopTilingDecoration, times(2)).onOverviewAnimationStateChange(any())
+    }
+
+    @Test
+    fun userChange_starting_allTilingSessionsShouldBeDestroyed() {
+        desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.put(
+            1,
+            desktopTilingDecoration,
+        )
+        desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.put(
+            2,
+            desktopTilingDecoration,
+        )
+
+        desktopTilingDecorViewModel.onUserChange()
+
+        verify(desktopTilingDecoration, times(2)).onUserChange()
+    }
+
+    companion object {
+        private val BOUNDS = Rect(1, 2, 3, 4)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt
new file mode 100644
index 0000000..0ee3f46
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt
@@ -0,0 +1,99 @@
+/*
+ * 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.tiling
+
+import android.content.res.Configuration
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.view.SurfaceControl
+import androidx.test.annotation.UiThreadTest
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.SyncTransactionQueue
+import java.util.function.Supplier
+import kotlin.test.Test
+import org.junit.Before
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DesktopTilingDividerWindowManagerTest : ShellTestCase() {
+    private lateinit var config: Configuration
+
+    private var windowName: String = "Tiling"
+
+    private val leashMock = mock<SurfaceControl>()
+
+    private val syncQueueMock = mock<SyncTransactionQueue>()
+
+    private val transitionHandlerMock = mock<DesktopTilingWindowDecoration>()
+
+    private val transactionSupplierMock = mock<Supplier<SurfaceControl.Transaction>>()
+
+    private val surfaceControl = mock<SurfaceControl>()
+
+    private val transaction = mock<SurfaceControl.Transaction>()
+
+    private lateinit var desktopTilingWindowManager: DesktopTilingDividerWindowManager
+
+    @Before
+    fun setup() {
+        config = Configuration()
+        config.setToDefaults()
+        desktopTilingWindowManager =
+            DesktopTilingDividerWindowManager(
+                config,
+                windowName,
+                mContext,
+                leashMock,
+                syncQueueMock,
+                transitionHandlerMock,
+                transactionSupplierMock,
+                BOUNDS,
+            )
+    }
+
+    @Test
+    @UiThreadTest
+    fun testWindowManager_isInitialisedAndReleased() {
+        whenever(transactionSupplierMock.get()).thenReturn(transaction)
+        whenever(transaction.hide(any())).thenReturn(transaction)
+        whenever(transaction.setRelativeLayer(any(), any(), any())).thenReturn(transaction)
+        whenever(transaction.setPosition(any(), any(), any())).thenReturn(transaction)
+        whenever(transaction.remove(any())).thenReturn(transaction)
+
+        desktopTilingWindowManager.generateViewHost(surfaceControl)
+
+        // Ensure a surfaceControl transaction runs to show the divider.
+        verify(transactionSupplierMock, times(1)).get()
+        verify(syncQueueMock, times(1)).runInSync(any())
+
+        desktopTilingWindowManager.release()
+        verify(transaction, times(1)).hide(any())
+        verify(transaction, times(1)).remove(any())
+        verify(transaction, times(1)).apply()
+    }
+
+    companion object {
+        private val BOUNDS = Rect(1, 2, 3, 4)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt
new file mode 100644
index 0000000..0b04a21
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt
@@ -0,0 +1,518 @@
+/*
+ * 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.tiling
+
+import android.app.ActivityManager
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Rect
+import android.os.IBinder
+import android.testing.AndroidTestingRunner
+import android.view.SurfaceControl
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.window.TransitionInfo
+import android.window.WindowContainerTransaction
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.desktopmode.DesktopRepository
+import com.android.wm.shell.desktopmode.DesktopTasksController
+import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
+import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator
+import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
+import com.android.wm.shell.windowdecor.DragResizeWindowGeometry
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Captor
+import org.mockito.kotlin.any
+import org.mockito.kotlin.capture
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DesktopTilingWindowDecorationTest : ShellTestCase() {
+
+    private val context: Context = mock()
+
+    private val syncQueue: SyncTransactionQueue = mock()
+
+    private val displayController: DisplayController = mock()
+    private val displayId: Int = 0
+
+    private val rootTdaOrganizer: RootTaskDisplayAreaOrganizer = mock()
+
+    private val transitions: Transitions = mock()
+
+    private val shellTaskOrganizer: ShellTaskOrganizer = mock()
+
+    private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler =
+        mock()
+
+    private val returnToDragStartAnimator: ReturnToDragStartAnimator = mock()
+
+    private val desktopWindowDecoration: DesktopModeWindowDecoration = mock()
+
+    private val displayLayout: DisplayLayout = mock()
+
+    private val resources: Resources = mock()
+    private val surfaceControlMock: SurfaceControl = mock()
+    private val transaction: SurfaceControl.Transaction = mock()
+    private val tiledTaskHelper: DesktopTilingWindowDecoration.AppResizingHelper = mock()
+    private val transition: IBinder = mock()
+    private val info: TransitionInfo = mock()
+    private val finishCallback: Transitions.TransitionFinishCallback = mock()
+    private val desktopRepository: DesktopRepository = mock()
+    private val desktopTilingDividerWindowManager: DesktopTilingDividerWindowManager = mock()
+    private lateinit var tilingDecoration: DesktopTilingWindowDecoration
+
+    private val split_divider_width = 10
+
+    @Captor private lateinit var wctCaptor: ArgumentCaptor<WindowContainerTransaction>
+
+    @Before
+    fun setUp() {
+        tilingDecoration =
+            DesktopTilingWindowDecoration(
+                context,
+                syncQueue,
+                displayController,
+                displayId,
+                rootTdaOrganizer,
+                transitions,
+                shellTaskOrganizer,
+                toggleResizeDesktopTaskTransitionHandler,
+                returnToDragStartAnimator,
+                desktopRepository,
+            )
+    }
+
+    @Test
+    fun taskTiled_toCorrectBounds_leftTile() {
+        val task1 = createFreeformTask()
+        val stableBounds = STABLE_BOUNDS_MOCK
+        whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+            (i.arguments.first() as Rect).set(stableBounds)
+        }
+        whenever(context.resources).thenReturn(resources)
+        whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+
+        tilingDecoration.onAppTiled(
+            task1,
+            desktopWindowDecoration,
+            DesktopTasksController.SnapPosition.LEFT,
+            BOUNDS,
+        )
+
+        verify(toggleResizeDesktopTaskTransitionHandler).startTransition(capture(wctCaptor), any())
+        for (change in wctCaptor.value.changes) {
+            val bounds = change.value.configuration.windowConfiguration.bounds
+            val leftBounds = getLeftTaskBounds()
+            assertRectEqual(bounds, leftBounds)
+        }
+    }
+
+    @Test
+    fun taskTiled_toCorrectBounds_rightTile() {
+        // Setup
+        val task1 = createFreeformTask()
+        val stableBounds = STABLE_BOUNDS_MOCK
+        whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+            (i.arguments.first() as Rect).set(stableBounds)
+        }
+        whenever(context.resources).thenReturn(resources)
+        whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+
+        tilingDecoration.onAppTiled(
+            task1,
+            desktopWindowDecoration,
+            DesktopTasksController.SnapPosition.RIGHT,
+            BOUNDS,
+        )
+
+        verify(toggleResizeDesktopTaskTransitionHandler).startTransition(capture(wctCaptor), any())
+        for (change in wctCaptor.value.changes) {
+            val bounds = change.value.configuration.windowConfiguration.bounds
+            val leftBounds = getRightTaskBounds()
+            assertRectEqual(bounds, leftBounds)
+        }
+    }
+
+    @Test
+    fun taskTiled_notAnimated_whenTilingPositionNotChange() {
+        val task1 = createFreeformTask()
+        val stableBounds = STABLE_BOUNDS_MOCK
+        whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+            (i.arguments.first() as Rect).set(stableBounds)
+        }
+        whenever(context.resources).thenReturn(resources)
+        whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+        whenever(desktopWindowDecoration.getLeash()).thenReturn(surfaceControlMock)
+
+        tilingDecoration.onAppTiled(
+            task1,
+            desktopWindowDecoration,
+            DesktopTasksController.SnapPosition.LEFT,
+            BOUNDS,
+        )
+        task1.configuration.windowConfiguration.setBounds(getLeftTaskBounds())
+        tilingDecoration.onAppTiled(
+            task1,
+            desktopWindowDecoration,
+            DesktopTasksController.SnapPosition.LEFT,
+            NON_STABLE_BOUNDS_MOCK,
+        )
+
+        verify(toggleResizeDesktopTaskTransitionHandler, times(1))
+            .startTransition(capture(wctCaptor), any())
+        verify(returnToDragStartAnimator, times(1)).start(any(), any(), any(), any(), any())
+        for (change in wctCaptor.value.changes) {
+            val bounds = change.value.configuration.windowConfiguration.bounds
+            val leftBounds = getLeftTaskBounds()
+            assertRectEqual(bounds, leftBounds)
+        }
+    }
+
+    @Test
+    fun taskNotTiled_notBroughtToFront_tilingNotInitialised() {
+        val task1 = createFreeformTask()
+        val task2 = createFreeformTask()
+        val stableBounds = STABLE_BOUNDS_MOCK
+        whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+            (i.arguments.first() as Rect).set(stableBounds)
+        }
+        whenever(context.resources).thenReturn(resources)
+        whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+
+        tilingDecoration.onAppTiled(
+            task1,
+            desktopWindowDecoration,
+            DesktopTasksController.SnapPosition.RIGHT,
+            BOUNDS,
+        )
+
+        assertThat(tilingDecoration.moveTiledPairToFront(task2)).isFalse()
+        verify(transitions, never()).startTransition(any(), any(), any())
+    }
+
+    @Test
+    fun taskNotTiled_notBroughtToFront_taskNotTiled() {
+        val task1 = createFreeformTask()
+        val task2 = createFreeformTask()
+        val task3 = createFreeformTask()
+        val stableBounds = STABLE_BOUNDS_MOCK
+        whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+            (i.arguments.first() as Rect).set(stableBounds)
+        }
+        whenever(context.resources).thenReturn(resources)
+        whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+
+        tilingDecoration.onAppTiled(
+            task1,
+            desktopWindowDecoration,
+            DesktopTasksController.SnapPosition.RIGHT,
+            BOUNDS,
+        )
+        tilingDecoration.onAppTiled(
+            task2,
+            desktopWindowDecoration,
+            DesktopTasksController.SnapPosition.LEFT,
+            BOUNDS,
+        )
+
+        assertThat(tilingDecoration.moveTiledPairToFront(task3)).isFalse()
+        verify(transitions, never()).startTransition(any(), any(), any())
+    }
+
+    @Test
+    fun taskTiled_broughtToFront_alreadyInFrontNoAction() {
+        val task1 = createFreeformTask()
+        val task2 = createFreeformTask()
+        val stableBounds = STABLE_BOUNDS_MOCK
+        whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+            (i.arguments.first() as Rect).set(stableBounds)
+        }
+        whenever(context.resources).thenReturn(resources)
+        whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+
+        tilingDecoration.onAppTiled(
+            task1,
+            desktopWindowDecoration,
+            DesktopTasksController.SnapPosition.RIGHT,
+            BOUNDS,
+        )
+        tilingDecoration.onAppTiled(
+            task2,
+            desktopWindowDecoration,
+            DesktopTasksController.SnapPosition.LEFT,
+            BOUNDS,
+        )
+        task1.isFocused = true
+
+        assertThat(tilingDecoration.moveTiledPairToFront(task1)).isFalse()
+        verify(transitions, never()).startTransition(any(), any(), any())
+    }
+
+    @Test
+    fun taskTiled_broughtToFront_bringToFront() {
+        val task1 = createFreeformTask()
+        val task2 = createFreeformTask()
+        val task3 = createFreeformTask()
+        val stableBounds = STABLE_BOUNDS_MOCK
+        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+            (i.arguments.first() as Rect).set(stableBounds)
+        }
+        whenever(context.resources).thenReturn(resources)
+        whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+        whenever(desktopWindowDecoration.getLeash()).thenReturn(surfaceControlMock)
+        whenever(desktopRepository.isVisibleTask(any())).thenReturn(true)
+        tilingDecoration.onAppTiled(
+            task1,
+            desktopWindowDecoration,
+            DesktopTasksController.SnapPosition.RIGHT,
+            BOUNDS,
+        )
+        tilingDecoration.onAppTiled(
+            task2,
+            desktopWindowDecoration,
+            DesktopTasksController.SnapPosition.LEFT,
+            BOUNDS,
+        )
+        task1.isFocused = true
+        task3.isFocused = true
+
+        assertThat(tilingDecoration.moveTiledPairToFront(task3)).isFalse()
+        assertThat(tilingDecoration.moveTiledPairToFront(task1)).isTrue()
+        verify(transitions, times(1)).startTransition(eq(TRANSIT_TO_FRONT), any(), eq(null))
+    }
+
+    @Test
+    fun taskTiledTasks_NotResized_BeforeTouchEndArrival() {
+        // Setup
+        val task1 = createFreeformTask()
+        val task2 = createFreeformTask()
+        val stableBounds = STABLE_BOUNDS_MOCK
+        whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+            (i.arguments.first() as Rect).set(stableBounds)
+        }
+        whenever(context.resources).thenReturn(resources)
+        whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+        desktopWindowDecoration.mTaskInfo = task1
+        task1.minWidth = 0
+        task1.minHeight = 0
+        initTiledTaskHelperMock(task1)
+        desktopWindowDecoration.mDecorWindowContext = context
+        whenever(resources.getBoolean(any())).thenReturn(true)
+
+        // Act
+        tilingDecoration.onAppTiled(
+            task1,
+            desktopWindowDecoration,
+            DesktopTasksController.SnapPosition.RIGHT,
+            BOUNDS,
+        )
+        tilingDecoration.onAppTiled(
+            task2,
+            desktopWindowDecoration,
+            DesktopTasksController.SnapPosition.LEFT,
+            BOUNDS,
+        )
+
+        tilingDecoration.leftTaskResizingHelper = tiledTaskHelper
+        tilingDecoration.rightTaskResizingHelper = tiledTaskHelper
+        tilingDecoration.onDividerHandleMoved(BOUNDS, transaction)
+
+        // Assert
+        verify(transaction, times(1)).apply()
+        // Show should be called twice for each tiled app, to show the veil and the icon for each
+        // of them.
+        verify(tiledTaskHelper, times(2)).showVeil(any())
+
+        // Move again
+        tilingDecoration.onDividerHandleMoved(BOUNDS, transaction)
+        verify(tiledTaskHelper, times(2)).updateVeil(any())
+        verify(transitions, never()).startTransition(any(), any(), any())
+
+        // End moving, no startTransition because bounds did not change.
+        tiledTaskHelper.newBounds.set(BOUNDS)
+        tilingDecoration.onDividerHandleDragEnd(BOUNDS, transaction)
+        verify(tiledTaskHelper, times(2)).hideVeil()
+        verify(transitions, never()).startTransition(any(), any(), any())
+
+        // Move then end again with bounds changing to ensure startTransition is called.
+        tilingDecoration.onDividerHandleMoved(BOUNDS, transaction)
+        tilingDecoration.onDividerHandleDragEnd(BOUNDS, transaction)
+        verify(transitions, times(1))
+            .startTransition(eq(TRANSIT_CHANGE), any(), eq(tilingDecoration))
+        // No hide veil until start animation is called.
+        verify(tiledTaskHelper, times(2)).hideVeil()
+
+        tilingDecoration.startAnimation(transition, info, transaction, transaction, finishCallback)
+        // the startAnimation function should hide the veils.
+        verify(tiledTaskHelper, times(4)).hideVeil()
+    }
+
+    @Test
+    fun taskTiled_shouldBeRemoved_whenTileBroken() {
+        val task1 = createFreeformTask()
+        val stableBounds = STABLE_BOUNDS_MOCK
+        whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+            (i.arguments.first() as Rect).set(stableBounds)
+        }
+        whenever(context.resources).thenReturn(resources)
+        whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+        whenever(tiledTaskHelper.taskInfo).thenReturn(task1)
+        whenever(tiledTaskHelper.desktopModeWindowDecoration).thenReturn(desktopWindowDecoration)
+        tilingDecoration.onAppTiled(
+            task1,
+            desktopWindowDecoration,
+            DesktopTasksController.SnapPosition.LEFT,
+            BOUNDS,
+        )
+        tilingDecoration.leftTaskResizingHelper = tiledTaskHelper
+
+        tilingDecoration.removeTaskIfTiled(task1.taskId)
+
+        assertThat(tilingDecoration.leftTaskResizingHelper).isNull()
+        verify(desktopWindowDecoration, times(1)).removeDragResizeListener(any())
+        verify(desktopWindowDecoration, times(1))
+            .updateDisabledResizingEdge(eq(DragResizeWindowGeometry.DisabledEdge.NONE), eq(false))
+        verify(tiledTaskHelper, times(1)).dispose()
+    }
+
+    @Test
+    fun taskNotTiled_shouldNotBeRemoved_whenNotTiled() {
+        val task1 = createFreeformTask()
+        val task2 = createFreeformTask()
+        val stableBounds = STABLE_BOUNDS_MOCK
+        whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+            (i.arguments.first() as Rect).set(stableBounds)
+        }
+        whenever(context.resources).thenReturn(resources)
+        whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+        whenever(tiledTaskHelper.taskInfo).thenReturn(task1)
+        whenever(tiledTaskHelper.desktopModeWindowDecoration).thenReturn(desktopWindowDecoration)
+        tilingDecoration.onAppTiled(
+            task1,
+            desktopWindowDecoration,
+            DesktopTasksController.SnapPosition.LEFT,
+            BOUNDS,
+        )
+        tilingDecoration.leftTaskResizingHelper = tiledTaskHelper
+
+        tilingDecoration.removeTaskIfTiled(task2.taskId)
+
+        assertThat(tilingDecoration.leftTaskResizingHelper).isNotNull()
+        verify(desktopWindowDecoration, never()).removeDragResizeListener(any())
+        verify(desktopWindowDecoration, never()).updateDisabledResizingEdge(any(), any())
+        verify(tiledTaskHelper, never()).dispose()
+    }
+
+    @Test
+    fun tasksTiled_shouldBeRemoved_whenSessionDestroyed() {
+        val task1 = createFreeformTask()
+        val task2 = createFreeformTask()
+        val stableBounds = STABLE_BOUNDS_MOCK
+        whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+        whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+            (i.arguments.first() as Rect).set(stableBounds)
+        }
+        whenever(context.resources).thenReturn(resources)
+        whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+        whenever(tiledTaskHelper.taskInfo).thenReturn(task1)
+        whenever(tiledTaskHelper.desktopModeWindowDecoration).thenReturn(desktopWindowDecoration)
+        tilingDecoration.onAppTiled(
+            task1,
+            desktopWindowDecoration,
+            DesktopTasksController.SnapPosition.LEFT,
+            BOUNDS,
+        )
+        tilingDecoration.onAppTiled(
+            task2,
+            desktopWindowDecoration,
+            DesktopTasksController.SnapPosition.RIGHT,
+            BOUNDS,
+        )
+        tilingDecoration.leftTaskResizingHelper = tiledTaskHelper
+        tilingDecoration.rightTaskResizingHelper = tiledTaskHelper
+        tilingDecoration.desktopTilingDividerWindowManager = desktopTilingDividerWindowManager
+
+        tilingDecoration.onUserChange()
+
+        assertThat(tilingDecoration.leftTaskResizingHelper).isNull()
+        assertThat(tilingDecoration.rightTaskResizingHelper).isNull()
+        verify(desktopWindowDecoration, times(2)).removeDragResizeListener(any())
+        verify(tiledTaskHelper, times(2)).dispose()
+    }
+
+    private fun initTiledTaskHelperMock(taskInfo: ActivityManager.RunningTaskInfo) {
+        whenever(tiledTaskHelper.bounds).thenReturn(BOUNDS)
+        whenever(tiledTaskHelper.taskInfo).thenReturn(taskInfo)
+        whenever(tiledTaskHelper.newBounds).thenReturn(Rect(BOUNDS))
+        whenever(tiledTaskHelper.desktopModeWindowDecoration).thenReturn(desktopWindowDecoration)
+    }
+
+    private fun assertRectEqual(rect1: Rect, rect2: Rect) {
+        assertThat(rect1.left).isEqualTo(rect2.left)
+        assertThat(rect1.right).isEqualTo(rect2.right)
+        assertThat(rect1.top).isEqualTo(rect2.top)
+        assertThat(rect1.bottom).isEqualTo(rect2.bottom)
+        return
+    }
+
+    private fun getRightTaskBounds(): Rect {
+        val stableBounds = STABLE_BOUNDS_MOCK
+        val destinationWidth = stableBounds.width() / 2
+        val leftBound = stableBounds.right - destinationWidth + split_divider_width / 2
+        return Rect(leftBound, stableBounds.top, stableBounds.right, stableBounds.bottom)
+    }
+
+    private fun getLeftTaskBounds(): Rect {
+        val stableBounds = STABLE_BOUNDS_MOCK
+        val destinationWidth = stableBounds.width() / 2
+        val rightBound = stableBounds.left + destinationWidth - split_divider_width / 2
+        return Rect(stableBounds.left, stableBounds.top, rightBound, stableBounds.bottom)
+    }
+
+    companion object {
+        private val NON_STABLE_BOUNDS_MOCK = Rect(50, 55, 100, 100)
+        private val STABLE_BOUNDS_MOCK = Rect(0, 0, 100, 100)
+        private val BOUNDS = Rect(1, 2, 3, 4)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/TilingDividerViewTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/TilingDividerViewTest.kt
new file mode 100644
index 0000000..fd5eb88
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/TilingDividerViewTest.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.tiling
+
+import android.graphics.Rect
+import android.os.SystemClock
+import android.testing.AndroidTestingRunner
+import android.view.InputDevice
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import androidx.test.annotation.UiThreadTest
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.R
+import com.android.wm.shell.ShellTestCase
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class TilingDividerViewTest : ShellTestCase() {
+
+    private lateinit var tilingDividerView: TilingDividerView
+
+    private val dividerMoveCallbackMock = mock<DividerMoveCallback>()
+
+    private val viewMock = mock<View>()
+
+    @Before
+    @UiThreadTest
+    fun setUp() {
+        tilingDividerView =
+            LayoutInflater.from(mContext).inflate(R.layout.tiling_split_divider, /* root= */ null)
+                as TilingDividerView
+        tilingDividerView.setup(dividerMoveCallbackMock, BOUNDS)
+        tilingDividerView.handleStartY = 0
+        tilingDividerView.handleEndY = 1500
+    }
+
+    @Test
+    @UiThreadTest
+    fun testCallbackOnTouch() {
+        val x = 5
+        val y = 5
+        val downTime: Long = SystemClock.uptimeMillis()
+
+        val downMotionEvent =
+            getMotionEvent(downTime, MotionEvent.ACTION_DOWN, x.toFloat(), y.toFloat())
+        tilingDividerView.handleMotionEvent(viewMock, downMotionEvent)
+        verify(dividerMoveCallbackMock, times(1)).onDividerMoveStart(any())
+
+        val motionEvent =
+            getMotionEvent(downTime, MotionEvent.ACTION_MOVE, x.toFloat(), y.toFloat())
+        tilingDividerView.handleMotionEvent(viewMock, motionEvent)
+        verify(dividerMoveCallbackMock, times(1)).onDividerMove(any())
+
+        val upMotionEvent =
+            getMotionEvent(downTime, MotionEvent.ACTION_UP, x.toFloat(), y.toFloat())
+        tilingDividerView.handleMotionEvent(viewMock, upMotionEvent)
+        verify(dividerMoveCallbackMock, times(1)).onDividerMovedEnd(any())
+    }
+
+    private fun getMotionEvent(eventTime: Long, action: Int, x: Float, y: Float): MotionEvent {
+        val properties = MotionEvent.PointerProperties()
+        properties.id = 0
+        properties.toolType = MotionEvent.TOOL_TYPE_UNKNOWN
+
+        val coords = MotionEvent.PointerCoords()
+        coords.pressure = 1f
+        coords.size = 1f
+        coords.x = x
+        coords.y = y
+
+        return MotionEvent.obtain(
+            eventTime,
+            eventTime,
+            action,
+            1,
+            arrayOf(properties),
+            arrayOf(coords),
+            0,
+            0,
+            1.0f,
+            1.0f,
+            0,
+            0,
+            InputDevice.SOURCE_TOUCHSCREEN,
+            0,
+        )
+    }
+
+    companion object {
+        private val BOUNDS = Rect(0, 0, 1500, 1500)
+    }
+}
diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java
index 2c8e352..57f5f52c 100644
--- a/media/java/android/media/audiopolicy/AudioPolicy.java
+++ b/media/java/android/media/audiopolicy/AudioPolicy.java
@@ -316,7 +316,7 @@
         @NonNull
         public Builder setMediaProjection(@NonNull MediaProjection projection) {
             if (projection == null) {
-                throw new IllegalArgumentException("Invalid null volume callback");
+                throw new IllegalArgumentException("Invalid null media projection");
             }
             mProjection = projection;
             return this;
diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig
index 5a8763f..81a2e6a 100644
--- a/packages/SettingsLib/aconfig/settingslib.aconfig
+++ b/packages/SettingsLib/aconfig/settingslib.aconfig
@@ -159,3 +159,10 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "hearing_devices_ambient_volume_control"
+    namespace: "accessibility"
+    description: "Enable the ambient volume control in device details and hearing devices dialog."
+    bug: "357878944"
+}
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 3d2c931..2a65d39 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -435,9 +435,9 @@
 }
 
 flag {
-    name: "status_bar_ron_chips"
+    name: "status_bar_notification_chips"
     namespace: "systemui"
-    description: "Show rich ongoing notifications as chips in the status bar"
+    description: "Show promoted ongoing notifications as chips in the status bar"
     bug: "361346412"
 }
 
@@ -991,6 +991,16 @@
 }
 
 flag {
+    name: "shortcut_helper_key_glyph"
+    namespace: "systemui"
+    description: "Allow showing key glyph in shortcut helper"
+    bug: "353902478"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
   name: "dream_overlay_bouncer_swipe_direction_filtering"
   namespace: "systemui"
   description: "do not initiate bouncer swipe when the direction is opposite of the expansion"
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 476cced..e329aae 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
@@ -181,9 +181,11 @@
 import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.communal.ui.viewmodel.ResizeInfo
+import com.android.systemui.communal.ui.viewmodel.ResizeableItemFrameViewModel
 import com.android.systemui.communal.util.DensityUtils.Companion.adjustedDp
 import com.android.systemui.communal.widgets.SmartspaceAppWidgetHostView
 import com.android.systemui.communal.widgets.WidgetConfigurator
+import com.android.systemui.lifecycle.rememberViewModel
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.phone.SystemUIDialogFactory
 import kotlin.math.max
@@ -665,6 +667,7 @@
     maxHeightPx: Int,
     modifier: Modifier = Modifier,
     alpha: () -> Float = { 1f },
+    viewModel: ResizeableItemFrameViewModel,
     onResize: (info: ResizeInfo) -> Unit = {},
     content: @Composable (modifier: Modifier) -> Unit,
 ) {
@@ -680,6 +683,7 @@
             enabled = enabled,
             alpha = alpha,
             modifier = modifier,
+            viewModel = viewModel,
             onResize = onResize,
             minHeightPx = minHeightPx,
             maxHeightPx = maxHeightPx,
@@ -796,6 +800,14 @@
                     false
                 }
 
+            val resizeableItemFrameViewModel =
+                rememberViewModel(
+                    key = item.size.span,
+                    traceName = "ResizeableItemFrame.viewModel.$index",
+                ) {
+                    ResizeableItemFrameViewModel()
+                }
+
             if (viewModel.isEditMode && dragDropState != null) {
                 val isItemDragging = dragDropState.draggingItemKey == item.key
                 val outlineAlpha by
@@ -821,6 +833,7 @@
                                 )
                             }
                             .thenIf(isItemDragging) { Modifier.zIndex(1f) },
+                    viewModel = resizeableItemFrameViewModel,
                     onResize = { resizeInfo -> contentListState.resize(index, resizeInfo) },
                     minHeightPx = widgetSizeInfo.minHeightPx,
                     maxHeightPx = widgetSizeInfo.maxHeightPx,
@@ -843,6 +856,7 @@
                             contentListState = contentListState,
                             interactionHandler = interactionHandler,
                             widgetSection = widgetSection,
+                            resizeableItemFrameViewModel = resizeableItemFrameViewModel,
                         )
                     }
                 }
@@ -857,6 +871,7 @@
                     contentListState = contentListState,
                     interactionHandler = interactionHandler,
                     widgetSection = widgetSection,
+                    resizeableItemFrameViewModel = resizeableItemFrameViewModel,
                 )
             }
         }
@@ -1080,6 +1095,7 @@
     contentListState: ContentListState,
     interactionHandler: RemoteViews.InteractionHandler?,
     widgetSection: CommunalAppWidgetSection,
+    resizeableItemFrameViewModel: ResizeableItemFrameViewModel,
 ) {
     when (model) {
         is CommunalContentModel.WidgetContent.Widget ->
@@ -1093,6 +1109,7 @@
                 index,
                 contentListState,
                 widgetSection,
+                resizeableItemFrameViewModel,
             )
         is CommunalContentModel.WidgetPlaceholder -> HighlightedItem(modifier)
         is CommunalContentModel.WidgetContent.DisabledWidget ->
@@ -1223,7 +1240,9 @@
     index: Int,
     contentListState: ContentListState,
     widgetSection: CommunalAppWidgetSection,
+    resizeableItemFrameViewModel: ResizeableItemFrameViewModel,
 ) {
+    val coroutineScope = rememberCoroutineScope()
     val context = LocalContext.current
     val accessibilityLabel =
         remember(model, context) {
@@ -1234,6 +1253,10 @@
     val placeWidgetActionLabel = stringResource(R.string.accessibility_action_label_place_widget)
     val unselectWidgetActionLabel =
         stringResource(R.string.accessibility_action_label_unselect_widget)
+
+    val shrinkWidgetLabel = stringResource(R.string.accessibility_action_label_shrink_widget)
+    val expandWidgetLabel = stringResource(R.string.accessibility_action_label_expand_widget)
+
     val selectedKey by viewModel.selectedKey.collectAsStateWithLifecycle()
     val selectedIndex =
         selectedKey?.let { key -> contentListState.list.indexOfFirst { it.key == key } }
@@ -1292,6 +1315,29 @@
                                 true
                             }
                         val actions = mutableListOf(deleteAction)
+
+                        if (communalWidgetResizing() && resizeableItemFrameViewModel.canShrink()) {
+                            actions.add(
+                                CustomAccessibilityAction(shrinkWidgetLabel) {
+                                    coroutineScope.launch {
+                                        resizeableItemFrameViewModel.shrinkToNextAnchor()
+                                    }
+                                    true
+                                }
+                            )
+                        }
+
+                        if (communalWidgetResizing() && resizeableItemFrameViewModel.canExpand()) {
+                            actions.add(
+                                CustomAccessibilityAction(expandWidgetLabel) {
+                                    coroutineScope.launch {
+                                        resizeableItemFrameViewModel.expandToNextAnchor()
+                                    }
+                                    true
+                                }
+                            )
+                        }
+
                         if (selectedIndex != null && selectedIndex != index) {
                             actions.add(
                                 CustomAccessibilityAction(placeWidgetActionLabel) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt
index 521330f..8e85432 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt
@@ -56,7 +56,6 @@
 import com.android.systemui.communal.ui.viewmodel.DragHandle
 import com.android.systemui.communal.ui.viewmodel.ResizeInfo
 import com.android.systemui.communal.ui.viewmodel.ResizeableItemFrameViewModel
-import com.android.systemui.lifecycle.rememberViewModel
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.combine
 
@@ -192,16 +191,12 @@
     maxHeightPx: Int = Int.MAX_VALUE,
     resizeMultiple: Int = 1,
     alpha: () -> Float = { 1f },
+    viewModel: ResizeableItemFrameViewModel,
     onResize: (info: ResizeInfo) -> Unit = {},
     content: @Composable () -> Unit,
 ) {
     val brush = SolidColor(outlineColor)
     val onResizeUpdated by rememberUpdatedState(onResize)
-    val viewModel =
-        rememberViewModel(key = currentSpan, traceName = "ResizeableItemFrame.viewModel") {
-            ResizeableItemFrameViewModel()
-        }
-
     val dragHandleHeight = verticalArrangement.spacing - outlinePadding * 2
     val isDragging by
         remember(viewModel) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
index 58801e0..e725ce5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
@@ -128,10 +128,10 @@
                         QSSceneAdapter.State.QS
                     }
                     else ->
-                        error(
-                            "Bad transition for QuickSettings: fromContent=$fromContent," +
-                                " toScene=$toContent"
-                        )
+                        // We are not in a transition between states that have QS, so just make
+                        // sure it's closed. This could be an issue if going from SplitShade to
+                        // a folded device.
+                        QSSceneAdapter.State.CLOSED
                 }
             }
         is TransitionState.Transition.OverlayTransition ->
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 491221f..bba3d69 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -516,13 +516,14 @@
                             .weight(1f)
                             .graphicsLayer { translationX = unfoldTranslationXForStartSide }
                 ) {
-                    BrightnessMirror(
-                        viewModel = brightnessMirrorViewModel,
-                        qsSceneAdapter = viewModel.qsSceneAdapter,
-                        // Need to use the offset measured from the container as the header
-                        // has to be accounted for
-                        measureFromContainer = true,
-                    )
+                    Box(modifier = Modifier.fillMaxSize()) {
+                        BrightnessMirror(
+                            viewModel = brightnessMirrorViewModel,
+                            qsSceneAdapter = viewModel.qsSceneAdapter,
+                            modifier = Modifier.align(Alignment.TopCenter),
+                            measureFromContainer = true,
+                        )
+                    }
                     Column(
                         verticalArrangement = Arrangement.Top,
                         modifier = Modifier.fillMaxSize().padding(bottom = bottomPadding),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java
index 58c3fec..bd33e52 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java
@@ -20,7 +20,9 @@
 
 import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -29,11 +31,12 @@
 import android.graphics.Rect;
 import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.FlagsParameterization;
+import android.view.GestureDetector;
 import android.view.GestureDetector.OnGestureListener;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.logging.UiEventLogger;
@@ -42,9 +45,12 @@
 import com.android.systemui.ambient.touch.scrim.ScrimController;
 import com.android.systemui.ambient.touch.scrim.ScrimManager;
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel;
+import com.android.systemui.flags.SceneContainerFlagParameterizationKt;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.kosmos.KosmosJavaAdapter;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.scene.domain.interactor.SceneInteractor;
+import com.android.systemui.scene.ui.view.WindowRootView;
 import com.android.systemui.shared.system.InputChannelCompat;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -58,10 +64,14 @@
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
+import java.util.List;
 import java.util.Optional;
 
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
 @SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedAndroidJunit4.class)
 @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
 @DisableFlags(Flags.FLAG_COMMUNAL_BOUNCER_DO_NOT_MODIFY_PLUGIN_OPEN)
 public class BouncerFullscreenSwipeTouchHandlerTest extends SysuiTestCase {
@@ -114,6 +124,11 @@
     @Mock
     KeyguardInteractor mKeyguardInteractor;
 
+    @Mock
+    WindowRootView mWindowRootView;
+
+    private SceneInteractor mSceneInteractor;
+
     private static final float TOUCH_REGION = .3f;
     private static final float MIN_BOUNCER_HEIGHT = .05f;
 
@@ -124,9 +139,21 @@
             /* flags= */ 0
     );
 
+    @Parameters(name = "{0}")
+    public static List<FlagsParameterization> getParams() {
+        return SceneContainerFlagParameterizationKt.parameterizeSceneContainerFlag();
+    }
+
+    public BouncerFullscreenSwipeTouchHandlerTest(FlagsParameterization flags) {
+        super();
+        mSetFlagsRule.setFlagsParameterization(flags);
+    }
+
     @Before
     public void setup() {
         mKosmos = new KosmosJavaAdapter(this);
+        mSceneInteractor = spy(mKosmos.getSceneInteractor());
+
         MockitoAnnotations.initMocks(this);
         mTouchHandler = new BouncerSwipeTouchHandler(
                 mKosmos.getTestScope(),
@@ -142,7 +169,9 @@
                 MIN_BOUNCER_HEIGHT,
                 mUiEventLogger,
                 mActivityStarter,
-                mKeyguardInteractor);
+                mKeyguardInteractor,
+                mSceneInteractor,
+                Optional.of(() -> mWindowRootView));
 
         when(mScrimManager.getCurrentController()).thenReturn(mScrimController);
         when(mValueAnimatorCreator.create(anyFloat(), anyFloat())).thenReturn(mValueAnimator);
@@ -153,6 +182,38 @@
     }
 
     /**
+     * Makes sure that touches go to the scene container when the flag is on.
+     */
+    @Test
+    @EnableFlags(Flags.FLAG_SCENE_CONTAINER)
+    public void testSwipeUp_sendsTouchesToWindowRootView() {
+        mTouchHandler.onGlanceableTouchAvailable(true);
+        mTouchHandler.onSessionStart(mTouchSession);
+        ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
+                ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
+        verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
+
+        final OnGestureListener gestureListener = gestureListenerCaptor.getValue();
+
+        final int screenHeight = 100;
+        final float distanceY = screenHeight * 0.42f;
+
+        final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
+                0, screenHeight, 0);
+        final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
+                0, screenHeight - distanceY, 0);
+
+        assertThat(gestureListener.onScroll(event1, event2, 0,
+                distanceY))
+                .isTrue();
+
+        // Ensure only called once
+        verify(mSceneInteractor).onRemoteUserInputStarted(any());
+        verify(mWindowRootView).dispatchTouchEvent(event1);
+        verify(mWindowRootView).dispatchTouchEvent(event2);
+    }
+
+    /**
      * Ensures expansion does not happen for full vertical swipes when touch is not available.
      */
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
index 9568167..494e0b4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
@@ -26,6 +26,7 @@
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
@@ -37,12 +38,12 @@
 import android.graphics.Region;
 import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.FlagsParameterization;
 import android.view.GestureDetector;
 import android.view.GestureDetector.OnGestureListener;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.logging.UiEventLogger;
@@ -52,9 +53,12 @@
 import com.android.systemui.ambient.touch.scrim.ScrimManager;
 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants;
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel;
+import com.android.systemui.flags.SceneContainerFlagParameterizationKt;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.kosmos.KosmosJavaAdapter;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.scene.domain.interactor.SceneInteractor;
+import com.android.systemui.scene.ui.view.WindowRootView;
 import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.shared.system.InputChannelCompat;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -70,10 +74,14 @@
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
+import java.util.List;
 import java.util.Optional;
 
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
 @SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedAndroidJunit4.class)
 @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
 public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
     private KosmosJavaAdapter mKosmos;
@@ -122,6 +130,9 @@
     Region mRegion;
 
     @Mock
+    WindowRootView mWindowRootView;
+
+    @Mock
     CommunalViewModel mCommunalViewModel;
 
     @Mock
@@ -130,6 +141,8 @@
     @Captor
     ArgumentCaptor<Rect> mRectCaptor;
 
+    private SceneInteractor mSceneInteractor;
+
     private static final float TOUCH_REGION = .3f;
     private static final int SCREEN_WIDTH_PX = 1024;
     private static final int SCREEN_HEIGHT_PX = 100;
@@ -142,9 +155,21 @@
             /* flags= */ 0
     );
 
+    @Parameters(name = "{0}")
+    public static List<FlagsParameterization> getParams() {
+        return SceneContainerFlagParameterizationKt.parameterizeSceneContainerFlag();
+    }
+
+    public BouncerSwipeTouchHandlerTest(FlagsParameterization flags) {
+        super();
+        mSetFlagsRule.setFlagsParameterization(flags);
+    }
+
     @Before
     public void setup() {
         mKosmos = new KosmosJavaAdapter(this);
+        mSceneInteractor = spy(mKosmos.getSceneInteractor());
+
         MockitoAnnotations.initMocks(this);
         mTouchHandler = new BouncerSwipeTouchHandler(
                 mKosmos.getTestScope(),
@@ -160,7 +185,10 @@
                 MIN_BOUNCER_HEIGHT,
                 mUiEventLogger,
                 mActivityStarter,
-                mKeyguardInteractor);
+                mKeyguardInteractor,
+                mSceneInteractor,
+                Optional.of(() -> mWindowRootView)
+        );
 
         when(mScrimManager.getCurrentController()).thenReturn(mScrimController);
         when(mValueAnimatorCreator.create(anyFloat(), anyFloat())).thenReturn(mValueAnimator);
@@ -367,6 +395,7 @@
      * Makes sure the expansion amount is proportional to (1 - scroll).
      */
     @Test
+    @DisableFlags(Flags.FLAG_SCENE_CONTAINER)
     public void testSwipeUp_setsCorrectExpansionAmount() {
         mTouchHandler.onSessionStart(mTouchSession);
         ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
@@ -380,6 +409,36 @@
     }
 
     /**
+     * Makes sure that touches go to the scene container when the flag is on.
+     */
+    @Test
+    @EnableFlags(Flags.FLAG_SCENE_CONTAINER)
+    public void testSwipeUp_sendsTouchesToWindowRootView() {
+        mTouchHandler.onSessionStart(mTouchSession);
+        ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
+                ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
+        verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
+
+        final OnGestureListener gestureListener = gestureListenerCaptor.getValue();
+
+        final float distanceY = SCREEN_HEIGHT_PX * 0.42f;
+
+        final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
+                0, SCREEN_HEIGHT_PX, 0);
+        final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
+                0, SCREEN_HEIGHT_PX - distanceY, 0);
+
+        assertThat(gestureListener.onScroll(event1, event2, 0,
+                distanceY))
+                .isTrue();
+
+        // Ensure only called once
+        verify(mSceneInteractor).onRemoteUserInputStarted(any());
+        verify(mWindowRootView).dispatchTouchEvent(event1);
+        verify(mWindowRootView).dispatchTouchEvent(event2);
+    }
+
+    /**
      * Verifies that swiping up when the lock pattern is not secure dismissed dream and consumes
      * the gesture.
      */
@@ -476,6 +535,7 @@
      * Tests that ending an upward swipe before the set threshold leads to bouncer collapsing down.
      */
     @Test
+    @DisableFlags(Flags.FLAG_SCENE_CONTAINER)
     public void testSwipeUpPositionBelowThreshold_collapsesBouncer() {
         final float swipeUpPercentage = .3f;
         final float expansion = 1 - swipeUpPercentage;
@@ -499,6 +559,7 @@
      * Tests that ending an upward swipe above the set threshold will continue the expansion.
      */
     @Test
+    @DisableFlags(Flags.FLAG_SCENE_CONTAINER)
     public void testSwipeUpPositionAboveThreshold_expandsBouncer() {
         final float swipeUpPercentage = .7f;
         final float expansion = 1 - swipeUpPercentage;
@@ -528,6 +589,7 @@
      * Tests that swiping up with a speed above the set threshold will continue the expansion.
      */
     @Test
+    @DisableFlags(Flags.FLAG_SCENE_CONTAINER)
     public void testSwipeUpVelocityAboveMin_expandsBouncer() {
         when(mFlingAnimationUtils.getMinVelocityPxPerSecond()).thenReturn((float) 0);
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt
index 38ea4497..fa5af51 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt
@@ -18,9 +18,9 @@
 import android.app.DreamManager
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
 import android.view.GestureDetector
 import android.view.MotionEvent
-import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
@@ -28,14 +28,20 @@
 import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
+import com.android.systemui.flags.andSceneContainer
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.data.repository.sceneContainerRepository
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.ui.view.WindowRootView
 import com.android.systemui.shade.ShadeViewController
 import com.android.systemui.shared.system.InputChannelCompat
 import com.android.systemui.statusbar.phone.CentralSurfaces
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
 import java.util.Optional
+import javax.inject.Provider
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -47,22 +53,29 @@
 import org.mockito.kotlin.times
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 @SmallTest
-@RunWith(AndroidJUnit4::class)
-class ShadeTouchHandlerTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class ShadeTouchHandlerTest(flags: FlagsParameterization) : SysuiTestCase() {
     private var kosmos = testKosmos()
     private var mCentralSurfaces = mock<CentralSurfaces>()
     private var mShadeViewController = mock<ShadeViewController>()
     private var mDreamManager = mock<DreamManager>()
     private var mTouchSession = mock<TouchSession>()
     private var communalViewModel = mock<CommunalViewModel>()
+    private var windowRootView = mock<WindowRootView>()
 
     private lateinit var mTouchHandler: ShadeTouchHandler
 
     private var mGestureListenerCaptor = argumentCaptor<GestureDetector.OnGestureListener>()
     private var mInputListenerCaptor = argumentCaptor<InputChannelCompat.InputEventListener>()
 
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags)
+    }
+
     @Before
     fun setup() {
         mTouchHandler =
@@ -73,7 +86,9 @@
                 mDreamManager,
                 communalViewModel,
                 kosmos.communalSettingsInteractor,
-                TOUCH_HEIGHT
+                kosmos.sceneInteractor,
+                Optional.of(Provider<WindowRootView> { windowRootView }),
+                TOUCH_HEIGHT,
             )
     }
 
@@ -97,7 +112,7 @@
 
     // Verifies that a swipe down forwards captured touches to central surfaces for handling.
     @Test
-    @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
+    @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX, Flags.FLAG_SCENE_CONTAINER)
     @EnableFlags(Flags.FLAG_COMMUNAL_HUB)
     fun testSwipeDown_communalEnabled_sentToCentralSurfaces() {
         kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true)
@@ -110,7 +125,11 @@
 
     // Verifies that a swipe down forwards captured touches to the shade view for handling.
     @Test
-    @DisableFlags(Flags.FLAG_COMMUNAL_HUB, Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
+    @DisableFlags(
+        Flags.FLAG_COMMUNAL_HUB,
+        Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX,
+        Flags.FLAG_SCENE_CONTAINER,
+    )
     fun testSwipeDown_communalDisabled_sentToShadeView() {
         swipe(Direction.DOWN)
 
@@ -121,7 +140,7 @@
     // Verifies that a swipe down while dreaming forwards captured touches to the shade view for
     // handling.
     @Test
-    @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
+    @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX, Flags.FLAG_SCENE_CONTAINER)
     fun testSwipeDown_dreaming_sentToShadeView() {
         whenever(mDreamManager.isDreaming).thenReturn(true)
         swipe(Direction.DOWN)
@@ -130,9 +149,39 @@
         verify(mShadeViewController, times(2)).handleExternalTouch(any())
     }
 
+    // Verifies that a swipe down forwards captured touches to the window root view for handling.
+    @Test
+    @EnableFlags(
+        Flags.FLAG_COMMUNAL_HUB,
+        Flags.FLAG_SCENE_CONTAINER,
+        Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX,
+    )
+    fun testSwipeDown_sceneContainerEnabled_sentToWindowRootView() {
+        mTouchHandler.onGlanceableTouchAvailable(true)
+
+        swipe(Direction.DOWN)
+
+        // Both motion events are sent for central surfaces to process.
+        assertThat(kosmos.sceneContainerRepository.isRemoteUserInputOngoing.value).isTrue()
+        verify(windowRootView, times(2)).dispatchTouchEvent(any())
+    }
+
+    // Verifies that a swipe down while dreaming forwards captured touches to the window root view
+    // for handling.
+    @Test
+    @EnableFlags(Flags.FLAG_SCENE_CONTAINER)
+    @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
+    fun testSwipeDown_sceneContainerEnabledFullscreenSwipeDisabled_sentToWindowRootView() {
+        swipe(Direction.DOWN)
+
+        // Both motion events are sent for the shade view to process.
+        assertThat(kosmos.sceneContainerRepository.isRemoteUserInputOngoing.value).isTrue()
+        verify(windowRootView, times(2)).dispatchTouchEvent(any())
+    }
+
     // Verifies that a swipe up is not forwarded to central surfaces.
     @Test
-    @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
+    @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX, Flags.FLAG_SCENE_CONTAINER)
     @EnableFlags(Flags.FLAG_COMMUNAL_HUB)
     fun testSwipeUp_communalEnabled_touchesNotSent() {
         kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true)
@@ -146,7 +195,11 @@
 
     // Verifies that a swipe up is not forwarded to the shade view.
     @Test
-    @DisableFlags(Flags.FLAG_COMMUNAL_HUB, Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
+    @DisableFlags(
+        Flags.FLAG_COMMUNAL_HUB,
+        Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX,
+        Flags.FLAG_SCENE_CONTAINER,
+    )
     fun testSwipeUp_communalDisabled_touchesNotSent() {
         swipe(Direction.UP)
 
@@ -155,6 +208,17 @@
         verify(mShadeViewController, never()).handleExternalTouch(any())
     }
 
+    // Verifies that a swipe up is not forwarded to the window root view.
+    @Test
+    @EnableFlags(Flags.FLAG_COMMUNAL_HUB, Flags.FLAG_SCENE_CONTAINER)
+    fun testSwipeUp_sceneContainerEnabled_touchesNotSent() {
+        swipe(Direction.UP)
+
+        // Motion events are not sent for window root view to process as the swipe is going in the
+        // wrong direction.
+        verify(windowRootView, never()).dispatchTouchEvent(any())
+    }
+
     @Test
     @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
     fun testCancelMotionEvent_popsTouchSession() {
@@ -243,10 +307,16 @@
 
     private enum class Direction {
         DOWN,
-        UP
+        UP,
     }
 
     companion object {
         private const val TOUCH_HEIGHT = 20
+
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return FlagsParameterization.allCombinationsOf().andSceneContainer()
+        }
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt
index 22b114c..0269577 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt
@@ -366,6 +366,106 @@
             assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f)
         }
 
+    @Test
+    fun testCanExpand_atTopPosition_withMultipleAnchors_returnsTrue() =
+        testScope.runTest {
+            val twoRowGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 1, currentRow = 0)
+
+            updateGridLayout(twoRowGrid)
+            assertThat(underTest.canExpand()).isTrue()
+            assertThat(underTest.bottomDragState.anchors.toList())
+                .containsAtLeast(0 to 0f, 1 to 45f)
+        }
+
+    @Test
+    fun testCanExpand_atTopPosition_withSingleAnchors_returnsFalse() =
+        testScope.runTest {
+            val oneRowGrid = singleSpanGrid.copy(totalSpans = 1, currentSpan = 1, currentRow = 0)
+            updateGridLayout(oneRowGrid)
+            assertThat(underTest.canExpand()).isFalse()
+        }
+
+    @Test
+    fun testCanExpand_atBottomPosition_withMultipleAnchors_returnsTrue() =
+        testScope.runTest {
+            val twoRowGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 1, currentRow = 1)
+            updateGridLayout(twoRowGrid)
+            assertThat(underTest.canExpand()).isTrue()
+            assertThat(underTest.topDragState.anchors.toList()).containsAtLeast(0 to 0f, -1 to -45f)
+        }
+
+    @Test
+    fun testCanShrink_atMinimumHeight_returnsFalse() =
+        testScope.runTest {
+            val oneRowGrid = singleSpanGrid.copy(totalSpans = 1, currentSpan = 1, currentRow = 0)
+            updateGridLayout(oneRowGrid)
+            assertThat(underTest.canShrink()).isFalse()
+        }
+
+    @Test
+    fun testCanShrink_atFullSize_checksBottomDragState() = runTestWithSnapshots {
+        val twoSpanGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 2, currentRow = 0)
+        updateGridLayout(twoSpanGrid)
+
+        assertThat(underTest.canShrink()).isTrue()
+        assertThat(underTest.bottomDragState.anchors.toList()).containsAtLeast(0 to 0f, -1 to -45f)
+    }
+
+    @Test
+    fun testResizeByAccessibility_expandFromBottom_usesTopDragState() = runTestWithSnapshots {
+        val resizeInfo by collectLastValue(underTest.resizeInfo)
+
+        val twoSpanGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 1, currentRow = 1)
+        updateGridLayout(twoSpanGrid)
+
+        underTest.expandToNextAnchor()
+
+        assertThat(resizeInfo).isEqualTo(ResizeInfo(1, DragHandle.TOP))
+    }
+
+    @Test
+    fun testResizeByAccessibility_expandFromTop_usesBottomDragState() = runTestWithSnapshots {
+        val resizeInfo by collectLastValue(underTest.resizeInfo)
+
+        val twoSpanGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 1, currentRow = 0)
+        updateGridLayout(twoSpanGrid)
+
+        underTest.expandToNextAnchor()
+
+        assertThat(resizeInfo).isEqualTo(ResizeInfo(1, DragHandle.BOTTOM))
+    }
+
+    @Test
+    fun testResizeByAccessibility_shrinkFromFull_usesBottomDragState() = runTestWithSnapshots {
+        val resizeInfo by collectLastValue(underTest.resizeInfo)
+
+        val twoSpanGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 2, currentRow = 0)
+        updateGridLayout(twoSpanGrid)
+
+        underTest.shrinkToNextAnchor()
+
+        assertThat(resizeInfo).isEqualTo(ResizeInfo(-1, DragHandle.BOTTOM))
+    }
+
+    @Test
+    fun testResizeByAccessibility_cannotResizeAtMinSize() = runTestWithSnapshots {
+        val resizeInfo by collectLastValue(underTest.resizeInfo)
+
+        // Set up grid at minimum size
+        val minSizeGrid =
+            singleSpanGrid.copy(
+                totalSpans = 2,
+                currentSpan = 1,
+                minHeightPx = singleSpanGrid.minHeightPx,
+                currentRow = 0,
+            )
+        updateGridLayout(minSizeGrid)
+
+        underTest.shrinkToNextAnchor()
+
+        assertThat(resizeInfo).isNull()
+    }
+
     @Test(expected = IllegalArgumentException::class)
     fun testIllegalState_maxHeightLessThanMinHeight() =
         testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
index 83d2617..32fa160 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
@@ -20,6 +20,7 @@
 import android.app.admin.DevicePolicyManager
 import android.content.Intent
 import android.os.UserHandle
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.SmallTest
 import com.android.internal.widget.LockPatternUtils
 import com.android.keyguard.logging.KeyguardQuickAffordancesLogger
@@ -81,6 +82,7 @@
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
 @DisableSceneContainer
+@FlakyTest(bugId = 292574995, detail = "NullPointer on MockMakerTypeMockability.mockable()")
 class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
 
     companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
index 527aeaa..83f95ea 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
@@ -28,6 +28,8 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
@@ -604,6 +606,46 @@
         assertThat(mPagedTileLayoutListening).isFalse();
     }
 
+    @Test
+    public void reAttach_configurationChangePending_triggersConfigurationListener() {
+        mController.onViewDetached();
+
+        when(mQSPanel.hadConfigurationChangeWhileDetached()).thenReturn(true);
+        clearInvocations(mQSLogger);
+
+        mController.onViewAttached();
+
+        verify(mQSLogger).logOnConfigurationChanged(
+                anyInt(),
+                anyInt(),
+                anyBoolean(),
+                anyBoolean(),
+                anyInt(),
+                anyInt(),
+                anyString()
+        );
+    }
+
+    @Test
+    public void reAttach_noConfigurationChangePending_doesntTriggerConfigurationListener() {
+        mController.onViewDetached();
+
+        when(mQSPanel.hadConfigurationChangeWhileDetached()).thenReturn(false);
+        clearInvocations(mQSLogger);
+
+        mController.onViewAttached();
+
+        verify(mQSLogger, never()).logOnConfigurationChanged(
+                anyInt(),
+                anyInt(),
+                anyBoolean(),
+                anyBoolean(),
+                anyInt(),
+                anyInt(),
+                anyString()
+        );
+    }
+
 
     private boolean usingMediaPlayer() {
         return !SceneContainerFlag.isEnabled();
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/demo/ui/viewmodel/DemoNotifChipViewModelTest.kt
similarity index 74%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/demo/ui/viewmodel/DemoNotifChipViewModelTest.kt
index 118dea6..69a7627 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/demo/ui/viewmodel/DemoNotifChipViewModelTest.kt
@@ -14,17 +14,17 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel
+package com.android.systemui.statusbar.chips.notification.demo.ui.viewmodel
 
 import android.content.packageManager
 import android.graphics.drawable.BitmapDrawable
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_STATUS_BAR_RON_CHIPS
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
 import com.android.systemui.statusbar.chips.ui.model.ColorsModel
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import com.android.systemui.statusbar.commandline.CommandRegistry
@@ -40,13 +40,13 @@
 import org.mockito.kotlin.whenever
 
 @SmallTest
-class DemoRonChipViewModelTest : SysuiTestCase() {
+class DemoNotifChipViewModelTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val commandRegistry = kosmos.commandRegistry
     private val pw = PrintWriter(StringWriter())
 
-    private val underTest = kosmos.demoRonChipViewModel
+    private val underTest = kosmos.demoNotifChipViewModel
 
     @Before
     fun setUp() {
@@ -56,61 +56,61 @@
     }
 
     @Test
-    @DisableFlags(FLAG_STATUS_BAR_RON_CHIPS)
+    @DisableFlags(StatusBarNotifChips.FLAG_NAME)
     fun chip_flagOff_hidden() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
-            addDemoRonChip()
+            addDemoNotifChip()
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
         }
 
     @Test
-    @EnableFlags(FLAG_STATUS_BAR_RON_CHIPS)
+    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
     fun chip_noPackage_hidden() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
-            commandRegistry.onShellCommand(pw, arrayOf("demo-ron"))
+            commandRegistry.onShellCommand(pw, arrayOf("demo-notif"))
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
         }
 
     @Test
-    @EnableFlags(FLAG_STATUS_BAR_RON_CHIPS)
+    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
     fun chip_hasPackage_shown() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
-            commandRegistry.onShellCommand(pw, arrayOf("demo-ron", "-p", "com.android.systemui"))
+            commandRegistry.onShellCommand(pw, arrayOf("demo-notif", "-p", "com.android.systemui"))
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
         }
 
     @Test
-    @EnableFlags(FLAG_STATUS_BAR_RON_CHIPS)
+    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
     fun chip_hasText_shownWithText() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
             commandRegistry.onShellCommand(
                 pw,
-                arrayOf("demo-ron", "-p", "com.android.systemui", "-t", "test")
+                arrayOf("demo-notif", "-p", "com.android.systemui", "-t", "test"),
             )
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Text::class.java)
         }
 
     @Test
-    @EnableFlags(FLAG_STATUS_BAR_RON_CHIPS)
+    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
     fun chip_supportsColor() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
             commandRegistry.onShellCommand(
                 pw,
-                arrayOf("demo-ron", "-p", "com.android.systemui", "-c", "#434343")
+                arrayOf("demo-notif", "-p", "com.android.systemui", "-c", "#434343"),
             )
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
@@ -119,28 +119,28 @@
         }
 
     @Test
-    @EnableFlags(FLAG_STATUS_BAR_RON_CHIPS)
+    @EnableFlags(StatusBarNotifChips.FLAG_NAME)
     fun chip_hasHideArg_hidden() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
             // First, show a chip
-            addDemoRonChip()
+            addDemoNotifChip()
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
 
             // Then, hide the chip
-            commandRegistry.onShellCommand(pw, arrayOf("demo-ron", "--hide"))
+            commandRegistry.onShellCommand(pw, arrayOf("demo-notif", "--hide"))
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
         }
 
-    private fun addDemoRonChip() {
-        Companion.addDemoRonChip(commandRegistry, pw)
+    private fun addDemoNotifChip() {
+        addDemoNotifChip(commandRegistry, pw)
     }
 
     companion object {
-        fun addDemoRonChip(commandRegistry: CommandRegistry, pw: PrintWriter) {
-            commandRegistry.onShellCommand(pw, arrayOf("demo-ron", "-p", "com.android.systemui"))
+        fun addDemoNotifChip(commandRegistry: CommandRegistry, pw: PrintWriter) {
+            commandRegistry.onShellCommand(pw, arrayOf("demo-notif", "-p", "com.android.systemui"))
         }
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
index 2872900..eb5d931 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
@@ -19,12 +19,11 @@
 import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_STATUS_BAR_RON_CHIPS
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.chips.ron.ui.viewmodel.notifChipsViewModel
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
@@ -42,7 +41,7 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @OptIn(ExperimentalCoroutinesApi::class)
-@EnableFlags(FLAG_STATUS_BAR_RON_CHIPS)
+@EnableFlags(StatusBarNotifChips.FLAG_NAME)
 class NotifChipsViewModelTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
index 26ce7b9..e96def6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
@@ -25,7 +25,6 @@
 import android.view.View
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_STATUS_BAR_RON_CHIPS
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.coroutines.collectLastValue
@@ -40,7 +39,8 @@
 import com.android.systemui.screenrecord.data.repository.screenRecordRepository
 import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE
 import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection
-import com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel.demoRonChipViewModel
+import com.android.systemui.statusbar.chips.notification.demo.ui.viewmodel.demoNotifChipViewModel
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
 import com.android.systemui.statusbar.phone.SystemUIDialog
@@ -66,13 +66,11 @@
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 
-/**
- * Tests for [OngoingActivityChipsViewModel] when the [FLAG_STATUS_BAR_RON_CHIPS] flag is disabled.
- */
+/** Tests for [OngoingActivityChipsViewModel] when the [StatusBarNotifChips] flag is disabled. */
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @OptIn(ExperimentalCoroutinesApi::class)
-@DisableFlags(FLAG_STATUS_BAR_RON_CHIPS)
+@DisableFlags(StatusBarNotifChips.FLAG_NAME)
 class OngoingActivityChipsViewModelTest : SysuiTestCase() {
     private val kosmos = Kosmos().also { it.testCase = this }
     private val testScope = kosmos.testScope
@@ -99,11 +97,11 @@
     @Before
     fun setUp() {
         setUpPackageManagerForMediaProjection(kosmos)
-        kosmos.demoRonChipViewModel.start()
+        kosmos.demoNotifChipViewModel.start()
         val icon =
             BitmapDrawable(
                 context.resources,
-                Bitmap.createBitmap(/* width= */ 100, /* height= */ 100, Bitmap.Config.ARGB_8888)
+                Bitmap.createBitmap(/* width= */ 100, /* height= */ 100, Bitmap.Config.ARGB_8888),
             )
         whenever(kosmos.packageManager.getApplicationIcon(any<String>())).thenReturn(icon)
     }
@@ -325,7 +323,7 @@
             latest: OngoingActivityChipModel?,
             chipView: View,
             dialog: SystemUIDialog,
-            kosmos: Kosmos
+            kosmos: Kosmos,
         ): DialogInterface.OnClickListener {
             // Capture the action that would get invoked when the user clicks "Stop" on the dialog
             lateinit var dialogStopAction: DialogInterface.OnClickListener
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
similarity index 93%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
index c5b857f..b12d7c5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
@@ -24,7 +24,6 @@
 import android.view.View
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_STATUS_BAR_RON_CHIPS
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
@@ -37,9 +36,10 @@
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE
 import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection
+import com.android.systemui.statusbar.chips.notification.demo.ui.viewmodel.DemoNotifChipViewModelTest.Companion.addDemoNotifChip
+import com.android.systemui.statusbar.chips.notification.demo.ui.viewmodel.demoNotifChipViewModel
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
 import com.android.systemui.statusbar.chips.notification.ui.viewmodel.NotifChipsViewModelTest.Companion.assertIsNotifChip
-import com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel.DemoRonChipViewModelTest.Companion.addDemoRonChip
-import com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel.demoRonChipViewModel
 import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
@@ -73,14 +73,12 @@
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 
-/**
- * Tests for [OngoingActivityChipsViewModel] when the [FLAG_STATUS_BAR_RON_CHIPS] flag is enabled.
- */
+/** Tests for [OngoingActivityChipsViewModel] when the [StatusBarNotifChips] flag is enabled. */
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @OptIn(ExperimentalCoroutinesApi::class)
-@EnableFlags(FLAG_STATUS_BAR_RON_CHIPS)
-class OngoingActivityChipsWithRonsViewModelTest : SysuiTestCase() {
+@EnableFlags(StatusBarNotifChips.FLAG_NAME)
+class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val systemClock = kosmos.fakeSystemClock
@@ -110,7 +108,7 @@
     @Before
     fun setUp() {
         setUpPackageManagerForMediaProjection(kosmos)
-        kosmos.demoRonChipViewModel.start()
+        kosmos.demoNotifChipViewModel.start()
         val icon =
             BitmapDrawable(
                 context.resources,
@@ -119,7 +117,7 @@
         whenever(kosmos.packageManager.getApplicationIcon(any<String>())).thenReturn(icon)
     }
 
-    // Even though the `primaryChip` flow isn't used when the RONs flag is on, still test that the
+    // Even though the `primaryChip` flow isn't used when the notifs flag is on, still test that the
     // flow has the right behavior to verify that we don't break any existing functionality.
 
     @Test
@@ -256,13 +254,13 @@
         testScope.runTest {
             screenRecordState.value = ScreenRecordModel.Recording
             callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
-            addDemoRonChip(commandRegistry, pw)
+            addDemoNotifChip(commandRegistry, pw)
 
             val latest by collectLastValue(underTest.chips)
 
             assertIsScreenRecordChip(latest!!.primary)
             assertIsCallChip(latest!!.secondary)
-            // Demo RON chip is dropped
+            // Demo notif chip is dropped
         }
 
     @Test
@@ -389,7 +387,7 @@
     fun primaryChip_higherPriorityChipAdded_lowerPriorityChipReplaced() =
         testScope.runTest {
             // Start with just the lowest priority chip shown
-            addDemoRonChip(commandRegistry, pw)
+            addDemoNotifChip(commandRegistry, pw)
             // And everything else hidden
             callRepo.setOngoingCallState(OngoingCallModel.NoCall)
             mediaProjectionState.value = MediaProjectionState.NotProjecting
@@ -397,7 +395,7 @@
 
             val latest by collectLastValue(underTest.primaryChip)
 
-            assertIsDemoRonChip(latest)
+            assertIsDemoNotifChip(latest)
 
             // WHEN the higher priority call chip is added
             callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
@@ -431,7 +429,7 @@
             mediaProjectionState.value =
                 MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
             callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
-            addDemoRonChip(commandRegistry, pw)
+            addDemoNotifChip(commandRegistry, pw)
 
             val latest by collectLastValue(underTest.primaryChip)
 
@@ -453,15 +451,15 @@
             // WHEN the higher priority call is removed
             callRepo.setOngoingCallState(OngoingCallModel.NoCall)
 
-            // THEN the lower priority demo RON is used
-            assertIsDemoRonChip(latest)
+            // THEN the lower priority demo notif is used
+            assertIsDemoNotifChip(latest)
         }
 
     @Test
     fun chips_movesChipsAroundAccordingToPriority() =
         testScope.runTest {
             // Start with just the lowest priority chip shown
-            addDemoRonChip(commandRegistry, pw)
+            addDemoNotifChip(commandRegistry, pw)
             // And everything else hidden
             callRepo.setOngoingCallState(OngoingCallModel.NoCall)
             mediaProjectionState.value = MediaProjectionState.NotProjecting
@@ -469,16 +467,16 @@
 
             val latest by collectLastValue(underTest.chips)
 
-            assertIsDemoRonChip(latest!!.primary)
+            assertIsDemoNotifChip(latest!!.primary)
             assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
 
             // WHEN the higher priority call chip is added
             callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
 
-            // THEN the higher priority call chip is used as primary and demo ron is demoted to
+            // THEN the higher priority call chip is used as primary and demo notif is demoted to
             // secondary
             assertIsCallChip(latest!!.primary)
-            assertIsDemoRonChip(latest!!.secondary)
+            assertIsDemoNotifChip(latest!!.secondary)
 
             // WHEN the higher priority media projection chip is added
             mediaProjectionState.value =
@@ -489,7 +487,7 @@
                 )
 
             // THEN the higher priority media projection chip is used as primary and call is demoted
-            // to secondary (and demo RON is dropped altogether)
+            // to secondary (and demo notif is dropped altogether)
             assertIsShareToAppChip(latest!!.primary)
             assertIsCallChip(latest!!.secondary)
 
@@ -503,15 +501,15 @@
             screenRecordState.value = ScreenRecordModel.DoingNothing
             callRepo.setOngoingCallState(OngoingCallModel.NoCall)
 
-            // THEN media projection and demo RON remain
+            // THEN media projection and demo notif remain
             assertIsShareToAppChip(latest!!.primary)
-            assertIsDemoRonChip(latest!!.secondary)
+            assertIsDemoNotifChip(latest!!.secondary)
 
             // WHEN media projection is dropped
             mediaProjectionState.value = MediaProjectionState.NotProjecting
 
-            // THEN demo RON is promoted to primary
-            assertIsDemoRonChip(latest!!.primary)
+            // THEN demo notif is promoted to primary
+            assertIsDemoNotifChip(latest!!.primary)
             assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
         }
 
@@ -624,7 +622,7 @@
             assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false))
         }
 
-    private fun assertIsDemoRonChip(latest: OngoingActivityChipModel?) {
+    private fun assertIsDemoNotifChip(latest: OngoingActivityChipModel?) {
         assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
         assertThat((latest as OngoingActivityChipModel.Shown).icon)
             .isInstanceOf(OngoingActivityChipModel.ChipIcon.FullColorAppIcon::class.java)
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 0aa5ccf..c838180 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1311,6 +1311,10 @@
     <string name="communal_widget_picker_description">Anyone can view widgets on your lock screen, even if your tablet\'s locked.</string>
     <!-- Label for accessibility action to unselect a widget in edit mode. [CHAR LIMIT=NONE] -->
     <string name="accessibility_action_label_unselect_widget">unselect widget</string>
+    <!-- Label for accessibility action to shrink a widget in edit mode. [CHAR LIMIT=NONE] -->
+    <string name="accessibility_action_label_shrink_widget">Decrease height</string>
+    <!-- Label for accessibility action to expand a widget in edit mode. [CHAR LIMIT=NONE] -->
+    <string name="accessibility_action_label_expand_widget">Increase height</string>
     <!-- Title shown above information regarding lock screen widgets. [CHAR LIMIT=50] -->
     <string name="communal_widgets_disclaimer_title">Lock screen widgets</string>
     <!-- Information about lock screen widgets presented to the user. [CHAR LIMIT=NONE] -->
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt
index 223a21d..e365b77 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt
@@ -27,6 +27,7 @@
 import android.view.MotionEvent
 import android.view.VelocityTracker
 import androidx.annotation.VisibleForTesting
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.internal.logging.UiEvent
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.Flags
@@ -38,6 +39,9 @@
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.scene.ui.view.WindowRootView
 import com.android.systemui.shade.ShadeExpansionChangeEvent
 import com.android.systemui.statusbar.NotificationShadeWindowController
 import com.android.systemui.statusbar.phone.CentralSurfaces
@@ -45,12 +49,12 @@
 import java.util.Optional
 import javax.inject.Inject
 import javax.inject.Named
+import javax.inject.Provider
 import kotlin.math.abs
 import kotlin.math.hypot
 import kotlin.math.max
 import kotlin.math.min
 import kotlinx.coroutines.CoroutineScope
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 /** Monitor for tracking touches on the DreamOverlay to bring up the bouncer. */
 class BouncerSwipeTouchHandler
@@ -74,6 +78,8 @@
     private val uiEventLogger: UiEventLogger,
     private val activityStarter: ActivityStarter,
     private val keyguardInteractor: KeyguardInteractor,
+    private val sceneInteractor: SceneInteractor,
+    private val windowRootViewProvider: Optional<Provider<WindowRootView>>,
 ) : TouchHandler {
     /** An interface for creating ValueAnimators. */
     interface ValueAnimatorCreator {
@@ -100,6 +106,8 @@
             currentScrimController = controller
         }
 
+    private val windowRootView by lazy { windowRootViewProvider.get().get() }
+
     /** Determines whether the touch handler should process touches in fullscreen swiping mode */
     private var touchAvailable = false
 
@@ -109,7 +117,7 @@
                 e1: MotionEvent?,
                 e2: MotionEvent,
                 distanceX: Float,
-                distanceY: Float
+                distanceY: Float,
             ): Boolean {
                 if (capture == null) {
                     capture =
@@ -128,6 +136,11 @@
                         expanded = false
                         // Since the user is dragging the bouncer up, set scrimmed to false.
                         currentScrimController?.show()
+
+                        if (SceneContainerFlag.isEnabled) {
+                            sceneInteractor.onRemoteUserInputStarted("bouncer touch handler")
+                            e1?.apply { windowRootView.dispatchTouchEvent(e1) }
+                        }
                     }
                 }
                 if (capture != true) {
@@ -152,20 +165,27 @@
                             /* cancelAction= */ null,
                             /* dismissShade= */ true,
                             /* afterKeyguardGone= */ true,
-                            /* deferred= */ false
+                            /* deferred= */ false,
                         )
                         return true
                     }
 
-                    // For consistency, we adopt the expansion definition found in the
-                    // PanelViewController. In this case, expansion refers to the view above the
-                    // bouncer. As that view's expansion shrinks, the bouncer appears. The bouncer
-                    // is fully hidden at full expansion (1) and fully visible when fully collapsed
-                    // (0).
-                    touchSession?.apply {
-                        val screenTravelPercentage =
-                            (abs((this@outer.y - e2.y).toDouble()) / getBounds().height()).toFloat()
-                        setPanelExpansion(1 - screenTravelPercentage)
+                    if (SceneContainerFlag.isEnabled) {
+                        windowRootView.dispatchTouchEvent(e2)
+                    } else {
+                        // For consistency, we adopt the expansion definition found in the
+                        // PanelViewController. In this case, expansion refers to the view above the
+                        // bouncer. As that view's expansion shrinks, the bouncer appears. The
+                        // bouncer
+                        // is fully hidden at full expansion (1) and fully visible when fully
+                        // collapsed
+                        // (0).
+                        touchSession?.apply {
+                            val screenTravelPercentage =
+                                (abs((this@outer.y - e2.y).toDouble()) / getBounds().height())
+                                    .toFloat()
+                            setPanelExpansion(1 - screenTravelPercentage)
+                        }
                     }
                 }
 
@@ -194,7 +214,7 @@
             ShadeExpansionChangeEvent(
                 /* fraction= */ currentExpansion,
                 /* expanded= */ expanded,
-                /* tracking= */ true
+                /* tracking= */ true,
             )
         currentScrimController?.expand(event)
     }
@@ -347,7 +367,7 @@
                     currentHeight,
                     targetHeight,
                     velocity,
-                    viewHeight
+                    viewHeight,
                 )
             } else {
                 // Shows the bouncer, i.e., fully collapses the space above the bouncer.
@@ -356,7 +376,7 @@
                     currentHeight,
                     targetHeight,
                     velocity,
-                    viewHeight
+                    viewHeight,
                 )
             }
             animator.start()
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt
index 1951a23..50e62a8 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt
@@ -22,19 +22,23 @@
 import android.view.InputEvent
 import android.view.MotionEvent
 import androidx.annotation.VisibleForTesting
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.Flags
 import com.android.systemui.ambient.touch.TouchHandler.TouchSession
 import com.android.systemui.ambient.touch.dagger.ShadeModule
 import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.scene.ui.view.WindowRootView
 import com.android.systemui.shade.ShadeViewController
 import com.android.systemui.statusbar.phone.CentralSurfaces
 import java.util.Optional
 import javax.inject.Inject
 import javax.inject.Named
+import javax.inject.Provider
 import kotlin.math.abs
 import kotlinx.coroutines.CoroutineScope
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 /**
  * [ShadeTouchHandler] is responsible for handling swipe down gestures over dream to bring down the
@@ -49,8 +53,10 @@
     private val dreamManager: DreamManager,
     private val communalViewModel: CommunalViewModel,
     private val communalSettingsInteractor: CommunalSettingsInteractor,
+    private val sceneInteractor: SceneInteractor,
+    private val windowRootViewProvider: Optional<Provider<WindowRootView>>,
     @param:Named(ShadeModule.NOTIFICATION_SHADE_GESTURE_INITIATION_HEIGHT)
-    private val initiationHeight: Int
+    private val initiationHeight: Int,
 ) : TouchHandler {
     /**
      * Tracks whether or not we are capturing a given touch. Will be null before and after a touch.
@@ -60,6 +66,8 @@
     /** Determines whether the touch handler should process touches in fullscreen swiping mode */
     private var touchAvailable = false
 
+    private val windowRootView by lazy { windowRootViewProvider.get().get() }
+
     init {
         if (Flags.hubmodeFullscreenVerticalSwipeFix()) {
             scope.launch {
@@ -100,7 +108,7 @@
                     e1: MotionEvent?,
                     e2: MotionEvent,
                     distanceX: Float,
-                    distanceY: Float
+                    distanceY: Float,
                 ): Boolean {
                     if (capture == null) {
                         // Only capture swipes that are going downwards.
@@ -110,6 +118,10 @@
                                 if (Flags.hubmodeFullscreenVerticalSwipeFix()) touchAvailable
                                 else true
                         if (capture == true) {
+                            if (SceneContainerFlag.isEnabled) {
+                                sceneInteractor.onRemoteUserInputStarted("shade touch handler")
+                            }
+
                             // Send the initial touches over, as the input listener has already
                             // processed these touches.
                             e1?.apply { sendTouchEvent(this) }
@@ -123,7 +135,7 @@
                     e1: MotionEvent?,
                     e2: MotionEvent,
                     velocityX: Float,
-                    velocityY: Float
+                    velocityY: Float,
                 ): Boolean {
                     return capture == true
                 }
@@ -132,6 +144,11 @@
     }
 
     private fun sendTouchEvent(event: MotionEvent) {
+        if (SceneContainerFlag.isEnabled) {
+            windowRootView.dispatchTouchEvent(event)
+            return
+        }
+
         if (communalSettingsInteractor.isCommunalFlagEnabled() && !dreamManager.isDreaming) {
             // Send touches to central surfaces only when on the glanceable hub while not dreaming.
             // While sending touches where while dreaming will open the shade, the shade
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/ShadeModule.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/ShadeModule.java
index bc2f354..1c781d6 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/ShadeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/ShadeModule.java
@@ -22,8 +22,10 @@
 import com.android.systemui.ambient.touch.TouchHandler;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.res.R;
+import com.android.systemui.scene.ui.view.WindowRootView;
 
 import dagger.Binds;
+import dagger.BindsOptionalOf;
 import dagger.Module;
 import dagger.Provides;
 import dagger.multibindings.IntoSet;
@@ -51,6 +53,13 @@
             ShadeTouchHandler touchHandler);
 
     /**
+     * Window root view is used to send touches to the scene container. Declaring as optional as it
+     * may not be present on all SysUI variants.
+     */
+    @BindsOptionalOf
+    abstract WindowRootView bindWindowRootView();
+
+    /**
      * Provides the height of the gesture area for notification swipe down.
      */
     @Provides
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt
index db4bee7..bde5d0f 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt
@@ -17,6 +17,7 @@
 
 import androidx.compose.foundation.gestures.AnchoredDraggableState
 import androidx.compose.foundation.gestures.DraggableAnchors
+import androidx.compose.foundation.gestures.snapTo
 import androidx.compose.runtime.snapshotFlow
 import com.android.app.tracing.coroutines.coroutineScopeTraced as coroutineScope
 import com.android.systemui.lifecycle.ExclusiveActivatable
@@ -81,6 +82,72 @@
             get() = roundDownToMultiple(getSpansForPx(minHeightPx))
     }
 
+    /** Check if widget can expanded based on current drag states */
+    fun canExpand(): Boolean {
+        return getNextAnchor(bottomDragState, moveUp = false) != null ||
+            getNextAnchor(topDragState, moveUp = true) != null
+    }
+
+    /** Check if widget can shrink based on current drag states */
+    fun canShrink(): Boolean {
+        return getNextAnchor(bottomDragState, moveUp = true) != null ||
+            getNextAnchor(topDragState, moveUp = false) != null
+    }
+
+    /** Get the next anchor value in the specified direction */
+    private fun getNextAnchor(state: AnchoredDraggableState<Int>, moveUp: Boolean): Int? {
+        var nextAnchor: Int? = null
+        var nextAnchorDiff = Int.MAX_VALUE
+        val currentValue = state.currentValue
+
+        for (i in 0 until state.anchors.size) {
+            val anchor = state.anchors.anchorAt(i) ?: continue
+            if (anchor == currentValue) continue
+
+            val diff =
+                if (moveUp) {
+                    currentValue - anchor
+                } else {
+                    anchor - currentValue
+                }
+
+            if (diff in 1..<nextAnchorDiff) {
+                nextAnchor = anchor
+                nextAnchorDiff = diff
+            }
+        }
+
+        return nextAnchor
+    }
+
+    /** Handle expansion to the next anchor */
+    suspend fun expandToNextAnchor() {
+        if (!canExpand()) return
+        val bottomAnchor = getNextAnchor(state = bottomDragState, moveUp = false)
+        if (bottomAnchor != null) {
+            bottomDragState.snapTo(bottomAnchor)
+            return
+        }
+        val topAnchor =
+            getNextAnchor(
+                state = topDragState,
+                moveUp = true, // Moving up to expand
+            )
+        topAnchor?.let { topDragState.snapTo(it) }
+    }
+
+    /** Handle shrinking to the next anchor */
+    suspend fun shrinkToNextAnchor() {
+        if (!canShrink()) return
+        val topAnchor = getNextAnchor(state = topDragState, moveUp = false)
+        if (topAnchor != null) {
+            topDragState.snapTo(topAnchor)
+            return
+        }
+        val bottomAnchor = getNextAnchor(state = bottomDragState, moveUp = true)
+        bottomAnchor?.let { bottomDragState.snapTo(it) }
+    }
+
     /**
      * The layout information necessary in order to calculate the pixel offsets of the drag anchor
      * points.
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index c6be0dd..b966ad4 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -35,6 +35,7 @@
 import com.android.systemui.dock.DockManagerImpl;
 import com.android.systemui.doze.DozeHost;
 import com.android.systemui.education.dagger.ContextualEducationModule;
+import com.android.systemui.emergency.EmergencyGestureModule;
 import com.android.systemui.inputdevice.tutorial.KeyboardTouchpadTutorialModule;
 import com.android.systemui.keyboard.shortcut.ShortcutHelperModule;
 import com.android.systemui.keyguard.ui.composable.blueprint.DefaultBlueprintModule;
@@ -123,6 +124,7 @@
         CollapsedStatusBarFragmentStartableModule.class,
         ConnectingDisplayViewModel.StartableModule.class,
         DefaultBlueprintModule.class,
+        EmergencyGestureModule.class,
         GestureModule.class,
         HeadsUpModule.class,
         KeyboardShortcutsModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 7a6ca08..1ffbbd2 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -65,7 +65,6 @@
 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;
@@ -503,10 +502,10 @@
         mDreamOverlayContainerViewController =
                 dreamOverlayComponent.getDreamOverlayContainerViewController();
 
-        if (!SceneContainerFlag.isEnabled()) {
-            mTouchMonitor = ambientTouchComponent.getTouchMonitor();
-            mTouchMonitor.init();
-        }
+        // Touch monitor are also used with SceneContainer. See individual touch handlers for
+        // handling of SceneContainer.
+        mTouchMonitor = ambientTouchComponent.getTouchMonitor();
+        mTouchMonitor.init();
 
         mStateController.setShouldShowComplications(shouldShowComplications());
 
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
index 12984efb..85fb90d 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
@@ -30,8 +30,10 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dreams.DreamOverlayContainerView;
 import com.android.systemui.res.R;
+import com.android.systemui.scene.ui.view.WindowRootView;
 import com.android.systemui.touch.TouchInsetManager;
 
+import dagger.BindsOptionalOf;
 import dagger.Module;
 import dagger.Provides;
 
@@ -54,6 +56,13 @@
     public static final String DREAM_IN_TRANSLATION_Y_DURATION =
             "dream_in_complications_translation_y_duration";
 
+    /**
+     * Window root view is used to send touches to the scene container. Declaring as optional as it
+     * may not be present on all SysUI variants.
+     */
+    @BindsOptionalOf
+    abstract WindowRootView bindWindowRootView();
+
     /** */
     @Provides
     @DreamOverlayComponent.DreamOverlayScope
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
index 5ba780f..42a6877 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
@@ -31,6 +31,9 @@
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor;
 import com.android.systemui.communal.domain.interactor.CommunalInteractor;
 import com.android.systemui.dreams.touch.dagger.CommunalTouchModule;
+import com.android.systemui.scene.domain.interactor.SceneInteractor;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
+import com.android.systemui.scene.ui.view.WindowRootView;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 
 import kotlinx.coroutines.Job;
@@ -42,6 +45,7 @@
 
 import javax.inject.Inject;
 import javax.inject.Named;
+import javax.inject.Provider;
 
 /** {@link TouchHandler} responsible for handling touches to open communal hub. **/
 public class CommunalTouchHandler implements TouchHandler {
@@ -51,6 +55,8 @@
     private final CommunalInteractor mCommunalInteractor;
 
     private final ConfigurationInteractor mConfigurationInteractor;
+    private final SceneInteractor mSceneInteractor;
+    private final WindowRootView mWindowRootView;
     private Boolean mIsEnabled = false;
 
     private ArrayList<Job> mFlows = new ArrayList<>();
@@ -69,12 +75,16 @@
             @Named(CommunalTouchModule.COMMUNAL_GESTURE_INITIATION_WIDTH) int initiationWidth,
             CommunalInteractor communalInteractor,
             ConfigurationInteractor configurationInteractor,
+            SceneInteractor sceneInteractor,
+            Optional<Provider<WindowRootView>> windowRootViewProvider,
             Lifecycle lifecycle) {
         mInitiationWidth = initiationWidth;
         mCentralSurfaces = centralSurfaces;
         mLifecycle = lifecycle;
         mCommunalInteractor = communalInteractor;
         mConfigurationInteractor = configurationInteractor;
+        mSceneInteractor = sceneInteractor;
+        mWindowRootView = windowRootViewProvider.get().get();
 
         mFlows.add(collectFlow(
                 mLifecycle,
@@ -125,8 +135,15 @@
     private void handleSessionStart(CentralSurfaces surfaces, TouchSession session) {
         // Notification shade window has its own logic to be visible if the hub is open, no need to
         // do anything here other than send touch events over.
+        if (SceneContainerFlag.isEnabled()) {
+            mSceneInteractor.onRemoteUserInputStarted("communal touch handler");
+        }
         session.registerInputListener(ev -> {
-            surfaces.handleCommunalHubTouch((MotionEvent) ev);
+            if (SceneContainerFlag.isEnabled()) {
+                mWindowRootView.dispatchTouchEvent((MotionEvent) ev);
+            } else {
+                surfaces.handleCommunalHubTouch((MotionEvent) ev);
+            }
             if (ev != null && ((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) {
                 var unused = session.pop();
             }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index d3bed27..602f593 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -116,6 +116,8 @@
     @Nullable
     private View mMediaViewPlaceHolderForScene;
 
+    private boolean mHadConfigurationChangeWhileDetached;
+
     public QSPanel(Context context, AttributeSet attrs) {
         super(context, attrs);
         mUsingMediaPlayer = useQsMediaPlayer(context);
@@ -425,10 +427,23 @@
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
+        if (!isAttachedToWindow()) {
+            mHadConfigurationChangeWhileDetached = true;
+        }
         mOnConfigurationChangedListeners.forEach(
                 listener -> listener.onConfigurationChange(newConfig));
     }
 
+    final boolean hadConfigurationChangeWhileDetached() {
+        return mHadConfigurationChangeWhileDetached;
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mHadConfigurationChangeWhileDetached = false;
+    }
+
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index 6cf5b32..85bcc25 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -234,6 +234,12 @@
             mMediaHost.addVisibilityChangeListener(mMediaHostVisibilityListener);
         }
         mView.addOnConfigurationChangedListener(mOnConfigurationChangedListener);
+        // We were not attached and the configuration may have changed, trigger the listener.
+        if (mView.hadConfigurationChangeWhileDetached()) {
+            mOnConfigurationChangedListener.onConfigurationChange(
+                    getContext().getResources().getConfiguration()
+            );
+        }
         setTiles();
         mLastOrientation = getResources().getConfiguration().orientation;
         mLastScreenLayout = getResources().getConfiguration().screenLayout;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsModule.kt
index be733d4..8ce0dbf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsModule.kt
@@ -20,7 +20,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogBufferFactory
-import com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel.DemoRonChipViewModel
+import com.android.systemui.statusbar.chips.notification.demo.ui.viewmodel.DemoNotifChipViewModel
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
@@ -31,8 +31,8 @@
 abstract class StatusBarChipsModule {
     @Binds
     @IntoMap
-    @ClassKey(DemoRonChipViewModel::class)
-    abstract fun binds(impl: DemoRonChipViewModel): CoreStartable
+    @ClassKey(DemoNotifChipViewModel::class)
+    abstract fun binds(impl: DemoNotifChipViewModel): CoreStartable
 
     companion object {
         @Provides
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/demo/ui/viewmodel/DemoNotifChipViewModel.kt
similarity index 82%
rename from packages/SystemUI/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModel.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/demo/ui/viewmodel/DemoNotifChipViewModel.kt
index cce9a16..5fa19dd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/demo/ui/viewmodel/DemoNotifChipViewModel.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel
+package com.android.systemui.statusbar.chips.notification.demo.ui.viewmodel
 
 import android.content.pm.PackageManager
 import android.content.pm.PackageManager.NameNotFoundException
@@ -22,7 +22,7 @@
 import com.android.systemui.CoreStartable
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.chips.ron.shared.StatusBarRonChips
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
 import com.android.systemui.statusbar.chips.ui.model.ColorsModel
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel
@@ -37,25 +37,25 @@
 import kotlinx.coroutines.flow.asStateFlow
 
 /**
- * A view model that will emit demo RON chips (rich ongoing notification chips) from [chip] based on
- * adb commands sent by the user.
+ * A view model that will emit demo promoted ongoing notification chips from [chip] based on adb
+ * commands sent by the user.
  *
  * Example adb commands:
  *
  * To show a chip with the SysUI icon and custom text and color:
  * ```
- * adb shell cmd statusbar demo-ron -p com.android.systemui -t 10min -c "\\#434343"
+ * adb shell cmd statusbar demo-notif -p com.android.systemui -t 10min -c "\\#434343"
  * ```
  *
  * To hide the chip:
  * ```
- * adb shell cmd statusbar demo-ron --hide
+ * adb shell cmd statusbar demo-notif --hide
  * ```
  *
- * See [DemoRonCommand] for more information on the adb command spec.
+ * See [DemoNotifCommand] for more information on the adb command spec.
  */
 @SysUISingleton
-class DemoRonChipViewModel
+class DemoNotifChipViewModel
 @Inject
 constructor(
     private val commandRegistry: CommandRegistry,
@@ -63,19 +63,19 @@
     private val systemClock: SystemClock,
 ) : OngoingActivityChipViewModel, CoreStartable {
     override fun start() {
-        commandRegistry.registerCommand("demo-ron") { DemoRonCommand() }
+        commandRegistry.registerCommand(DEMO_COMMAND_NAME) { DemoNotifCommand() }
     }
 
     private val _chip =
         MutableStateFlow<OngoingActivityChipModel>(OngoingActivityChipModel.Hidden())
     override val chip: StateFlow<OngoingActivityChipModel> = _chip.asStateFlow()
 
-    private inner class DemoRonCommand : ParseableCommand("demo-ron") {
+    private inner class DemoNotifCommand : ParseableCommand(DEMO_COMMAND_NAME) {
         private val packageName: String? by
             param(
                 longName = "packageName",
                 shortName = "p",
-                description = "The package name for the demo RON app",
+                description = "The package name for app \"posting\" the demo notification",
                 valueParser = Type.String,
             )
 
@@ -99,15 +99,12 @@
             )
 
         private val hide by
-            flag(
-                longName = "hide",
-                description = "Hides any existing demo RON chip",
-            )
+            flag(longName = "hide", description = "Hides any existing demo notification chip")
 
         override fun execute(pw: PrintWriter) {
-            if (!StatusBarRonChips.isEnabled) {
+            if (!StatusBarNotifChips.isEnabled) {
                 pw.println(
-                    "Error: com.android.systemui.status_bar_ron_chips must be enabled " +
+                    "Error: com.android.systemui.status_bar_notification_chips must be enabled " +
                         "before using this demo feature"
                 )
                 return
@@ -167,8 +164,12 @@
                 return null
             }
             return OngoingActivityChipModel.ChipIcon.FullColorAppIcon(
-                Icon.Loaded(drawable = iconDrawable, contentDescription = null),
+                Icon.Loaded(drawable = iconDrawable, contentDescription = null)
             )
         }
     }
+
+    companion object {
+        private const val DEMO_COMMAND_NAME = "demo-notif"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ron/shared/StatusBarRonChips.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/shared/StatusBarNotifChips.kt
similarity index 87%
rename from packages/SystemUI/src/com/android/systemui/statusbar/chips/ron/shared/StatusBarRonChips.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/shared/StatusBarNotifChips.kt
index 4ef1909..47ffbaf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ron/shared/StatusBarRonChips.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/shared/StatusBarNotifChips.kt
@@ -14,17 +14,17 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.chips.ron.shared
+package com.android.systemui.statusbar.chips.notification.shared
 
 import com.android.systemui.Flags
 import com.android.systemui.flags.FlagToken
 import com.android.systemui.flags.RefactorFlagUtils
 
-/** Helper for reading or using the status bar RON chips flag state. */
+/** Helper for reading or using the status bar promoted notification chips flag state. */
 @Suppress("NOTHING_TO_INLINE")
-object StatusBarRonChips {
+object StatusBarNotifChips {
     /** The aconfig flag name */
-    const val FLAG_NAME = Flags.FLAG_STATUS_BAR_RON_CHIPS
+    const val FLAG_NAME = Flags.FLAG_STATUS_BAR_NOTIFICATION_CHIPS
 
     /** A token used for dependency declaration */
     val token: FlagToken
@@ -33,7 +33,7 @@
     /** Is the refactor enabled */
     @JvmStatic
     inline val isEnabled
-        get() = Flags.statusBarRonChips()
+        get() = Flags.statusBarNotificationChips()
 
     /**
      * Called to ensure code is only run when the flag is enabled. This protects users from the
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
index 2220caab..f4462a4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
@@ -28,7 +28,7 @@
 import com.android.systemui.common.ui.binder.IconViewBinder
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.chips.ron.shared.StatusBarRonChips
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
 import com.android.systemui.statusbar.chips.ui.view.ChipChronometer
@@ -102,7 +102,7 @@
                 defaultIconView.tintView(iconTint)
             }
             is OngoingActivityChipModel.ChipIcon.FullColorAppIcon -> {
-                StatusBarRonChips.assertInNewMode()
+                StatusBarNotifChips.assertInNewMode()
                 IconViewBinder.bind(icon.impl, defaultIconView)
                 defaultIconView.visibility = View.VISIBLE
                 defaultIconView.untintView()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
index 2366572..cf07af1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
@@ -20,7 +20,7 @@
 import com.android.systemui.Flags
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.chips.ron.shared.StatusBarRonChips
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
 
 /** Model representing the display of an ongoing activity as a chip in the status bar. */
 sealed class OngoingActivityChipModel {
@@ -91,10 +91,7 @@
             override val onClickListener: View.OnClickListener?,
         ) : Shown(icon, colors, onClickListener) {
             init {
-                check(StatusBarRonChips.isEnabled) {
-                    "OngoingActivityChipModel.Shown.ShortTimeDelta created even though " +
-                        "Flags.statusBarRonChips is not enabled"
-                }
+                StatusBarNotifChips.assertInNewMode()
             }
 
             override val logName = "Shown.ShortTimeDelta"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
index 954386e..ed32597 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.statusbar.chips.ui.viewmodel
 
-import com.android.systemui.Flags
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.log.LogBuffer
@@ -24,9 +23,9 @@
 import com.android.systemui.statusbar.chips.StatusBarChipsLog
 import com.android.systemui.statusbar.chips.call.ui.viewmodel.CallChipViewModel
 import com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel.CastToOtherDeviceChipViewModel
+import com.android.systemui.statusbar.chips.notification.demo.ui.viewmodel.DemoNotifChipViewModel
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
 import com.android.systemui.statusbar.chips.notification.ui.viewmodel.NotifChipsViewModel
-import com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel.DemoRonChipViewModel
-import com.android.systemui.statusbar.chips.ron.shared.StatusBarRonChips
 import com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.ScreenRecordChipViewModel
 import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.ShareToAppChipViewModel
 import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel
@@ -57,7 +56,7 @@
     castToOtherDeviceChipViewModel: CastToOtherDeviceChipViewModel,
     callChipViewModel: CallChipViewModel,
     notifChipsViewModel: NotifChipsViewModel,
-    demoRonChipViewModel: DemoRonChipViewModel,
+    demoNotifChipViewModel: DemoNotifChipViewModel,
     @StatusBarChipsLog private val logger: LogBuffer,
 ) {
     private enum class ChipType {
@@ -66,8 +65,8 @@
         CastToOtherDevice,
         Call,
         Notification,
-        /** A demo of a RON chip (rich ongoing notification chip), used just for testing. */
-        DemoRon,
+        /** A demo of a notification chip, used just for testing. */
+        DemoNotification,
     }
 
     /** Model that helps us internally track the various chip states from each of the types. */
@@ -89,7 +88,7 @@
             val castToOtherDevice: OngoingActivityChipModel.Hidden,
             val call: OngoingActivityChipModel.Hidden,
             val notifs: OngoingActivityChipModel.Hidden,
-            val demoRon: OngoingActivityChipModel.Hidden,
+            val demoNotif: OngoingActivityChipModel.Hidden,
         ) : InternalChipModel
     }
 
@@ -99,7 +98,7 @@
         val castToOtherDevice: OngoingActivityChipModel = OngoingActivityChipModel.Hidden(),
         val call: OngoingActivityChipModel = OngoingActivityChipModel.Hidden(),
         val notifs: List<OngoingActivityChipModel.Shown> = emptyList(),
-        val demoRon: OngoingActivityChipModel = OngoingActivityChipModel.Hidden(),
+        val demoNotif: OngoingActivityChipModel = OngoingActivityChipModel.Hidden(),
     )
 
     /** Bundles all the incoming chips into one object to easily pass to various flows. */
@@ -110,8 +109,8 @@
                 castToOtherDeviceChipViewModel.chip,
                 callChipViewModel.chip,
                 notifChipsViewModel.chips,
-                demoRonChipViewModel.chip,
-            ) { screenRecord, shareToApp, castToOtherDevice, call, notifs, demoRon ->
+                demoNotifChipViewModel.chip,
+            ) { screenRecord, shareToApp, castToOtherDevice, call, notifs, demoNotif ->
                 logger.log(
                     TAG,
                     LogLevel.INFO,
@@ -129,9 +128,9 @@
                         str1 = call.logName
                         // TODO(b/364653005): Log other information for notification chips.
                         str2 = notifs.map { it.logName }.toString()
-                        str3 = demoRon.logName
+                        str3 = demoNotif.logName
                     },
-                    { "... > Call=$str1 > Notifs=$str2 > DemoRon=$str3" },
+                    { "... > Call=$str1 > Notifs=$str2 > DemoNotif=$str3" },
                 )
                 ChipBundle(
                     screenRecord = screenRecord,
@@ -139,7 +138,7 @@
                     castToOtherDevice = castToOtherDevice,
                     call = call,
                     notifs = notifs,
-                    demoRon = demoRon,
+                    demoNotif = demoNotif,
                 )
             }
             // Some of the chips could have timers in them and we don't want the start time
@@ -198,9 +197,9 @@
      * actually displaying the chip.
      */
     val chips: StateFlow<MultipleOngoingActivityChipsModel> =
-        if (!Flags.statusBarRonChips()) {
-            // Multiple chips are only allowed with RONs. If the flag isn't on, use just the
-            // primary chip.
+        if (!StatusBarNotifChips.isEnabled) {
+            // Multiple chips are only allowed with notification chips. If the flag isn't on, use
+            // just the primary chip.
             primaryChip
                 .map {
                     MultipleOngoingActivityChipsModel(
@@ -282,11 +281,12 @@
                     remainingChips =
                         bundle.copy(notifs = bundle.notifs.subList(1, bundle.notifs.size)),
                 )
-            bundle.demoRon is OngoingActivityChipModel.Shown -> {
-                StatusBarRonChips.assertInNewMode()
+            bundle.demoNotif is OngoingActivityChipModel.Shown -> {
+                StatusBarNotifChips.assertInNewMode()
                 MostImportantChipResult(
-                    mostImportantChip = InternalChipModel.Shown(ChipType.DemoRon, bundle.demoRon),
-                    remainingChips = bundle.copy(demoRon = OngoingActivityChipModel.Hidden()),
+                    mostImportantChip =
+                        InternalChipModel.Shown(ChipType.DemoNotification, bundle.demoNotif),
+                    remainingChips = bundle.copy(demoNotif = OngoingActivityChipModel.Hidden()),
                 )
             }
             else -> {
@@ -296,7 +296,7 @@
                 check(bundle.castToOtherDevice is OngoingActivityChipModel.Hidden)
                 check(bundle.call is OngoingActivityChipModel.Hidden)
                 check(bundle.notifs.isEmpty())
-                check(bundle.demoRon is OngoingActivityChipModel.Hidden)
+                check(bundle.demoNotif is OngoingActivityChipModel.Hidden)
                 MostImportantChipResult(
                     mostImportantChip =
                         InternalChipModel.Hidden(
@@ -305,7 +305,7 @@
                             castToOtherDevice = bundle.castToOtherDevice,
                             call = bundle.call,
                             notifs = OngoingActivityChipModel.Hidden(),
-                            demoRon = bundle.demoRon,
+                            demoNotif = bundle.demoNotif,
                         ),
                     // All the chips are already hidden, so no need to filter anything out of the
                     // bundle.
@@ -334,7 +334,7 @@
                 ChipType.CastToOtherDevice -> new.castToOtherDevice
                 ChipType.Call -> new.call
                 ChipType.Notification -> new.notifs
-                ChipType.DemoRon -> new.demoRon
+                ChipType.DemoNotification -> new.demoNotif
             }
         } else if (new is InternalChipModel.Shown) {
             // If we have a chip to show, always show it.
@@ -356,7 +356,7 @@
                 castToOtherDevice = OngoingActivityChipModel.Hidden(),
                 call = OngoingActivityChipModel.Hidden(),
                 notifs = OngoingActivityChipModel.Hidden(),
-                demoRon = OngoingActivityChipModel.Hidden(),
+                demoNotif = OngoingActivityChipModel.Hidden(),
             )
 
         private val DEFAULT_MULTIPLE_INTERNAL_HIDDEN_MODEL =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesModule.java
index 525f3de..72cd63f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesModule.java
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.dagger;
 
 import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.emergency.EmergencyGestureModule;
 import com.android.systemui.statusbar.notification.dagger.NotificationsModule;
 import com.android.systemui.statusbar.notification.row.NotificationRowModule;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -32,7 +31,7 @@
  */
 @Module(includes = {CentralSurfacesDependenciesModule.class,
         StatusBarNotificationPresenterModule.class,
-        NotificationsModule.class, NotificationRowModule.class, EmergencyGestureModule.class})
+        NotificationsModule.class, NotificationRowModule.class})
 public interface CentralSurfacesModule {
     /**
      * Provides our instance of CentralSurfaces which is considered optional.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
index 5900fb0..697a6ce 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
@@ -17,7 +17,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.statusbar.chips.ron.shared.StatusBarRonChips
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
 import com.android.systemui.statusbar.notification.collection.render.NotifStats
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel
@@ -78,7 +78,7 @@
 
     /** The notifications that are promoted and ongoing. Sorted by priority order. */
     val promotedOngoingNotifications: Flow<List<ActiveNotificationModel>> =
-        if (StatusBarRonChips.isEnabled) {
+        if (StatusBarNotifChips.isEnabled) {
             // TODO(b/364653005): Filter all the notifications down to just the promoted ones.
             // TODO(b/364653005): [ongoingCallNotification] should be incorporated into this flow
             // instead of being separate.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
index e5ce25d..bf30322 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
@@ -309,7 +309,20 @@
         }
     }
 
-    /** Set onClickListener for the manage/history button. */
+    /** Set onClickListener for the notification settings button. */
+    public void setSettingsButtonClickListener(OnClickListener listener) {
+        mSettingsButton.setOnClickListener(listener);
+    }
+
+    /** Set onClickListener for the notification history button. */
+    public void setHistoryButtonClickListener(OnClickListener listener) {
+        mHistoryButton.setOnClickListener(listener);
+    }
+
+    /**
+     * Set onClickListener for the manage/history button. This is replaced by two separate buttons
+     * in the redesign.
+     */
     public void setManageButtonClickListener(OnClickListener listener) {
         mManageOrHistoryButton.setOnClickListener(listener);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
index ddd9cdd..34894a2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
@@ -18,9 +18,12 @@
 
 import android.view.View
 import androidx.lifecycle.lifecycleScope
+import com.android.app.tracing.coroutines.launchTraced as launch
+import com.android.internal.jank.InteractionJankMonitor
 import com.android.systemui.Flags
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.statusbar.notification.NotificationActivityStarter
+import com.android.systemui.statusbar.notification.NotificationActivityStarter.SettingsIntent
 import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
 import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
@@ -29,7 +32,6 @@
 import com.android.systemui.util.ui.value
 import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.coroutineScope
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 /** Binds a [FooterView] to its [view model][FooterViewModel]. */
 object FooterViewBinder {
@@ -74,6 +76,9 @@
                     notificationActivityStarter,
                 )
             }
+        } else {
+            bindSettingsButtonListener(footer, notificationActivityStarter)
+            bindHistoryButtonListener(footer, notificationActivityStarter)
         }
         launch { bindMessage(footer, viewModel) }
     }
@@ -117,6 +122,34 @@
         }
     }
 
+    private fun bindSettingsButtonListener(
+        footer: FooterView,
+        notificationActivityStarter: NotificationActivityStarter,
+    ) {
+        val settingsIntent =
+            SettingsIntent.forNotificationSettings(
+                cujType = InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_HISTORY_BUTTON
+            )
+        val onClickListener = { view: View ->
+            notificationActivityStarter.startSettingsIntent(view, settingsIntent)
+        }
+        footer.setSettingsButtonClickListener(onClickListener)
+    }
+
+    private fun bindHistoryButtonListener(
+        footer: FooterView,
+        notificationActivityStarter: NotificationActivityStarter,
+    ) {
+        val settingsIntent =
+            SettingsIntent.forNotificationHistory(
+                cujType = InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_HISTORY_BUTTON
+            )
+        val onClickListener = { view: View ->
+            notificationActivityStarter.startSettingsIntent(view, settingsIntent)
+        }
+        footer.setHistoryButtonClickListener(onClickListener)
+    }
+
     private suspend fun bindManageOrHistoryButton(
         footer: FooterView,
         viewModel: FooterViewModel,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
index a3f4cd2..d8021fa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
@@ -98,7 +98,6 @@
     val manageButtonShouldLaunchHistory =
         notificationSettingsInteractor.isNotificationHistoryEnabled
 
-    // TODO(b/366003631): When inlining the flag, consider adding this to FooterButtonViewModel.
     val manageOrHistoryButtonClick: Flow<SettingsIntent> by lazy {
         if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode()) {
             flowOf(SettingsIntent(Intent(Settings.ACTION_NOTIFICATION_SETTINGS)))
@@ -124,7 +123,11 @@
             else R.string.manage_notifications_text
         }
 
-    /** The button for managing notification settings or opening notification history. */
+    /**
+     * The button for managing notification settings or opening notification history. This is
+     * replaced by two separate buttons in the redesign. These are currently static, and therefore
+     * not modeled here, but if that changes we can also add them as FooterButtonViewModels.
+     */
     val manageOrHistoryButton: FooterButtonViewModel =
         FooterButtonViewModel(
             labelId = manageOrHistoryButtonText,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index dde83b9..c1d72e4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -6953,12 +6953,10 @@
 
     /** Use {@link ScrollViewFields#intrinsicStackHeight}, when SceneContainerFlag is enabled. */
     private int getContentHeight() {
-        SceneContainerFlag.assertInLegacyMode();
         return mContentHeight;
     }
 
     private void setContentHeight(int contentHeight) {
-        SceneContainerFlag.assertInLegacyMode();
         mContentHeight = contentHeight;
     }
 
@@ -6967,12 +6965,10 @@
      * @return the height of the content ignoring the footer.
      */
     public float getIntrinsicContentHeight() {
-        SceneContainerFlag.assertInLegacyMode();
         return mIntrinsicContentHeight;
     }
 
     private void setIntrinsicContentHeight(float intrinsicContentHeight) {
-        SceneContainerFlag.assertInLegacyMode();
         mIntrinsicContentHeight = intrinsicContentHeight;
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index f85785f..013141b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -56,7 +56,7 @@
 import com.android.systemui.statusbar.OperatorNameView;
 import com.android.systemui.statusbar.OperatorNameViewController;
 import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.chips.ron.shared.StatusBarRonChips;
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips;
 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays;
 import com.android.systemui.statusbar.core.StatusBarSimpleFragment;
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger;
@@ -657,7 +657,7 @@
         }
         boolean showSecondaryOngoingActivityChip =
                 Flags.statusBarScreenSharingChips()
-                        && StatusBarRonChips.isEnabled()
+                        && StatusBarNotifChips.isEnabled()
                         && mHasSecondaryOngoingActivity;
 
         return new StatusBarVisibilityModel(
@@ -699,7 +699,7 @@
 
         boolean showSecondaryOngoingActivityChip =
                 // Secondary chips are only supported when RONs are enabled.
-                StatusBarRonChips.isEnabled()
+                StatusBarNotifChips.isEnabled()
                         && visibilityModel.getShowSecondaryOngoingActivityChip()
                         && !disableNotifications;
         if (showSecondaryOngoingActivityChip) {
@@ -832,7 +832,7 @@
     }
 
     private void showSecondaryOngoingActivityChip(boolean animate) {
-        StatusBarRonChips.assertInNewMode();
+        StatusBarNotifChips.assertInNewMode();
         StatusBarSimpleFragment.assertInLegacyMode();
         animateShow(mSecondaryOngoingActivityChip, animate);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
index 473f956..11d7339 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
 import com.android.systemui.statusbar.chips.ui.binder.OngoingActivityChipBinder
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import com.android.systemui.statusbar.core.StatusBarSimpleFragment
@@ -84,7 +85,7 @@
                     }
                 }
 
-                if (Flags.statusBarScreenSharingChips() && !Flags.statusBarRonChips()) {
+                if (Flags.statusBarScreenSharingChips() && !StatusBarNotifChips.isEnabled) {
                     val primaryChipView: View =
                         view.requireViewById(R.id.ongoing_activity_chip_primary)
                     launch {
@@ -120,7 +121,7 @@
                     }
                 }
 
-                if (Flags.statusBarScreenSharingChips() && Flags.statusBarRonChips()) {
+                if (Flags.statusBarScreenSharingChips() && StatusBarNotifChips.isEnabled) {
                     val primaryChipView: View =
                         view.requireViewById(R.id.ongoing_activity_chip_primary)
                     val secondaryChipView: View =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
index 8aaa121..3c922dc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
@@ -13,6 +13,7 @@
  */
 package com.android.systemui.qs
 
+import android.content.res.Configuration
 import android.graphics.Rect
 import android.platform.test.flag.junit.FlagsParameterization
 import android.testing.TestableContext
@@ -91,7 +92,9 @@
 
     @After
     fun tearDown() {
-        ViewUtils.detachView(qsPanel)
+        if (qsPanel.isAttachedToWindow) {
+            ViewUtils.detachView(qsPanel)
+        }
     }
 
     @Test
@@ -119,7 +122,7 @@
         qsPanel.tileLayout?.addTile(
             QSPanelControllerBase.TileRecord(
                 mock(QSTile::class.java),
-                QSTileViewImpl(themedContext)
+                QSTileViewImpl(themedContext),
             )
         )
 
@@ -129,7 +132,7 @@
         qsPanel.setUsingHorizontalLayout(/* horizontal */ true, mediaView, /* force */ true)
         qsPanel.measure(
             /* width */ View.MeasureSpec.makeMeasureSpec(3000, View.MeasureSpec.EXACTLY),
-            /* height */ View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY)
+            /* height */ View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY),
         )
         qsPanel.layout(0, 0, qsPanel.measuredWidth, qsPanel.measuredHeight)
 
@@ -147,7 +150,7 @@
         val padding = 10
         themedContext.orCreateTestableResources.addOverride(
             R.dimen.qs_panel_padding_bottom,
-            padding
+            padding,
         )
         qsPanel.updatePadding()
         assertThat(qsPanel.paddingBottom).isEqualTo(padding)
@@ -160,7 +163,7 @@
         themedContext.orCreateTestableResources.addOverride(R.dimen.qs_panel_padding_top, padding)
         themedContext.orCreateTestableResources.addOverride(
             R.dimen.qs_panel_padding_top,
-            paddingCombined
+            paddingCombined,
         )
 
         qsPanel.updatePadding()
@@ -267,6 +270,62 @@
         assertThat(qsPanel.tileLayout!!.maxColumns).isEqualTo(2)
     }
 
+    @Test
+    fun noPendingConfigChangesAtBeginning() {
+        assertThat(qsPanel.hadConfigurationChangeWhileDetached()).isFalse()
+    }
+
+    @Test
+    fun configChangesWhileDetached_pendingConfigChanges() {
+        ViewUtils.detachView(qsPanel)
+
+        qsPanel.onConfigurationChanged(Configuration())
+
+        assertThat(qsPanel.hadConfigurationChangeWhileDetached()).isTrue()
+    }
+
+    @Test
+    fun configChangesWhileDetached_reattach_pendingConfigChanges() {
+        ViewUtils.detachView(qsPanel)
+
+        qsPanel.onConfigurationChanged(Configuration())
+        testableLooper.runWithLooper { ViewUtils.attachView(qsPanel) }
+
+        assertThat(qsPanel.hadConfigurationChangeWhileDetached()).isTrue()
+    }
+
+    @Test
+    fun configChangesWhileDetached_reattach_detach_pendingConfigChanges_reset() {
+        ViewUtils.detachView(qsPanel)
+
+        qsPanel.onConfigurationChanged(Configuration())
+
+        testableLooper.runWithLooper { ViewUtils.attachView(qsPanel) }
+        ViewUtils.detachView(qsPanel)
+
+        assertThat(qsPanel.hadConfigurationChangeWhileDetached()).isFalse()
+    }
+
+    @Test
+    fun configChangeWhileAttached_noPendingConfigChanges() {
+        qsPanel.onConfigurationChanged(Configuration())
+
+        assertThat(qsPanel.hadConfigurationChangeWhileDetached()).isFalse()
+    }
+
+    @Test
+    fun configChangeWhileAttachedWithPending_doesntResetPending() {
+        ViewUtils.detachView(qsPanel)
+
+        qsPanel.onConfigurationChanged(Configuration())
+
+        testableLooper.runWithLooper { ViewUtils.attachView(qsPanel) }
+
+        qsPanel.onConfigurationChanged(Configuration())
+
+        assertThat(qsPanel.hadConfigurationChangeWhileDetached()).isTrue()
+    }
+
     companion object {
         @Parameters(name = "{0}") @JvmStatic fun getParams() = parameterizeSceneContainerFlag()
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index e26a7ea..d01c1ca 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -16,7 +16,6 @@
 
 import static android.view.Display.DEFAULT_DISPLAY;
 
-import static com.android.systemui.Flags.FLAG_STATUS_BAR_RON_CHIPS;
 import static com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS;
 import static com.android.systemui.Flags.FLAG_STATUS_BAR_SIMPLE_FRAGMENT;
 import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED;
@@ -61,6 +60,7 @@
 import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.OperatorNameViewController;
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips;
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder;
@@ -633,7 +633,7 @@
     @Test
     @EnableFlags({
             FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS,
-            FLAG_STATUS_BAR_RON_CHIPS,
+            StatusBarNotifChips.FLAG_NAME,
             FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
     public void hasPrimaryOngoingActivity_viewsUnchangedWhenSimpleFragmentFlagOn() {
         resumeAndGetFragment();
@@ -660,8 +660,8 @@
 
     @Test
     @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    @DisableFlags({FLAG_STATUS_BAR_RON_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
-    public void hasSecondaryOngoingActivity_butRonsFlagOff_secondaryChipHidden() {
+    @DisableFlags({StatusBarNotifChips.FLAG_NAME, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+    public void hasSecondaryOngoingActivity_butNotifsFlagOff_secondaryChipHidden() {
         resumeAndGetFragment();
 
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
@@ -673,7 +673,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_RON_CHIPS})
+    @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
     @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
     public void hasSecondaryOngoingActivity_flagOn_secondaryChipShownAndNotificationIconsHidden() {
         resumeAndGetFragment();
@@ -689,8 +689,8 @@
 
     @Test
     @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    @DisableFlags({FLAG_STATUS_BAR_RON_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
-    public void hasOngoingActivityButNotificationIconsDisabled_chipHidden_ronsFlagOff() {
+    @DisableFlags({StatusBarNotifChips.FLAG_NAME, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+    public void hasOngoingActivityButNotificationIconsDisabled_chipHidden_notifsFlagOff() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
@@ -705,9 +705,9 @@
     }
 
     @Test
-    @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_RON_CHIPS})
+    @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
     @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
-    public void hasOngoingActivitiesButNotificationIconsDisabled_chipsHidden_ronsFlagOn() {
+    public void hasOngoingActivitiesButNotificationIconsDisabled_chipsHidden_notifsFlagOn() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
@@ -724,8 +724,8 @@
 
     @Test
     @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    @DisableFlags({FLAG_STATUS_BAR_RON_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
-    public void hasOngoingActivityButAlsoHun_chipHidden_ronsFlagOff() {
+    @DisableFlags({StatusBarNotifChips.FLAG_NAME, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+    public void hasOngoingActivityButAlsoHun_chipHidden_notifsFlagOff() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
@@ -740,9 +740,9 @@
     }
 
     @Test
-    @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_RON_CHIPS})
+    @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
     @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
-    public void hasOngoingActivitiesButAlsoHun_chipsHidden_ronsFlagOn() {
+    public void hasOngoingActivitiesButAlsoHun_chipsHidden_notifsFlagOn() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
@@ -759,8 +759,8 @@
 
     @Test
     @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    @DisableFlags({FLAG_STATUS_BAR_RON_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
-    public void primaryOngoingActivityEnded_chipHidden_ronsFlagOff() {
+    @DisableFlags({StatusBarNotifChips.FLAG_NAME, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+    public void primaryOngoingActivityEnded_chipHidden_notifsFlagOff() {
         resumeAndGetFragment();
 
         // Ongoing activity started
@@ -781,9 +781,9 @@
     }
 
     @Test
-    @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_RON_CHIPS})
+    @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
     @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
-    public void primaryOngoingActivityEnded_chipHidden_ronsFlagOn() {
+    public void primaryOngoingActivityEnded_chipHidden_notifsFlagOn() {
         resumeAndGetFragment();
 
         // Ongoing activity started
@@ -804,7 +804,7 @@
     }
 
     @Test
-    @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_RON_CHIPS})
+    @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
     @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
     public void secondaryOngoingActivityEnded_chipHidden() {
         resumeAndGetFragment();
@@ -828,8 +828,8 @@
 
     @Test
     @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    @DisableFlags({FLAG_STATUS_BAR_RON_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
-    public void hasOngoingActivity_hidesNotifsWithoutAnimation_ronsFlagOff() {
+    @DisableFlags({StatusBarNotifChips.FLAG_NAME, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+    public void hasOngoingActivity_hidesNotifsWithoutAnimation_notifsFlagOff() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
         // Enable animations for testing so that we can verify we still aren't animating
         fragment.enableAnimationsForTesting();
@@ -846,9 +846,9 @@
     }
 
     @Test
-    @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_RON_CHIPS})
+    @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
     @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
-    public void hasOngoingActivity_hidesNotifsWithoutAnimation_ronsFlagOn() {
+    public void hasOngoingActivity_hidesNotifsWithoutAnimation_notifsFlagOn() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
         // Enable animations for testing so that we can verify we still aren't animating
         fragment.enableAnimationsForTesting();
@@ -866,8 +866,8 @@
 
     @Test
     @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    @DisableFlags({FLAG_STATUS_BAR_RON_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
-    public void screenSharingChipsEnabled_ignoresOngoingCallController_ronsFlagOff() {
+    @DisableFlags({StatusBarNotifChips.FLAG_NAME, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+    public void screenSharingChipsEnabled_ignoresOngoingCallController_notifsFlagOff() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
         // WHEN there *is* an ongoing call via old callback
@@ -898,9 +898,9 @@
     }
 
     @Test
-    @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_RON_CHIPS})
+    @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
     @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
-    public void screenSharingChipsEnabled_ignoresOngoingCallController_ronsFlagOn() {
+    public void screenSharingChipsEnabled_ignoresOngoingCallController_notifsFlagOn() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
         // WHEN there *is* an ongoing call via old callback
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/demo/ui/viewmodel/DemoNotifChipViewModelKosmos.kt
similarity index 85%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/demo/ui/viewmodel/DemoNotifChipViewModelKosmos.kt
index c0d65a0..2316a2f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/demo/ui/viewmodel/DemoNotifChipViewModelKosmos.kt
@@ -14,16 +14,16 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel
+package com.android.systemui.statusbar.chips.notification.demo.ui.viewmodel
 
 import android.content.packageManager
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.statusbar.commandline.commandRegistry
 import com.android.systemui.util.time.fakeSystemClock
 
-val Kosmos.demoRonChipViewModel: DemoRonChipViewModel by
+val Kosmos.demoNotifChipViewModel: DemoNotifChipViewModel by
     Kosmos.Fixture {
-        DemoRonChipViewModel(
+        DemoNotifChipViewModel(
             commandRegistry = commandRegistry,
             packageManager = packageManager,
             systemClock = fakeSystemClock,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ron/ui/viewmodel/NotifChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt
similarity index 85%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ron/ui/viewmodel/NotifChipsViewModelKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt
index 11d1cb7..af24c37 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ron/ui/viewmodel/NotifChipsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt
@@ -14,10 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.chips.ron.ui.viewmodel
+package com.android.systemui.statusbar.chips.notification.ui.viewmodel
 
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.statusbar.chips.notification.ui.viewmodel.NotifChipsViewModel
 import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
 
 val Kosmos.notifChipsViewModel: NotifChipsViewModel by
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt
index b2be0b2..0300bf4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt
@@ -20,8 +20,8 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.statusbar.chips.call.ui.viewmodel.callChipViewModel
 import com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel.castToOtherDeviceChipViewModel
-import com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel.demoRonChipViewModel
-import com.android.systemui.statusbar.chips.ron.ui.viewmodel.notifChipsViewModel
+import com.android.systemui.statusbar.chips.notification.demo.ui.viewmodel.demoNotifChipViewModel
+import com.android.systemui.statusbar.chips.notification.ui.viewmodel.notifChipsViewModel
 import com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.screenRecordChipViewModel
 import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.shareToAppChipViewModel
 import com.android.systemui.statusbar.chips.statusBarChipsLogger
@@ -35,7 +35,7 @@
             castToOtherDeviceChipViewModel = castToOtherDeviceChipViewModel,
             callChipViewModel = callChipViewModel,
             notifChipsViewModel = notifChipsViewModel,
-            demoRonChipViewModel = demoRonChipViewModel,
+            demoNotifChipViewModel = demoNotifChipViewModel,
             logger = statusBarChipsLogger,
         )
     }
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 51034d2..384cf46 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -31,16 +31,13 @@
 
 import static com.android.internal.util.CollectionUtils.any;
 import static com.android.internal.util.Preconditions.checkState;
-import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
 import static com.android.server.companion.utils.PackageUtils.enforceUsesCompanionDeviceFeature;
-import static com.android.server.companion.utils.PackageUtils.getPackageInfo;
 import static com.android.server.companion.utils.PackageUtils.isRestrictedSettingsAllowed;
 import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanManageAssociationsForPackage;
 import static com.android.server.companion.utils.PermissionsUtils.enforceCallerIsSystemOr;
 import static com.android.server.companion.utils.PermissionsUtils.enforceCallerIsSystemOrCanInteractWithUserId;
 
 import static java.util.Objects.requireNonNull;
-import static java.util.concurrent.TimeUnit.MINUTES;
 
 import android.annotation.EnforcePermission;
 import android.annotation.NonNull;
@@ -69,31 +66,22 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.net.MacAddress;
-import android.net.NetworkPolicyManager;
 import android.os.Binder;
-import android.os.Environment;
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
 import android.os.PowerExemptionManager;
 import android.os.PowerManagerInternal;
 import android.os.RemoteException;
-import android.os.ServiceManager;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.permission.flags.Flags;
-import android.util.ArraySet;
 import android.util.ExceptionUtils;
 import android.util.Slog;
 
-import com.android.internal.app.IAppOpsService;
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.notification.NotificationAccessConfirmationActivityContract;
-import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
 import com.android.server.FgThread;
@@ -114,35 +102,27 @@
 import com.android.server.companion.devicepresence.ObservableUuid;
 import com.android.server.companion.devicepresence.ObservableUuidStore;
 import com.android.server.companion.transport.CompanionTransportManager;
-import com.android.server.pm.UserManagerInternal;
 import com.android.server.wm.ActivityTaskManagerInternal;
 
-import java.io.File;
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.Collection;
 import java.util.List;
-import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 
 @SuppressLint("LongLogTag")
 public class CompanionDeviceManagerService extends SystemService {
     private static final String TAG = "CDM_CompanionDeviceManagerService";
 
     private static final long PAIR_WITHOUT_PROMPT_WINDOW_MS = 10 * 60 * 1000; // 10 min
-
-    private static final String PREF_FILE_NAME = "companion_device_preferences.xml";
-    private static final String PREF_KEY_AUTO_REVOKE_GRANTS_DONE = "auto_revoke_grants_done";
     private static final int MAX_CN_LENGTH = 500;
 
-    private final ActivityTaskManagerInternal mAtmInternal;
-    private final ActivityManagerInternal mAmInternal;
-    private final IAppOpsService mAppOpsManager;
-    private final PowerExemptionManager mPowerExemptionManager;
-    private final PackageManagerInternal mPackageManagerInternal;
-
     private final AssociationStore mAssociationStore;
     private final SystemDataTransferRequestStore mSystemDataTransferRequestStore;
     private final ObservableUuidStore mObservableUuidStore;
+
+    private final CompanionExemptionProcessor mCompanionExemptionProcessor;
     private final AssociationRequestsProcessor mAssociationRequestsProcessor;
     private final SystemDataTransferProcessor mSystemDataTransferProcessor;
     private final BackupRestoreProcessor mBackupRestoreProcessor;
@@ -156,12 +136,15 @@
         super(context);
 
         final ActivityManager activityManager = context.getSystemService(ActivityManager.class);
-        mPowerExemptionManager = context.getSystemService(PowerExemptionManager.class);
-        mAppOpsManager = IAppOpsService.Stub.asInterface(
-                ServiceManager.getService(Context.APP_OPS_SERVICE));
-        mAtmInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
-        mAmInternal = LocalServices.getService(ActivityManagerInternal.class);
-        mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
+        final PowerExemptionManager powerExemptionManager = context.getSystemService(
+                PowerExemptionManager.class);
+        final AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);
+        final ActivityTaskManagerInternal atmInternal = LocalServices.getService(
+                ActivityTaskManagerInternal.class);
+        final ActivityManagerInternal amInternal = LocalServices.getService(
+                ActivityManagerInternal.class);
+        final PackageManagerInternal packageManagerInternal = LocalServices.getService(
+                PackageManagerInternal.class);
         final UserManager userManager = context.getSystemService(UserManager.class);
         final PowerManagerInternal powerManagerInternal = LocalServices.getService(
                 PowerManagerInternal.class);
@@ -173,25 +156,29 @@
 
         // Init processors
         mAssociationRequestsProcessor = new AssociationRequestsProcessor(context,
-                mPackageManagerInternal, mAssociationStore);
-        mBackupRestoreProcessor = new BackupRestoreProcessor(context, mPackageManagerInternal,
+                packageManagerInternal, mAssociationStore);
+        mBackupRestoreProcessor = new BackupRestoreProcessor(context, packageManagerInternal,
                 mAssociationStore, associationDiskStore, mSystemDataTransferRequestStore,
                 mAssociationRequestsProcessor);
 
         mCompanionAppBinder = new CompanionAppBinder(context);
 
+        mCompanionExemptionProcessor = new CompanionExemptionProcessor(context,
+                powerExemptionManager, appOpsManager, packageManagerInternal, atmInternal,
+                amInternal, mAssociationStore);
+
         mDevicePresenceProcessor = new DevicePresenceProcessor(context,
                 mCompanionAppBinder, userManager, mAssociationStore, mObservableUuidStore,
-                powerManagerInternal);
+                powerManagerInternal, mCompanionExemptionProcessor);
 
         mTransportManager = new CompanionTransportManager(context, mAssociationStore);
 
         mDisassociationProcessor = new DisassociationProcessor(context, activityManager,
-                mAssociationStore, mPackageManagerInternal, mDevicePresenceProcessor,
+                mAssociationStore, packageManagerInternal, mDevicePresenceProcessor,
                 mCompanionAppBinder, mSystemDataTransferRequestStore, mTransportManager);
 
         mSystemDataTransferProcessor = new SystemDataTransferProcessor(this,
-                mPackageManagerInternal, mAssociationStore,
+                packageManagerInternal, mAssociationStore,
                 mSystemDataTransferRequestStore, mTransportManager);
 
         // TODO(b/279663946): move context sync to a dedicated system service
@@ -202,7 +189,6 @@
     public void onStart() {
         // Init association stores
         mAssociationStore.refreshCache();
-        mAssociationStore.registerLocalListener(mAssociationStoreChangeListener);
 
         // Init UUID store
         mObservableUuidStore.getObservableUuidsForUser(getContext().getUserId());
@@ -240,11 +226,11 @@
 
         if (associations.isEmpty()) return;
 
-        updateAtm(userId, associations);
+        mCompanionExemptionProcessor.updateAtm(userId, associations);
 
-        BackgroundThread.getHandler().sendMessageDelayed(
-                obtainMessage(CompanionDeviceManagerService::maybeGrantAutoRevokeExemptions, this),
-                MINUTES.toMillis(10));
+        try (ExecutorService executor = Executors.newSingleThreadExecutor()) {
+            executor.execute(mCompanionExemptionProcessor::updateAutoRevokeExemptions);
+        }
     }
 
     @Override
@@ -274,18 +260,16 @@
             mObservableUuidStore.removeObservableUuid(userId, uuid.getUuid(), packageName);
         }
 
-        mCompanionAppBinder.onPackagesChanged(userId);
+        mCompanionAppBinder.onPackagesChanged(userId, packageName);
     }
 
     private void onPackageModifiedInternal(@UserIdInt int userId, @NonNull String packageName) {
-        final List<AssociationInfo> associationsForPackage =
+        final List<AssociationInfo> associations =
                 mAssociationStore.getAssociationsByPackage(userId, packageName);
-        for (AssociationInfo association : associationsForPackage) {
-            updateSpecialAccessPermissionForAssociatedPackage(association.getUserId(),
-                    association.getPackageName());
+        if (!associations.isEmpty()) {
+            mCompanionExemptionProcessor.exemptPackage(userId, packageName, false);
+            mCompanionAppBinder.onPackagesChanged(userId, packageName);
         }
-
-        mCompanionAppBinder.onPackagesChanged(userId);
     }
 
     private void onPackageAddedInternal(@UserIdInt int userId, @NonNull String packageName) {
@@ -765,130 +749,6 @@
         }
     }
 
-    /**
-     * Update special access for the association's package
-     */
-    public void updateSpecialAccessPermissionForAssociatedPackage(int userId, String packageName) {
-        final PackageInfo packageInfo =
-                getPackageInfo(getContext(), userId, packageName);
-
-        Binder.withCleanCallingIdentity(() -> updateSpecialAccessPermissionAsSystem(packageInfo));
-    }
-
-    private void updateSpecialAccessPermissionAsSystem(PackageInfo packageInfo) {
-        if (packageInfo == null) {
-            return;
-        }
-
-        if (containsEither(packageInfo.requestedPermissions,
-                android.Manifest.permission.RUN_IN_BACKGROUND,
-                android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) {
-            mPowerExemptionManager.addToPermanentAllowList(packageInfo.packageName);
-        } else {
-            try {
-                mPowerExemptionManager.removeFromPermanentAllowList(packageInfo.packageName);
-            } catch (UnsupportedOperationException e) {
-                Slog.w(TAG, packageInfo.packageName + " can't be removed from power save"
-                        + " whitelist. It might due to the package is whitelisted by the system.");
-            }
-        }
-
-        NetworkPolicyManager networkPolicyManager = NetworkPolicyManager.from(getContext());
-        try {
-            if (containsEither(packageInfo.requestedPermissions,
-                    android.Manifest.permission.USE_DATA_IN_BACKGROUND,
-                    android.Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND)) {
-                networkPolicyManager.addUidPolicy(
-                        packageInfo.applicationInfo.uid,
-                        NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
-            } else {
-                networkPolicyManager.removeUidPolicy(
-                        packageInfo.applicationInfo.uid,
-                        NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
-            }
-        } catch (IllegalArgumentException e) {
-            Slog.e(TAG, e.getMessage());
-        }
-
-        exemptFromAutoRevoke(packageInfo.packageName, packageInfo.applicationInfo.uid);
-    }
-
-    private void exemptFromAutoRevoke(String packageName, int uid) {
-        try {
-            mAppOpsManager.setMode(
-                    AppOpsManager.OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED,
-                    uid,
-                    packageName,
-                    AppOpsManager.MODE_IGNORED);
-        } catch (RemoteException e) {
-            Slog.w(TAG, "Error while granting auto revoke exemption for " + packageName, e);
-        }
-    }
-
-    private void updateAtm(int userId, List<AssociationInfo> associations) {
-        final Set<Integer> companionAppUids = new ArraySet<>();
-        for (AssociationInfo association : associations) {
-            final int uid = mPackageManagerInternal.getPackageUid(association.getPackageName(),
-                    0, userId);
-            if (uid >= 0) {
-                companionAppUids.add(uid);
-            }
-        }
-        if (mAtmInternal != null) {
-            mAtmInternal.setCompanionAppUids(userId, companionAppUids);
-        }
-        if (mAmInternal != null) {
-            // Make a copy of the set and send it to ActivityManager.
-            mAmInternal.setCompanionAppUids(userId, new ArraySet<>(companionAppUids));
-        }
-    }
-
-    private void maybeGrantAutoRevokeExemptions() {
-        Slog.d(TAG, "maybeGrantAutoRevokeExemptions()");
-
-        PackageManager pm = getContext().getPackageManager();
-        for (int userId : LocalServices.getService(UserManagerInternal.class).getUserIds()) {
-            SharedPreferences pref = getContext().getSharedPreferences(
-                    new File(Environment.getUserSystemDirectory(userId), PREF_FILE_NAME),
-                    Context.MODE_PRIVATE);
-            if (pref.getBoolean(PREF_KEY_AUTO_REVOKE_GRANTS_DONE, false)) {
-                continue;
-            }
-
-            try {
-                final List<AssociationInfo> associations =
-                        mAssociationStore.getActiveAssociationsByUser(userId);
-                for (AssociationInfo a : associations) {
-                    try {
-                        int uid = pm.getPackageUidAsUser(a.getPackageName(), userId);
-                        exemptFromAutoRevoke(a.getPackageName(), uid);
-                    } catch (PackageManager.NameNotFoundException e) {
-                        Slog.w(TAG, "Unknown companion package: " + a.getPackageName(), e);
-                    }
-                }
-            } finally {
-                pref.edit().putBoolean(PREF_KEY_AUTO_REVOKE_GRANTS_DONE, true).apply();
-            }
-        }
-    }
-
-    private final AssociationStore.OnChangeListener mAssociationStoreChangeListener =
-            new AssociationStore.OnChangeListener() {
-                @Override
-                public void onAssociationChanged(int changeType, AssociationInfo association) {
-                    Slog.d(TAG, "onAssociationChanged changeType=[" + changeType
-                            + "], association=[" + association);
-
-                    final int userId = association.getUserId();
-                    final List<AssociationInfo> updatedAssociations =
-                            mAssociationStore.getActiveAssociationsByUser(userId);
-
-                    updateAtm(userId, updatedAssociations);
-                    updateSpecialAccessPermissionForAssociatedPackage(association.getUserId(),
-                            association.getPackageName());
-                }
-            };
-
     private final PackageMonitor mPackageMonitor = new PackageMonitor() {
         @Override
         public void onPackageRemoved(String packageName, int uid) {
@@ -911,10 +771,6 @@
         }
     };
 
-    private static <T> boolean containsEither(T[] array, T a, T b) {
-        return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b);
-    }
-
     private class LocalService implements CompanionDeviceManagerServiceInternal {
 
         @Override
diff --git a/services/companion/java/com/android/server/companion/CompanionExemptionProcessor.java b/services/companion/java/com/android/server/companion/CompanionExemptionProcessor.java
new file mode 100644
index 0000000..6ddb569
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/CompanionExemptionProcessor.java
@@ -0,0 +1,217 @@
+/*
+ * 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.server.companion;
+
+import static android.app.AppOpsManager.MODE_ALLOWED;
+import static android.app.AppOpsManager.MODE_IGNORED;
+
+import static com.android.server.companion.utils.PackageUtils.getPackageInfo;
+
+import android.annotation.SuppressLint;
+import android.app.ActivityManagerInternal;
+import android.app.AppOpsManager;
+import android.companion.AssociationInfo;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
+import android.net.NetworkPolicyManager;
+import android.os.Binder;
+import android.os.Environment;
+import android.os.PowerExemptionManager;
+import android.util.ArraySet;
+import android.util.Slog;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.server.LocalServices;
+import com.android.server.companion.association.AssociationStore;
+import com.android.server.pm.UserManagerInternal;
+import com.android.server.wm.ActivityTaskManagerInternal;
+
+import java.io.File;
+import java.util.List;
+import java.util.Set;
+
+@SuppressLint("LongLogTag")
+public class CompanionExemptionProcessor {
+
+    private static final String TAG = "CDM_CompanionExemptionProcessor";
+
+    private static final String PREF_FILE_NAME = "companion_device_preferences.xml";
+    private static final String PREF_KEY_AUTO_REVOKE_GRANTS_DONE = "auto_revoke_grants_done";
+
+    private final Context mContext;
+    private final PowerExemptionManager mPowerExemptionManager;
+    private final AppOpsManager mAppOpsManager;
+    private final PackageManagerInternal mPackageManager;
+    private final ActivityTaskManagerInternal mAtmInternal;
+    private final ActivityManagerInternal mAmInternal;
+    private final AssociationStore mAssociationStore;
+
+    public CompanionExemptionProcessor(Context context, PowerExemptionManager powerExemptionManager,
+            AppOpsManager appOpsManager, PackageManagerInternal packageManager,
+            ActivityTaskManagerInternal atmInternal, ActivityManagerInternal amInternal,
+            AssociationStore associationStore) {
+        mContext = context;
+        mPowerExemptionManager = powerExemptionManager;
+        mAppOpsManager = appOpsManager;
+        mPackageManager = packageManager;
+        mAtmInternal = atmInternal;
+        mAmInternal = amInternal;
+        mAssociationStore = associationStore;
+
+        mAssociationStore.registerLocalListener(new AssociationStore.OnChangeListener() {
+            @Override
+            public void onAssociationChanged(int changeType, AssociationInfo association) {
+                final int userId = association.getUserId();
+                final List<AssociationInfo> updatedAssociations =
+                        mAssociationStore.getActiveAssociationsByUser(userId);
+
+                updateAtm(userId, updatedAssociations);
+            }
+        });
+    }
+
+    /**
+     * Update ActivityManager and ActivityTaskManager exemptions
+     */
+    public void updateAtm(int userId, List<AssociationInfo> associations) {
+        final Set<Integer> companionAppUids = new ArraySet<>();
+        for (AssociationInfo association : associations) {
+            int uid = mPackageManager.getPackageUid(association.getPackageName(), 0, userId);
+            if (uid >= 0) {
+                companionAppUids.add(uid);
+            }
+        }
+        if (mAtmInternal != null) {
+            mAtmInternal.setCompanionAppUids(userId, companionAppUids);
+        }
+        if (mAmInternal != null) {
+            // Make a copy of the set and send it to ActivityManager.
+            mAmInternal.setCompanionAppUids(userId, new ArraySet<>(companionAppUids));
+        }
+    }
+
+    /**
+     * Update special access for the association's package
+     */
+    public void exemptPackage(int userId, String packageName, boolean hasPresentDevices) {
+        final PackageInfo packageInfo = getPackageInfo(mContext, userId, packageName);
+
+        Binder.withCleanCallingIdentity(
+                () -> exemptPackageAsSystem(userId, packageInfo, hasPresentDevices));
+    }
+
+    @SuppressLint("MissingPermission")
+    private void exemptPackageAsSystem(int userId, PackageInfo packageInfo,
+            boolean hasPresentDevices) {
+        if (packageInfo == null) {
+            return;
+        }
+
+        // If the app has run-in-bg permission and present devices, add it to power saver allowlist.
+        if (containsEither(packageInfo.requestedPermissions,
+                android.Manifest.permission.RUN_IN_BACKGROUND,
+                android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)
+                && hasPresentDevices) {
+            mPowerExemptionManager.addToPermanentAllowList(packageInfo.packageName);
+        } else {
+            try {
+                mPowerExemptionManager.removeFromPermanentAllowList(packageInfo.packageName);
+            } catch (UnsupportedOperationException e) {
+                Slog.w(TAG, packageInfo.packageName + " can't be removed from power save"
+                        + " allowlist. It might be due to the package being allowlisted by the"
+                        + " system.");
+            }
+        }
+
+        // If the app has run-in-bg permission and present device, allow metered network use.
+        NetworkPolicyManager networkPolicyManager = NetworkPolicyManager.from(mContext);
+        try {
+            if (containsEither(packageInfo.requestedPermissions,
+                    android.Manifest.permission.USE_DATA_IN_BACKGROUND,
+                    android.Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND)
+                    && hasPresentDevices) {
+                networkPolicyManager.addUidPolicy(
+                        packageInfo.applicationInfo.uid,
+                        NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
+            } else {
+                networkPolicyManager.removeUidPolicy(
+                        packageInfo.applicationInfo.uid,
+                        NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
+            }
+        } catch (IllegalArgumentException e) {
+            Slog.e(TAG, e.getMessage());
+        }
+
+        updateAutoRevokeExemption(packageInfo.packageName, packageInfo.applicationInfo.uid,
+                !mAssociationStore.getActiveAssociationsByPackage(userId,
+                        packageInfo.packageName).isEmpty());
+    }
+
+    /**
+     * Update auto revoke exemptions.
+     * If the app has any association, exempt it from permission auto revoke.
+     */
+    public void updateAutoRevokeExemptions() {
+        Slog.d(TAG, "maybeGrantAutoRevokeExemptions()");
+
+        PackageManager pm = mContext.getPackageManager();
+        for (int userId : LocalServices.getService(UserManagerInternal.class).getUserIds()) {
+            SharedPreferences pref = mContext.getSharedPreferences(
+                    new File(Environment.getUserSystemDirectory(userId), PREF_FILE_NAME),
+                    Context.MODE_PRIVATE);
+            if (pref.getBoolean(PREF_KEY_AUTO_REVOKE_GRANTS_DONE, false)) {
+                continue;
+            }
+
+            try {
+                final List<AssociationInfo> associations =
+                        mAssociationStore.getActiveAssociationsByUser(userId);
+                for (AssociationInfo a : associations) {
+                    try {
+                        int uid = pm.getPackageUidAsUser(a.getPackageName(), userId);
+                        updateAutoRevokeExemption(a.getPackageName(), uid, true);
+                    } catch (PackageManager.NameNotFoundException e) {
+                        Slog.w(TAG, "Unknown companion package: " + a.getPackageName(), e);
+                    }
+                }
+            } finally {
+                pref.edit().putBoolean(PREF_KEY_AUTO_REVOKE_GRANTS_DONE, true).apply();
+            }
+        }
+    }
+
+    @SuppressLint("MissingPermission")
+    private void updateAutoRevokeExemption(String packageName, int uid, boolean hasAssociations) {
+        try {
+            mAppOpsManager.setMode(
+                    AppOpsManager.OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED,
+                    uid,
+                    packageName,
+                    hasAssociations ? MODE_IGNORED : MODE_ALLOWED);
+        } catch (Exception e) {
+            Slog.e(TAG, "Error while granting auto revoke exemption for " + packageName, e);
+        }
+    }
+
+    private <T> boolean containsEither(T[] array, T a, T b) {
+        return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b);
+    }
+
+}
diff --git a/services/companion/java/com/android/server/companion/devicepresence/CompanionAppBinder.java b/services/companion/java/com/android/server/companion/devicepresence/CompanionAppBinder.java
index 60f4688..48accbf 100644
--- a/services/companion/java/com/android/server/companion/devicepresence/CompanionAppBinder.java
+++ b/services/companion/java/com/android/server/companion/devicepresence/CompanionAppBinder.java
@@ -95,8 +95,8 @@
     /**
      * On package changed.
      */
-    public void onPackagesChanged(@UserIdInt int userId) {
-        mCompanionServicesRegister.invalidate(userId);
+    public void onPackagesChanged(@UserIdInt int userId, String packageName) {
+        mCompanionServicesRegister.forUser(userId).remove(packageName);
     }
 
     /**
@@ -309,10 +309,6 @@
             return forUser(userId).getOrDefault(packageName, Collections.emptyList());
         }
 
-        synchronized void invalidate(@UserIdInt int userId) {
-            remove(userId);
-        }
-
         @Override
         protected final @NonNull Map<String, List<ComponentName>> create(@UserIdInt int userId) {
             return PackageUtils.getCompanionServicesForUser(mContext, userId);
diff --git a/services/companion/java/com/android/server/companion/devicepresence/DevicePresenceProcessor.java b/services/companion/java/com/android/server/companion/devicepresence/DevicePresenceProcessor.java
index a374d27..7b4dd7d 100644
--- a/services/companion/java/com/android/server/companion/devicepresence/DevicePresenceProcessor.java
+++ b/services/companion/java/com/android/server/companion/devicepresence/DevicePresenceProcessor.java
@@ -57,6 +57,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.CollectionUtils;
+import com.android.server.companion.CompanionExemptionProcessor;
 import com.android.server.companion.association.AssociationStore;
 
 import java.io.PrintWriter;
@@ -101,6 +102,8 @@
     private final PowerManagerInternal mPowerManagerInternal;
     @NonNull
     private final UserManager mUserManager;
+    @NonNull
+    private final CompanionExemptionProcessor mCompanionExemptionProcessor;
 
     // NOTE: Same association may appear in more than one of the following sets at the same time.
     // (E.g. self-managed devices that have MAC addresses, could be reported as present by their
@@ -111,7 +114,7 @@
     @NonNull
     private final Set<Integer> mNearbyBleDevices = new HashSet<>();
     @NonNull
-    private final Set<Integer> mReportedSelfManagedDevices = new HashSet<>();
+    private final Set<Integer> mConnectedSelfManagedDevices = new HashSet<>();
     @NonNull
     private final Set<ParcelUuid> mConnectedUuidDevices = new HashSet<>();
     @NonNull
@@ -146,7 +149,8 @@
             @NonNull UserManager userManager,
             @NonNull AssociationStore associationStore,
             @NonNull ObservableUuidStore observableUuidStore,
-            @NonNull PowerManagerInternal powerManagerInternal) {
+            @NonNull PowerManagerInternal powerManagerInternal,
+            @NonNull CompanionExemptionProcessor companionExemptionProcessor) {
         mContext = context;
         mCompanionAppBinder = companionAppBinder;
         mAssociationStore = associationStore;
@@ -156,6 +160,7 @@
                 mObservableUuidStore, this);
         mBleDeviceProcessor = new BleDeviceProcessor(associationStore, this);
         mPowerManagerInternal = powerManagerInternal;
+        mCompanionExemptionProcessor = companionExemptionProcessor;
     }
 
     /** Initialize {@link DevicePresenceProcessor} */
@@ -404,7 +409,7 @@
      * nearby (for "self-managed" associations).
      */
     public boolean isDevicePresent(int associationId) {
-        return mReportedSelfManagedDevices.contains(associationId)
+        return mConnectedSelfManagedDevices.contains(associationId)
                 || mConnectedBtDevices.contains(associationId)
                 || mNearbyBleDevices.contains(associationId)
                 || mSimulated.contains(associationId);
@@ -451,7 +456,7 @@
      * notifyDeviceAppeared()}
      */
     public void onSelfManagedDeviceConnected(int associationId) {
-        onDevicePresenceEvent(mReportedSelfManagedDevices,
+        onDevicePresenceEvent(mConnectedSelfManagedDevices,
                 associationId, EVENT_SELF_MANAGED_APPEARED);
     }
 
@@ -467,7 +472,7 @@
      * notifyDeviceDisappeared()}
      */
     public void onSelfManagedDeviceDisconnected(int associationId) {
-        onDevicePresenceEvent(mReportedSelfManagedDevices,
+        onDevicePresenceEvent(mConnectedSelfManagedDevices,
                 associationId, EVENT_SELF_MANAGED_DISAPPEARED);
     }
 
@@ -475,7 +480,7 @@
      * Marks a "self-managed" device as disconnected when binderDied.
      */
     public void onSelfManagedDeviceReporterBinderDied(int associationId) {
-        onDevicePresenceEvent(mReportedSelfManagedDevices,
+        onDevicePresenceEvent(mConnectedSelfManagedDevices,
                 associationId, EVENT_SELF_MANAGED_DISAPPEARED);
     }
 
@@ -683,6 +688,7 @@
 
                 if (association.shouldBindWhenPresent()) {
                     bindApplicationIfNeeded(userId, packageName, association.isSelfManaged());
+                    mCompanionExemptionProcessor.exemptPackage(userId, packageName, true);
                 } else {
                     return;
                 }
@@ -715,6 +721,7 @@
                 // Check if there are other devices associated to the app that are present.
                 if (!shouldBindPackage(userId, packageName)) {
                     mCompanionAppBinder.unbindCompanionApp(userId, packageName);
+                    mCompanionExemptionProcessor.exemptPackage(userId, packageName, false);
                 }
                 break;
             default:
@@ -940,7 +947,7 @@
 
         mConnectedBtDevices.remove(id);
         mNearbyBleDevices.remove(id);
-        mReportedSelfManagedDevices.remove(id);
+        mConnectedSelfManagedDevices.remove(id);
         mSimulated.remove(id);
         synchronized (mBtDisconnectedDevices) {
             mBtDisconnectedDevices.remove(id);
@@ -1100,7 +1107,7 @@
         out.append("Companion Device Present: ");
         if (mConnectedBtDevices.isEmpty()
                 && mNearbyBleDevices.isEmpty()
-                && mReportedSelfManagedDevices.isEmpty()) {
+                && mConnectedSelfManagedDevices.isEmpty()) {
             out.append("<empty>\n");
             return;
         } else {
@@ -1130,11 +1137,11 @@
         }
 
         out.append("  Self-Reported Devices: ");
-        if (mReportedSelfManagedDevices.isEmpty()) {
+        if (mConnectedSelfManagedDevices.isEmpty()) {
             out.append("<empty>\n");
         } else {
             out.append("\n");
-            for (int associationId : mReportedSelfManagedDevices) {
+            for (int associationId : mConnectedSelfManagedDevices) {
                 AssociationInfo a = mAssociationStore.getAssociationById(associationId);
                 out.append("    ").append(a.toShortString()).append('\n');
             }
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 5682c33..bf415a3 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -797,6 +797,10 @@
                     @Override
                     public void onDeviceDiscoveryDone(List<HdmiDeviceInfo> deviceInfos) {
                         for (HdmiDeviceInfo info : deviceInfos) {
+                            if (!isInputReady(info.getDeviceId())) {
+                                mService.getHdmiCecNetwork().removeCecDevice(
+                                        HdmiCecLocalDeviceTv.this, info.getLogicalAddress());
+                            }
                             mService.getHdmiCecNetwork().addCecDevice(info);
                         }
 
diff --git a/services/core/java/com/android/server/inputmethod/ImeBindingState.java b/services/core/java/com/android/server/inputmethod/ImeBindingState.java
index f78ea84..5deed39 100644
--- a/services/core/java/com/android/server/inputmethod/ImeBindingState.java
+++ b/services/core/java/com/android/server/inputmethod/ImeBindingState.java
@@ -20,6 +20,7 @@
 import static android.server.inputmethod.InputMethodManagerServiceProto.CUR_FOCUSED_WINDOW_SOFT_INPUT_MODE;
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 import android.os.IBinder;
@@ -86,10 +87,10 @@
                 InputMethodDebug.softInputModeToString(mFocusedWindowSoftInputMode));
     }
 
-    void dump(String prefix, Printer p) {
-        p.println(prefix + "mFocusedWindow()=" + mFocusedWindow);
-        p.println(prefix + "softInputMode=" + InputMethodDebug.softInputModeToString(
-                mFocusedWindowSoftInputMode));
+    void dump(@NonNull Printer p, @NonNull String prefix) {
+        p.println(prefix + "mFocusedWindow=" + mFocusedWindow);
+        p.println(prefix + "mFocusedWindowSoftInputMode="
+                + InputMethodDebug.softInputModeToString(mFocusedWindowSoftInputMode));
         p.println(prefix + "mFocusedWindowClient=" + mFocusedWindowClient);
     }
 
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
index ec1993a..477660d 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
@@ -61,6 +61,7 @@
 import com.android.server.EventLogTags;
 import com.android.server.wm.WindowManagerInternal;
 
+import java.io.PrintWriter;
 import java.util.concurrent.CountDownLatch;
 
 /**
@@ -733,4 +734,25 @@
     void setBackDisposition(@BackDispositionMode int backDisposition) {
         mBackDisposition = backDisposition;
     }
+
+    @GuardedBy("ImfLock.class")
+    void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
+        pw.println(prefix + "mSelectedMethodId=" + mSelectedMethodId);
+        pw.println(prefix + "mCurrentSubtype=" + mCurrentSubtype);
+        pw.println(prefix + "mCurSeq=" + mCurSeq);
+        pw.println(prefix + "mCurId=" + mCurId);
+        pw.println(prefix + "mHasMainConnection=" + mHasMainConnection);
+        pw.println(prefix + "mVisibleBound=" + mVisibleBound);
+        pw.println(prefix + "mCurToken=" + mCurToken);
+        pw.println(prefix + "mCurTokenDisplayId=" + mCurTokenDisplayId);
+        pw.println(prefix + "mCurHostInputToken=" + getCurHostInputToken());
+        pw.println(prefix + "mCurIntent=" + mCurIntent);
+        pw.println(prefix + "mCurMethod=" + mCurMethod);
+        pw.println(prefix + "mImeWindowVis=" + mImeWindowVis);
+        pw.println(prefix + "mBackDisposition=" + mBackDisposition);
+        pw.println(prefix + "mDisplayIdToShowIme=" + mDisplayIdToShowIme);
+        pw.println(prefix + "mDeviceIdToShowIme=" + mDeviceIdToShowIme);
+        pw.println(prefix + "mSupportsStylusHw=" + mSupportsStylusHw);
+        pw.println(prefix + "mSupportsConnectionlessStylusHw=" + mSupportsConnectionlessStylusHw);
+    }
 }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 83044c2..131b9ba 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -6063,42 +6063,40 @@
 
     @BinderThread
     @Override
-    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+    public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @Nullable String[] args) {
         if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
 
         PriorityDump.dump(mPriorityDumper, fd, pw, args);
     }
 
     @BinderThread
-    private void dumpAsStringNoCheck(FileDescriptor fd, PrintWriter pw, String[] args,
-            boolean isCritical) {
+    private void dumpAsStringNoCheck(@NonNull FileDescriptor fd, @NonNull PrintWriter pw,
+            @NonNull String[] args, boolean isCritical) {
         final int argUserId = parseUserIdFromDumpArgs(args);
         final Printer p = new PrintWriterPrinter(pw);
-        p.println("Current Input Method Manager state:");
+        p.println("Input Method Manager Service state:");
         p.println("  mSystemReady=" + mSystemReady);
         p.println("  mInteractive=" + mIsInteractive);
         p.println("  mConcurrentMultiUserModeEnabled=" + mConcurrentMultiUserModeEnabled);
         p.println("  ENABLE_HIDE_IME_CAPTION_BAR="
                 + InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR);
+        final int currentImeUserId;
         synchronized (ImfLock.class) {
+            currentImeUserId = mCurrentImeUserId;
+            p.println("  mCurrentImeUserId=" + currentImeUserId);
             p.println("  mStylusIds=" + (mStylusIds != null
                     ? Arrays.toString(mStylusIds.toArray()) : ""));
         }
+        // TODO(b/305849394): Make mMenuController multi-user aware.
         if (Flags.imeSwitcherRevamp()) {
             p.println("  mMenuControllerNew:");
-            mMenuControllerNew.dump(p, "  ");
+            mMenuControllerNew.dump(p, "    ");
         } else {
             p.println("  mMenuController:");
-            mMenuController.dump(p, "  ");
+            mMenuController.dump(p, "    ");
         }
-        if (mConcurrentMultiUserModeEnabled && argUserId == UserHandle.USER_NULL) {
-            mUserDataRepository.forAllUserData(
-                    u -> dumpAsStringNoCheckForUser(u, fd, pw, args, isCritical));
-        } else {
-            final int userId = argUserId != UserHandle.USER_NULL ? argUserId : mCurrentImeUserId;
-            final var userData = getUserData(userId);
-            dumpAsStringNoCheckForUser(userData, fd, pw, args, isCritical);
-        }
+        dumpClientController(p);
+        dumpUserRepository(p);
 
         // TODO(b/365868861): Make StartInputHistory and ImeTracker multi-user aware.
         synchronized (ImfLock.class) {
@@ -6112,12 +6110,18 @@
         p.println("  mImeTrackerService#History:");
         mImeTrackerService.dump(pw, "    ");
 
-        dumpUserRepository(p);
-        dumpClientStates(p);
+        if (mConcurrentMultiUserModeEnabled && argUserId == UserHandle.USER_NULL) {
+            mUserDataRepository.forAllUserData(
+                    u -> dumpAsStringNoCheckForUser(u, fd, pw, args, isCritical));
+        } else {
+            final int userId = argUserId != UserHandle.USER_NULL ? argUserId : currentImeUserId;
+            final var userData = getUserData(userId);
+            dumpAsStringNoCheckForUser(userData, fd, pw, args, isCritical);
+        }
     }
 
     @UserIdInt
-    private static int parseUserIdFromDumpArgs(String[] args) {
+    private static int parseUserIdFromDumpArgs(@NonNull String[] args) {
         final int userIdx = Arrays.binarySearch(args, "--user");
         if (userIdx == -1 || userIdx == args.length - 1) {
             return UserHandle.USER_NULL;
@@ -6127,44 +6131,37 @@
 
     // TODO(b/356239178): Update dump format output to better group per-user info.
     @BinderThread
-    private void dumpAsStringNoCheckForUser(UserData userData, FileDescriptor fd, PrintWriter pw,
-            String[] args, boolean isCritical) {
+    private void dumpAsStringNoCheckForUser(@NonNull UserData userData, @NonNull FileDescriptor fd,
+            @NonNull PrintWriter pw, @NonNull String[] args, boolean isCritical) {
         final Printer p = new PrintWriterPrinter(pw);
-        IInputMethodInvoker method;
         ClientState client;
+        IInputMethodInvoker method;
         p.println("  UserId=" + userData.mUserId);
         synchronized (ImfLock.class) {
-            final InputMethodSettings settings = InputMethodSettingsRepository.get(
-                    userData.mUserId);
+            final var bindingController = userData.mBindingController;
+            client = userData.mCurClient;
+            method = bindingController.getCurMethod();
+            p.println("    mBindingController:");
+            bindingController.dump(pw, "      ");
+            p.println("    mCurClient=" + client);
+            p.println("    mFocusedWindowPerceptible=" + mFocusedWindowPerceptible);
+            p.println("    mImeBindingState:");
+            userData.mImeBindingState.dump(p, "      ");
+            p.println("    mBoundToMethod=" + userData.mBoundToMethod);
+            p.println("    mEnabledSession=" + userData.mEnabledSession);
+            p.println("    mVisibilityStateComputer:");
+            userData.mVisibilityStateComputer.dump(pw, "      ");
+            p.println("    mInFullscreenMode=" + userData.mInFullscreenMode);
+
+            final var settings = InputMethodSettingsRepository.get(userData.mUserId);
             final List<InputMethodInfo> methodList = settings.getMethodList();
-            int numImes = methodList.size();
+            final int numImes = methodList.size();
             p.println("    Input Methods:");
             for (int i = 0; i < numImes; i++) {
-                InputMethodInfo info = methodList.get(i);
+                final InputMethodInfo info = methodList.get(i);
                 p.println("      InputMethod #" + i + ":");
                 info.dump(p, "        ");
             }
-            final var bindingController = userData.mBindingController;
-            p.println("        mCurMethodId=" + bindingController.getSelectedMethodId());
-            client = userData.mCurClient;
-            p.println("        mCurClient=" + client + " mCurSeq="
-                    + bindingController.getSequenceNumber());
-            p.println("        mFocusedWindowPerceptible=" + mFocusedWindowPerceptible);
-            userData.mImeBindingState.dump(/* prefix= */ "  ", p);
-            p.println("        mCurId=" + bindingController.getCurId());
-            p.println("        mHaveConnection=" + bindingController.hasMainConnection());
-            p.println("        mBoundToMethod=" + userData.mBoundToMethod);
-            p.println("        mVisibleBound=" + bindingController.isVisibleBound());
-            p.println("        mCurToken=" + bindingController.getCurToken());
-            p.println("        mCurTokenDisplayId=" + bindingController.getCurTokenDisplayId());
-            p.println("        mCurHostInputToken=" + bindingController.getCurHostInputToken());
-            p.println("        mCurIntent=" + bindingController.getCurIntent());
-            method = bindingController.getCurMethod();
-            p.println("        mCurMethod=" + method);
-            p.println("        mEnabledSession=" + userData.mEnabledSession);
-            final var visibilityStateComputer = userData.mVisibilityStateComputer;
-            visibilityStateComputer.dump(pw, "  ");
-            p.println("        mInFullscreenMode=" + userData.mInFullscreenMode);
         }
 
         // Exit here for critical dump, as remaining sections require IPCs to other processes.
@@ -6172,7 +6169,7 @@
             return;
         }
 
-        p.println(" ");
+        p.println("");
         if (client != null) {
             pw.flush();
             try {
@@ -6184,25 +6181,23 @@
             p.println("No input method client.");
         }
         synchronized (ImfLock.class) {
-            if (userData.mImeBindingState.mFocusedWindowClient != null
-                    && client != userData.mImeBindingState.mFocusedWindowClient) {
-                p.println(" ");
-                p.println("Warning: Current input method client doesn't match the last focused. "
-                        + "window.");
+            final var focusedWindowClient = userData.mImeBindingState.mFocusedWindowClient;
+            if (focusedWindowClient != null && client != focusedWindowClient) {
+                p.println("");
+                p.println("Warning: Current input method client doesn't match the last focused"
+                        + " window.");
                 p.println("Dumping input method client in the last focused window just in case.");
-                p.println(" ");
+                p.println("");
                 pw.flush();
                 try {
-                    TransferPipe.dumpAsync(
-                            userData.mImeBindingState.mFocusedWindowClient.mClient.asBinder(), fd,
-                            args);
+                    TransferPipe.dumpAsync(focusedWindowClient.mClient.asBinder(), fd, args);
                 } catch (IOException | RemoteException e) {
                     p.println("Failed to dump input method client in focused window: " + e);
                 }
             }
         }
 
-        p.println(" ");
+        p.println("");
         if (method != null) {
             pw.flush();
             try {
@@ -6215,56 +6210,51 @@
         }
     }
 
-    private void dumpClientStates(Printer p) {
-        p.println(" ClientStates:");
+    private void dumpClientController(@NonNull Printer p) {
+        p.println("  mClientController:");
         // TODO(b/324907325): Remove the suppress warnings once b/324907325 is fixed.
         @SuppressWarnings("GuardedBy") Consumer<ClientState> clientControllerDump = c -> {
-            p.println("   " + c + ":");
-            p.println("    client=" + c.mClient);
-            p.println("    fallbackInputConnection="
-                    + c.mFallbackInputConnection);
-            p.println("    sessionRequested="
-                    + c.mSessionRequested);
-            p.println("    sessionRequestedForAccessibility="
+            p.println("    " + c + ":");
+            p.println("      client=" + c.mClient);
+            p.println("      fallbackInputConnection=" + c.mFallbackInputConnection);
+            p.println("      sessionRequested=" + c.mSessionRequested);
+            p.println("      sessionRequestedForAccessibility="
                     + c.mSessionRequestedForAccessibility);
-            p.println("    curSession=" + c.mCurSession);
-            p.println("    selfReportedDisplayId=" + c.mSelfReportedDisplayId);
-            p.println("    uid=" + c.mUid);
-            p.println("    pid=" + c.mPid);
+            p.println("      curSession=" + c.mCurSession);
+            p.println("      selfReportedDisplayId=" + c.mSelfReportedDisplayId);
+            p.println("      uid=" + c.mUid);
+            p.println("      pid=" + c.mPid);
         };
         synchronized (ImfLock.class) {
             mClientController.forAllClients(clientControllerDump);
         }
     }
 
-    private void dumpUserRepository(Printer p) {
-        p.println("  mUserDataRepository=");
+    private void dumpUserRepository(@NonNull Printer p) {
+        p.println("  mUserDataRepository:");
         // TODO(b/324907325): Remove the suppress warnings once b/324907325 is fixed.
-        @SuppressWarnings("GuardedBy") Consumer<UserData> userDataDump =
-                u -> {
-                    p.println("    mUserId=" + u.mUserId);
-                    p.println("      unlocked=" + u.mIsUnlockingOrUnlocked.get());
-                    p.println("      hasMainConnection="
-                            + u.mBindingController.hasMainConnection());
-                    p.println("      isVisibleBound=" + u.mBindingController.isVisibleBound());
-                    p.println("      boundToMethod=" + u.mBoundToMethod);
-                    p.println("      curClient=" + u.mCurClient);
-                    if (u.mCurEditorInfo != null) {
-                        p.println("      curEditorInfo:");
-                        u.mCurEditorInfo.dump(p, "        ", false /* dumpExtras */);
-                    } else {
-                        p.println("      curEditorInfo: null");
-                    }
-                    p.println("      imeBindingState:");
-                    u.mImeBindingState.dump("        ", p);
-                    p.println("      enabledSession=" + u.mEnabledSession);
-                    p.println("      inFullscreenMode=" + u.mInFullscreenMode);
-                    p.println("      imeDrawsNavBar=" + u.mImeDrawsNavBar.get());
-                    p.println("      switchingController:");
-                    u.mSwitchingController.dump(p, "        ");
-                    p.println("      mLastEnabledInputMethodsStr="
-                            + u.mLastEnabledInputMethodsStr);
-                };
+        @SuppressWarnings("GuardedBy") Consumer<UserData> userDataDump = u -> {
+            p.println("    userId=" + u.mUserId);
+            p.println("      unlocked=" + u.mIsUnlockingOrUnlocked.get());
+            p.println("      hasMainConnection=" + u.mBindingController.hasMainConnection());
+            p.println("      isVisibleBound=" + u.mBindingController.isVisibleBound());
+            p.println("      boundToMethod=" + u.mBoundToMethod);
+            p.println("      curClient=" + u.mCurClient);
+            if (u.mCurEditorInfo != null) {
+                p.println("      curEditorInfo:");
+                u.mCurEditorInfo.dump(p, "        ", false /* dumpExtras */);
+            } else {
+                p.println("      curEditorInfo: null");
+            }
+            p.println("      imeBindingState:");
+            u.mImeBindingState.dump(p, "        ");
+            p.println("      enabledSession=" + u.mEnabledSession);
+            p.println("      inFullscreenMode=" + u.mInFullscreenMode);
+            p.println("      imeDrawsNavBar=" + u.mImeDrawsNavBar.get());
+            p.println("      switchingController:");
+            u.mSwitchingController.dump(p, "        ");
+            p.println("      mLastEnabledInputMethodsStr=" + u.mLastEnabledInputMethodsStr);
+        };
         synchronized (ImfLock.class) {
             mUserDataRepository.forAllUserData(userDataDump);
         }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
index b5ee068..248fa60 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
@@ -287,10 +287,10 @@
 
     void dump(@NonNull Printer pw, @NonNull String prefix) {
         final boolean showing = isisInputMethodPickerShownForTestLocked();
-        pw.println(prefix + "  isShowing: " + showing);
+        pw.println(prefix + "isShowing: " + showing);
 
         if (showing) {
-            pw.println(prefix + "  imList: " + mImList);
+            pw.println(prefix + "imList: " + mImList);
         }
     }
 
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java
index 1d0e3c6..6abd5aa 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java
@@ -187,10 +187,10 @@
 
     void dump(@NonNull Printer pw, @NonNull String prefix) {
         final boolean showing = isShowing();
-        pw.println(prefix + "  isShowing: " + showing);
+        pw.println(prefix + "isShowing: " + showing);
 
         if (showing) {
-            pw.println(prefix + "  menuItems: " + mMenuItems);
+            pw.println(prefix + "menuItems: " + mMenuItems);
         }
     }
 
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 19ac1ec..58b1e49 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -71,6 +71,7 @@
 import android.app.admin.DevicePolicyManagerInternal;
 import android.companion.virtual.VirtualDeviceManager;
 import android.content.ComponentName;
+import android.content.ContentProvider;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -649,11 +650,11 @@
             int userId, int callingUid, int callingPid,
             boolean includeInstantApps, boolean resolveForStart) {
         if (!mUserManager.exists(userId)) return Collections.emptyList();
-        enforceCrossUserOrProfilePermission(callingUid,
+        enforceCrossUserOrProfilePermission(Binder.getCallingUid(),
                 userId,
                 false /*requireFullPermission*/,
                 false /*checkShell*/,
-                "query intent receivers");
+                "query intent services");
         final String instantAppPkgName = getInstantAppPackageName(callingUid);
         flags = updateFlagsForResolve(flags, userId, callingUid, includeInstantApps,
                 false /* isImplicitImageCaptureIntentAndNotSetByDpc */);
@@ -2208,10 +2209,10 @@
             return true;
         }
         boolean permissionGranted = requireFullPermission ? hasPermission(
-                Manifest.permission.INTERACT_ACROSS_USERS_FULL)
+                Manifest.permission.INTERACT_ACROSS_USERS_FULL, callingUid)
                 : (hasPermission(
-                        android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
-                        || hasPermission(Manifest.permission.INTERACT_ACROSS_USERS));
+                        android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, callingUid)
+                        || hasPermission(Manifest.permission.INTERACT_ACROSS_USERS, callingUid));
         if (!permissionGranted) {
             if (Process.isIsolatedUid(callingUid) && isKnownIsolatedComputeApp(callingUid)) {
                 return checkIsolatedOwnerHasPermission(callingUid, requireFullPermission);
@@ -4669,7 +4670,7 @@
 
         if (!forceAllowCrossUser) {
             enforceCrossUserPermission(
-                    callingUid,
+                    Binder.getCallingUid(),
                     userId,
                     false /* requireFullPermission */,
                     false /* checkShell */,
@@ -4752,8 +4753,14 @@
             int callingUid) {
         if (!mUserManager.exists(userId)) return null;
         flags = updateFlagsForComponent(flags, userId);
-        final ProviderInfo providerInfo = mComponentResolver.queryProvider(this, name, flags,
-                userId);
+
+        // Callers of this API may not always separate the userID and authority. Let's parse it
+        // before resolving
+        String authorityWithoutUserId = ContentProvider.getAuthorityWithoutUserId(name);
+        userId = ContentProvider.getUserIdFromAuthority(name, userId);
+
+        final ProviderInfo providerInfo = mComponentResolver.queryProvider(this,
+                authorityWithoutUserId, flags, userId);
         boolean checkedGrants = false;
         if (providerInfo != null) {
             // Looking for cross-user grants before enforcing the typical cross-users permissions
@@ -4767,7 +4774,7 @@
         if (!checkedGrants) {
             boolean enforceCrossUser = true;
 
-            if (isAuthorityRedirectedForCloneProfile(name)) {
+            if (isAuthorityRedirectedForCloneProfile(authorityWithoutUserId)) {
                 final UserManagerInternal umInternal = mInjector.getUserManagerInternal();
 
                 UserInfo userInfo = umInternal.getUserInfo(UserHandle.getUserId(callingUid));
@@ -5242,7 +5249,7 @@
     @Override
     public int getComponentEnabledSetting(@NonNull ComponentName component, int callingUid,
             @UserIdInt int userId) {
-        enforceCrossUserPermission(callingUid, userId, false /*requireFullPermission*/,
+        enforceCrossUserPermission(Binder.getCallingUid(), userId, false /*requireFullPermission*/,
                 false /*checkShell*/, "getComponentEnabled");
         return getComponentEnabledSettingInternal(component, callingUid, userId);
     }
diff --git a/services/core/java/com/android/server/pm/PackageInstallerHistoricalSession.java b/services/core/java/com/android/server/pm/PackageInstallerHistoricalSession.java
index ea37d8e..99eac37 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerHistoricalSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerHistoricalSession.java
@@ -79,6 +79,8 @@
     private final String mSessionErrorMessage;
     private final String mPreVerifiedDomains;
     private final String mPackageName;
+    private final int mInitialVerificationPolicy;
+    private final int mCurrentVerificationPolicy;
 
     PackageInstallerHistoricalSession(int sessionId, int userId, int originalInstallerUid,
             String originalInstallerPackageName, InstallSource installSource, int installerUid,
@@ -90,7 +92,8 @@
             int[] childSessionIds, boolean sessionApplied, boolean sessionFailed,
             boolean sessionReady, int sessionErrorCode, String sessionErrorMessage,
             PreapprovalDetails preapprovalDetails, DomainSet preVerifiedDomains,
-            String packageNameFromApk) {
+            String packageNameFromApk, int initialVerificationPolicy,
+            int currentVerificationPolicy) {
         this.sessionId = sessionId;
         this.userId = userId;
         this.mOriginalInstallerUid = originalInstallerUid;
@@ -140,6 +143,8 @@
 
         this.mPackageName = preapprovalDetails != null ? preapprovalDetails.getPackageName()
                 : packageNameFromApk != null ? packageNameFromApk : params.appPackageName;
+        this.mInitialVerificationPolicy = initialVerificationPolicy;
+        this.mCurrentVerificationPolicy = currentVerificationPolicy;
     }
 
     void dump(IndentingPrintWriter pw) {
@@ -184,6 +189,8 @@
         pw.printPair("mPreapprovalDetails", mPreapprovalDetails);
         pw.printPair("mPreVerifiedDomains", mPreVerifiedDomains);
         pw.printPair("mAppPackageName", mPackageName);
+        pw.printPair("mInitialVerificationPolicy", mInitialVerificationPolicy);
+        pw.printPair("mCurrentVerificationPolicy", mCurrentVerificationPolicy);
         pw.println();
 
         pw.decreaseIndent();
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index a59f4bd..ef09976 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -30,6 +30,7 @@
 import static android.content.pm.PackageManager.INSTALL_UNARCHIVE_DRAFT;
 import static android.os.Process.INVALID_UID;
 import static android.os.Process.SYSTEM_UID;
+import static android.os.UserHandle.USER_SYSTEM;
 
 import static com.android.server.pm.PackageArchiver.isArchivingEnabled;
 import static com.android.server.pm.PackageInstallerSession.isValidVerificationPolicy;
@@ -154,7 +155,6 @@
 import java.util.TreeMap;
 import java.util.TreeSet;
 import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.IntPredicate;
 import java.util.function.Supplier;
 
@@ -281,11 +281,12 @@
     };
 
     /**
-     * Default verification policy for incoming installation sessions.
-     * TODO(b/360129657): update the default policy.
+     * Default verification policy for incoming installation sessions, mapped from userId to policy.
      */
-    private final AtomicInteger mVerificationPolicy = new AtomicInteger(
-            VERIFICATION_POLICY_BLOCK_FAIL_WARN);
+    @GuardedBy("mVerificationPolicyPerUser")
+    private final SparseIntArray mVerificationPolicyPerUser = new SparseIntArray(1);
+    // TODO(b/360129657): update the default policy.
+    private static final int DEFAULT_VERIFICATION_POLICY = VERIFICATION_POLICY_BLOCK_FAIL_WARN;
 
     private static final class Lifecycle extends SystemService {
         private final PackageInstallerService mPackageInstallerService;
@@ -342,6 +343,9 @@
                 context, mInstallThread.getLooper(), new AppStateHelper(context));
         mPackageArchiver = new PackageArchiver(mContext, mPm);
         mVerifierController = new VerifierController(mContext, mInstallHandler);
+        synchronized (mVerificationPolicyPerUser) {
+            mVerificationPolicyPerUser.put(USER_SYSTEM, DEFAULT_VERIFICATION_POLICY);
+        }
 
         LocalServices.getService(SystemServiceManager.class).startService(
                 new Lifecycle(context, this));
@@ -1051,12 +1055,17 @@
         InstallSource installSource = InstallSource.create(installerPackageName,
                 originatingPackageName, requestedInstallerPackageName, requestedInstallerPackageUid,
                 requestedInstallerPackageName, installerAttributionTag, params.packageSource);
+        final int verificationPolicy;
+        synchronized (mVerificationPolicyPerUser) {
+            verificationPolicy = mVerificationPolicyPerUser.get(
+                    userId, DEFAULT_VERIFICATION_POLICY);
+        }
         session = new PackageInstallerSession(mInternalCallback, mContext, mPm, this,
                 mSilentUpdatePolicy, mInstallThread.getLooper(), mStagingManager, sessionId,
                 userId, callingUid, installSource, params, createdMillis, 0L, stageDir, stageCid,
                 null, null, false, false, false, false, null, SessionInfo.INVALID_ID,
                 false, false, false, PackageManager.INSTALL_UNKNOWN, "", null,
-                mVerifierController, mVerificationPolicy.get());
+                mVerifierController, verificationPolicy, verificationPolicy);
 
         synchronized (mSessions) {
             mSessions.put(sessionId, session);
@@ -1882,14 +1891,22 @@
 
     @Override
     @EnforcePermission(android.Manifest.permission.VERIFICATION_AGENT)
-    public @PackageInstaller.VerificationPolicy int getVerificationPolicy() {
+    public @PackageInstaller.VerificationPolicy int getVerificationPolicy(int userId) {
         getVerificationPolicy_enforcePermission();
-        return mVerificationPolicy.get();
+        synchronized (mVerificationPolicyPerUser) {
+            if (mVerificationPolicyPerUser.indexOfKey(userId) < 0) {
+                throw new IllegalStateException(
+                        "Verification policy for user " + userId + " does not exist."
+                                + " Does the user exist?");
+            }
+            return mVerificationPolicyPerUser.get(userId);
+        }
     }
 
     @Override
     @EnforcePermission(android.Manifest.permission.VERIFICATION_AGENT)
-    public boolean setVerificationPolicy(@PackageInstaller.VerificationPolicy int policy) {
+    public boolean setVerificationPolicy(@PackageInstaller.VerificationPolicy int policy,
+            int userId) {
         setVerificationPolicy_enforcePermission();
         final int callingUid = getCallingUid();
         // Only the verifier currently bound by the system can change the policy, except for Shell
@@ -1899,12 +1916,31 @@
         if (!isValidVerificationPolicy(policy)) {
             return false;
         }
-        if (policy != mVerificationPolicy.get()) {
-            mVerificationPolicy.set(policy);
+        synchronized (mVerificationPolicyPerUser) {
+            if (mVerificationPolicyPerUser.indexOfKey(userId) < 0) {
+                throw new IllegalStateException(
+                        "Verification policy for user " + userId + " does not exist."
+                                + " Does the user exist?");
+            }
+            if (policy != mVerificationPolicyPerUser.get(userId)) {
+                mVerificationPolicyPerUser.put(userId, policy);
+            }
         }
         return true;
     }
 
+    void onUserAdded(int userId) {
+        synchronized (mVerificationPolicyPerUser) {
+            mVerificationPolicyPerUser.put(userId, DEFAULT_VERIFICATION_POLICY);
+        }
+    }
+
+    void onUserRemoved(int userId) {
+        synchronized (mVerificationPolicyPerUser) {
+            mVerificationPolicyPerUser.delete(userId);
+        }
+    }
+
     private static int getSessionCount(SparseArray<PackageInstallerSession> sessions,
             int installerUid) {
         int count = 0;
@@ -2301,6 +2337,9 @@
         }
         mSilentUpdatePolicy.dump(pw);
         mGentleUpdateHelper.dump(pw);
+        synchronized (mVerificationPolicyPerUser) {
+            pw.printPair("VerificationPolicyPerUser", mVerificationPolicyPerUser.toString());
+        }
     }
 
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 6ea5369..2a92de5 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -320,7 +320,8 @@
     private static final String ATTR_APPLICATION_ENABLED_SETTING_PERSISTENT =
             "applicationEnabledSettingPersistent";
     private static final String ATTR_DOMAIN = "domain";
-    private static final String ATTR_VERIFICATION_POLICY = "verificationPolicy";
+    private static final String ATTR_INITIAL_VERIFICATION_POLICY = "initialVerificationPolicy";
+    private static final String ATTR_CURRENT_VERIFICATION_POLICY = "currentVerificationPolicy";
 
     private static final String PROPERTY_NAME_INHERIT_NATIVE = "pi.inherit_native_on_dont_kill";
     private static final int[] EMPTY_CHILD_SESSION_ARRAY = EmptyArray.INT;
@@ -418,10 +419,14 @@
     private final PackageSessionProvider mSessionProvider;
     private final SilentUpdatePolicy mSilentUpdatePolicy;
     /**
-     * The verification policy applied to this session, which might be different from the default
-     * verification policy used by the system.
+     * The initial verification policy assigned to this session when it was first created.
      */
-    private final AtomicInteger mVerificationPolicy;
+    private final int mInitialVerificationPolicy;
+    /**
+     * The active verification policy, which might be different from the initial verification policy
+     * assigned to this session or the default policy currently used by the system.
+     */
+    private final AtomicInteger mCurrentVerificationPolicy;
     /**
      * Note all calls must be done outside {@link #mLock} to prevent lock inversion.
      */
@@ -1182,7 +1187,8 @@
             boolean isFailed, boolean isApplied, int sessionErrorCode,
             String sessionErrorMessage, DomainSet preVerifiedDomains,
             @NonNull VerifierController verifierController,
-            @PackageInstaller.VerificationPolicy int verificationPolicy) {
+            @PackageInstaller.VerificationPolicy int initialVerificationPolicy,
+            @PackageInstaller.VerificationPolicy int currentVerificationPolicy) {
         mCallback = callback;
         mContext = context;
         mPm = pm;
@@ -1192,7 +1198,8 @@
         mHandler = new Handler(looper, mHandlerCallback);
         mStagingManager = stagingManager;
         mVerifierController = verifierController;
-        mVerificationPolicy = new AtomicInteger(verificationPolicy);
+        mInitialVerificationPolicy = initialVerificationPolicy;
+        mCurrentVerificationPolicy = new AtomicInteger(currentVerificationPolicy);
 
         this.sessionId = sessionId;
         this.userId = userId;
@@ -1302,7 +1309,8 @@
                     mStageDirInUse, mDestroyed, mFds.size(), mBridges.size(), mFinalStatus,
                     mFinalMessage, params, mParentSessionId, getChildSessionIdsLocked(),
                     mSessionApplied, mSessionFailed, mSessionReady, mSessionErrorCode,
-                    mSessionErrorMessage, mPreapprovalDetails, mPreVerifiedDomains, mPackageName);
+                    mSessionErrorMessage, mPreapprovalDetails, mPreVerifiedDomains, mPackageName,
+                    mInitialVerificationPolicy, mCurrentVerificationPolicy.get());
         }
     }
 
@@ -2887,8 +2895,8 @@
             final VerifierCallback verifierCallback = new VerifierCallback();
             if (!mVerifierController.startVerificationSession(mPm::snapshotComputer, userId,
                     sessionId, getPackageName(), Uri.fromFile(stageDir), signingInfo,
-                    declaredLibraries, mVerificationPolicy.get(), /* extensionParams= */ null,
-                    verifierCallback, /* retry= */ false)) {
+                    declaredLibraries, mCurrentVerificationPolicy.get(),
+                    /* extensionParams= */ null, verifierCallback, /* retry= */ false)) {
                 // A verifier is installed but cannot be connected.
                 verifierCallback.onConnectionFailed();
             }
@@ -2967,7 +2975,7 @@
          * verification policy for this session.
          */
         public @PackageInstaller.VerificationPolicy int getVerificationPolicy() {
-            return mVerificationPolicy.get();
+            return mCurrentVerificationPolicy.get();
         }
         /**
          * Called by the VerifierController when the verifier requests to change the verification
@@ -2977,7 +2985,7 @@
             if (!isValidVerificationPolicy(policy)) {
                 return false;
             }
-            mVerificationPolicy.set(policy);
+            mCurrentVerificationPolicy.set(policy);
             return true;
         }
         /**
@@ -3023,7 +3031,7 @@
             // TODO: handle extension response
             mHandler.post(() -> {
                 if (statusReceived.isVerified()
-                        || mVerificationPolicy.get() == VERIFICATION_POLICY_NONE) {
+                        || mCurrentVerificationPolicy.get() == VERIFICATION_POLICY_NONE) {
                     // Continue with the rest of the verification and installation.
                     resumeVerify();
                     return;
@@ -3067,13 +3075,14 @@
         }
 
         private void handleNonPackageBlockedFailure(Runnable onFailWarning, Runnable onFailClosed) {
-            final Runnable r = switch (mVerificationPolicy.get()) {
+            final Runnable r = switch (mCurrentVerificationPolicy.get()) {
                 case VERIFICATION_POLICY_NONE, VERIFICATION_POLICY_BLOCK_FAIL_OPEN ->
                         PackageInstallerSession.this::resumeVerify;
                 case VERIFICATION_POLICY_BLOCK_FAIL_WARN -> onFailWarning;
                 case VERIFICATION_POLICY_BLOCK_FAIL_CLOSED -> onFailClosed;
                 default -> {
-                    Log.wtf(TAG, "Unknown verification policy: " + mVerificationPolicy.get());
+                    Log.wtf(TAG, "Unknown verification policy: "
+                            + mCurrentVerificationPolicy.get());
                     yield onFailClosed;
                 }
             };
@@ -5436,12 +5445,21 @@
     }
 
     /**
+     * @return the initial policy for the verification request assigned to the session when created.
+     */
+    @VisibleForTesting
+    public @PackageInstaller.VerificationPolicy int getInitialVerificationPolicy() {
+        assertCallerIsOwnerOrRoot();
+        return mInitialVerificationPolicy;
+    }
+
+    /**
      * @return the current policy for the verification request associated with this session.
      */
     @VisibleForTesting
-    public @PackageInstaller.VerificationPolicy int getVerificationPolicy() {
+    public @PackageInstaller.VerificationPolicy int getCurrentVerificationPolicy() {
         assertCallerIsOwnerOrRoot();
-        return mVerificationPolicy.get();
+        return mCurrentVerificationPolicy.get();
     }
 
     void setSessionReady() {
@@ -5681,6 +5699,8 @@
         if (mPreVerifiedDomains != null) {
             pw.printPair("mPreVerifiedDomains", mPreVerifiedDomains);
         }
+        pw.printPair("mInitialVerificationPolicy", mInitialVerificationPolicy);
+        pw.printPair("mCurrentVerificationPolicy", mCurrentVerificationPolicy.get());
         pw.println();
 
         pw.decreaseIndent();
@@ -5905,7 +5925,9 @@
             out.attributeInt(null, ATTR_INSTALL_REASON, params.installReason);
             writeBooleanAttribute(out, ATTR_APPLICATION_ENABLED_SETTING_PERSISTENT,
                     params.applicationEnabledSettingPersistent);
-            out.attributeInt(null, ATTR_VERIFICATION_POLICY, mVerificationPolicy.get());
+            out.attributeInt(null, ATTR_INITIAL_VERIFICATION_POLICY, mInitialVerificationPolicy);
+            out.attributeInt(null, ATTR_CURRENT_VERIFICATION_POLICY,
+                    mCurrentVerificationPolicy.get());
 
             final boolean isDataLoader = params.dataLoaderParams != null;
             writeBooleanAttribute(out, ATTR_IS_DATALOADER, isDataLoader);
@@ -6056,8 +6078,10 @@
         final boolean sealed = in.getAttributeBoolean(null, ATTR_SEALED, false);
         final int parentSessionId = in.getAttributeInt(null, ATTR_PARENT_SESSION_ID,
                 SessionInfo.INVALID_ID);
-        final int verificationPolicy = in.getAttributeInt(null, ATTR_VERIFICATION_POLICY,
-                VERIFICATION_POLICY_NONE);
+        final int initialVerificationPolicy = in.getAttributeInt(null,
+                ATTR_INITIAL_VERIFICATION_POLICY, VERIFICATION_POLICY_NONE);
+        final int currentVerificationPolicy = in.getAttributeInt(null,
+                ATTR_CURRENT_VERIFICATION_POLICY, VERIFICATION_POLICY_NONE);
 
         final SessionParams params = new SessionParams(
                 SessionParams.MODE_INVALID);
@@ -6233,6 +6257,6 @@
                 stageCid, fileArray, checksumsMap, prepared, committed, destroyed, sealed,
                 childSessionIdsArray, parentSessionId, isReady, isFailed, isApplied,
                 sessionErrorCode, sessionErrorMessage, preVerifiedDomains, verifierController,
-                verificationPolicy);
+                initialVerificationPolicy, currentVerificationPolicy);
     }
 }
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 807445e..9d48efe9 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -4421,6 +4421,7 @@
             mPendingBroadcasts.remove(userId);
             mAppsFilter.onUserDeleted(snapshotComputer(), userId);
             mPermissionManager.onUserRemoved(userId);
+            mInstallerService.onUserRemoved(userId);
         }
         mInstantAppRegistry.onUserRemoved(userId);
         mPackageMonitorCallbackHelper.onUserRemoved(userId);
@@ -4471,6 +4472,7 @@
             mLegacyPermissionManager.grantDefaultPermissions(userId);
             mPermissionManager.setDefaultPermissionGrantFingerprint(Build.FINGERPRINT, userId);
             mDomainVerificationManager.clearUser(userId);
+            mInstallerService.onUserAdded(userId);
         }
     }
 
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index f8e56e1..7ef3582 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -4676,7 +4676,7 @@
         try {
             final IPackageInstaller installer = mInterface.getPackageInstaller();
             // TODO(b/360129657): global verification policy should be per user
-            final int policy = installer.getVerificationPolicy();
+            final int policy = installer.getVerificationPolicy(translatedUserId);
             pw.println(policy);
         } catch (Exception e) {
             pw.println("Failure [" + e.getMessage() + "]");
@@ -4717,7 +4717,8 @@
         try {
             final IPackageInstaller installer = mInterface.getPackageInstaller();
             // TODO(b/360129657): global verification policy should be per user
-            final boolean success = installer.setVerificationPolicy(Integer.parseInt(policyStr));
+            final boolean success = installer.setVerificationPolicy(Integer.parseInt(policyStr),
+                    translatedUserId);
             if (!success) {
                 pw.println("Failure setting verification policy.");
                 return 1;
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 6baab25..7eb0c42 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -28,6 +28,7 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import static com.android.server.utils.TimingsTraceAndSlog.SYSTEM_SERVER_TIMING_TAG;
+import static com.android.tradeinmode.flags.Flags.enableTradeInMode;
 
 import android.annotation.NonNull;
 import android.annotation.StringRes;
@@ -1399,10 +1400,6 @@
         mSystemServiceManager.startService(BatteryService.class);
         t.traceEnd();
 
-        t.traceBegin("StartTradeInModeService");
-        mSystemServiceManager.startService(TradeInModeService.class);
-        t.traceEnd();
-
         // Tracks application usage stats.
         t.traceBegin("StartUsageService");
         mSystemServiceManager.startService(UsageStatsService.class);
@@ -1772,6 +1769,13 @@
                 mSystemServiceManager.startService(AdvancedProtectionService.Lifecycle.class);
                 t.traceEnd();
             }
+
+            if (!isWatch && !isTv && !isAutomotive && enableTradeInMode()) {
+                t.traceBegin("StartTradeInModeService");
+                mSystemServiceManager.startService(TradeInModeService.class);
+                t.traceEnd();
+            }
+
         } catch (Throwable e) {
             Slog.e("System", "******************************************");
             Slog.e("System", "************ Failure starting core service");
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
index 2bc8af1..5bb6b19 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
@@ -85,6 +85,8 @@
     private static final String DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD =
             "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 0";
 
+    private final DeviceFlagsValueProvider mFlagsValueProvider = new DeviceFlagsValueProvider();
+
     private Instrumentation mInstrumentation;
     private UiDevice mUiDevice;
     private Context mContext;
@@ -95,7 +97,7 @@
     private boolean mShowImeWithHardKeyboardEnabled;
 
     @Rule
-    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+    public final CheckFlagsRule mCheckFlagsRule = new CheckFlagsRule(mFlagsValueProvider);
 
     @Before
     public void setUp() throws Exception {
@@ -159,7 +161,7 @@
 
         // Press home key to hide soft keyboard.
         Log.i(TAG, "Press home");
-        if (Flags.refactorInsetsController()) {
+        if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
             assertThat(mUiDevice.pressHome()).isTrue();
             // The IME visibility is only sent at the end of the animation. Therefore, we have to
             // wait until the visibility was sent to the server and the IME window hidden.
@@ -774,7 +776,7 @@
         backButtonUiObject.click();
         mInstrumentation.waitForIdleSync();
 
-        if (Flags.refactorInsetsController()) {
+        if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
             // The IME visibility is only sent at the end of the animation. Therefore, we have to
             // wait until the visibility was sent to the server and the IME window hidden.
             eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isFalse());
@@ -812,7 +814,7 @@
         backButtonUiObject.longClick();
         mInstrumentation.waitForIdleSync();
 
-        if (Flags.refactorInsetsController()) {
+        if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
             // The IME visibility is only sent at the end of the animation. Therefore, we have to
             // wait until the visibility was sent to the server and the IME window hidden.
             eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isFalse());
@@ -900,7 +902,7 @@
         assertWithMessage("Input Method Switcher Menu is shown")
                 .that(isInputMethodPickerShown(imm))
                 .isTrue();
-        if (Flags.refactorInsetsController()) {
+        if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
             // The IME visibility is only sent at the end of the animation. Therefore, we have to
             // wait until the visibility was sent to the server and the IME window hidden.
             eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isFalse());
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
index 6afcae7..3aeab09 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
@@ -76,8 +76,10 @@
 @RunWith(AndroidJUnit4.class)
 public class DefaultImeVisibilityApplierTest extends InputMethodManagerServiceTestBase {
 
+    private final DeviceFlagsValueProvider mFlagsValueProvider = new DeviceFlagsValueProvider();
+
     @Rule
-    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+    public final CheckFlagsRule mCheckFlagsRule = new CheckFlagsRule(mFlagsValueProvider);
     private DefaultImeVisibilityApplier mVisibilityApplier;
 
     @Before
@@ -151,7 +153,7 @@
             mVisibilityApplier.applyImeVisibility(mWindowToken, ImeTracker.Token.empty(),
                     STATE_HIDE_IME_EXPLICIT, eq(SoftInputShowHideReason.NOT_SET), mUserId);
         }
-        if (Flags.refactorInsetsController()) {
+        if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
             verifySetImeVisibility(true /* setVisible */, false /* invoked */);
             verifySetImeVisibility(false /* setVisible */, true /* invoked */);
         } else {
@@ -168,7 +170,7 @@
             mVisibilityApplier.applyImeVisibility(mWindowToken, ImeTracker.Token.empty(),
                     STATE_HIDE_IME_NOT_ALWAYS, eq(SoftInputShowHideReason.NOT_SET), mUserId);
         }
-        if (Flags.refactorInsetsController()) {
+        if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
             verifySetImeVisibility(true /* setVisible */, false /* invoked */);
             verifySetImeVisibility(false /* setVisible */, true /* invoked */);
         } else {
@@ -182,7 +184,7 @@
             mVisibilityApplier.applyImeVisibility(mWindowToken, ImeTracker.Token.empty(),
                     STATE_SHOW_IME_IMPLICIT, eq(SoftInputShowHideReason.NOT_SET), mUserId);
         }
-        if (Flags.refactorInsetsController()) {
+        if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
             verifySetImeVisibility(true /* setVisible */, true /* invoked */);
             verifySetImeVisibility(false /* setVisible */, false /* invoked */);
         } else {
@@ -260,7 +262,7 @@
             verify(mVisibilityApplier).applyImeVisibility(
                     eq(mWindowToken), any(), eq(STATE_HIDE_IME),
                     eq(SoftInputShowHideReason.NOT_SET), eq(mUserId) /* userId */);
-            if (!Flags.refactorInsetsController()) {
+            if (!mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
                 verify(mInputMethodManagerService.mWindowManagerInternal).hideIme(eq(mWindowToken),
                         eq(displayIdToShowIme), and(not(eq(statsToken)), notNull()));
             }
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
index 4d956b2..c958bd3 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
@@ -39,8 +39,10 @@
 import android.os.IBinder;
 import android.os.LocaleList;
 import android.os.RemoteException;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.util.Log;
 import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.Flags;
 import android.window.ImeOnBackInvokedDispatcher;
 
 import com.android.internal.inputmethod.IInputMethodClient;
@@ -89,6 +91,9 @@
             };
     private static final int DEFAULT_SOFT_INPUT_FLAG =
             StartInputFlags.VIEW_HAS_FOCUS | StartInputFlags.IS_TEXT_EDITOR;
+
+    private final DeviceFlagsValueProvider mFlagsValueProvider = new DeviceFlagsValueProvider();
+
     @Mock
     VirtualDeviceManagerInternal mMockVdmInternal;
 
@@ -125,7 +130,7 @@
             case SOFT_INPUT_STATE_UNSPECIFIED:
                 boolean showSoftInput =
                         (mSoftInputAdjustment == SOFT_INPUT_ADJUST_RESIZE) || mIsLargeScreen;
-                if (android.view.inputmethod.Flags.refactorInsetsController()) {
+                if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
                     verifySetImeVisibility(true /* setVisible */, showSoftInput /* invoked */);
                     // A hide can only be triggered if there is no editorFocused, which this test
                     // always sets.
@@ -141,7 +146,7 @@
                 break;
             case SOFT_INPUT_STATE_VISIBLE:
             case SOFT_INPUT_STATE_ALWAYS_VISIBLE:
-                if (android.view.inputmethod.Flags.refactorInsetsController()) {
+                if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
                     verifySetImeVisibility(true /* setVisible */, true /* invoked */);
                     verifySetImeVisibility(false /* setVisible */, false /* invoked */);
                 } else {
@@ -150,7 +155,7 @@
                 }
                 break;
             case SOFT_INPUT_STATE_UNCHANGED:
-                if (android.view.inputmethod.Flags.refactorInsetsController()) {
+                if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
                     verifySetImeVisibility(true /* setVisible */, false /* invoked */);
                     verifySetImeVisibility(false /* setVisible */, false /* invoked */);
                 } else {
@@ -160,7 +165,7 @@
                 break;
             case SOFT_INPUT_STATE_HIDDEN:
             case SOFT_INPUT_STATE_ALWAYS_HIDDEN:
-                if (android.view.inputmethod.Flags.refactorInsetsController()) {
+                if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
                     verifySetImeVisibility(true /* setVisible */, false /* invoked */);
                     // In this case, we don't have to manipulate the requested visible types of
                     // the WindowState, as they're already in the correct state
@@ -192,7 +197,7 @@
             case SOFT_INPUT_STATE_UNSPECIFIED:
                 boolean hideSoftInput =
                         (mSoftInputAdjustment != SOFT_INPUT_ADJUST_RESIZE) && !mIsLargeScreen;
-                if (android.view.inputmethod.Flags.refactorInsetsController()) {
+                if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
                     // A show can only be triggered in forward navigation
                     verifySetImeVisibility(false /* setVisible */, false /* invoked */);
                     // A hide can only be triggered if there is no editorFocused, which this test
@@ -209,7 +214,7 @@
             case SOFT_INPUT_STATE_VISIBLE:
             case SOFT_INPUT_STATE_HIDDEN:
             case SOFT_INPUT_STATE_UNCHANGED: // Do nothing
-                if (android.view.inputmethod.Flags.refactorInsetsController()) {
+                if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
                     verifySetImeVisibility(true /* setVisible */, false /* invoked */);
                     verifySetImeVisibility(false /* setVisible */, false /* invoked */);
                 } else {
@@ -218,7 +223,7 @@
                 }
                 break;
             case SOFT_INPUT_STATE_ALWAYS_VISIBLE:
-                if (android.view.inputmethod.Flags.refactorInsetsController()) {
+                if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
                     verifySetImeVisibility(true /* setVisible */, true /* invoked */);
                     verifySetImeVisibility(false /* setVisible */, false /* invoked */);
                 } else {
@@ -227,7 +232,7 @@
                 }
                 break;
             case SOFT_INPUT_STATE_ALWAYS_HIDDEN:
-                if (android.view.inputmethod.Flags.refactorInsetsController()) {
+                if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
                     verifySetImeVisibility(true /* setVisible */, false /* invoked */);
                     // In this case, we don't have to manipulate the requested visible types of
                     // the WindowState, as they're already in the correct state
diff --git a/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt b/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt
index 5c4716d..7d5532f 100644
--- a/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt
+++ b/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt
@@ -57,6 +57,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
+import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mockito.any
 import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.doReturn
@@ -383,6 +384,10 @@
                     android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)) {
                 PackageManager.PERMISSION_GRANTED
             }
+            whenever(this.checkPermission(
+                eq(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL), anyInt(), anyInt())) {
+                PackageManager.PERMISSION_GRANTED
+            }
         }
         val mockSharedLibrariesImpl: SharedLibrariesImpl = mock {
             whenever(this.snapshot()) { this@mock }
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt
index 1e89359..09d0e4a 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt
@@ -22,6 +22,7 @@
 import android.content.pm.PackageInstaller.SessionParams.PERMISSION_STATE_DENIED
 import android.content.pm.PackageInstaller.SessionParams.PERMISSION_STATE_GRANTED
 import android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED
+import android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_OPEN
 import android.content.pm.PackageManager
 import android.content.pm.verify.domain.DomainSet
 import android.os.Parcel
@@ -199,7 +200,8 @@
             /* stagedSessionErrorMessage */ "some error",
             /* preVerifiedDomains */ DomainSet(setOf("com.foo", "com.bar")),
             /* VerifierController */ mock(VerifierController::class.java),
-            VERIFICATION_POLICY_BLOCK_FAIL_CLOSED
+            /* initialVerificationPolicy */ VERIFICATION_POLICY_BLOCK_FAIL_OPEN,
+            /* currentVerificationPolicy */ VERIFICATION_POLICY_BLOCK_FAIL_CLOSED
         )
     }
 
@@ -342,7 +344,8 @@
         assertThat(expected.childSessionIds).asList()
             .containsExactlyElementsIn(actual.childSessionIds.toList())
         assertThat(expected.preVerifiedDomains).isEqualTo(actual.preVerifiedDomains)
-        assertThat(expected.verificationPolicy).isEqualTo(actual.verificationPolicy)
+        assertThat(expected.initialVerificationPolicy).isEqualTo(actual.initialVerificationPolicy)
+        assertThat(expected.currentVerificationPolicy).isEqualTo(actual.currentVerificationPolicy)
     }
 
     private fun assertInstallSourcesEquivalent(expected: InstallSource, actual: InstallSource) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java
index 124c41e..591e8df 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java
@@ -742,7 +742,10 @@
                 /* stagedSessionErrorMessage */ "no error",
                 /* preVerifiedDomains */ null,
                 /* verifierController */ null,
-                /* verificationPolicy */ PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED);
+                /* initialVerificationPolicy */ 
+                PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED,
+                /* currentVerificationPolicy */
+                PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED);
 
         StagingManager.StagedSession stagedSession = spy(session.mStagedSession);
         doReturn(packageName).when(stagedSession).getPackageName();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/SystemZenRulesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/SystemZenRulesTest.java
index 5de323b..4d82c3c 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/SystemZenRulesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/SystemZenRulesTest.java
@@ -209,18 +209,30 @@
     }
 
     @Test
-    public void getShortDaysSummary_onlyDays() {
+    public void getDaysOfWeekShort_summarizesDays() {
         ScheduleInfo scheduleInfo = new ScheduleInfo();
         scheduleInfo.startHour = 10;
         scheduleInfo.endHour = 16;
         scheduleInfo.days = new int[] {Calendar.MONDAY, Calendar.TUESDAY,
                 Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY};
 
-        assertThat(SystemZenRules.getShortDaysSummary(mContext, scheduleInfo))
+        assertThat(SystemZenRules.getDaysOfWeekShort(mContext, scheduleInfo))
                 .isEqualTo("Mon-Fri");
     }
 
     @Test
+    public void getDaysOfWeekFull_summarizesDays() {
+        ScheduleInfo scheduleInfo = new ScheduleInfo();
+        scheduleInfo.startHour = 10;
+        scheduleInfo.endHour = 16;
+        scheduleInfo.days = new int[] {Calendar.MONDAY, Calendar.TUESDAY,
+                Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY};
+
+        assertThat(SystemZenRules.getDaysOfWeekFull(mContext, scheduleInfo))
+                .isEqualTo("Monday to Friday");
+    }
+
+    @Test
     public void getTimeSummary_onlyTime() {
         ScheduleInfo scheduleInfo = new ScheduleInfo();
         scheduleInfo.startHour = 11;
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
index 4ac567c..1c6bd11 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
@@ -109,11 +109,7 @@
         if (motionEventHelper.inputMethod == TOUCH
             && Flags.enableHoldToDragAppHandle()) {
             // Touch requires hold-to-drag.
-            val downTime = SystemClock.uptimeMillis()
-            motionEventHelper.actionDown(startX, startY, time = downTime)
-            SystemClock.sleep(100L) // hold for 100ns before starting the move.
-            motionEventHelper.actionMove(startX, startY, startX, endY, 100, downTime = downTime)
-            motionEventHelper.actionUp(startX, endY, downTime = downTime)
+            motionEventHelper.holdToDrag(startX, startY, startX, endY, steps = 100)
         } else {
             device.drag(startX, startY, startX, endY, 100)
         }
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MotionEventHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MotionEventHelper.kt
index 86a0b0f..1fe6088 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MotionEventHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MotionEventHelper.kt
@@ -54,7 +54,15 @@
         injectMotionEvent(ACTION_UP, x, y, downTime = downTime)
     }
 
-    fun actionMove(startX: Int, startY: Int, endX: Int, endY: Int, steps: Int, downTime: Long) {
+    fun actionMove(
+        startX: Int,
+        startY: Int,
+        endX: Int,
+        endY: Int,
+        steps: Int,
+        downTime: Long,
+        withMotionEventInjectDelay: Boolean = false
+    ) {
         val incrementX = (endX - startX).toFloat() / (steps - 1)
         val incrementY = (endY - startY).toFloat() / (steps - 1)
 
@@ -65,9 +73,33 @@
 
             val moveEvent = getMotionEvent(downTime, time, ACTION_MOVE, x, y)
             injectMotionEvent(moveEvent)
+            if (withMotionEventInjectDelay) {
+                SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS)
+            }
         }
     }
 
+    /**
+     * Drag from [startX], [startY] to [endX], [endY] with a "hold" period after touching down
+     * and before moving.
+     */
+    fun holdToDrag(startX: Int, startY: Int, endX: Int, endY: Int, steps: Int) {
+        val downTime = SystemClock.uptimeMillis()
+        actionDown(startX, startY, time = downTime)
+        SystemClock.sleep(100L) // Hold before dragging.
+        actionMove(
+            startX,
+            startY,
+            endX,
+            endY,
+            steps,
+            downTime,
+            withMotionEventInjectDelay = true
+        )
+        SystemClock.sleep(REGULAR_CLICK_LENGTH)
+        actionUp(startX, endX, downTime)
+    }
+
     private fun injectMotionEvent(
         action: Int,
         x: Int,
@@ -120,4 +152,9 @@
         event.displayId = 0
         return event
     }
+
+    companion object {
+        private const val MOTION_EVENT_INJECTION_DELAY_MILLIS = 5L
+        private const val REGULAR_CLICK_LENGTH = 100L
+    }
 }
\ No newline at end of file
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index 498e431..232b402 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -1574,7 +1574,10 @@
   // If the file path ends with .flata, .jar, .jack, or .zip the file is treated
   // as ZIP archive and the files within are merged individually.
   // Otherwise the file is processed on its own.
-  bool MergePath(const std::string& path, bool override) {
+  bool MergePath(std::string path, bool override) {
+    if (path.size() > 2 && util::StartsWith(path, "'") && util::EndsWith(path, "'")) {
+      path = path.substr(1, path.size() - 2);
+    }
     if (util::EndsWith(path, ".flata") || util::EndsWith(path, ".jar") ||
         util::EndsWith(path, ".jack") || util::EndsWith(path, ".zip")) {
       return MergeArchive(path, override);