Merge "[Catalyst] Support Set API" into main
diff --git a/ACTIVITY_SECURITY_OWNERS b/ACTIVITY_SECURITY_OWNERS
new file mode 100644
index 0000000..c39842e
--- /dev/null
+++ b/ACTIVITY_SECURITY_OWNERS
@@ -0,0 +1,2 @@
+haok@google.com
+wnan@google.com
\ No newline at end of file
diff --git a/INTENT_OWNERS b/INTENT_OWNERS
index 58b5f2a..c828215 100644
--- a/INTENT_OWNERS
+++ b/INTENT_OWNERS
@@ -1,3 +1,4 @@
 include /PACKAGE_MANAGER_OWNERS
 include /services/core/java/com/android/server/wm/OWNERS
 include /services/core/java/com/android/server/am/OWNERS
+include /ACTIVITY_SECURITY_OWNERS
\ No newline at end of file
diff --git a/apex/jobscheduler/framework/aconfig/job.aconfig b/apex/jobscheduler/framework/aconfig/job.aconfig
index 5f55075..4be16a6 100644
--- a/apex/jobscheduler/framework/aconfig/job.aconfig
+++ b/apex/jobscheduler/framework/aconfig/job.aconfig
@@ -30,3 +30,10 @@
    description: "Enables automatic cancellation of jobs due to leaked JobParameters, reducing unnecessary battery drain and improving system efficiency. This includes logging and traces for better issue diagnosis."
    bug: "349688611"
 }
+
+flag {
+    name: "ignore_important_while_foreground"
+    namespace: "backstage_power"
+    description: "Ignore the important_while_foreground flag and change the related APIs to be not effective"
+    bug: "374175032"
+}
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index 5f57c39..cc2d104 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -809,9 +809,21 @@
     }
 
     /**
+     * <p class="caution"><strong>Note:</strong> Beginning with
+     * {@link android.os.Build.VERSION_CODES#B}, this flag will be ignored and no longer
+     * function effectively, regardless of the calling app's target SDK version.
+     * Calling this method will always return {@code false}.
+     *
      * @see JobInfo.Builder#setImportantWhileForeground(boolean)
+     *
+     * @deprecated Use {@link #isExpedited()} instead.
      */
+    @FlaggedApi(Flags.FLAG_IGNORE_IMPORTANT_WHILE_FOREGROUND)
+    @Deprecated
     public boolean isImportantWhileForeground() {
+        if (Flags.ignoreImportantWhileForeground()) {
+            return false;
+        }
         return (flags & FLAG_IMPORTANT_WHILE_FOREGROUND) != 0;
     }
 
@@ -2124,6 +2136,13 @@
          * <p>
          * Jobs marked as important-while-foreground are given {@link #PRIORITY_HIGH} by default.
          *
+         * <p class="caution"><strong>Note:</strong> Beginning with
+         * {@link android.os.Build.VERSION_CODES#B}, this flag will be ignored and no longer
+         * function effectively, regardless of the calling app's target SDK version.
+         * {link #isImportantWhileForeground()} will always return {@code false}.
+         * Apps should use {link #setExpedited(boolean)} with {@code true} to indicate
+         * that this job is important and needs to run as soon as possible.
+         *
          * @param importantWhileForeground whether to relax doze restrictions for this job when the
          *                                 app is in the foreground. False by default.
          * @see JobInfo#isImportantWhileForeground()
@@ -2131,6 +2150,12 @@
          */
         @Deprecated
         public Builder setImportantWhileForeground(boolean importantWhileForeground) {
+            if (Flags.ignoreImportantWhileForeground()) {
+                Log.w(TAG, "Requested important-while-foreground flag for job" + mJobId
+                        + " is ignored and takes no effect");
+                return this;
+            }
+
             if (importantWhileForeground) {
                 mFlags |= FLAG_IMPORTANT_WHILE_FOREGROUND;
                 if (mPriority == PRIORITY_DEFAULT) {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index 97c6e25..4c1951a 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -5864,6 +5864,9 @@
             pw.print(android.app.job.Flags.FLAG_BACKUP_JOBS_EXEMPTION,
                     android.app.job.Flags.backupJobsExemption());
             pw.println();
+            pw.print(android.app.job.Flags.FLAG_IGNORE_IMPORTANT_WHILE_FOREGROUND,
+                    android.app.job.Flags.ignoreImportantWhileForeground());
+            pw.println();
             pw.decreaseIndent();
             pw.println();
 
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
index ac240cc..68303e8 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
@@ -436,6 +436,9 @@
             case android.app.job.Flags.FLAG_BACKUP_JOBS_EXEMPTION:
                 pw.println(android.app.job.Flags.backupJobsExemption());
                 break;
+            case android.app.job.Flags.FLAG_IGNORE_IMPORTANT_WHILE_FOREGROUND:
+                pw.println(android.app.job.Flags.ignoreImportantWhileForeground());
+                break;
             default:
                 pw.println("Unknown flag: " + flagName);
                 break;
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java
index d5c9ae6..abec170 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java
@@ -210,7 +210,9 @@
     }
 
     private boolean updateTaskStateLocked(JobStatus task, final long nowElapsed) {
-        final boolean allowInIdle = ((task.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0)
+        final boolean allowInIdle =
+                (!android.app.job.Flags.ignoreImportantWhileForeground()
+                        && ((task.getFlags() & JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0))
                 && (mForegroundUids.get(task.getSourceUid()) || isTempWhitelistedLocked(task));
         final boolean whitelisted = isWhitelistedLocked(task);
         final boolean enableTask = !mDeviceIdleMode || whitelisted || allowInIdle;
@@ -219,7 +221,8 @@
 
     @Override
     public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
-        if ((jobStatus.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) {
+        if (!android.app.job.Flags.ignoreImportantWhileForeground()
+                && (jobStatus.getFlags() & JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) {
             mAllowInIdleJobs.add(jobStatus);
         }
         updateTaskStateLocked(jobStatus, sElapsedRealtimeClock.millis());
@@ -227,7 +230,8 @@
 
     @Override
     public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) {
-        if ((jobStatus.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) {
+        if (!android.app.job.Flags.ignoreImportantWhileForeground()
+                && (jobStatus.getFlags() & JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) {
             mAllowInIdleJobs.remove(jobStatus);
         }
     }
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
index 37e2fe2..ff4af69 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -793,7 +793,9 @@
                     || isTopStartedJobLocked(jobStatus)
                     || isUidInForeground(jobStatus.getSourceUid());
             final boolean isJobImportant = jobStatus.getEffectivePriority() >= JobInfo.PRIORITY_HIGH
-                    || (jobStatus.getFlags() & JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0;
+                    || (!android.app.job.Flags.ignoreImportantWhileForeground()
+                            && (jobStatus.getFlags()
+                                    & JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0);
             if (isInPrivilegedState && isJobImportant) {
                 return mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS;
             }
diff --git a/core/api/current.txt b/core/api/current.txt
index 94481b6..13e1210 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -9113,7 +9113,7 @@
     method public long getTriggerContentUpdateDelay();
     method @Nullable public android.app.job.JobInfo.TriggerContentUri[] getTriggerContentUris();
     method public boolean isExpedited();
-    method public boolean isImportantWhileForeground();
+    method @Deprecated @FlaggedApi("android.app.job.ignore_important_while_foreground") public boolean isImportantWhileForeground();
     method public boolean isPeriodic();
     method public boolean isPersisted();
     method public boolean isPrefetch();
@@ -19034,7 +19034,9 @@
     method @FlaggedApi("android.hardware.biometrics.last_authentication_time") @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public long getLastAuthenticationTime(int);
     method @NonNull @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public android.hardware.biometrics.BiometricManager.Strings getStrings(int);
     field public static final int BIOMETRIC_ERROR_HW_UNAVAILABLE = 1; // 0x1
+    field @FlaggedApi("android.hardware.biometrics.identity_check_api") public static final int BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE = 20; // 0x14
     field public static final int BIOMETRIC_ERROR_NONE_ENROLLED = 11; // 0xb
+    field @FlaggedApi("android.hardware.biometrics.identity_check_api") public static final int BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS = 21; // 0x15
     field public static final int BIOMETRIC_ERROR_NO_HARDWARE = 12; // 0xc
     field public static final int BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED = 15; // 0xf
     field @FlaggedApi("android.hardware.biometrics.last_authentication_time") public static final long BIOMETRIC_NO_AUTHENTICATION = -1L; // 0xffffffffffffffffL
@@ -19045,6 +19047,7 @@
     field public static final int BIOMETRIC_STRONG = 15; // 0xf
     field public static final int BIOMETRIC_WEAK = 255; // 0xff
     field public static final int DEVICE_CREDENTIAL = 32768; // 0x8000
+    field @FlaggedApi("android.hardware.biometrics.identity_check_api") public static final int IDENTITY_CHECK = 65536; // 0x10000
   }
 
   public static class BiometricManager.Strings {
@@ -19077,8 +19080,10 @@
     field public static final int BIOMETRIC_ERROR_CANCELED = 5; // 0x5
     field public static final int BIOMETRIC_ERROR_HW_NOT_PRESENT = 12; // 0xc
     field public static final int BIOMETRIC_ERROR_HW_UNAVAILABLE = 1; // 0x1
+    field @FlaggedApi("android.hardware.biometrics.identity_check_api") public static final int BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE = 20; // 0x14
     field public static final int BIOMETRIC_ERROR_LOCKOUT = 7; // 0x7
     field public static final int BIOMETRIC_ERROR_LOCKOUT_PERMANENT = 9; // 0x9
+    field @FlaggedApi("android.hardware.biometrics.identity_check_api") public static final int BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS = 21; // 0x15
     field public static final int BIOMETRIC_ERROR_NO_BIOMETRICS = 11; // 0xb
     field public static final int BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL = 14; // 0xe
     field public static final int BIOMETRIC_ERROR_NO_SPACE = 4; // 0x4
@@ -34377,6 +34382,7 @@
     method public static android.os.VibrationEffect createWaveform(long[], int[], int);
     method public int describeContents();
     method @NonNull public static android.os.VibrationEffect.Composition startComposition();
+    method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public static android.os.VibrationEffect.WaveformEnvelopeBuilder startWaveformEnvelope();
     field @NonNull public static final android.os.Parcelable.Creator<android.os.VibrationEffect> CREATOR;
     field public static final int DEFAULT_AMPLITUDE = -1; // 0xffffffff
     field public static final int EFFECT_CLICK = 0; // 0x0
@@ -34400,6 +34406,11 @@
     field public static final int PRIMITIVE_TICK = 7; // 0x7
   }
 
+  @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public static final class VibrationEffect.WaveformEnvelopeBuilder {
+    method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect.WaveformEnvelopeBuilder addControlPoint(@FloatRange(from=0, to=1) float, @FloatRange(from=0) float, int);
+    method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect build();
+  }
+
   public abstract class Vibrator {
     method public final int areAllEffectsSupported(@NonNull int...);
     method public final boolean areAllPrimitivesSupported(@NonNull int...);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 79bea01..58ab073 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -2771,6 +2771,17 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.os.vibrator.PrimitiveSegment> CREATOR;
   }
 
+  @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public final class PwleSegment extends android.os.vibrator.VibrationEffectSegment {
+    method public int describeContents();
+    method public long getDuration();
+    method public float getEndAmplitude();
+    method public float getEndFrequencyHz();
+    method public float getStartAmplitude();
+    method public float getStartFrequencyHz();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.os.vibrator.PwleSegment> CREATOR;
+  }
+
   public final class RampSegment extends android.os.vibrator.VibrationEffectSegment {
     method public int describeContents();
     method public long getDuration();
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/appwidget/flags.aconfig b/core/java/android/appwidget/flags.aconfig
index 3839b5f..e5c94fc 100644
--- a/core/java/android/appwidget/flags.aconfig
+++ b/core/java/android/appwidget/flags.aconfig
@@ -55,7 +55,7 @@
   name: "remote_views_proto"
   namespace: "app_widgets"
   description: "Enable support for persisting RemoteViews previews to Protobuf"
-  bug: "306546610"
+  bug: "364420494"
 }
 
 flag {
diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java
index 1d4403c..8e02ffd 100644
--- a/core/java/android/content/pm/UserProperties.java
+++ b/core/java/android/content/pm/UserProperties.java
@@ -16,9 +16,11 @@
 
 package android.content.pm;
 
+import android.Manifest;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
@@ -513,6 +515,7 @@
      * Note that, internally, this does not perform an exact copy.
      * @hide
      */
+    @SuppressLint("MissingPermission")
     public UserProperties(UserProperties orig,
             boolean exposeAllFields,
             boolean hasManagePermission,
@@ -614,12 +617,10 @@
      *    {@link #SHOW_IN_SETTINGS_SEPARATE},
      *    and {@link #SHOW_IN_SETTINGS_NO}.
      *
-     * <p> The caller must have {@link android.Manifest.permission#MANAGE_USERS} to query this
-     * property.
-     *
      * @return whether, and how, a profile should be shown in the Settings.
      * @hide
      */
+    @RequiresPermission(Manifest.permission.MANAGE_USERS)
     public @ShowInSettings int getShowInSettings() {
         if (isPresent(INDEX_SHOW_IN_SETTINGS)) return mShowInSettings;
         if (mDefaultProperties != null) return mDefaultProperties.mShowInSettings;
@@ -690,6 +691,8 @@
     /**
      * Returns whether a profile should be started when its parent starts (unless in quiet mode).
      * This only applies for users that have parents (i.e. for profiles).
+     *
+     * Only available to the SYSTEM uid.
      * @hide
      */
     public boolean getStartWithParent() {
@@ -708,6 +711,8 @@
      * Returns whether an app in the profile should be deleted when the same package in
      * the parent user is being deleted.
      * This only applies for users that have parents (i.e. for profiles).
+     *
+     * Only available to the SYSTEM uid.
      * @hide
      */
     public boolean getDeleteAppWithParent() {
@@ -726,6 +731,8 @@
      * Returns whether the user should always
      * be {@link android.os.UserManager#isUserVisible() visible}.
      * The intended usage is for the Communal Profile, which is running and accessible at all times.
+     *
+     * Only available to the SYSTEM uid.
      * @hide
      */
     public boolean getAlwaysVisible() {
@@ -747,6 +754,7 @@
      * Possible return values include
      * {@link #INHERIT_DEVICE_POLICY_FROM_PARENT} or {@link #INHERIT_DEVICE_POLICY_NO}
      *
+     * Only available to the SYSTEM uid.
      * @hide
      */
     public @InheritDevicePolicy int getInheritDevicePolicy() {
@@ -777,6 +785,7 @@
      * @return whether contacts access from an associated profile is enabled for the user
      * @hide
      */
+    @RequiresPermission(Manifest.permission.MANAGE_USERS)
     public boolean getUseParentsContacts() {
         if (isPresent(INDEX_USE_PARENTS_CONTACTS)) return mUseParentsContacts;
         if (mDefaultProperties != null) return mDefaultProperties.mUseParentsContacts;
@@ -796,7 +805,9 @@
 
     /**
      * Returns true if user needs to update default
-     * {@link com.android.server.pm.CrossProfileIntentFilter} with its parents during an OTA update
+     * {@link com.android.server.pm.CrossProfileIntentFilter} with its parents during an OTA update.
+     *
+     * Only available to the SYSTEM uid.
      * @hide
      */
     public boolean getUpdateCrossProfileIntentFiltersOnOTA() {
@@ -863,6 +874,7 @@
      * checks is not guaranteed when the property is false and may vary depending on user types.
      * @hide
      */
+    @RequiresPermission(Manifest.permission.MANAGE_USERS)
     public boolean isAuthAlwaysRequiredToDisableQuietMode() {
         if (isPresent(INDEX_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE)) {
             return mAuthAlwaysRequiredToDisableQuietMode;
@@ -894,6 +906,8 @@
      * locking for a user can happen if either the device configuration is set or if this property
      * is set. When both, the config and the property value is false, the user storage is always
      * locked when the user is stopped.
+     *
+     * Only available to the SYSTEM uid.
      * @hide
      */
     public boolean getAllowStoppingUserWithDelayedLocking() {
@@ -915,6 +929,8 @@
 
     /**
      * Returns the user's {@link CrossProfileIntentFilterAccessControlLevel}.
+     *
+     * Only available to the SYSTEM uid.
      * @hide
      */
     public @CrossProfileIntentFilterAccessControlLevel int
@@ -944,6 +960,7 @@
      * Returns the user's {@link CrossProfileIntentResolutionStrategy}.
      * @return user's {@link CrossProfileIntentResolutionStrategy}.
      *
+     * Only available to the SYSTEM uid.
      * @hide
      */
     public @CrossProfileIntentResolutionStrategy int getCrossProfileIntentResolutionStrategy() {
@@ -1052,32 +1069,47 @@
 
     @Override
     public String toString() {
+        StringBuilder s = new StringBuilder();
+        s.append("UserProperties{");
+        s.append("mPropertiesPresent="); s.append(Long.toBinaryString(mPropertiesPresent));
+        try {
+            s.append(listPropertiesAsStringBuilder());
+        } catch (SecurityException e) {
+            // Caller doesn't have permission to see all the properties. Just don't share them.
+        }
+        s.append("}");
+        return s.toString();
+    }
+
+    private StringBuilder listPropertiesAsStringBuilder() {
+        final StringBuilder s = new StringBuilder();
+
         // Please print in increasing order of PropertyIndex.
-        return "UserProperties{"
-                + "mPropertiesPresent=" + Long.toBinaryString(mPropertiesPresent)
-                + ", mShowInLauncher=" + getShowInLauncher()
-                + ", mStartWithParent=" + getStartWithParent()
-                + ", mShowInSettings=" + getShowInSettings()
-                + ", mInheritDevicePolicy=" + getInheritDevicePolicy()
-                + ", mUseParentsContacts=" + getUseParentsContacts()
-                + ", mUpdateCrossProfileIntentFiltersOnOTA="
-                + getUpdateCrossProfileIntentFiltersOnOTA()
-                + ", mCrossProfileIntentFilterAccessControl="
-                + getCrossProfileIntentFilterAccessControl()
-                + ", mCrossProfileIntentResolutionStrategy="
-                + getCrossProfileIntentResolutionStrategy()
-                + ", mMediaSharedWithParent=" + isMediaSharedWithParent()
-                + ", mCredentialShareableWithParent=" + isCredentialShareableWithParent()
-                + ", mAuthAlwaysRequiredToDisableQuietMode="
-                + isAuthAlwaysRequiredToDisableQuietMode()
-                + ", mAllowStoppingUserWithDelayedLocking="
-                + getAllowStoppingUserWithDelayedLocking()
-                + ", mDeleteAppWithParent=" + getDeleteAppWithParent()
-                + ", mAlwaysVisible=" + getAlwaysVisible()
-                + ", mCrossProfileContentSharingStrategy=" + getCrossProfileContentSharingStrategy()
-                + ", mProfileApiVisibility=" + getProfileApiVisibility()
-                + ", mItemsRestrictedOnHomeScreen=" + areItemsRestrictedOnHomeScreen()
-                + "}";
+        s.append(", mShowInLauncher="); s.append(getShowInLauncher());
+        s.append(", mStartWithParent="); s.append(getStartWithParent());
+        s.append(", mShowInSettings="); s.append(getShowInSettings());
+        s.append(", mInheritDevicePolicy="); s.append(getInheritDevicePolicy());
+        s.append(", mUseParentsContacts="); s.append(getUseParentsContacts());
+        s.append(", mUpdateCrossProfileIntentFiltersOnOTA=");
+        s.append(getUpdateCrossProfileIntentFiltersOnOTA());
+        s.append(", mCrossProfileIntentFilterAccessControl=");
+        s.append(getCrossProfileIntentFilterAccessControl());
+        s.append(", mCrossProfileIntentResolutionStrategy=");
+        s.append(getCrossProfileIntentResolutionStrategy());
+        s.append(", mMediaSharedWithParent="); s.append(isMediaSharedWithParent());
+        s.append(", mCredentialShareableWithParent="); s.append(isCredentialShareableWithParent());
+        s.append(", mAuthAlwaysRequiredToDisableQuietMode=");
+        s.append(isAuthAlwaysRequiredToDisableQuietMode());
+        s.append(", mAllowStoppingUserWithDelayedLocking=");
+        s.append(getAllowStoppingUserWithDelayedLocking());
+        s.append(", mDeleteAppWithParent="); s.append(getDeleteAppWithParent());
+        s.append(", mAlwaysVisible="); s.append(getAlwaysVisible());
+        s.append(", mCrossProfileContentSharingStrategy=");
+        s.append(getCrossProfileContentSharingStrategy());
+        s.append(", mProfileApiVisibility="); s.append(getProfileApiVisibility());
+        s.append(", mItemsRestrictedOnHomeScreen="); s.append(areItemsRestrictedOnHomeScreen());
+
+        return s;
     }
 
     /**
diff --git a/core/java/android/database/BulkCursorNative.java b/core/java/android/database/BulkCursorNative.java
index 8ea450c..41585b3 100644
--- a/core/java/android/database/BulkCursorNative.java
+++ b/core/java/android/database/BulkCursorNative.java
@@ -53,7 +53,7 @@
 
         return new BulkCursorProxy(obj);
     }
-    
+
     @Override
     public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
             throws RemoteException {
@@ -79,7 +79,7 @@
                     reply.writeNoException();
                     return true;
                 }
-                
+
                 case CLOSE_TRANSACTION: {
                     data.enforceInterface(IBulkCursor.descriptor);
                     close();
@@ -212,15 +212,22 @@
         Parcel reply = Parcel.obtain();
         try {
             data.writeInterfaceToken(IBulkCursor.descriptor);
-
-            mRemote.transact(CLOSE_TRANSACTION, data, reply, 0);
-            DatabaseUtils.readExceptionFromParcel(reply);
+            // If close() is being called from the finalizer thread, do not wait for a reply from
+            // the remote side.
+            final boolean fromFinalizer =
+                    android.database.sqlite.Flags.onewayFinalizerClose()
+                    && "FinalizerDaemon".equals(Thread.currentThread().getName());
+            mRemote.transact(CLOSE_TRANSACTION, data, reply,
+                    fromFinalizer ? IBinder.FLAG_ONEWAY: 0);
+            if (!fromFinalizer) {
+                DatabaseUtils.readExceptionFromParcel(reply);
+            }
         } finally {
             data.recycle();
             reply.recycle();
         }
     }
-    
+
     public int requery(IContentObserver observer) throws RemoteException {
         Parcel data = Parcel.obtain();
         Parcel reply = Parcel.obtain();
@@ -282,4 +289,3 @@
         }
     }
 }
-
diff --git a/core/java/android/database/sqlite/flags.aconfig b/core/java/android/database/sqlite/flags.aconfig
index d5a7db8..826b908 100644
--- a/core/java/android/database/sqlite/flags.aconfig
+++ b/core/java/android/database/sqlite/flags.aconfig
@@ -2,6 +2,13 @@
 container: "system"
 
 flag {
+     name: "oneway_finalizer_close"
+     namespace: "system_performance"
+     description: "Make BuildCursorNative.close oneway if in the the finalizer"
+     bug: "368221351"
+}
+
+flag {
      name: "sqlite_apis_35"
      is_exported: true
      namespace: "system_performance"
diff --git a/core/java/android/hardware/biometrics/BiometricConstants.java b/core/java/android/hardware/biometrics/BiometricConstants.java
index 9355937..f649e47 100644
--- a/core/java/android/hardware/biometrics/BiometricConstants.java
+++ b/core/java/android/hardware/biometrics/BiometricConstants.java
@@ -164,15 +164,18 @@
     int BIOMETRIC_ERROR_POWER_PRESSED = 19;
 
     /**
-     * Mandatory biometrics is not in effect.
-     * @hide
+     * Identity Check is currently not active.
+     *
+     * This device either doesn't have this feature enabled, or it's not considered in a
+     * high-risk environment that requires extra security measures for accessing sensitive data.
      */
-    int BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE = 20;
+    @FlaggedApi(Flags.FLAG_IDENTITY_CHECK_API)
+    int BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE = 20;
 
     /**
-     * Biometrics is not allowed to verify in apps.
-     * @hide
+     * Biometrics is not allowed to verify the user in apps.
      */
+    @FlaggedApi(Flags.FLAG_IDENTITY_CHECK_API)
     int BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS = 21;
 
     /**
@@ -204,6 +207,8 @@
             BIOMETRIC_ERROR_NEGATIVE_BUTTON,
             BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL,
             BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED,
+            BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE,
+            BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS,
             BIOMETRIC_PAUSED_REJECTED})
     @Retention(RetentionPolicy.SOURCE)
     @interface Errors {}
diff --git a/core/java/android/hardware/biometrics/BiometricManager.java b/core/java/android/hardware/biometrics/BiometricManager.java
index a4f7485f..c690c67 100644
--- a/core/java/android/hardware/biometrics/BiometricManager.java
+++ b/core/java/android/hardware/biometrics/BiometricManager.java
@@ -87,16 +87,19 @@
             BiometricConstants.BIOMETRIC_ERROR_LOCKOUT;
 
     /**
-     * Mandatory biometrics is not effective.
-     * @hide
+     * Identity Check is currently not active.
+     *
+     * This device either doesn't have this feature enabled, or it's not considered in a
+     * high-risk environment that requires extra security measures for accessing sensitive data.
      */
-    public static final int BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE =
-            BiometricConstants.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE;
+    @FlaggedApi(Flags.FLAG_IDENTITY_CHECK_API)
+    public static final int BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE =
+            BiometricConstants.BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE;
 
     /**
-     * Biometrics is not allowed to verify in apps.
-     * @hide
+     * Biometrics is not allowed to verify the user in apps.
      */
+    @FlaggedApi(Flags.FLAG_IDENTITY_CHECK_API)
     public static final int BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS =
             BiometricConstants.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS;
 
@@ -136,7 +139,7 @@
             BIOMETRIC_ERROR_NO_HARDWARE,
             BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED,
             BIOMETRIC_ERROR_LOCKOUT,
-            BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE})
+            BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE})
     @Retention(RetentionPolicy.SOURCE)
     public @interface BiometricError {}
 
@@ -160,7 +163,7 @@
                 BIOMETRIC_WEAK,
                 BIOMETRIC_CONVENIENCE,
                 DEVICE_CREDENTIAL,
-                MANDATORY_BIOMETRICS,
+                IDENTITY_CHECK,
         })
         @Retention(RetentionPolicy.SOURCE)
         @interface Types {}
@@ -239,20 +242,24 @@
         int DEVICE_CREDENTIAL = 1 << 15;
 
         /**
-         * The bit is used to request for mandatory biometrics.
+         * The bit is used to request for Identity Check.
          *
-         * <p> The requirements to trigger mandatory biometrics are as follows:
-         * 1. User must have enabled the toggle for mandatory biometrics is settings
-         * 2. User must have enrollments for all {@link #BIOMETRIC_STRONG} sensors available
-         * 3. The device must not be in a trusted location
+         * Identity Check is a feature which requires class 3 biometric authentication to access
+         * sensitive surfaces when the device is outside trusted places.
+         *
+         * <p> The requirements to trigger Identity Check are as follows:
+         * 1. User must have enabled the toggle for Identity Check in settings
+         * 2. User must have enrollments for at least one {@link #BIOMETRIC_STRONG} sensor
+         * 3. The device is determined to be in a high risk environment, for example if it is
+         *    outside of the user's trusted locations or fails to meet similar conditions.
+         * 4. The Identity Check requirements bit must be true
          * </p>
          *
          * <p> If all the above conditions are satisfied, only {@link #BIOMETRIC_STRONG} sensors
          * will be eligible for authentication, and device credential fallback will be dropped.
-         * @hide
          */
-        int MANDATORY_BIOMETRICS = 1 << 16;
-
+        @FlaggedApi(Flags.FLAG_IDENTITY_CHECK_API)
+        int IDENTITY_CHECK = 1 << 16;
     }
 
     /**
diff --git a/core/java/android/hardware/biometrics/PromptInfo.java b/core/java/android/hardware/biometrics/PromptInfo.java
index df5d864..e23ffeb 100644
--- a/core/java/android/hardware/biometrics/PromptInfo.java
+++ b/core/java/android/hardware/biometrics/PromptInfo.java
@@ -199,7 +199,7 @@
         } else if (mContentView != null && isContentViewMoreOptionsButtonUsed()) {
             return true;
         } else if (Flags.mandatoryBiometrics()
-                && (mAuthenticators & BiometricManager.Authenticators.MANDATORY_BIOMETRICS)
+                && (mAuthenticators & BiometricManager.Authenticators.IDENTITY_CHECK)
                 != 0) {
             return true;
         }
diff --git a/core/java/android/hardware/biometrics/flags.aconfig b/core/java/android/hardware/biometrics/flags.aconfig
index 26ffa11..52a4898 100644
--- a/core/java/android/hardware/biometrics/flags.aconfig
+++ b/core/java/android/hardware/biometrics/flags.aconfig
@@ -47,3 +47,10 @@
   description: "This flag controls Whether to enable fp unlock when screen turns off on udfps devices"
   bug: "373792870"
 }
+
+flag {
+  name: "identity_check_api"
+  namespace: "biometrics_framework"
+  description: "This flag is for API changes related to Identity Check"
+  bug: "373424727"
+}
diff --git a/core/java/android/os/CombinedVibration.java b/core/java/android/os/CombinedVibration.java
index f1d3957..8fbba4f 100644
--- a/core/java/android/os/CombinedVibration.java
+++ b/core/java/android/os/CombinedVibration.java
@@ -194,7 +194,6 @@
         int[] getAvailableVibratorIds();
 
         /** Adapts a {@link VibrationEffect} to a given vibrator. */
-        @NonNull
         VibrationEffect adaptToVibrator(int vibratorId, @NonNull VibrationEffect effect);
     }
 
@@ -442,6 +441,10 @@
             boolean hasSameEffects = true;
             for (int vibratorId : adapter.getAvailableVibratorIds()) {
                 VibrationEffect newEffect = adapter.adaptToVibrator(vibratorId, mEffect);
+                if (newEffect == null) {
+                    // The vibration effect contains unsupported segments and cannot be played.
+                    return null;
+                }
                 combination.addVibrator(vibratorId, newEffect);
                 hasSameEffects &= mEffect.equals(newEffect);
             }
@@ -649,6 +652,10 @@
                 int vibratorId = mEffects.keyAt(i);
                 VibrationEffect effect = mEffects.valueAt(i);
                 VibrationEffect newEffect = adapter.adaptToVibrator(vibratorId, effect);
+                if (newEffect == null) {
+                    // The vibration effect contains unsupported segments and cannot be played.
+                    return null;
+                }
                 combination.addVibrator(vibratorId, newEffect);
                 hasSameEffects &= effect.equals(newEffect);
             }
diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java
index 61dd11f..d8094ab 100644
--- a/core/java/android/os/VibrationEffect.java
+++ b/core/java/android/os/VibrationEffect.java
@@ -37,6 +37,7 @@
 import android.os.vibrator.Flags;
 import android.os.vibrator.PrebakedSegment;
 import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.PwleSegment;
 import android.os.vibrator.RampSegment;
 import android.os.vibrator.StepSegment;
 import android.os.vibrator.VibrationEffectSegment;
@@ -1692,6 +1693,147 @@
     }
 
     /**
+     * Start building a waveform vibration.
+     *
+     * <p>The waveform envelope builder offers more flexibility for creating waveform effects,
+     * allowing control over vibration amplitude and frequency via smooth transitions between
+     * values.
+     *
+     * <p>Note: To check whether waveform envelope effects are supported, use
+     * {@link Vibrator#areEnvelopeEffectsSupported()}.
+     *
+     * @see VibrationEffect.WaveformEnvelopeBuilder
+     */
+    @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    @NonNull
+    public static VibrationEffect.WaveformEnvelopeBuilder startWaveformEnvelope() {
+        return new WaveformEnvelopeBuilder();
+    }
+
+    /**
+     * A builder for waveform effects described by its envelope.
+     *
+     * <p>Waveform effect envelopes are defined by one or more control points describing a target
+     * vibration amplitude and frequency, and a duration to reach those targets. The vibrator
+     * will perform smooth transitions between control points.
+     *
+     * <p>For example, the following code ramps a vibrator from off to full amplitude at 120Hz over
+     * 100ms, holds that state for 200ms, and then ramps back down over 100ms:
+     *
+     * <pre>{@code
+     * VibrationEffect effect = VibrationEffect.startWaveformEnvelope()
+     *     .addControlPoint(1.0f, 120f, 100)
+     *     .addControlPoint(1.0f, 120f, 200)
+     *     .addControlPoint(0.0f, 120f, 100)
+     *     .build();
+     * }</pre>
+     *
+     * <p>It is crucial to ensure that the frequency range used in your effect is compatible with
+     * the device's capabilities. The framework will not play any frequencies that fall partially
+     * or completely outside the device's supported range. It will also not attempt to correct or
+     * modify these frequencies.
+     *
+     * <p>Therefore, it is strongly recommended that you design your haptic effects with the
+     * device's frequency profile in mind. You can obtain the supported frequency range and other
+     * relevant frequency-related information by getting the
+     * {@link android.os.vibrator.VibratorFrequencyProfile} using the
+     * {@link Vibrator#getFrequencyProfile()} method.
+     *
+     * <p>In addition to these limitations, when designing vibration patterns, it is important to
+     * consider the physical limitations of the vibration actuator. These limitations include
+     * factors such as the maximum number of control points allowed in an envelope effect, the
+     * minimum and maximum durations permitted for each control point, and the maximum overall
+     * duration of the effect. If a pattern exceeds the maximum number of allowed control points,
+     * the framework will automatically break down the effect to ensure it plays correctly.
+     *
+     * <p>You can use the following APIs to obtain these limits:
+     * <ul>
+     * <li>Maximum envelope control points: {@link Vibrator#getMaxEnvelopeEffectSize()}</li>
+     * <li>Minimum control point duration:
+     * {@link Vibrator#getMinEnvelopeEffectControlPointDurationMillis()}</li>
+     * <li>Maximum control point duration:
+     * {@link Vibrator#getMaxEnvelopeEffectControlPointDurationMillis()}</li>
+     * <li>Maximum total effect duration: {@link Vibrator#getMaxEnvelopeEffectDurationMillis()}</li>
+     * </ul>
+     *
+     * @see VibrationEffect#startWaveformEnvelope()
+     */
+    @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public static final class WaveformEnvelopeBuilder {
+
+        private ArrayList<PwleSegment> mSegments = new ArrayList<>();
+        private float mLastAmplitude = 0f;
+        private float mLastFrequencyHz = 0f;
+
+        private WaveformEnvelopeBuilder() {}
+
+        /**
+         * Adds a new control point to the end of this waveform envelope.
+         *
+         * <p>Amplitude defines the vibrator's strength at this frequency, ranging from 0 (off) to 1
+         * (maximum achievable strength). This value scales linearly with output strength, not
+         * perceived intensity. It's determined by the actuator response curve.
+         *
+         * <p>Frequency must be greater than zero and within the supported range. To determine
+         * the supported range, use {@link Vibrator#getFrequencyProfile()}. This method returns a
+         * {@link android.os.vibrator.VibratorFrequencyProfile} object, which contains the
+         * minimum and maximum frequencies, among other frequency-related information. Creating
+         * effects using frequencies outside this range will result in the vibration not playing.
+         *
+         * <p>Time specifies the duration (in milliseconds) for the vibrator to smoothly transition
+         * from the previous control point to this new one. It must be greater than zero. To
+         * transition as quickly as possible, use
+         * {@link Vibrator#getMinEnvelopeEffectControlPointDurationMillis()}.
+         *
+         * @param amplitude   The amplitude value between 0 and 1, inclusive. 0 represents the
+         *                    vibrator being off, and 1 represents the maximum achievable amplitude
+         *                    at this frequency.
+         * @param frequencyHz The frequency in Hz, must be greater than zero.
+         * @param timeMillis  The transition time in milliseconds.
+         */
+        @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+        @SuppressWarnings("MissingGetterMatchingBuilder") // No getters to segments once created.
+        @NonNull
+        public WaveformEnvelopeBuilder addControlPoint(
+                @FloatRange(from = 0, to = 1) float amplitude,
+                @FloatRange(from = 0) float frequencyHz, int timeMillis) {
+
+            if (mSegments.isEmpty()) {
+                mLastFrequencyHz = frequencyHz;
+            }
+
+            mSegments.add(new PwleSegment(mLastAmplitude, amplitude, mLastFrequencyHz, frequencyHz,
+                    timeMillis));
+
+            mLastAmplitude = amplitude;
+            mLastFrequencyHz = frequencyHz;
+
+            return this;
+        }
+
+        /**
+         * Build the waveform as a single {@link VibrationEffect}.
+         *
+         * <p>The {@link WaveformEnvelopeBuilder} object is still valid after this call, so you can
+         * continue adding more primitives to it and generating more {@link VibrationEffect}s by
+         * calling this method again.
+         *
+         * @return The {@link VibrationEffect} resulting from the list of control points.
+         */
+        @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+        @NonNull
+        public VibrationEffect build() {
+            if (mSegments.isEmpty()) {
+                throw new IllegalStateException(
+                        "WaveformEnvelopeBuilder must have at least one control point to build.");
+            }
+            VibrationEffect effect = new Composed(mSegments, /* repeatIndex= */ -1);
+            effect.validate();
+            return effect;
+        }
+    }
+
+    /**
      * A builder for waveform haptic effects.
      *
      * <p>Waveform vibrations constitute of one or more timed transitions to new sets of vibration
diff --git a/core/java/android/os/vibrator/PwleSegment.java b/core/java/android/os/vibrator/PwleSegment.java
new file mode 100644
index 0000000..9074bde
--- /dev/null
+++ b/core/java/android/os/vibrator/PwleSegment.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os.vibrator;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.VibrationEffect;
+import android.os.VibratorInfo;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * A {@link VibrationEffectSegment} that represents a smooth transition from the starting
+ * amplitude and frequency to new values over a specified duration.
+ *
+ * <p>The amplitudes are expressed by float values in the range [0, 1], representing the relative
+ * output acceleration for the vibrator. The frequencies are expressed in hertz by positive finite
+ * float values.
+ * @hide
+ */
+@TestApi
+@FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+public final class PwleSegment extends VibrationEffectSegment {
+    private final float mStartAmplitude;
+    private final float mStartFrequencyHz;
+    private final float mEndAmplitude;
+    private final float mEndFrequencyHz;
+    private final int mDuration;
+
+    PwleSegment(@android.annotation.NonNull Parcel in) {
+        this(in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat(), in.readInt());
+    }
+
+    /** @hide */
+    @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public PwleSegment(float startAmplitude, float endAmplitude, float startFrequencyHz,
+            float endFrequencyHz, int duration) {
+        mStartAmplitude = startAmplitude;
+        mEndAmplitude = endAmplitude;
+        mStartFrequencyHz = startFrequencyHz;
+        mEndFrequencyHz = endFrequencyHz;
+        mDuration = duration;
+    }
+
+    public float getStartAmplitude() {
+        return mStartAmplitude;
+    }
+
+    public float getEndAmplitude() {
+        return mEndAmplitude;
+    }
+
+    public float getStartFrequencyHz() {
+        return mStartFrequencyHz;
+    }
+
+    public float getEndFrequencyHz() {
+        return mEndFrequencyHz;
+    }
+
+    @Override
+    public long getDuration() {
+        return mDuration;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof PwleSegment)) {
+            return false;
+        }
+        PwleSegment other = (PwleSegment) o;
+        return Float.compare(mStartAmplitude, other.mStartAmplitude) == 0
+                && Float.compare(mEndAmplitude, other.mEndAmplitude) == 0
+                && Float.compare(mStartFrequencyHz, other.mStartFrequencyHz) == 0
+                && Float.compare(mEndFrequencyHz, other.mEndFrequencyHz) == 0
+                && mDuration == other.mDuration;
+    }
+
+    /** @hide */
+    @Override
+    public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) {
+        boolean areFeaturesSupported = vibratorInfo.areEnvelopeEffectsSupported();
+
+        // Check that the frequency is within the supported range
+        float minFrequency = vibratorInfo.getFrequencyProfile().getMinFrequencyHz();
+        float maxFrequency = vibratorInfo.getFrequencyProfile().getMaxFrequencyHz();
+
+        areFeaturesSupported &=
+                mStartFrequencyHz >= minFrequency && mStartFrequencyHz <= maxFrequency
+                        && mEndFrequencyHz >= minFrequency && mEndFrequencyHz <= maxFrequency;
+
+        return areFeaturesSupported;
+    }
+
+    /** @hide */
+    @Override
+    public boolean isHapticFeedbackCandidate() {
+        return true;
+    }
+
+    /** @hide */
+    @Override
+    public void validate() {
+        Preconditions.checkArgumentPositive(mStartFrequencyHz,
+                "Start frequency must be greater than zero.");
+        Preconditions.checkArgumentPositive(mEndFrequencyHz,
+                "End frequency must be greater than zero.");
+        Preconditions.checkArgumentPositive(mDuration, "Time must be greater than zero.");
+
+        Preconditions.checkArgumentInRange(mStartAmplitude, 0f, 1f, "startAmplitude");
+        Preconditions.checkArgumentInRange(mEndAmplitude, 0f, 1f, "endAmplitude");
+    }
+
+    /** @hide */
+    @NonNull
+    @Override
+    public PwleSegment resolve(int defaultAmplitude) {
+        return this;
+    }
+
+    /** @hide */
+    @NonNull
+    @Override
+    public PwleSegment scale(float scaleFactor) {
+        float newStartAmplitude = VibrationEffect.scale(mStartAmplitude, scaleFactor);
+        float newEndAmplitude = VibrationEffect.scale(mEndAmplitude, scaleFactor);
+        if (Float.compare(mStartAmplitude, newStartAmplitude) == 0
+                && Float.compare(mEndAmplitude, newEndAmplitude) == 0) {
+            return this;
+        }
+        return new PwleSegment(newStartAmplitude, newEndAmplitude, mStartFrequencyHz,
+                mEndFrequencyHz,
+                mDuration);
+    }
+
+    /** @hide */
+    @NonNull
+    @Override
+    public PwleSegment scaleLinearly(float scaleFactor) {
+        float newStartAmplitude = VibrationEffect.scaleLinearly(mStartAmplitude, scaleFactor);
+        float newEndAmplitude = VibrationEffect.scaleLinearly(mEndAmplitude, scaleFactor);
+        if (Float.compare(mStartAmplitude, newStartAmplitude) == 0
+                && Float.compare(mEndAmplitude, newEndAmplitude) == 0) {
+            return this;
+        }
+        return new PwleSegment(newStartAmplitude, newEndAmplitude, mStartFrequencyHz,
+                mEndFrequencyHz,
+                mDuration);
+    }
+
+    /** @hide */
+    @NonNull
+    @Override
+    public PwleSegment applyEffectStrength(int effectStrength) {
+        return this;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mStartAmplitude, mEndAmplitude, mStartFrequencyHz, mEndFrequencyHz,
+                mDuration);
+    }
+
+    @Override
+    public String toString() {
+        return "Pwle{startAmplitude=" + mStartAmplitude
+                + ", endAmplitude=" + mEndAmplitude
+                + ", startFrequencyHz=" + mStartFrequencyHz
+                + ", endFrequencyHz=" + mEndFrequencyHz
+                + ", duration=" + mDuration
+                + "}";
+    }
+
+    /** @hide */
+    @Override
+    public String toDebugString() {
+        return String.format(Locale.US, "Pwle=%dms(amplitude=%.2f @ %.2fHz to %.2f @ %.2fHz)",
+                mDuration,
+                mStartAmplitude,
+                mStartFrequencyHz,
+                mEndAmplitude,
+                mEndFrequencyHz);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(PARCEL_TOKEN_PWLE);
+        dest.writeFloat(mStartAmplitude);
+        dest.writeFloat(mEndAmplitude);
+        dest.writeFloat(mStartFrequencyHz);
+        dest.writeFloat(mEndFrequencyHz);
+        dest.writeInt(mDuration);
+    }
+
+    @android.annotation.NonNull
+    public static final Creator<PwleSegment> CREATOR =
+            new Creator<PwleSegment>() {
+                @Override
+                public PwleSegment createFromParcel(Parcel in) {
+                    // Skip the type token
+                    in.readInt();
+                    return new PwleSegment(in);
+                }
+
+                @Override
+                public PwleSegment[] newArray(int size) {
+                    return new PwleSegment[size];
+                }
+            };
+}
diff --git a/core/java/android/os/vibrator/VibrationEffectSegment.java b/core/java/android/os/vibrator/VibrationEffectSegment.java
index dadc849..b934e11 100644
--- a/core/java/android/os/vibrator/VibrationEffectSegment.java
+++ b/core/java/android/os/vibrator/VibrationEffectSegment.java
@@ -46,6 +46,7 @@
     static final int PARCEL_TOKEN_PRIMITIVE = 2;
     static final int PARCEL_TOKEN_STEP = 3;
     static final int PARCEL_TOKEN_RAMP = 4;
+    static final int PARCEL_TOKEN_PWLE = 5;
 
     /** Prevent subclassing from outside of this package */
     VibrationEffectSegment() {
@@ -223,6 +224,11 @@
                             return new PrebakedSegment(in);
                         case PARCEL_TOKEN_PRIMITIVE:
                             return new PrimitiveSegment(in);
+                        case PARCEL_TOKEN_PWLE:
+                            if (Flags.normalizedPwleEffects()) {
+                                return new PwleSegment(in);
+                            }
+                            // Fall through if the flag is not enabled.
                         default:
                             throw new IllegalStateException(
                                     "Unexpected vibration event type token in parcel.");
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/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/proto/android/service/appwidget.proto b/core/proto/android/service/appwidget.proto
index 97350ef..fb90719 100644
--- a/core/proto/android/service/appwidget.proto
+++ b/core/proto/android/service/appwidget.proto
@@ -20,6 +20,8 @@
 option java_multiple_files = true;
 option java_outer_classname = "AppWidgetServiceProto";
 
+import "frameworks/base/core/proto/android/widget/remoteviews.proto";
+
 // represents the object holding the dump info of the app widget service
 message AppWidgetServiceDumpProto {
     repeated WidgetProto widgets = 1; // the array of bound widgets
@@ -38,3 +40,14 @@
     optional int32 maxHeight = 9;
     optional bool restoreCompleted = 10;
 }
+
+// represents a set of widget previews for a particular provider
+message GeneratedPreviewsProto {
+    repeated Preview previews = 1;
+
+    // represents a particular RemoteViews preview, which may be set for multiple categories
+    message Preview {
+        repeated int32 widget_categories = 1;
+        optional android.widget.RemoteViewsProto views = 2;
+    }
+}
\ No newline at end of file
diff --git a/core/res/res/values-watch-v36/colors.xml b/core/res/res/values-watch-v36/colors.xml
index 6cb9b85..4bc2a66 100644
--- a/core/res/res/values-watch-v36/colors.xml
+++ b/core/res/res/values-watch-v36/colors.xml
@@ -15,9 +15,4 @@
   -->
 <!-- TODO(b/372524566): update color token's value to match material3 design. -->
 <resources>
-    <color name="system_primary_dark">#E9DDFF</color>
-    <color name="system_primary_fixed_dim">#D0BCFF</color>
-    <color name="system_on_primary_dark">#210F48</color>
-    <color name="system_primary_container_dark">#4D3D76</color>
-    <color name="system_on_primary_container_dark">#F6EDFF</color>
-</resources>
+</resources>
\ No newline at end of file
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/BroadcastRadioTests/src/android/hardware/radio/RadioAlertUnitTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioAlertUnitTest.java
index 7afdde2..9cfb9af 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioAlertUnitTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioAlertUnitTest.java
@@ -21,6 +21,7 @@
 import android.os.Parcel;
 import android.platform.test.annotations.EnableFlags;
 
+import com.google.common.primitives.Ints;
 import com.google.common.truth.Expect;
 
 import org.junit.Rule;
@@ -34,6 +35,20 @@
 
     private static final int TEST_FLAGS = 0;
     private static final int CREATOR_ARRAY_SIZE = 3;
+    private static final int TEST_STATUS = RadioAlert.STATUS_ACTUAL;
+    private static final int TEST_TYPE = RadioAlert.MESSAGE_TYPE_ALERT;
+    private static final int[] TEST_CATEGORIES_1 = new int[]{RadioAlert.CATEGORY_CBRNE,
+            RadioAlert.CATEGORY_GEO};
+    private static final int[] TEST_CATEGORIES_2 = new int[]{RadioAlert.CATEGORY_CBRNE,
+            RadioAlert.CATEGORY_FIRE};
+    private static final int TEST_URGENCY_1 = RadioAlert.URGENCY_EXPECTED;
+    private static final int TEST_URGENCY_2 = RadioAlert.URGENCY_FUTURE;
+    private static final int TEST_SEVERITY_1 = RadioAlert.SEVERITY_SEVERE;
+    private static final int TEST_SEVERITY_2 = RadioAlert.SEVERITY_MODERATE;
+    private static final int TEST_CERTAINTY_1 = RadioAlert.CERTAINTY_POSSIBLE;
+    private static final int TEST_CERTAINTY_2 = RadioAlert.CERTAINTY_UNLIKELY;
+    private static final String TEST_DESCRIPTION_MESSAGE_1 = "Test Alert Description Message 1.";
+    private static final String TEST_DESCRIPTION_MESSAGE_2 = "Test Alert Description Message 2.";
     private static final String TEST_GEOCODE_VALUE_NAME = "SAME";
     private static final String TEST_GEOCODE_VALUE_1 = "006109";
     private static final String TEST_GEOCODE_VALUE_2 = "006009";
@@ -54,6 +69,18 @@
             List.of(TEST_POLYGON), List.of(TEST_GEOCODE_1));
     private static final RadioAlert.AlertArea TEST_AREA_2 = new RadioAlert.AlertArea(
             new ArrayList<>(), List.of(TEST_GEOCODE_1, TEST_GEOCODE_2));
+    private static final List<RadioAlert.AlertArea> TEST_AREA_LIST_1 = List.of(TEST_AREA_1);
+    private static final List<RadioAlert.AlertArea> TEST_AREA_LIST_2 = List.of(TEST_AREA_2);
+    private static final String TEST_LANGUAGE_1 = "en-US";
+
+    private static final RadioAlert.AlertInfo TEST_ALERT_INFO_1 = new RadioAlert.AlertInfo(
+            TEST_CATEGORIES_1, TEST_URGENCY_1, TEST_SEVERITY_1, TEST_CERTAINTY_1,
+            TEST_DESCRIPTION_MESSAGE_1, TEST_AREA_LIST_1, TEST_LANGUAGE_1);
+    private static final RadioAlert.AlertInfo TEST_ALERT_INFO_2 = new RadioAlert.AlertInfo(
+            TEST_CATEGORIES_2, TEST_URGENCY_2, TEST_SEVERITY_2, TEST_CERTAINTY_2,
+            TEST_DESCRIPTION_MESSAGE_2, TEST_AREA_LIST_2, /* language= */ null);
+    private static final RadioAlert TEST_ALERT = new RadioAlert(TEST_STATUS, TEST_TYPE,
+            List.of(TEST_ALERT_INFO_1, TEST_ALERT_INFO_2));
 
     @Rule
     public final Expect mExpect = Expect.create();
@@ -374,4 +401,209 @@
         mExpect.withMessage("Non-alert-area object").that(TEST_AREA_1)
                 .isNotEqualTo(TEST_GEOCODE_1);
     }
+
+    @Test
+    public void constructor_withNullCategories_forAlertInfo_fails() {
+        NullPointerException thrown = assertThrows(NullPointerException.class, () ->
+                new RadioAlert.AlertInfo(/* categories= */ null, TEST_URGENCY_1, TEST_SEVERITY_1,
+                        TEST_CERTAINTY_1, TEST_DESCRIPTION_MESSAGE_1, TEST_AREA_LIST_1,
+                        TEST_LANGUAGE_1));
+
+        mExpect.withMessage("Exception for alert info constructor with null categories")
+                .that(thrown).hasMessageThat().contains("Categories can not be null");
+    }
+
+    @Test
+    public void constructor_withNullAreaList_forAlertInfo_fails() {
+        NullPointerException thrown = assertThrows(NullPointerException.class, () ->
+                new RadioAlert.AlertInfo(TEST_CATEGORIES_1, TEST_URGENCY_1, TEST_SEVERITY_1,
+                        TEST_CERTAINTY_1, TEST_DESCRIPTION_MESSAGE_1, /* areaList= */ null,
+                        TEST_LANGUAGE_1));
+
+        mExpect.withMessage("Exception for alert info constructor with null area list")
+                .that(thrown).hasMessageThat().contains("Area list can not be null");
+    }
+
+    @Test
+    public void getCategories_forAlertInfo() {
+        mExpect.withMessage("Radio alert info categories")
+                .that(Ints.asList(TEST_ALERT_INFO_1.getCategories()))
+                .containsExactlyElementsIn(Ints.asList(TEST_CATEGORIES_1));
+    }
+
+    @Test
+    public void getUrgency_forAlertInfo() {
+        mExpect.withMessage("Radio alert info urgency")
+                .that(TEST_ALERT_INFO_1.getUrgency()).isEqualTo(TEST_URGENCY_1);
+    }
+
+    @Test
+    public void getSeverity_forAlertInfo() {
+        mExpect.withMessage("Radio alert info severity")
+                .that(TEST_ALERT_INFO_1.getSeverity()).isEqualTo(TEST_SEVERITY_1);
+    }
+
+    @Test
+    public void getCertainty_forAlertInfo() {
+        mExpect.withMessage("Radio alert info certainty")
+                .that(TEST_ALERT_INFO_1.getCertainty()).isEqualTo(TEST_CERTAINTY_1);
+    }
+
+    @Test
+    public void getDescription_forAlertInfo() {
+        mExpect.withMessage("Radio alert info description")
+                .that(TEST_ALERT_INFO_1.getDescription()).isEqualTo(TEST_DESCRIPTION_MESSAGE_1);
+    }
+
+    @Test
+    public void getAreas_forAlertInfo() {
+        mExpect.withMessage("Radio alert info areas")
+                .that(TEST_ALERT_INFO_1.getAreas()).containsAtLeastElementsIn(TEST_AREA_LIST_1);
+    }
+
+    @Test
+    public void getLanguage_forAlertInfo() {
+        mExpect.withMessage("Radio alert language")
+                .that(TEST_ALERT_INFO_1.getLanguage()).isEqualTo(TEST_LANGUAGE_1);
+    }
+
+    @Test
+    public void describeContents_forAlertInfo() {
+        mExpect.withMessage("Contents of alert info")
+                .that(TEST_ALERT_INFO_1.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void writeToParcel_forAlertInfoWithNullLanguage() {
+        Parcel parcel = Parcel.obtain();
+
+        TEST_ALERT_INFO_2.writeToParcel(parcel, TEST_FLAGS);
+
+        parcel.setDataPosition(0);
+        RadioAlert.AlertInfo alertInfoFromParcel = RadioAlert.AlertInfo.CREATOR
+                .createFromParcel(parcel);
+        mExpect.withMessage("Alert info from parcel with null language")
+                .that(alertInfoFromParcel).isEqualTo(TEST_ALERT_INFO_2);
+    }
+
+    @Test
+    public void writeToParcel_forAlertInfoWithNonnullLanguage() {
+        Parcel parcel = Parcel.obtain();
+
+        TEST_ALERT_INFO_1.writeToParcel(parcel, TEST_FLAGS);
+
+        parcel.setDataPosition(0);
+        RadioAlert.AlertInfo alertInfoFromParcel = RadioAlert.AlertInfo.CREATOR
+                .createFromParcel(parcel);
+        mExpect.withMessage("Alert info with nonnull language from parcel")
+                .that(alertInfoFromParcel).isEqualTo(TEST_ALERT_INFO_1);
+    }
+
+    @Test
+    public void newArray_forAlertInfoCreator() {
+        RadioAlert.AlertInfo[] alertInfos = RadioAlert.AlertInfo.CREATOR
+                .newArray(CREATOR_ARRAY_SIZE);
+
+        mExpect.withMessage("Alert infos").that(alertInfos).hasLength(CREATOR_ARRAY_SIZE);
+    }
+
+    @Test
+    public void hashCode_withSameAlertInfos() {
+        RadioAlert.AlertInfo alertInfoCompared = new RadioAlert.AlertInfo(
+                TEST_CATEGORIES_1, TEST_URGENCY_1, TEST_SEVERITY_1, TEST_CERTAINTY_1,
+                TEST_DESCRIPTION_MESSAGE_1, TEST_AREA_LIST_1, TEST_LANGUAGE_1);
+
+        mExpect.withMessage("Hash code of the same alert info")
+                .that(alertInfoCompared.hashCode()).isEqualTo(TEST_ALERT_INFO_1.hashCode());
+    }
+
+    @Test
+    public void constructor_forRadioAlert() {
+        NullPointerException thrown = assertThrows(NullPointerException.class, () ->
+                new RadioAlert(TEST_STATUS, TEST_TYPE, /* infoList= */ null));
+
+        mExpect.withMessage("Exception for alert constructor with null alert info list")
+                .that(thrown).hasMessageThat().contains("Alert info list can not be null");
+    }
+
+    @Test
+    public void equals_withDifferentAlertInfo() {
+        mExpect.withMessage("Different alert info").that(TEST_ALERT_INFO_1)
+                .isNotEqualTo(TEST_ALERT_INFO_2);
+    }
+
+    @Test
+    @SuppressWarnings("TruthIncompatibleType")
+    public void equals_withDifferentTypeObject_forAlertInfo() {
+        mExpect.withMessage("Non-alert-info object").that(TEST_ALERT_INFO_1)
+                .isNotEqualTo(TEST_AREA_1);
+    }
+
+    @Test
+    public void getStatus() {
+        mExpect.withMessage("Radio alert status").that(TEST_ALERT.getStatus())
+                .isEqualTo(TEST_STATUS);
+    }
+
+    @Test
+    public void getMessageType() {
+        mExpect.withMessage("Radio alert message type")
+                .that(TEST_ALERT.getMessageType()).isEqualTo(TEST_TYPE);
+    }
+
+    @Test
+    public void getInfoList() {
+        mExpect.withMessage("Radio alert info list").that(TEST_ALERT.getInfoList())
+                .containsExactly(TEST_ALERT_INFO_1, TEST_ALERT_INFO_2);
+    }
+
+    @Test
+    public void describeContents() {
+        mExpect.withMessage("Contents of radio alert")
+                .that(TEST_ALERT.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void writeToParcel() {
+        Parcel parcel = Parcel.obtain();
+
+        TEST_ALERT.writeToParcel(parcel, TEST_FLAGS);
+
+        parcel.setDataPosition(0);
+        RadioAlert alertFromParcel = RadioAlert.CREATOR.createFromParcel(parcel);
+        mExpect.withMessage("Alert from parcel").that(alertFromParcel)
+                .isEqualTo(TEST_ALERT);
+    }
+
+    @Test
+    public void hashCode_withSameAlerts() {
+        RadioAlert alertCompared = new RadioAlert(TEST_STATUS, TEST_TYPE,
+                List.of(TEST_ALERT_INFO_1, TEST_ALERT_INFO_2));
+
+        mExpect.withMessage("Hash code of the same alert")
+                .that(alertCompared.hashCode()).isEqualTo(TEST_ALERT.hashCode());
+    }
+
+    @Test
+    public void newArray_forAlertCreator() {
+        RadioAlert[] alerts = RadioAlert.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+        mExpect.withMessage("Alerts").that(alerts).hasLength(CREATOR_ARRAY_SIZE);
+    }
+
+    @Test
+    public void equals_withDifferentAlert() {
+        RadioAlert differentAlert = new RadioAlert(TEST_STATUS, TEST_TYPE,
+                List.of(TEST_ALERT_INFO_2));
+
+        mExpect.withMessage("Different alert").that(TEST_ALERT)
+                .isNotEqualTo(differentAlert);
+    }
+
+    @Test
+    @SuppressWarnings("TruthIncompatibleType")
+    public void equals_withDifferentTypeObject() {
+        mExpect.withMessage("Non-alert object").that(TEST_ALERT)
+                .isNotEqualTo(TEST_ALERT_INFO_2);
+    }
 }
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/overlaytests/handle_config_change/Android.bp b/core/tests/overlaytests/handle_config_change/Android.bp
index 2b31d0a..42b60e8 100644
--- a/core/tests/overlaytests/handle_config_change/Android.bp
+++ b/core/tests/overlaytests/handle_config_change/Android.bp
@@ -38,7 +38,7 @@
         "device-tests",
     ],
     // All APKs required by the tests
-    data: [
+    device_common_data: [
         ":OverlayResApp",
     ],
     per_testcase_directory: true,
diff --git a/core/tests/vibrator/src/android/os/VibrationEffectTest.java b/core/tests/vibrator/src/android/os/VibrationEffectTest.java
index 8acf2ed..8a250bd 100644
--- a/core/tests/vibrator/src/android/os/VibrationEffectTest.java
+++ b/core/tests/vibrator/src/android/os/VibrationEffectTest.java
@@ -46,6 +46,7 @@
 import android.os.vibrator.PrimitiveSegment;
 import android.os.vibrator.StepSegment;
 import android.os.vibrator.VibrationEffectSegment;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 
 import com.android.internal.R;
@@ -401,6 +402,21 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void computeLegacyPattern_effectsViaStartWaveformEnvelope() {
+        // Effects created via startWaveformEnvelope are not expected to be converted to long[]
+        // patterns, as they are not configured to always play with the default amplitude.
+        VibrationEffect effect = VibrationEffect.startWaveformEnvelope()
+                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 20)
+                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 50)
+                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 80)
+                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 40)
+                .build();
+
+        assertNull(effect.computeCreateWaveformOffOnTimingsOrNull());
+    }
+
+    @Test
     public void computeLegacyPattern_effectsViaStartWaveform() {
         // Effects created via startWaveform are not expected to be converted to long[] patterns, as
         // they are not configured to always play with the default amplitude.
@@ -595,6 +611,45 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testValidateWaveformEnvelopeBuilder() {
+        VibrationEffect.startWaveformEnvelope()
+                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 20)
+                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 50)
+                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 80)
+                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 40)
+                .build()
+                .validate();
+
+        assertThrows(IllegalStateException.class,
+                () -> VibrationEffect.startWaveformEnvelope().build().validate());
+        assertThrows(IllegalArgumentException.class,
+                () -> VibrationEffect.startWaveformEnvelope()
+                        .addControlPoint(/*amplitude=*/ -1.0f, /*frequencyHz=*/ 60f,
+                                /*timeMillis=*/ 20)
+                        .build()
+                        .validate());
+        assertThrows(IllegalArgumentException.class,
+                () -> VibrationEffect.startWaveformEnvelope()
+                        .addControlPoint(/*amplitude=*/ 1.1f, /*frequencyHz=*/ 60f,
+                                /*timeMillis=*/ 20)
+                        .build()
+                        .validate());
+        assertThrows(IllegalArgumentException.class,
+                () -> VibrationEffect.startWaveformEnvelope()
+                        .addControlPoint(/*amplitude=*/ 0.8f, /*frequencyHz=*/ 0f,
+                                /*timeMillis=*/ 20)
+                        .build()
+                        .validate());
+        assertThrows(IllegalArgumentException.class,
+                () -> VibrationEffect.startWaveformEnvelope()
+                        .addControlPoint(/*amplitude=*/ 0.8f, /*frequencyHz=*/ 100f,
+                                /*timeMillis=*/ 0)
+                        .build()
+                        .validate());
+    }
+
+    @Test
     public void testValidateWaveformBuilder() {
         // Cover builder methods
         VibrationEffect.startWaveform(targetAmplitude(1))
@@ -1190,6 +1245,13 @@
                 .addTransition(Duration.ofMillis(500), targetAmplitude(0))
                 .build()
                 .isHapticFeedbackCandidate());
+        assertFalse(VibrationEffect.startWaveformEnvelope()
+                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 200)
+                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 500)
+                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 800)
+                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 400)
+                .build()
+                .isHapticFeedbackCandidate());
     }
 
     @Test
@@ -1203,6 +1265,12 @@
                 .addTransition(Duration.ofMillis(300), targetAmplitude(0))
                 .build()
                 .isHapticFeedbackCandidate());
+        assertTrue(VibrationEffect.startWaveformEnvelope()
+                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 500)
+                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 400)
+                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 100)
+                .build()
+                .isHapticFeedbackCandidate());
     }
 
     @Test
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/goldens/robolectric/phone/dark_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/dark_portrait_bubbles_education.png
index e540b45..a163d92 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/dark_portrait_bubbles_education.png
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/dark_portrait_bubbles_education.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/light_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/light_portrait_bubbles_education.png
index e540b45..25d2e34c 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/light_portrait_bubbles_education.png
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/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 3650f68..d124c02 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -378,6 +378,17 @@
 }
 
 flag {
+    name: "status_bar_show_audio_only_projection_chip"
+    namespace: "systemui"
+    description: "Show chip on the left side of the status bar when a user is only sharing *audio* "
+        "during a media projection"
+    bug: "373308507"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "status_bar_use_repos_for_call_chip"
     namespace: "systemui"
     description: "Use repositories as the source of truth for call notifications shown as a chip in"
@@ -729,6 +740,13 @@
 }
 
 flag {
+    name: "smartspace_swipe_event_logging"
+    namespace: "systemui"
+    description: "Log card swipe events in smartspace"
+    bug: "374150422"
+}
+
+flag {
    name: "pin_input_field_styled_focus_state"
    namespace: "systemui"
    description: "Enables styled focus states on pin input field if keyboard is connected"
@@ -991,6 +1009,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"
@@ -1631,4 +1659,11 @@
   metadata {
     purpose: PURPOSE_BUGFIX
   }
-}
\ No newline at end of file
+}
+
+flag {
+  name: "shade_expands_on_status_bar_long_press"
+  namespace: "systemui"
+  description: "Expands the shade on long press of any status bar"
+  bug: "371224114"
+}
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..5e1ac1f 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,
@@ -711,7 +715,7 @@
             WidgetSizeInfo(minHeightPx, maxHeightPx)
         }
     } else {
-        WidgetSizeInfo(0, Int.MAX_VALUE)
+        WidgetSizeInfo(0, 0)
     }
 }
 
@@ -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/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..7816d3b 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 {
@@ -380,6 +480,24 @@
     fun testIllegalState_resizeMultipleZeroOrNegative() =
         testScope.runTest { updateGridLayout(singleSpanGrid.copy(resizeMultiple = 0)) }
 
+    @Test
+    fun testZeroHeights_cannotResize() = runTestWithSnapshots {
+        val zeroHeightGrid =
+            singleSpanGrid.copy(
+                totalSpans = 2,
+                currentSpan = 1,
+                currentRow = 0,
+                minHeightPx = 0,
+                maxHeightPx = 0,
+            )
+        updateGridLayout(zeroHeightGrid)
+
+        val topState = underTest.topDragState
+        val bottomState = underTest.bottomDragState
+        assertThat(topState.anchors.toList()).containsExactly(0 to 0f)
+        assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f)
+    }
+
     private fun TestScope.updateGridLayout(gridLayout: GridLayout) {
         underTest.setGridLayoutInfo(
             verticalItemSpacingPx = gridLayout.verticalItemSpacingPx,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/PerDisplayStoreImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/PerDisplayStoreImplTest.kt
index 1dd8ca9..6a0781b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/PerDisplayStoreImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/PerDisplayStoreImplTest.kt
@@ -20,9 +20,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.kosmos.unconfinedTestDispatcher
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.runBlocking
@@ -37,7 +36,7 @@
 @SmallTest
 class PerDisplayStoreImplTest : SysuiTestCase() {
 
-    private val kosmos = testKosmos().also { it.testDispatcher = it.unconfinedTestDispatcher }
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
     private val testScope = kosmos.testScope
     private val fakeDisplayRepository = kosmos.displayRepository
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt
index 0145f17..4a422f0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt
@@ -23,8 +23,9 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.kosmos.unconfinedTestDispatcher
-import com.android.systemui.kosmos.unconfinedTestScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.res.R
 import com.android.systemui.settings.FakeUserTracker
 import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
@@ -33,7 +34,7 @@
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.settings.unconfinedDispatcherFakeSettings
+import com.android.systemui.util.settings.fakeSettings
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.advanceUntilIdle
@@ -51,10 +52,10 @@
 @RunWith(AndroidJUnit4::class)
 class KeyguardQuickAffordanceLegacySettingSyncerTest : SysuiTestCase() {
 
-    private val kosmos = testKosmos()
-    private val testDispatcher = kosmos.unconfinedTestDispatcher
-    private val testScope = kosmos.unconfinedTestScope
-    private val settings = kosmos.unconfinedDispatcherFakeSettings
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+    private val testDispatcher = kosmos.testDispatcher
+    private val testScope = kosmos.testScope
+    private val settings = kosmos.fakeSettings
 
     @Mock private lateinit var sharedPrefs: FakeSharedPreferences
 
@@ -79,13 +80,7 @@
                 context = context,
                 userFileManager =
                     mock {
-                        whenever(
-                                getSharedPreferences(
-                                    anyString(),
-                                    anyInt(),
-                                    anyInt(),
-                                )
-                            )
+                        whenever(getSharedPreferences(anyString(), anyInt(), anyInt()))
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = FakeUserTracker(),
@@ -109,17 +104,14 @@
         testScope.runTest {
             val job = underTest.startSyncing()
 
-            settings.putInt(
-                Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
-                1,
-            )
+            settings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 1)
 
             assertThat(
                     selectionManager
                         .getSelections()
                         .getOrDefault(
                             KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
-                            emptyList()
+                            emptyList(),
                         )
                 )
                 .contains(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS)
@@ -132,21 +124,15 @@
         testScope.runTest {
             val job = underTest.startSyncing()
 
-            settings.putInt(
-                Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
-                1,
-            )
-            settings.putInt(
-                Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
-                0,
-            )
+            settings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 1)
+            settings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 0)
 
             assertThat(
                     selectionManager
                         .getSelections()
                         .getOrDefault(
                             KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
-                            emptyList()
+                            emptyList(),
                         )
                 )
                 .doesNotContain(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS)
@@ -161,7 +147,7 @@
 
             selectionManager.setSelections(
                 KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
-                listOf(BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET)
+                listOf(BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET),
             )
 
             advanceUntilIdle()
@@ -177,11 +163,11 @@
 
             selectionManager.setSelections(
                 KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
-                listOf(BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET)
+                listOf(BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET),
             )
             selectionManager.setSelections(
                 KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
-                emptyList()
+                emptyList(),
             )
 
             assertThat(settings.getInt(Settings.Secure.LOCKSCREEN_SHOW_WALLET)).isEqualTo(0)
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/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
index 785d5a8..02825a5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
@@ -21,10 +21,13 @@
 import android.os.Binder
 import android.os.Handler
 import android.os.UserHandle
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.view.ContentRecordingSession
 import android.view.Display
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.applicationCoroutineScope
@@ -74,7 +77,8 @@
         }
 
     @Test
-    fun mediaProjectionState_onStart_emitsNotProjecting() =
+    @DisableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+    fun mediaProjectionState_onStart_flagOff_emitsNotProjecting() =
         testScope.runTest {
             val state by collectLastValue(repo.mediaProjectionState)
 
@@ -84,6 +88,35 @@
         }
 
     @Test
+    @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+    fun mediaProjectionState_onStart_flagOn_emitsProjectingNoScreen() =
+        testScope.runTest {
+            val state by collectLastValue(repo.mediaProjectionState)
+
+            fakeMediaProjectionManager.dispatchOnStart()
+
+            assertThat(state).isInstanceOf(MediaProjectionState.Projecting.NoScreen::class.java)
+        }
+
+    @Test
+    @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+    fun mediaProjectionState_noScreen_hasHostPackage() =
+        testScope.runTest {
+            val state by collectLastValue(repo.mediaProjectionState)
+
+            val info =
+                MediaProjectionInfo(
+                    /* packageName= */ "com.media.projection.repository.test",
+                    /* handle= */ UserHandle.getUserHandleForUid(UserHandle.myUserId()),
+                    /* launchCookie = */ null,
+                )
+            fakeMediaProjectionManager.dispatchOnStart(info)
+
+            assertThat((state as MediaProjectionState.Projecting).hostPackage)
+                .isEqualTo("com.media.projection.repository.test")
+        }
+
+    @Test
     fun mediaProjectionState_onStop_emitsNotProjecting() =
         testScope.runTest {
             val state by collectLastValue(repo.mediaProjectionState)
@@ -212,7 +245,7 @@
                 )
             fakeMediaProjectionManager.dispatchOnSessionSet(
                 info = info,
-                session = ContentRecordingSession.createTaskSession(token.asBinder())
+                session = ContentRecordingSession.createTaskSession(token.asBinder()),
             )
 
             assertThat((state as MediaProjectionState.Projecting.SingleTask).hostPackage)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index 3850891..4995920 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -651,6 +651,7 @@
         }
 
     @Test
+    @DisableFlags(Flags.FLAG_TRANSITION_RACE_CONDITION)
     fun switchToAOD_whenAvailable_whenDeviceSleepsLocked() =
         testScope.runTest {
             kosmos.lockscreenSceneTransitionInteractor.start()
@@ -680,6 +681,7 @@
         }
 
     @Test
+    @DisableFlags(Flags.FLAG_TRANSITION_RACE_CONDITION)
     fun switchToDozing_whenAodUnavailable_whenDeviceSleepsLocked() =
         testScope.runTest {
             kosmos.lockscreenSceneTransitionInteractor.start()
@@ -701,6 +703,56 @@
         }
 
     @Test
+    @EnableFlags(Flags.FLAG_TRANSITION_RACE_CONDITION)
+    fun switchToAOD_whenAvailable_whenDeviceSleepsLocked_transitionFlagEnabled() =
+        testScope.runTest {
+            kosmos.lockscreenSceneTransitionInteractor.start()
+            val asleepState by collectLastValue(kosmos.keyguardInteractor.asleepKeyguardState)
+            val transitionState =
+                prepareState(isDeviceUnlocked = false, initialSceneKey = Scenes.Shade)
+            kosmos.keyguardRepository.setAodAvailable(true)
+            runCurrent()
+            assertThat(asleepState).isEqualTo(KeyguardState.AOD)
+            underTest.start()
+            powerInteractor.setAsleepForTest()
+            runCurrent()
+            transitionState.value =
+                ObservableTransitionState.Transition(
+                    fromScene = Scenes.Shade,
+                    toScene = Scenes.Lockscreen,
+                    currentScene = flowOf(Scenes.Lockscreen),
+                    progress = flowOf(0.5f),
+                    isInitiatedByUserInput = true,
+                    isUserInputOngoing = flowOf(false),
+                )
+            runCurrent()
+
+            assertThat(kosmos.keyguardTransitionRepository.currentTransitionInfo.to)
+                .isEqualTo(KeyguardState.AOD)
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_TRANSITION_RACE_CONDITION)
+    fun switchToDozing_whenAodUnavailable_whenDeviceSleepsLocked_transitionFlagEnabled() =
+        testScope.runTest {
+            kosmos.lockscreenSceneTransitionInteractor.start()
+            val asleepState by collectLastValue(kosmos.keyguardInteractor.asleepKeyguardState)
+            val transitionState =
+                prepareState(isDeviceUnlocked = false, initialSceneKey = Scenes.Shade)
+            kosmos.keyguardRepository.setAodAvailable(false)
+            runCurrent()
+            assertThat(asleepState).isEqualTo(KeyguardState.DOZING)
+            underTest.start()
+            powerInteractor.setAsleepForTest()
+            runCurrent()
+            transitionState.value = Transition(from = Scenes.Shade, to = Scenes.Lockscreen)
+            runCurrent()
+
+            assertThat(kosmos.keyguardTransitionRepository.currentTransitionInfo.to)
+                .isEqualTo(KeyguardState.DOZING)
+        }
+
+    @Test
     fun switchToGoneWhenDoubleTapPowerGestureIsTriggeredFromGone() =
         testScope.runTest {
             val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
index 5005d16..e33ce9c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
@@ -92,7 +92,7 @@
         createAndSetDelegate(
             MediaProjectionState.Projecting.EntireScreen(
                 HOST_PACKAGE,
-                hostDeviceName = "My Favorite Device"
+                hostDeviceName = "My Favorite Device",
             )
         )
 
@@ -118,8 +118,8 @@
             MediaProjectionState.Projecting.SingleTask(
                 HOST_PACKAGE,
                 hostDeviceName = null,
-                createTask(taskId = 1, baseIntent = baseIntent)
-            ),
+                createTask(taskId = 1, baseIntent = baseIntent),
+            )
         )
 
         underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
@@ -141,8 +141,8 @@
             MediaProjectionState.Projecting.SingleTask(
                 HOST_PACKAGE,
                 hostDeviceName = "My Favorite Device",
-                createTask(taskId = 1, baseIntent = baseIntent)
-            ),
+                createTask(taskId = 1, baseIntent = baseIntent),
+            )
         )
 
         underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
@@ -169,8 +169,8 @@
             MediaProjectionState.Projecting.SingleTask(
                 HOST_PACKAGE,
                 hostDeviceName = null,
-                createTask(taskId = 1, baseIntent = baseIntent)
-            ),
+                createTask(taskId = 1, baseIntent = baseIntent),
+            )
         )
 
         underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
@@ -198,7 +198,7 @@
                 HOST_PACKAGE,
                 hostDeviceName = "My Favorite Device",
                 createTask(taskId = 1, baseIntent = baseIntent),
-            ),
+            )
         )
 
         underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
@@ -235,7 +235,7 @@
             verify(sysuiDialog)
                 .setPositiveButton(
                     eq(R.string.cast_to_other_device_stop_dialog_button),
-                    clickListener.capture()
+                    clickListener.capture(),
                 )
 
             // Verify that clicking the button stops the recording
@@ -254,7 +254,8 @@
                 kosmos.applicationContext,
                 stopAction = kosmos.mediaProjectionChipInteractor::stopProjecting,
                 ProjectionChipModel.Projecting(
-                    ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE,
+                    ProjectionChipModel.Receiver.CastToOtherDevice,
+                    ProjectionChipModel.ContentType.Screen,
                     state,
                 ),
             )
@@ -268,7 +269,7 @@
             MediaProjectionState.Projecting.SingleTask(
                 HOST_PACKAGE,
                 hostDeviceName = null,
-                createTask(taskId = 1)
+                createTask(taskId = 1),
             )
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
index 77992db..01e5501 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
@@ -17,9 +17,11 @@
 package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel
 
 import android.content.DialogInterface
+import android.platform.test.annotations.EnableFlags
 import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.internal.jank.Cuj
+import com.android.systemui.Flags.FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.mockDialogTransitionAnimator
@@ -135,6 +137,29 @@
         }
 
     @Test
+    @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+    fun chip_projectionIsAudioOnly_otherDevicePackage_isShownAsIconOnly() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+            mediaRouterRepo.castDevices.value = emptyList()
+
+            mediaProjectionRepo.mediaProjectionState.value =
+                MediaProjectionState.Projecting.NoScreen(
+                    hostPackage = CAST_TO_OTHER_DEVICES_PACKAGE
+                )
+
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java)
+            val icon =
+                (((latest as OngoingActivityChipModel.Shown).icon)
+                        as OngoingActivityChipModel.ChipIcon.SingleColorIcon)
+                    .impl as Icon.Resource
+            assertThat(icon.res).isEqualTo(R.drawable.ic_cast_connected)
+            // This content description is just generic "Casting", not "Casting screen"
+            assertThat((icon.contentDescription as ContentDescription.Resource).res)
+                .isEqualTo(R.string.accessibility_casting)
+        }
+
+    @Test
     fun chip_projectionIsEntireScreenState_otherDevicesPackage_isShownAsTimer_forScreen() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
@@ -292,6 +317,18 @@
         }
 
     @Test
+    @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+    fun chip_projectionIsNoScreenState_normalPackage_isHidden() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            mediaProjectionRepo.mediaProjectionState.value =
+                MediaProjectionState.Projecting.NoScreen(NORMAL_PACKAGE)
+
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+        }
+
+    @Test
     fun chip_projectionIsSingleTaskState_normalPackage_isHidden() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
@@ -387,12 +424,7 @@
 
             clickListener!!.onClick(chipView)
             verify(kosmos.mockDialogTransitionAnimator)
-                .showFromView(
-                    eq(mockScreenCastDialog),
-                    eq(chipBackgroundView),
-                    any(),
-                    anyBoolean(),
-                )
+                .showFromView(eq(mockScreenCastDialog), eq(chipBackgroundView), any(), anyBoolean())
         }
 
     @Test
@@ -412,12 +444,7 @@
 
             clickListener!!.onClick(chipView)
             verify(kosmos.mockDialogTransitionAnimator)
-                .showFromView(
-                    eq(mockScreenCastDialog),
-                    eq(chipBackgroundView),
-                    any(),
-                    anyBoolean(),
-                )
+                .showFromView(eq(mockScreenCastDialog), eq(chipBackgroundView), any(), anyBoolean())
         }
 
     @Test
@@ -461,12 +488,7 @@
 
             val cujCaptor = argumentCaptor<DialogCuj>()
             verify(kosmos.mockDialogTransitionAnimator)
-                .showFromView(
-                    any(),
-                    any(),
-                    cujCaptor.capture(),
-                    anyBoolean(),
-                )
+                .showFromView(any(), any(), cujCaptor.capture(), anyBoolean())
 
             assertThat(cujCaptor.firstValue.cujType)
                 .isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP)
@@ -494,12 +516,7 @@
 
             val cujCaptor = argumentCaptor<DialogCuj>()
             verify(kosmos.mockDialogTransitionAnimator)
-                .showFromView(
-                    any(),
-                    any(),
-                    cujCaptor.capture(),
-                    anyBoolean(),
-                )
+                .showFromView(any(), any(), cujCaptor.capture(), anyBoolean())
 
             assertThat(cujCaptor.firstValue.cujType)
                 .isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
index d0c5e7a..611318a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
@@ -21,7 +21,9 @@
 import android.content.packageManager
 import android.content.pm.PackageManager
 import android.content.pm.ResolveInfo
+import android.platform.test.annotations.EnableFlags
 import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.Kosmos
@@ -65,7 +67,23 @@
         }
 
     @Test
-    fun projection_singleTaskState_otherDevicesPackage_isCastToOtherDeviceType() =
+    @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+    fun projection_noScreenState_otherDevicesPackage_isCastToOtherAndAudio() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.projection)
+
+            mediaProjectionRepo.mediaProjectionState.value =
+                MediaProjectionState.Projecting.NoScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
+
+            assertThat(latest).isInstanceOf(ProjectionChipModel.Projecting::class.java)
+            assertThat((latest as ProjectionChipModel.Projecting).receiver)
+                .isEqualTo(ProjectionChipModel.Receiver.CastToOtherDevice)
+            assertThat((latest as ProjectionChipModel.Projecting).contentType)
+                .isEqualTo(ProjectionChipModel.ContentType.Audio)
+        }
+
+    @Test
+    fun projection_singleTaskState_otherDevicesPackage_isCastToOtherAndScreen() =
         testScope.runTest {
             val latest by collectLastValue(underTest.projection)
 
@@ -73,31 +91,49 @@
                 MediaProjectionState.Projecting.SingleTask(
                     CAST_TO_OTHER_DEVICES_PACKAGE,
                     hostDeviceName = null,
-                    createTask(taskId = 1)
+                    createTask(taskId = 1),
                 )
 
             assertThat(latest).isInstanceOf(ProjectionChipModel.Projecting::class.java)
-            assertThat((latest as ProjectionChipModel.Projecting).type)
-                .isEqualTo(ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE)
+            assertThat((latest as ProjectionChipModel.Projecting).receiver)
+                .isEqualTo(ProjectionChipModel.Receiver.CastToOtherDevice)
+            assertThat((latest as ProjectionChipModel.Projecting).contentType)
+                .isEqualTo(ProjectionChipModel.ContentType.Screen)
         }
 
     @Test
-    fun projection_entireScreenState_otherDevicesPackage_isCastToOtherDeviceChipType() =
+    fun projection_entireScreenState_otherDevicesPackage_isCastToOtherAndScreen() =
         testScope.runTest {
             val latest by collectLastValue(underTest.projection)
 
             mediaProjectionRepo.mediaProjectionState.value =
-                MediaProjectionState.Projecting.EntireScreen(
-                    CAST_TO_OTHER_DEVICES_PACKAGE,
-                )
+                MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
 
             assertThat(latest).isInstanceOf(ProjectionChipModel.Projecting::class.java)
-            assertThat((latest as ProjectionChipModel.Projecting).type)
-                .isEqualTo(ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE)
+            assertThat((latest as ProjectionChipModel.Projecting).receiver)
+                .isEqualTo(ProjectionChipModel.Receiver.CastToOtherDevice)
+            assertThat((latest as ProjectionChipModel.Projecting).contentType)
+                .isEqualTo(ProjectionChipModel.ContentType.Screen)
         }
 
     @Test
-    fun projection_singleTaskState_normalPackage_isShareToAppChipType() =
+    @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+    fun projection_noScreenState_normalPackage_isShareToAppAndAudio() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.projection)
+
+            mediaProjectionRepo.mediaProjectionState.value =
+                MediaProjectionState.Projecting.NoScreen(NORMAL_PACKAGE)
+
+            assertThat(latest).isInstanceOf(ProjectionChipModel.Projecting::class.java)
+            assertThat((latest as ProjectionChipModel.Projecting).receiver)
+                .isEqualTo(ProjectionChipModel.Receiver.ShareToApp)
+            assertThat((latest as ProjectionChipModel.Projecting).contentType)
+                .isEqualTo(ProjectionChipModel.ContentType.Audio)
+        }
+
+    @Test
+    fun projection_singleTaskState_normalPackage_isShareToAppAndScreen() =
         testScope.runTest {
             val latest by collectLastValue(underTest.projection)
 
@@ -109,12 +145,14 @@
                 )
 
             assertThat(latest).isInstanceOf(ProjectionChipModel.Projecting::class.java)
-            assertThat((latest as ProjectionChipModel.Projecting).type)
-                .isEqualTo(ProjectionChipModel.Type.SHARE_TO_APP)
+            assertThat((latest as ProjectionChipModel.Projecting).receiver)
+                .isEqualTo(ProjectionChipModel.Receiver.ShareToApp)
+            assertThat((latest as ProjectionChipModel.Projecting).contentType)
+                .isEqualTo(ProjectionChipModel.ContentType.Screen)
         }
 
     @Test
-    fun projection_entireScreenState_normalPackage_isShareToAppChipType() =
+    fun projection_entireScreenState_normalPackage_isShareToAppAndScreen() =
         testScope.runTest {
             val latest by collectLastValue(underTest.projection)
 
@@ -122,8 +160,10 @@
                 MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
 
             assertThat(latest).isInstanceOf(ProjectionChipModel.Projecting::class.java)
-            assertThat((latest as ProjectionChipModel.Projecting).type)
-                .isEqualTo(ProjectionChipModel.Type.SHARE_TO_APP)
+            assertThat((latest as ProjectionChipModel.Projecting).receiver)
+                .isEqualTo(ProjectionChipModel.Receiver.ShareToApp)
+            assertThat((latest as ProjectionChipModel.Projecting).contentType)
+                .isEqualTo(ProjectionChipModel.ContentType.Screen)
         }
 
     companion object {
@@ -140,14 +180,14 @@
                 whenever(
                         this.checkPermission(
                             Manifest.permission.REMOTE_DISPLAY_PROVIDER,
-                            CAST_TO_OTHER_DEVICES_PACKAGE
+                            CAST_TO_OTHER_DEVICES_PACKAGE,
                         )
                     )
                     .thenReturn(PackageManager.PERMISSION_GRANTED)
                 whenever(
                         this.checkPermission(
                             Manifest.permission.REMOTE_DISPLAY_PROVIDER,
-                            NORMAL_PACKAGE
+                            NORMAL_PACKAGE,
                         )
                     )
                     .thenReturn(PackageManager.PERMISSION_DENIED)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegateTest.kt
new file mode 100644
index 0000000..411d306
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegateTest.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips.sharetoapp.ui.view
+
+import android.content.DialogInterface
+import android.content.applicationContext
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.mediaProjectionChipInteractor
+import com.android.systemui.statusbar.chips.mediaprojection.ui.view.endMediaProjectionDialogHelper
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class EndGenericShareToAppDialogDelegateTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val sysuiDialog = mock<SystemUIDialog>()
+    private val underTest =
+        EndGenericShareToAppDialogDelegate(
+            kosmos.endMediaProjectionDialogHelper,
+            kosmos.applicationContext,
+            stopAction = kosmos.mediaProjectionChipInteractor::stopProjecting,
+        )
+
+    @Test
+    fun positiveButton_clickStopsRecording() =
+        kosmos.testScope.runTest {
+            underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+            assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isFalse()
+
+            val clickListener = argumentCaptor<DialogInterface.OnClickListener>()
+            verify(sysuiDialog).setPositiveButton(any(), clickListener.capture())
+            clickListener.firstValue.onClick(mock<DialogInterface>(), 0)
+            runCurrent()
+
+            assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isTrue()
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegateTest.kt
similarity index 93%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegateTest.kt
index 325a42b..6885a6b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegateTest.kt
@@ -50,10 +50,10 @@
 
 @SmallTest
 @OptIn(ExperimentalCoroutinesApi::class)
-class EndShareToAppDialogDelegateTest : SysuiTestCase() {
+class EndShareScreenToAppDialogDelegateTest : SysuiTestCase() {
     private val kosmos = Kosmos().also { it.testCase = this }
     private val sysuiDialog = mock<SystemUIDialog>()
-    private lateinit var underTest: EndShareToAppDialogDelegate
+    private lateinit var underTest: EndShareScreenToAppDialogDelegate
 
     @Test
     fun icon() {
@@ -117,7 +117,7 @@
             MediaProjectionState.Projecting.SingleTask(
                 HOST_PACKAGE,
                 hostDeviceName = null,
-                createTask(taskId = 1, baseIntent = baseIntent)
+                createTask(taskId = 1, baseIntent = baseIntent),
             )
         )
 
@@ -142,7 +142,7 @@
             MediaProjectionState.Projecting.SingleTask(
                 HOST_PACKAGE,
                 hostDeviceName = null,
-                createTask(taskId = 1, baseIntent = baseIntent)
+                createTask(taskId = 1, baseIntent = baseIntent),
             )
         )
 
@@ -181,7 +181,7 @@
             verify(sysuiDialog)
                 .setPositiveButton(
                     eq(R.string.share_to_app_stop_dialog_button),
-                    clickListener.capture()
+                    clickListener.capture(),
                 )
 
             // Verify that clicking the button stops the recording
@@ -195,12 +195,13 @@
 
     private fun createAndSetDelegate(state: MediaProjectionState.Projecting) {
         underTest =
-            EndShareToAppDialogDelegate(
+            EndShareScreenToAppDialogDelegate(
                 kosmos.endMediaProjectionDialogHelper,
                 kosmos.applicationContext,
                 stopAction = kosmos.mediaProjectionChipInteractor::stopProjecting,
                 ProjectionChipModel.Projecting(
-                    ProjectionChipModel.Type.SHARE_TO_APP,
+                    ProjectionChipModel.Receiver.ShareToApp,
+                    ProjectionChipModel.ContentType.Screen,
                     state,
                 ),
             )
@@ -213,7 +214,7 @@
             MediaProjectionState.Projecting.SingleTask(
                 HOST_PACKAGE,
                 hostDeviceName = null,
-                createTask(taskId = 1)
+                createTask(taskId = 1),
             )
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
index 791a21d..d7d57c8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
@@ -17,12 +17,15 @@
 package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel
 
 import android.content.DialogInterface
+import android.platform.test.annotations.EnableFlags
 import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.internal.jank.Cuj
+import com.android.systemui.Flags.FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.mockDialogTransitionAnimator
+import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.Kosmos
@@ -35,7 +38,8 @@
 import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.CAST_TO_OTHER_DEVICES_PACKAGE
 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.sharetoapp.ui.view.EndShareToAppDialogDelegate
+import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndGenericShareToAppDialogDelegate
+import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndShareScreenToAppDialogDelegate
 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.view.ChipBackgroundContainer
@@ -62,7 +66,8 @@
     private val mediaProjectionRepo = kosmos.fakeMediaProjectionRepository
     private val systemClock = kosmos.fakeSystemClock
 
-    private val mockShareDialog = mock<SystemUIDialog>()
+    private val mockScreenShareDialog = mock<SystemUIDialog>()
+    private val mockGenericShareDialog = mock<SystemUIDialog>()
     private val chipBackgroundView = mock<ChipBackgroundContainer>()
     private val chipView =
         mock<View>().apply {
@@ -80,8 +85,10 @@
     fun setUp() {
         setUpPackageManagerForMediaProjection(kosmos)
 
-        whenever(kosmos.mockSystemUIDialogFactory.create(any<EndShareToAppDialogDelegate>()))
-            .thenReturn(mockShareDialog)
+        whenever(kosmos.mockSystemUIDialogFactory.create(any<EndShareScreenToAppDialogDelegate>()))
+            .thenReturn(mockScreenShareDialog)
+        whenever(kosmos.mockSystemUIDialogFactory.create(any<EndGenericShareToAppDialogDelegate>()))
+            .thenReturn(mockGenericShareDialog)
     }
 
     @Test
@@ -95,6 +102,21 @@
         }
 
     @Test
+    @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+    fun chip_noScreenState_otherDevicesPackage_isHidden() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            mediaProjectionRepo.mediaProjectionState.value =
+                MediaProjectionState.Projecting.NoScreen(
+                    CAST_TO_OTHER_DEVICES_PACKAGE,
+                    hostDeviceName = null,
+                )
+
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+        }
+
+    @Test
     fun chip_singleTaskState_otherDevicesPackage_isHidden() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
@@ -121,6 +143,26 @@
         }
 
     @Test
+    @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+    fun chip_noScreenState_normalPackage_isShownAsIconOnly() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            mediaProjectionRepo.mediaProjectionState.value =
+                MediaProjectionState.Projecting.NoScreen(NORMAL_PACKAGE, hostDeviceName = null)
+
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java)
+            val icon =
+                (((latest as OngoingActivityChipModel.Shown).icon)
+                        as OngoingActivityChipModel.ChipIcon.SingleColorIcon)
+                    .impl as Icon.Resource
+            assertThat(icon.res).isEqualTo(R.drawable.ic_present_to_all)
+            // This content description is just generic "Sharing content", not "Sharing screen"
+            assertThat((icon.contentDescription as ContentDescription.Resource).res)
+                .isEqualTo(R.string.share_to_app_chip_accessibility_label_generic)
+        }
+
+    @Test
     fun chip_singleTaskState_normalPackage_isShownAsTimer() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
@@ -170,7 +212,7 @@
 
             // WHEN the stop action on the dialog is clicked
             val dialogStopAction =
-                getStopActionFromDialog(latest, chipView, mockShareDialog, kosmos)
+                getStopActionFromDialog(latest, chipView, mockScreenShareDialog, kosmos)
             dialogStopAction.onClick(mock<DialogInterface>(), 0)
 
             // THEN the chip is immediately hidden...
@@ -222,7 +264,28 @@
         }
 
     @Test
-    fun chip_entireScreen_clickListenerShowsShareDialog() =
+    @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+    fun chip_noScreen_clickListenerShowsGenericShareDialog() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+            mediaProjectionRepo.mediaProjectionState.value =
+                MediaProjectionState.Projecting.NoScreen(NORMAL_PACKAGE)
+
+            val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+            assertThat(clickListener).isNotNull()
+
+            clickListener!!.onClick(chipView)
+            verify(kosmos.mockDialogTransitionAnimator)
+                .showFromView(
+                    eq(mockGenericShareDialog),
+                    eq(chipBackgroundView),
+                    any(),
+                    anyBoolean(),
+                )
+        }
+
+    @Test
+    fun chip_entireScreen_clickListenerShowsScreenShareDialog() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
             mediaProjectionRepo.mediaProjectionState.value =
@@ -234,7 +297,7 @@
             clickListener!!.onClick(chipView)
             verify(kosmos.mockDialogTransitionAnimator)
                 .showFromView(
-                    eq(mockShareDialog),
+                    eq(mockScreenShareDialog),
                     eq(chipBackgroundView),
                     any(),
                     anyBoolean(),
@@ -242,7 +305,7 @@
         }
 
     @Test
-    fun chip_singleTask_clickListenerShowsShareDialog() =
+    fun chip_singleTask_clickListenerShowsScreenShareDialog() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
             mediaProjectionRepo.mediaProjectionState.value =
@@ -258,7 +321,7 @@
             clickListener!!.onClick(chipView)
             verify(kosmos.mockDialogTransitionAnimator)
                 .showFromView(
-                    eq(mockShareDialog),
+                    eq(mockScreenShareDialog),
                     eq(chipBackgroundView),
                     any(),
                     anyBoolean(),
@@ -281,12 +344,7 @@
 
             val cujCaptor = argumentCaptor<DialogCuj>()
             verify(kosmos.mockDialogTransitionAnimator)
-                .showFromView(
-                    any(),
-                    any(),
-                    cujCaptor.capture(),
-                    anyBoolean(),
-                )
+                .showFromView(any(), any(), cujCaptor.capture(), anyBoolean())
 
             assertThat(cujCaptor.firstValue.cujType)
                 .isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/StatusBarOrchestratorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/StatusBarOrchestratorTest.kt
index 7923097..659e53f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/StatusBarOrchestratorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/StatusBarOrchestratorTest.kt
@@ -22,9 +22,8 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
-import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.kosmos.unconfinedTestDispatcher
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.plugins.DarkIconDispatcher
 import com.android.systemui.plugins.mockPluginDependencyProvider
 import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -57,7 +56,7 @@
 @RunWith(AndroidJUnit4::class)
 class StatusBarOrchestratorTest : SysuiTestCase() {
 
-    private val kosmos = testKosmos().also { it.testDispatcher = it.unconfinedTestDispatcher }
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
     private val testScope = kosmos.testScope
     private val fakeStatusBarModePerDisplayRepository = kosmos.fakeStatusBarModePerDisplayRepository
     private val mockPluginDependencyProvider = kosmos.mockPluginDependencyProvider
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/MultiDisplayStatusBarContentInsetsProviderStoreTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/MultiDisplayStatusBarContentInsetsProviderStoreTest.kt
index 0eebab0..4a26fdf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/MultiDisplayStatusBarContentInsetsProviderStoreTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/MultiDisplayStatusBarContentInsetsProviderStoreTest.kt
@@ -21,9 +21,8 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.display.data.repository.displayRepository
-import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.kosmos.unconfinedTestDispatcher
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.testKosmos
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.runTest
@@ -37,7 +36,7 @@
 @RunWith(AndroidJUnit4::class)
 class MultiDisplayStatusBarContentInsetsProviderStoreTest : SysuiTestCase() {
 
-    private val kosmos = testKosmos().also { it.testDispatcher = it.unconfinedTestDispatcher }
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
     private val testScope = kosmos.testScope
     private val fakeDisplayRepository = kosmos.displayRepository
     private val underTest = kosmos.multiDisplayStatusBarContentInsetsProviderStore
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
index 1af0f79..b03c679 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
@@ -24,14 +24,15 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.kosmos.unconfinedTestDispatcher
-import com.android.systemui.kosmos.unconfinedTestScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.settings.FakeUserTracker
 import com.android.systemui.testKosmos
 import com.android.systemui.user.data.model.SelectedUserModel
 import com.android.systemui.user.data.model.SelectionStatus
 import com.android.systemui.user.data.model.UserSwitcherSettingsModel
-import com.android.systemui.util.settings.unconfinedDispatcherFakeGlobalSettings
+import com.android.systemui.util.settings.fakeGlobalSettings
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
@@ -52,10 +53,10 @@
 @RunWith(AndroidJUnit4::class)
 class UserRepositoryImplTest : SysuiTestCase() {
 
-    private val kosmos = testKosmos()
-    private val testDispatcher = kosmos.unconfinedTestDispatcher
-    private val testScope = kosmos.unconfinedTestScope
-    private val globalSettings = kosmos.unconfinedDispatcherFakeGlobalSettings
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+    private val testDispatcher = kosmos.testDispatcher
+    private val testScope = kosmos.testScope
+    private val globalSettings = kosmos.fakeGlobalSettings
 
     @Mock private lateinit var manager: UserManager
 
@@ -131,11 +132,7 @@
             whenever(mainUser.identifier).thenReturn(mainUserId)
 
             underTest = create(testScope.backgroundScope)
-            val initialExpectedValue =
-                setUpUsers(
-                    count = 3,
-                    selectedIndex = 0,
-                )
+            val initialExpectedValue = setUpUsers(count = 3, selectedIndex = 0)
             var userInfos: List<UserInfo>? = null
             var selectedUserInfo: UserInfo? = null
             val job1 = underTest.userInfos.onEach { userInfos = it }.launchIn(this)
@@ -146,11 +143,7 @@
             assertThat(selectedUserInfo).isEqualTo(initialExpectedValue[0])
             assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id)
 
-            val secondExpectedValue =
-                setUpUsers(
-                    count = 4,
-                    selectedIndex = 1,
-                )
+            val secondExpectedValue = setUpUsers(count = 4, selectedIndex = 1)
             underTest.refreshUsers()
             assertThat(userInfos).isEqualTo(secondExpectedValue)
             assertThat(selectedUserInfo).isEqualTo(secondExpectedValue[1])
@@ -158,11 +151,7 @@
 
             val selectedNonGuestUserId = selectedUserInfo?.id
             val thirdExpectedValue =
-                setUpUsers(
-                    count = 2,
-                    isLastGuestUser = true,
-                    selectedIndex = 1,
-                )
+                setUpUsers(count = 2, isLastGuestUser = true, selectedIndex = 1)
             underTest.refreshUsers()
             assertThat(userInfos).isEqualTo(thirdExpectedValue)
             assertThat(selectedUserInfo).isEqualTo(thirdExpectedValue[1])
@@ -177,12 +166,7 @@
     fun refreshUsers_sortsByCreationTime_guestUserLast() =
         testScope.runTest {
             underTest = create(testScope.backgroundScope)
-            val unsortedUsers =
-                setUpUsers(
-                    count = 3,
-                    selectedIndex = 0,
-                    isLastGuestUser = true,
-                )
+            val unsortedUsers = setUpUsers(count = 3, selectedIndex = 0, isLastGuestUser = true)
             unsortedUsers[0].creationTime = 999
             unsortedUsers[1].creationTime = 900
             unsortedUsers[2].creationTime = 950
@@ -207,10 +191,7 @@
     ): List<UserInfo> {
         val userInfos =
             (0 until count).map { index ->
-                createUserInfo(
-                    index,
-                    isGuest = isLastGuestUser && index == count - 1,
-                )
+                createUserInfo(index, isGuest = isLastGuestUser && index == count - 1)
             }
         whenever(manager.aliveUsers).thenReturn(userInfos)
         tracker.set(userInfos, selectedIndex)
@@ -224,16 +205,10 @@
             var selectedUserInfo: UserInfo? = null
             val job = underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this)
 
-            setUpUsers(
-                count = 2,
-                selectedIndex = 0,
-            )
+            setUpUsers(count = 2, selectedIndex = 0)
             tracker.onProfileChanged()
             assertThat(selectedUserInfo?.id).isEqualTo(0)
-            setUpUsers(
-                count = 2,
-                selectedIndex = 1,
-            )
+            setUpUsers(count = 2, selectedIndex = 1)
             tracker.onProfileChanged()
             assertThat(selectedUserInfo?.id).isEqualTo(1)
             job.cancel()
@@ -287,10 +262,7 @@
             job.cancel()
         }
 
-    private fun createUserInfo(
-        id: Int,
-        isGuest: Boolean,
-    ): UserInfo {
+    private fun createUserInfo(id: Int, isGuest: Boolean): UserInfo {
         val flags = 0
         return UserInfo(
             id,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelTest.kt
index faf01ed..1e6e52a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelTest.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.plugins.fakeVolumeDialogController
 import com.android.systemui.testKosmos
+import com.android.systemui.volume.data.repository.audioSystemRepository
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.TestScope
@@ -65,8 +66,8 @@
 
             setUpRingerModeAndOpenDrawer(normalRingerMode)
 
-            assertThat(ringerViewModel).isNotNull()
-            assertThat(ringerViewModel?.drawerState)
+            assertThat(ringerViewModel).isInstanceOf(RingerViewModelState.Available::class.java)
+            assertThat((ringerViewModel as RingerViewModelState.Available).uiModel.drawerState)
                 .isEqualTo(RingerDrawerState.Open(normalRingerMode))
         }
 
@@ -80,8 +81,8 @@
             underTest.onRingerButtonClicked(normalRingerMode)
             controller.getState()
 
-            assertThat(ringerViewModel).isNotNull()
-            assertThat(ringerViewModel?.drawerState)
+            assertThat(ringerViewModel).isInstanceOf(RingerViewModelState.Available::class.java)
+            assertThat((ringerViewModel as RingerViewModelState.Available).uiModel.drawerState)
                 .isEqualTo(RingerDrawerState.Closed(normalRingerMode))
         }
 
@@ -97,16 +98,12 @@
             controller.getState()
             runCurrent()
 
-            assertThat(ringerViewModel).isNotNull()
-            assertThat(
-                    ringerViewModel
-                        ?.availableButtons
-                        ?.get(ringerViewModel!!.currentButtonIndex)
-                        ?.ringerMode
-                )
+            assertThat(ringerViewModel).isInstanceOf(RingerViewModelState.Available::class.java)
+
+            var uiModel = (ringerViewModel as RingerViewModelState.Available).uiModel
+            assertThat(uiModel.availableButtons[uiModel.currentButtonIndex]?.ringerMode)
                 .isEqualTo(vibrateRingerMode)
-            assertThat(ringerViewModel?.drawerState)
-                .isEqualTo(RingerDrawerState.Closed(vibrateRingerMode))
+            assertThat(uiModel.drawerState).isEqualTo(RingerDrawerState.Closed(vibrateRingerMode))
 
             val silentRingerMode = RingerMode(RINGER_MODE_SILENT)
             // Open drawer
@@ -118,27 +115,48 @@
             controller.getState()
             runCurrent()
 
-            assertThat(ringerViewModel).isNotNull()
-            assertThat(
-                    ringerViewModel
-                        ?.availableButtons
-                        ?.get(ringerViewModel!!.currentButtonIndex)
-                        ?.ringerMode
-                )
+            assertThat(ringerViewModel).isInstanceOf(RingerViewModelState.Available::class.java)
+
+            uiModel = (ringerViewModel as RingerViewModelState.Available).uiModel
+            assertThat(uiModel.availableButtons[uiModel.currentButtonIndex]?.ringerMode)
                 .isEqualTo(silentRingerMode)
-            assertThat(ringerViewModel?.drawerState)
-                .isEqualTo(RingerDrawerState.Closed(silentRingerMode))
+            assertThat(uiModel.drawerState).isEqualTo(RingerDrawerState.Closed(silentRingerMode))
             assertThat(controller.hasScheduledTouchFeedback).isFalse()
             assertThat(vibratorHelper.totalVibrations).isEqualTo(2)
         }
 
-    private fun TestScope.setUpRingerModeAndOpenDrawer(selectedRingerMode: RingerMode) {
-        controller.setStreamVolume(STREAM_RING, 50)
-        controller.setRingerMode(selectedRingerMode.value, false)
-        runCurrent()
+    @Test
+    fun onVolumeSingleMode_ringerIsUnavailable() =
+        testScope.runTest {
+            val ringerViewModel by collectLastValue(underTest.ringerViewModel)
 
+            kosmos.audioSystemRepository.setIsSingleVolume(true)
+            setUpRingerMode(RingerMode(RINGER_MODE_NORMAL))
+
+            assertThat(ringerViewModel).isInstanceOf(RingerViewModelState.Unavailable::class.java)
+        }
+
+    @Test
+    fun setUnsupportedRingerMode_ringerIsUnavailable() =
+        testScope.runTest {
+            val ringerViewModel by collectLastValue(underTest.ringerViewModel)
+
+            controller.setHasVibrator(false)
+            setUpRingerMode(RingerMode(RINGER_MODE_VIBRATE))
+
+            assertThat(ringerViewModel).isInstanceOf(RingerViewModelState.Unavailable::class.java)
+        }
+
+    private fun TestScope.setUpRingerModeAndOpenDrawer(selectedRingerMode: RingerMode) {
+        setUpRingerMode(selectedRingerMode)
         underTest.onRingerButtonClicked(RingerMode(selectedRingerMode.value))
         controller.getState()
         runCurrent()
     }
+
+    private fun TestScope.setUpRingerMode(selectedRingerMode: RingerMode) {
+        controller.setStreamVolume(STREAM_RING, 50)
+        controller.setRingerMode(selectedRingerMode.value, false)
+        runCurrent()
+    }
 }
diff --git a/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceConfigPlugin.kt b/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceConfigPlugin.kt
index 84f39af..d16017a 100644
--- a/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceConfigPlugin.kt
+++ b/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceConfigPlugin.kt
@@ -23,4 +23,6 @@
     val isDefaultDateWeatherDisabled: Boolean
     /** Gets if Smartspace should use ViewPager2 */
     val isViewPager2Enabled: Boolean
+    /** Gets if card swipe event should be logged */
+    val isSwipeEventLoggingEnabled: Boolean
 }
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 0aa5ccf..fe720b9 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -342,8 +342,12 @@
 
     <!-- Content description for the status bar chip shown to the user when they're sharing their screen to another app on the device [CHAR LIMIT=NONE] -->
     <string name="share_to_app_chip_accessibility_label">Sharing screen</string>
+    <!-- Content description for the status bar chip shown to the user when they're sharing their screen or audio to another app on the device [CHAR LIMIT=NONE] -->
+    <string name="share_to_app_chip_accessibility_label_generic">Sharing content</string>
     <!-- Title for a dialog shown to the user that will let them stop sharing their screen to another app on the device [CHAR LIMIT=50] -->
     <string name="share_to_app_stop_dialog_title">Stop sharing screen?</string>
+    <!-- Title for a dialog shown to the user that will let them stop sharing their screen or audio to another app on the device [CHAR LIMIT=50] -->
+    <string name="share_to_app_stop_dialog_title_generic">Stop sharing?</string>
     <!-- Text telling a user that they're currently sharing their entire screen to [host_app_name] (i.e. [host_app_name] can currently see all screen content) [CHAR LIMIT=150] -->
     <string name="share_to_app_stop_dialog_message_entire_screen_with_host_app">You\'re currently sharing your entire screen with <xliff:g id="host_app_name" example="Screen Recorder App">%1$s</xliff:g></string>
     <!-- Text telling a user that they're currently sharing their entire screen to an app (but we don't know what app) [CHAR LIMIT=150] -->
@@ -352,6 +356,8 @@
     <string name="share_to_app_stop_dialog_message_single_app_specific">You\'re currently sharing <xliff:g id="app_being_shared_name" example="Photos App">%1$s</xliff:g></string>
     <!-- Text telling a user that they're currently sharing their screen [CHAR LIMIT=150] -->
     <string name="share_to_app_stop_dialog_message_single_app_generic">You\'re currently sharing an app</string>
+    <!-- Text telling a user that they're currently sharing something to an app [CHAR LIMIT=100] -->
+    <string name="share_to_app_stop_dialog_message_generic">You\'re currently sharing with an app</string>
     <!-- Button to stop screen sharing [CHAR LIMIT=35] -->
     <string name="share_to_app_stop_dialog_button">Stop sharing</string>
 
@@ -1311,6 +1317,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/mediaprojection/data/model/MediaProjectionState.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt
index 82b4825..2fa3405 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt
@@ -32,10 +32,8 @@
      *   media projection. Null if the media projection is going to this same device (e.g. another
      *   app is recording the screen).
      */
-    sealed class Projecting(
-        open val hostPackage: String,
-        open val hostDeviceName: String?,
-    ) : MediaProjectionState {
+    sealed class Projecting(open val hostPackage: String, open val hostDeviceName: String?) :
+        MediaProjectionState {
         /** The entire screen is being projected. */
         data class EntireScreen(
             override val hostPackage: String,
@@ -48,5 +46,11 @@
             override val hostDeviceName: String?,
             val task: RunningTaskInfo,
         ) : Projecting(hostPackage, hostDeviceName)
+
+        /** The screen is not being projected, only audio is being projected. */
+        data class NoScreen(
+            override val hostPackage: String,
+            override val hostDeviceName: String? = null,
+        ) : Projecting(hostPackage, hostDeviceName)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
index 5704e80..35efd75 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
@@ -23,6 +23,7 @@
 import android.os.Handler
 import android.view.ContentRecordingSession
 import android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY
+import com.android.systemui.Flags
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -94,7 +95,7 @@
                                 {},
                                 { "MediaProjectionManager.Callback#onStart" },
                             )
-                            trySendWithFailureLogging(CallbackEvent.OnStart, TAG)
+                            trySendWithFailureLogging(CallbackEvent.OnStart(info), TAG)
                         }
 
                         override fun onStop(info: MediaProjectionInfo?) {
@@ -109,7 +110,7 @@
 
                         override fun onRecordingSessionSet(
                             info: MediaProjectionInfo,
-                            session: ContentRecordingSession?
+                            session: ContentRecordingSession?,
                         ) {
                             logger.log(
                                 TAG,
@@ -142,7 +143,21 @@
             // #onRecordingSessionSet and we don't emit "Projecting".
             .mapLatest {
                 when (it) {
-                    is CallbackEvent.OnStart,
+                    is CallbackEvent.OnStart -> {
+                        if (!Flags.statusBarShowAudioOnlyProjectionChip()) {
+                            return@mapLatest MediaProjectionState.NotProjecting
+                        }
+                        // It's possible for a projection to be audio-only, in which case `OnStart`
+                        // will occur but `OnRecordingSessionSet` will not. We should still consider
+                        // us to be projecting even if only audio is projecting. See b/373308507.
+                        if (it.info != null) {
+                            MediaProjectionState.Projecting.NoScreen(
+                                hostPackage = it.info.packageName
+                            )
+                        } else {
+                            MediaProjectionState.NotProjecting
+                        }
+                    }
                     is CallbackEvent.OnStop -> MediaProjectionState.NotProjecting
                     is CallbackEvent.OnRecordingSessionSet -> stateForSession(it.info, it.session)
                 }
@@ -155,7 +170,7 @@
 
     private suspend fun stateForSession(
         info: MediaProjectionInfo,
-        session: ContentRecordingSession?
+        session: ContentRecordingSession?,
     ): MediaProjectionState {
         if (session == null) {
             return MediaProjectionState.NotProjecting
@@ -184,7 +199,7 @@
      * the correct callback ordering.
      */
     sealed interface CallbackEvent {
-        data object OnStart : CallbackEvent
+        data class OnStart(val info: MediaProjectionInfo?) : CallbackEvent
 
         data object OnStop : CallbackEvent
 
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt
index 118639c..ccc54f1 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt
@@ -68,6 +68,7 @@
                     }
                 }
                 is MediaProjectionState.Projecting.EntireScreen,
+                is MediaProjectionState.Projecting.NoScreen,
                 is MediaProjectionState.NotProjecting -> {
                     flowOf(TaskSwitchState.NotProjectingTask)
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpandsOnStatusBarLongPress.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpandsOnStatusBarLongPress.kt
new file mode 100644
index 0000000..6d8e898
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpandsOnStatusBarLongPress.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the shade expands on status bar long press flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object ShadeExpandsOnStatusBarLongPress {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_SHADE_EXPANDS_ON_STATUS_BAR_LONG_PRESS
+
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
+    /** Is the refactor enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.shadeExpandsOnStatusBarLongPress()
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This will throw an exception if
+     * the flag is not enabled to ensure that the refactor author catches issues in testing.
+     * Caution!! Using this check incorrectly will cause crashes in nextfood builds!
+     */
+    @JvmStatic
+    inline fun assertInNewMode() = RefactorFlagUtils.assertInNewMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt b/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt
index 0e1bf72..5db1dcb 100644
--- a/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.smartspace.config
 
+import com.android.systemui.Flags.smartspaceSwipeEventLogging
 import com.android.systemui.Flags.smartspaceViewpager2
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.plugins.BcSmartspaceConfigPlugin
@@ -27,4 +28,7 @@
 
     override val isViewPager2Enabled: Boolean
         get() = smartspaceViewpager2()
+
+    override val isSwipeEventLoggingEnabled: Boolean
+        get() = smartspaceSwipeEventLogging()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
index d4ad6ee..1107206 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
@@ -68,23 +68,24 @@
     private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
     @StatusBarChipsLog private val logger: LogBuffer,
 ) : OngoingActivityChipViewModel {
-    /**
-     * The cast chip to show, based only on MediaProjection API events.
-     *
-     * This chip will only be [OngoingActivityChipModel.Shown] when the user is casting their
-     * *screen*. If the user is only casting audio, this chip will be
-     * [OngoingActivityChipModel.Hidden].
-     */
+    /** The cast chip to show, based only on MediaProjection API events. */
     private val projectionChip: StateFlow<OngoingActivityChipModel> =
         mediaProjectionChipInteractor.projection
             .map { projectionModel ->
                 when (projectionModel) {
                     is ProjectionChipModel.NotProjecting -> OngoingActivityChipModel.Hidden()
                     is ProjectionChipModel.Projecting -> {
-                        if (projectionModel.type != ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE) {
-                            OngoingActivityChipModel.Hidden()
-                        } else {
-                            createCastScreenToOtherDeviceChip(projectionModel)
+                        when (projectionModel.receiver) {
+                            ProjectionChipModel.Receiver.CastToOtherDevice -> {
+                                when (projectionModel.contentType) {
+                                    ProjectionChipModel.ContentType.Screen ->
+                                        createCastScreenToOtherDeviceChip(projectionModel)
+                                    ProjectionChipModel.ContentType.Audio ->
+                                        createIconOnlyCastChip(deviceName = null)
+                                }
+                            }
+                            ProjectionChipModel.Receiver.ShareToApp ->
+                                OngoingActivityChipModel.Hidden()
                         }
                     }
                 }
@@ -98,9 +99,9 @@
      * This chip will be [OngoingActivityChipModel.Shown] when the user is casting their screen *or*
      * their audio.
      *
-     * The MediaProjection APIs are not invoked for casting *only audio* to another device because
-     * MediaProjection is only concerned with *screen* sharing (see b/342169876). We listen to
-     * MediaRouter APIs here to cover audio-only casting.
+     * The MediaProjection APIs are typically not invoked for casting *only audio* to another device
+     * because MediaProjection is only concerned with *screen* sharing (see b/342169876). We listen
+     * to MediaRouter APIs here to cover audio-only casting.
      *
      * Note that this means we will start showing the cast chip before the casting actually starts,
      * for **both** audio-only casting and screen casting. MediaRouter is aware of all
@@ -139,7 +140,7 @@
                         str1 = projection.logName
                         str2 = router.logName
                     },
-                    { "projectionChip=$str1 > routerChip=$str2" }
+                    { "projectionChip=$str1 > routerChip=$str2" },
                 )
 
                 // A consequence of b/269975671 is that MediaRouter and MediaProjection APIs fire at
@@ -186,7 +187,7 @@
     }
 
     private fun createCastScreenToOtherDeviceChip(
-        state: ProjectionChipModel.Projecting,
+        state: ProjectionChipModel.Projecting
     ): OngoingActivityChipModel.Shown {
         return OngoingActivityChipModel.Shown.Timer(
             icon =
@@ -195,7 +196,7 @@
                         CAST_TO_OTHER_DEVICE_ICON,
                         // This string is "Casting screen"
                         ContentDescription.Resource(
-                            R.string.cast_screen_to_other_device_chip_accessibility_label,
+                            R.string.cast_screen_to_other_device_chip_accessibility_label
                         ),
                     )
                 ),
@@ -236,9 +237,7 @@
         )
     }
 
-    private fun createCastScreenToOtherDeviceDialogDelegate(
-        state: ProjectionChipModel.Projecting,
-    ) =
+    private fun createCastScreenToOtherDeviceDialogDelegate(state: ProjectionChipModel.Projecting) =
         EndCastScreenToOtherDeviceDialogDelegate(
             endMediaProjectionDialogHelper,
             context,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt
index 8abe1d3..27b2465 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.chips.mediaprojection.domain.interactor
 
 import android.content.pm.PackageManager
+import com.android.systemui.Flags
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.log.LogBuffer
@@ -59,23 +60,43 @@
                         ProjectionChipModel.NotProjecting
                     }
                     is MediaProjectionState.Projecting -> {
-                        val type =
+                        val receiver =
                             if (packageHasCastingCapabilities(packageManager, state.hostPackage)) {
-                                ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE
+                                ProjectionChipModel.Receiver.CastToOtherDevice
                             } else {
-                                ProjectionChipModel.Type.SHARE_TO_APP
+                                ProjectionChipModel.Receiver.ShareToApp
                             }
+                        val contentType =
+                            if (Flags.statusBarShowAudioOnlyProjectionChip()) {
+                                when (state) {
+                                    is MediaProjectionState.Projecting.EntireScreen,
+                                    is MediaProjectionState.Projecting.SingleTask ->
+                                        ProjectionChipModel.ContentType.Screen
+                                    is MediaProjectionState.Projecting.NoScreen ->
+                                        ProjectionChipModel.ContentType.Audio
+                                }
+                            } else {
+                                ProjectionChipModel.ContentType.Screen
+                            }
+
                         logger.log(
                             TAG,
                             LogLevel.INFO,
                             {
-                                str1 = type.name
-                                str2 = state.hostPackage
-                                str3 = state.hostDeviceName
+                                bool1 = receiver == ProjectionChipModel.Receiver.CastToOtherDevice
+                                bool2 = contentType == ProjectionChipModel.ContentType.Screen
+                                str1 = state.hostPackage
+                                str2 = state.hostDeviceName
                             },
-                            { "State: Projecting(type=$str1 hostPackage=$str2 hostDevice=$str3)" }
+                            {
+                                "State: Projecting(" +
+                                    "receiver=${if (bool1) "CastToOtherDevice" else "ShareToApp"} " +
+                                    "contentType=${if (bool2) "Screen" else "Audio"} " +
+                                    "hostPackage=$str1 " +
+                                    "hostDevice=$str2)"
+                            },
                         )
-                        ProjectionChipModel.Projecting(type, state)
+                        ProjectionChipModel.Projecting(receiver, contentType, state)
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt
index 85682f5..c6283e9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt
@@ -28,16 +28,22 @@
 
     /** Media is currently being projected. */
     data class Projecting(
-        val type: Type,
+        val receiver: Receiver,
+        val contentType: ContentType,
         val projectionState: MediaProjectionState.Projecting,
     ) : ProjectionChipModel()
 
-    enum class Type {
-        /**
-         * This projection is sharing your phone screen content to another app on the same device.
-         */
-        SHARE_TO_APP,
-        /** This projection is sharing your phone screen content to a different device. */
-        CAST_TO_OTHER_DEVICE,
+    enum class Receiver {
+        /** This projection is sharing to another app on the same device. */
+        ShareToApp,
+        /** This projection is sharing to a different device. */
+        CastToOtherDevice,
+    }
+
+    enum class ContentType {
+        /** This projection is sharing your device's screen content. */
+        Screen,
+        /** This projection is sharing your device's audio (but *not* screen). */
+        Audio,
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegate.kt
new file mode 100644
index 0000000..8ec0567
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegate.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips.sharetoapp.ui.view
+
+import android.content.Context
+import android.os.Bundle
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
+import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.ShareToAppChipViewModel.Companion.SHARE_TO_APP_ICON
+import com.android.systemui.statusbar.phone.SystemUIDialog
+
+/**
+ * A dialog that lets the user stop an ongoing share-to-app event. The user could be sharing their
+ * screen or just sharing their audio. This dialog uses generic strings to handle both cases well.
+ */
+class EndGenericShareToAppDialogDelegate(
+    private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
+    private val context: Context,
+    private val stopAction: () -> Unit,
+) : SystemUIDialog.Delegate {
+    override fun createDialog(): SystemUIDialog {
+        return endMediaProjectionDialogHelper.createDialog(this)
+    }
+
+    override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+        val message = context.getString(R.string.share_to_app_stop_dialog_message_generic)
+        with(dialog) {
+            setIcon(SHARE_TO_APP_ICON)
+            setTitle(R.string.share_to_app_stop_dialog_title_generic)
+            setMessage(message)
+            // No custom on-click, because the dialog will automatically be dismissed when the
+            // button is clicked anyway.
+            setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
+            setPositiveButton(
+                R.string.share_to_app_stop_dialog_button,
+                endMediaProjectionDialogHelper.wrapStopAction(stopAction),
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegate.kt
similarity index 97%
rename from packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegate.kt
index d10bd77..053016e3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegate.kt
@@ -26,7 +26,7 @@
 import com.android.systemui.statusbar.phone.SystemUIDialog
 
 /** A dialog that lets the user stop an ongoing share-screen-to-app event. */
-class EndShareToAppDialogDelegate(
+class EndShareScreenToAppDialogDelegate(
     private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
     private val context: Context,
     private val stopAction: () -> Unit,
@@ -71,7 +71,7 @@
             if (hostAppName != null) {
                 context.getString(
                     R.string.share_to_app_stop_dialog_message_entire_screen_with_host_app,
-                    hostAppName
+                    hostAppName,
                 )
             } else {
                 context.getString(R.string.share_to_app_stop_dialog_message_entire_screen)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
index d99a916..11d077f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
@@ -32,7 +32,8 @@
 import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractor
 import com.android.systemui.statusbar.chips.mediaprojection.domain.model.ProjectionChipModel
 import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
-import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndShareToAppDialogDelegate
+import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndGenericShareToAppDialogDelegate
+import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndShareScreenToAppDialogDelegate
 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.ChipTransitionHelper
@@ -68,10 +69,17 @@
                 when (projectionModel) {
                     is ProjectionChipModel.NotProjecting -> OngoingActivityChipModel.Hidden()
                     is ProjectionChipModel.Projecting -> {
-                        if (projectionModel.type != ProjectionChipModel.Type.SHARE_TO_APP) {
-                            OngoingActivityChipModel.Hidden()
-                        } else {
-                            createShareToAppChip(projectionModel)
+                        when (projectionModel.receiver) {
+                            ProjectionChipModel.Receiver.ShareToApp -> {
+                                when (projectionModel.contentType) {
+                                    ProjectionChipModel.ContentType.Screen ->
+                                        createShareScreenToAppChip(projectionModel)
+                                    ProjectionChipModel.ContentType.Audio ->
+                                        createIconOnlyShareToAppChip()
+                                }
+                            }
+                            ProjectionChipModel.Receiver.CastToOtherDevice ->
+                                OngoingActivityChipModel.Hidden()
                         }
                     }
                 }
@@ -105,8 +113,8 @@
         mediaProjectionChipInteractor.stopProjecting()
     }
 
-    private fun createShareToAppChip(
-        state: ProjectionChipModel.Projecting,
+    private fun createShareScreenToAppChip(
+        state: ProjectionChipModel.Projecting
     ): OngoingActivityChipModel.Shown {
         return OngoingActivityChipModel.Shown.Timer(
             icon =
@@ -120,11 +128,33 @@
             // TODO(b/332662551): Maybe use a MediaProjection API to fetch this time.
             startTimeMs = systemClock.elapsedRealtime(),
             createDialogLaunchOnClickListener(
-                createShareToAppDialogDelegate(state),
+                createShareScreenToAppDialogDelegate(state),
+                dialogTransitionAnimator,
+                DialogCuj(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, tag = "Share to app"),
+                logger,
+                TAG,
+            ),
+        )
+    }
+
+    private fun createIconOnlyShareToAppChip(): OngoingActivityChipModel.Shown {
+        return OngoingActivityChipModel.Shown.IconOnly(
+            icon =
+                OngoingActivityChipModel.ChipIcon.SingleColorIcon(
+                    Icon.Resource(
+                        SHARE_TO_APP_ICON,
+                        ContentDescription.Resource(
+                            R.string.share_to_app_chip_accessibility_label_generic
+                        ),
+                    )
+                ),
+            colors = ColorsModel.Red,
+            createDialogLaunchOnClickListener(
+                createGenericShareToAppDialogDelegate(),
                 dialogTransitionAnimator,
                 DialogCuj(
                     Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP,
-                    tag = "Share to app",
+                    tag = "Share to app audio only",
                 ),
                 logger,
                 TAG,
@@ -132,14 +162,21 @@
         )
     }
 
-    private fun createShareToAppDialogDelegate(state: ProjectionChipModel.Projecting) =
-        EndShareToAppDialogDelegate(
+    private fun createShareScreenToAppDialogDelegate(state: ProjectionChipModel.Projecting) =
+        EndShareScreenToAppDialogDelegate(
             endMediaProjectionDialogHelper,
             context,
             stopAction = this::stopProjectingFromDialog,
             state,
         )
 
+    private fun createGenericShareToAppDialogDelegate() =
+        EndGenericShareToAppDialogDelegate(
+            endMediaProjectionDialogHelper,
+            context,
+            stopAction = this::stopProjectingFromDialog,
+        )
+
     companion object {
         @DrawableRes val SHARE_TO_APP_ICON = R.drawable.ic_present_to_all
         private const val TAG = "ShareToAppVM"
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/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/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt
index 7265b821..281e57f 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt
@@ -21,6 +21,7 @@
 import android.media.AudioManager.RINGER_MODE_SILENT
 import android.media.AudioManager.RINGER_MODE_VIBRATE
 import android.provider.Settings
+import com.android.settingslib.volume.data.repository.AudioSystemRepository
 import com.android.settingslib.volume.shared.model.RingerMode
 import com.android.systemui.plugins.VolumeDialogController
 import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
@@ -43,6 +44,7 @@
     @VolumeDialog private val coroutineScope: CoroutineScope,
     volumeDialogStateInteractor: VolumeDialogStateInteractor,
     private val controller: VolumeDialogController,
+    private val audioSystemRepository: AudioSystemRepository,
 ) {
 
     val ringerModel: Flow<VolumeDialogRingerModel> =
@@ -70,6 +72,7 @@
                 isMuted = it.level == 0 || it.muted,
                 level = it.level,
                 levelMax = it.levelMax,
+                isSingleVolume = audioSystemRepository.isSingleVolume,
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/shared/model/VolumeDialogRingerModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/shared/model/VolumeDialogRingerModel.kt
index cf23f1a..3c24e02 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/shared/model/VolumeDialogRingerModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/shared/model/VolumeDialogRingerModel.kt
@@ -31,4 +31,6 @@
     val level: Int,
     /** Ring stream maximum level */
     val levelMax: Int,
+    /** in single volume mode */
+    val isSingleVolume: Boolean,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerViewModelState.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerViewModelState.kt
new file mode 100644
index 0000000..78b00af
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerViewModelState.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.ringer.ui.viewmodel
+
+/** Models ringer view model state. */
+sealed class RingerViewModelState {
+
+    data class Available(val uiModel: RingerViewModel) : RingerViewModelState()
+
+    data object Unavailable : RingerViewModelState()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
index ac82ae3..5b73107 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
@@ -34,11 +34,10 @@
 import dagger.assisted.AssistedInject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.stateIn
 
@@ -56,13 +55,12 @@
 
     private val drawerState = MutableStateFlow<RingerDrawerState>(RingerDrawerState.Initial)
 
-    val ringerViewModel: Flow<RingerViewModel> =
+    val ringerViewModel: StateFlow<RingerViewModelState> =
         combine(interactor.ringerModel, drawerState) { ringerModel, state ->
                 ringerModel.toViewModel(state)
             }
             .flowOn(backgroundDispatcher)
-            .stateIn(coroutineScope, SharingStarted.Eagerly, null)
-            .filterNotNull()
+            .stateIn(coroutineScope, SharingStarted.Eagerly, RingerViewModelState.Unavailable)
 
     // Vibration attributes.
     private val sonificiationVibrationAttributes =
@@ -105,16 +103,22 @@
 
     private fun VolumeDialogRingerModel.toViewModel(
         drawerState: RingerDrawerState
-    ): RingerViewModel {
+    ): RingerViewModelState {
         val currentIndex = availableModes.indexOf(currentRingerMode)
         if (currentIndex == -1) {
             volumeDialogLogger.onCurrentRingerModeIsUnsupported(currentRingerMode)
         }
-        return RingerViewModel(
-            availableButtons = availableModes.map { mode -> toButtonViewModel(mode) },
-            currentButtonIndex = currentIndex,
-            drawerState = drawerState,
-        )
+        return if (currentIndex == -1 || isSingleVolume) {
+            RingerViewModelState.Unavailable
+        } else {
+            RingerViewModelState.Available(
+                RingerViewModel(
+                    availableButtons = availableModes.map { mode -> toButtonViewModel(mode) },
+                    currentButtonIndex = currentIndex,
+                    drawerState = drawerState,
+                )
+            )
+        }
     }
 
     private fun VolumeDialogRingerModel.toButtonViewModel(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayScopeRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayScopeRepositoryImplTest.kt
index 5d5c120..da7a723 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayScopeRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayScopeRepositoryImplTest.kt
@@ -23,7 +23,7 @@
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.kosmos.unconfinedTestDispatcher
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
@@ -38,7 +38,7 @@
 @SmallTest
 class DisplayScopeRepositoryImplTest : SysuiTestCase() {
 
-    private val kosmos = testKosmos().also { it.testDispatcher = it.unconfinedTestDispatcher }
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
     private val testScope = kosmos.testScope
     private val fakeDisplayRepository = kosmos.displayRepository
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt
index ff3186a..5a76489 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt
@@ -25,9 +25,8 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.display.shared.model.DisplayWindowProperties
 import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.kosmos.unconfinedTestDispatcher
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
@@ -44,7 +43,7 @@
 @SmallTest
 class DisplayWindowPropertiesRepositoryImplTest : SysuiTestCase() {
 
-    private val kosmos = testKosmos().also { it.testDispatcher = it.unconfinedTestDispatcher }
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
     private val fakeDisplayRepository = kosmos.displayRepository
     private val testScope = kosmos.testScope
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
index fa69fdd..929b0aa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
@@ -50,8 +50,9 @@
 import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRenderer
 import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRendererFactory
 import com.android.systemui.keyguard.ui.preview.KeyguardRemotePreviewManager
-import com.android.systemui.kosmos.unconfinedTestDispatcher
-import com.android.systemui.kosmos.unconfinedTestScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.res.R
 import com.android.systemui.scene.domain.interactor.sceneInteractor
@@ -67,7 +68,6 @@
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.settings.fakeSettings
-import com.android.systemui.util.settings.unconfinedDispatcherFakeSettings
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -89,10 +89,10 @@
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 class CustomizationProviderTest : SysuiTestCase() {
 
-    private val kosmos = testKosmos()
-    private val testDispatcher = kosmos.unconfinedTestDispatcher
-    private val testScope = kosmos.unconfinedTestScope
-    private val fakeSettings = kosmos.unconfinedDispatcherFakeSettings
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+    private val testDispatcher = kosmos.testDispatcher
+    private val testScope = kosmos.testScope
+    private val fakeSettings = kosmos.fakeSettings
 
     @Mock private lateinit var lockPatternUtils: LockPatternUtils
     @Mock private lateinit var keyguardStateController: KeyguardStateController
@@ -129,13 +129,7 @@
                 context = context,
                 userFileManager =
                     mock<UserFileManager>().apply {
-                        whenever(
-                                getSharedPreferences(
-                                    anyString(),
-                                    anyInt(),
-                                    anyInt(),
-                                )
-                            )
+                        whenever(getSharedPreferences(anyString(), anyInt(), anyInt()))
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = userTracker,
@@ -288,10 +282,7 @@
             val affordanceId = AFFORDANCE_2
             val affordanceName = AFFORDANCE_2_NAME
 
-            insertSelection(
-                slotId = slotId,
-                affordanceId = affordanceId,
-            )
+            insertSelection(slotId = slotId, affordanceId = affordanceId)
 
             assertThat(querySelections())
                 .isEqualTo(
@@ -311,14 +302,8 @@
             assertThat(querySlots())
                 .isEqualTo(
                     listOf(
-                        Slot(
-                            id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
-                            capacity = 1,
-                        ),
-                        Slot(
-                            id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
-                            capacity = 1,
-                        ),
+                        Slot(id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, capacity = 1),
+                        Slot(id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, capacity = 1),
                     )
                 )
             runCurrent()
@@ -330,16 +315,8 @@
             assertThat(queryAffordances())
                 .isEqualTo(
                     listOf(
-                        Affordance(
-                            id = AFFORDANCE_1,
-                            name = AFFORDANCE_1_NAME,
-                            iconResourceId = 1,
-                        ),
-                        Affordance(
-                            id = AFFORDANCE_2,
-                            name = AFFORDANCE_2_NAME,
-                            iconResourceId = 2,
-                        ),
+                        Affordance(id = AFFORDANCE_1, name = AFFORDANCE_1_NAME, iconResourceId = 1),
+                        Affordance(id = AFFORDANCE_2, name = AFFORDANCE_2_NAME, iconResourceId = 2),
                     )
                 )
         }
@@ -361,10 +338,7 @@
                 "${Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID} = ? AND" +
                     " ${Contract.LockScreenQuickAffordances.SelectionTable.Columns.AFFORDANCE_ID}" +
                     " = ?",
-                arrayOf(
-                    KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
-                    AFFORDANCE_2,
-                ),
+                arrayOf(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, AFFORDANCE_2),
             )
 
             assertThat(querySelections())
@@ -394,9 +368,7 @@
             context.contentResolver.delete(
                 Contract.LockScreenQuickAffordances.SelectionTable.URI,
                 Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID,
-                arrayOf(
-                    KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
-                ),
+                arrayOf(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END),
             )
 
             assertThat(querySelections())
@@ -428,31 +400,22 @@
             assertThat(result.containsKey(KeyguardRemotePreviewManager.KEY_PREVIEW_CALLBACK))
         }
 
-    private fun insertSelection(
-        slotId: String,
-        affordanceId: String,
-    ) {
+    private fun insertSelection(slotId: String, affordanceId: String) {
         context.contentResolver.insert(
             Contract.LockScreenQuickAffordances.SelectionTable.URI,
             ContentValues().apply {
                 put(Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID, slotId)
                 put(
                     Contract.LockScreenQuickAffordances.SelectionTable.Columns.AFFORDANCE_ID,
-                    affordanceId
+                    affordanceId,
                 )
-            }
+            },
         )
     }
 
     private fun querySelections(): List<Selection> {
         return context.contentResolver
-            .query(
-                Contract.LockScreenQuickAffordances.SelectionTable.URI,
-                null,
-                null,
-                null,
-                null,
-            )
+            .query(Contract.LockScreenQuickAffordances.SelectionTable.URI, null, null, null, null)
             ?.use { cursor ->
                 buildList {
                     val slotIdColumnIndex =
@@ -491,13 +454,7 @@
 
     private fun querySlots(): List<Slot> {
         return context.contentResolver
-            .query(
-                Contract.LockScreenQuickAffordances.SlotTable.URI,
-                null,
-                null,
-                null,
-                null,
-            )
+            .query(Contract.LockScreenQuickAffordances.SlotTable.URI, null, null, null, null)
             ?.use { cursor ->
                 buildList {
                     val idColumnIndex =
@@ -526,13 +483,7 @@
 
     private fun queryAffordances(): List<Affordance> {
         return context.contentResolver
-            .query(
-                Contract.LockScreenQuickAffordances.AffordanceTable.URI,
-                null,
-                null,
-                null,
-                null,
-            )
+            .query(Contract.LockScreenQuickAffordances.AffordanceTable.URI, null, null, null, null)
             ?.use { cursor ->
                 buildList {
                     val idColumnIndex =
@@ -564,22 +515,11 @@
             } ?: emptyList()
     }
 
-    data class Slot(
-        val id: String,
-        val capacity: Int,
-    )
+    data class Slot(val id: String, val capacity: Int)
 
-    data class Affordance(
-        val id: String,
-        val name: String,
-        val iconResourceId: Int,
-    )
+    data class Affordance(val id: String, val name: String, val iconResourceId: Int)
 
-    data class Selection(
-        val slotId: String,
-        val affordanceId: String,
-        val affordanceName: String,
-    )
+    data class Selection(val slotId: String, val affordanceId: String, val affordanceName: String)
 
     companion object {
         private const val AFFORDANCE_1 = "affordance_1"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
index 570c640..33b61a09 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
@@ -48,7 +48,7 @@
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.kosmos.unconfinedTestDispatcher
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.media.controls.MediaTestUtils
 import com.android.systemui.media.controls.domain.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA
 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
@@ -73,7 +73,7 @@
 import com.android.systemui.testKosmos
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.settings.GlobalSettings
-import com.android.systemui.util.settings.unconfinedDispatcherFakeSettings
+import com.android.systemui.util.settings.fakeSettings
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import java.util.Locale
@@ -119,9 +119,9 @@
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 @RunWith(ParameterizedAndroidJunit4::class)
 class MediaCarouselControllerTest(flags: FlagsParameterization) : SysuiTestCase() {
-    private val kosmos = testKosmos()
-    private val testDispatcher = kosmos.unconfinedTestDispatcher
-    private val secureSettings = kosmos.unconfinedDispatcherFakeSettings
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+    private val testDispatcher = kosmos.testDispatcher
+    private val secureSettings = kosmos.fakeSettings
 
     @Mock lateinit var mediaControlPanelFactory: Provider<MediaControlPanel>
     @Mock lateinit var mediaViewControllerFactory: Provider<MediaViewController>
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
index f8df707..a9a80b5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
@@ -8,9 +8,26 @@
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 
 var Kosmos.testDispatcher by Fixture { StandardTestDispatcher() }
-var Kosmos.unconfinedTestDispatcher by Fixture { UnconfinedTestDispatcher() }
+
+/**
+ * Force this Kosmos to use a [StandardTestDispatcher], regardless of the current Kosmos default. In
+ * short, no launch blocks will be run on this dispatcher until `TestCoroutineScheduler.runCurrent`
+ * is called. See [StandardTestDispatcher] for details.
+ *
+ * For details on this migration, see http://go/thetiger
+ */
+fun Kosmos.useStandardTestDispatcher() = apply { testDispatcher = StandardTestDispatcher() }
+
+/**
+ * Force this Kosmos to use an [UnconfinedTestDispatcher], regardless of the current Kosmos default.
+ * In short, launch blocks will be executed eagerly without waiting for
+ * `TestCoroutineScheduler.runCurrent`. See [UnconfinedTestDispatcher] for details.
+ *
+ * For details on this migration, see http://go/thetiger
+ */
+fun Kosmos.useUnconfinedTestDispatcher() = apply { testDispatcher = UnconfinedTestDispatcher() }
+
 var Kosmos.testScope by Fixture { TestScope(testDispatcher) }
-var Kosmos.unconfinedTestScope by Fixture { TestScope(unconfinedTestDispatcher) }
 var Kosmos.applicationCoroutineScope by Fixture { testScope.backgroundScope }
 var Kosmos.testCase: SysuiTestCase by Fixture()
 var Kosmos.backgroundCoroutineContext: CoroutineContext by Fixture {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeGlobalSettingsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeGlobalSettingsKosmos.kt
index 73d423c..35fa2af 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeGlobalSettingsKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeGlobalSettingsKosmos.kt
@@ -19,10 +19,5 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.testDispatcher
-import com.android.systemui.kosmos.unconfinedTestDispatcher
 
 val Kosmos.fakeGlobalSettings: FakeGlobalSettings by Fixture { FakeGlobalSettings(testDispatcher) }
-
-val Kosmos.unconfinedDispatcherFakeGlobalSettings: FakeGlobalSettings by Fixture {
-    FakeGlobalSettings(unconfinedTestDispatcher)
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettingsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettingsKosmos.kt
index e1daf9b..76ef202 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettingsKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettingsKosmos.kt
@@ -19,13 +19,8 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.testDispatcher
-import com.android.systemui.kosmos.unconfinedTestDispatcher
 import com.android.systemui.settings.userTracker
 
 val Kosmos.fakeSettings: FakeSettings by Fixture {
     FakeSettings(testDispatcher) { userTracker.userId }
 }
-
-val Kosmos.unconfinedDispatcherFakeSettings: FakeSettings by Fixture {
-    FakeSettings(unconfinedTestDispatcher) { userTracker.userId }
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt
index c2a1544..1addd91 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt
@@ -19,6 +19,7 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.plugins.volumeDialogController
+import com.android.systemui.volume.data.repository.audioSystemRepository
 import com.android.systemui.volume.dialog.domain.interactor.volumeDialogStateInteractor
 
 val Kosmos.volumeDialogRingerInteractor by
@@ -27,5 +28,6 @@
             coroutineScope = applicationCoroutineScope,
             volumeDialogStateInteractor = volumeDialogStateInteractor,
             controller = volumeDialogController,
+            audioSystemRepository = audioSystemRepository,
         )
     }
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index f6ac706..35998d9a 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -17,6 +17,7 @@
 package com.android.server.appwidget;
 
 import static android.appwidget.flags.Flags.remoteAdapterConversion;
+import static android.appwidget.flags.Flags.remoteViewsProto;
 import static android.appwidget.flags.Flags.removeAppWidgetServiceIoFromCriticalPath;
 import static android.appwidget.flags.Flags.securityPolicyInteractAcrossUsers;
 import static android.appwidget.flags.Flags.supportResumeRestoreAfterReboot;
@@ -31,6 +32,7 @@
 import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
 
 import android.Manifest;
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.PermissionName;
@@ -104,6 +106,7 @@
 import android.os.UserManager;
 import android.provider.DeviceConfig;
 import android.service.appwidget.AppWidgetServiceDumpProto;
+import android.service.appwidget.GeneratedPreviewsProto;
 import android.service.appwidget.WidgetProto;
 import android.text.TextUtils;
 import android.util.ArrayMap;
@@ -122,7 +125,9 @@
 import android.util.SparseLongArray;
 import android.util.TypedValue;
 import android.util.Xml;
+import android.util.proto.ProtoInputStream;
 import android.util.proto.ProtoOutputStream;
+import android.util.proto.ProtoUtils;
 import android.view.Display;
 import android.view.View;
 import android.widget.RemoteViews;
@@ -134,6 +139,7 @@
 import com.android.internal.appwidget.IAppWidgetHost;
 import com.android.internal.appwidget.IAppWidgetService;
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
+import com.android.internal.infra.AndroidFuture;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.os.SomeArgs;
 import com.android.internal.util.ArrayUtils;
@@ -221,6 +227,10 @@
     // XML attribute for widget ids that are pending deletion.
     // See {@link Provider#pendingDeletedWidgetIds}.
     private static final String PENDING_DELETED_IDS_ATTR = "pending_deleted_ids";
+    // Name of service directory in /data/system_ce/<user>/
+    private static final String APPWIDGET_CE_DATA_DIRNAME = "appwidget";
+    // Name of previews directory in /data/system_ce/<user>/appwidget/
+    private static final String WIDGET_PREVIEWS_DIRNAME = "previews";
 
     // Hard limit of number of hosts an app can create, note that the app that hosts the widgets
     // can have multiple instances of {@link AppWidgetHost}, typically in respect to different
@@ -316,6 +326,9 @@
 
     // Handler to the background thread that saves states to disk.
     private Handler mSaveStateHandler;
+    // Handler to the background thread that saves generated previews to disk. All operations that
+    // modify saved previews must be run on this Handler.
+    private Handler mSavePreviewsHandler;
     // Handler to the foreground thread that handles broadcasts related to user
     // and package events, as well as various internal events within
     // AppWidgetService.
@@ -359,6 +372,7 @@
         } else {
             mSaveStateHandler = BackgroundThread.getHandler();
         }
+        mSavePreviewsHandler = new Handler(BackgroundThread.get().getLooper());
         final ServiceThread serviceThread = new ServiceThread(TAG,
                 android.os.Process.THREAD_PRIORITY_FOREGROUND, false /* allowIo */);
         serviceThread.start();
@@ -378,7 +392,9 @@
                 SystemUiDeviceConfigFlags.GENERATED_PREVIEW_API_MAX_PROVIDERS,
                 DEFAULT_GENERATED_PREVIEW_MAX_PROVIDERS);
         mGeneratedPreviewsApiCounter = new ApiCounter(generatedPreviewResetInterval,
-                generatedPreviewMaxCallsPerInterval, generatedPreviewsMaxProviders);
+                generatedPreviewMaxCallsPerInterval,
+                // Set a limit on the number of providers if storing them in memory.
+                remoteViewsProto() ? Integer.MAX_VALUE : generatedPreviewsMaxProviders);
         DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_SYSTEMUI,
                 new HandlerExecutor(mCallbackHandler), this::handleSystemUiDeviceConfigChange);
 
@@ -644,7 +660,14 @@
         for (int i = 0; i < providerCount; i++) {
             Provider provider = mProviders.get(i);
             if (provider.id.uid == clearedUid) {
-                changed |= provider.clearGeneratedPreviewsLocked();
+                if (remoteViewsProto()) {
+                    changed |= clearGeneratedPreviewsAsync(provider);
+                } else {
+                    changed |= provider.clearGeneratedPreviewsLocked();
+                }
+                if (DEBUG) {
+                    Slog.e(TAG, "clearPreviewsForUidLocked " + provider + " changed " + changed);
+                }
             }
         }
         return changed;
@@ -3246,6 +3269,9 @@
         deleteWidgetsLocked(provider, UserHandle.USER_ALL);
         mProviders.remove(provider);
         mGeneratedPreviewsApiCounter.remove(provider.id);
+        if (remoteViewsProto()) {
+            clearGeneratedPreviewsAsync(provider);
+        }
 
         // no need to send the DISABLE broadcast, since the receiver is gone anyway
         cancelBroadcastsLocked(provider);
@@ -3824,6 +3850,14 @@
             } catch (IOException e) {
                 Slog.w(TAG, "Failed to read state: " + e);
             }
+
+            if (remoteViewsProto()) {
+                try {
+                    loadGeneratedPreviewCategoriesLocked(profileId);
+                } catch (IOException e) {
+                    Slog.w(TAG, "Failed to read preview categories: " + e);
+                }
+            }
         }
 
         if (version >= 0) {
@@ -4593,6 +4627,12 @@
                         keep.add(providerId);
                         // Use the new AppWidgetProviderInfo.
                         provider.setPartialInfoLocked(info);
+                        // Clear old previews
+                        if (remoteViewsProto()) {
+                            clearGeneratedPreviewsAsync(provider);
+                        } else {
+                            provider.clearGeneratedPreviewsLocked();
+                        }
                         // If it's enabled
                         final int M = provider.widgets.size();
                         if (M > 0) {
@@ -4884,6 +4924,7 @@
         mSecurityPolicy.enforceCallFromPackage(callingPackage);
         ensureWidgetCategoryCombinationIsValid(widgetCategory);
 
+        AndroidFuture<RemoteViews> result = null;
         synchronized (mLock) {
             ensureGroupStateLoadedLocked(profileId);
             final int providerCount = mProviders.size();
@@ -4917,10 +4958,23 @@
                                 callingPackage);
                 if (providerIsInCallerProfile && !shouldFilterAppAccess
                         && (providerIsInCallerPackage || hasBindAppWidgetPermission)) {
-                    return provider.getGeneratedPreviewLocked(widgetCategory);
+                    if (remoteViewsProto()) {
+                        result = getGeneratedPreviewsAsync(provider, widgetCategory);
+                    } else {
+                        return provider.getGeneratedPreviewLocked(widgetCategory);
+                    }
                 }
             }
         }
+
+        if (result != null) {
+            try {
+                return result.get();
+            } catch (Exception e) {
+                Slog.e(TAG, "Failed to get generated previews Future result", e);
+                return null;
+            }
+        }
         // Either the provider does not exist or the caller does not have permission to access its
         // previews.
         return null;
@@ -4950,8 +5004,12 @@
                         providerComponent + " is not a valid AppWidget provider");
             }
             if (mGeneratedPreviewsApiCounter.tryApiCall(providerId)) {
-                provider.setGeneratedPreviewLocked(widgetCategories, preview);
-                scheduleNotifyGroupHostsForProvidersChangedLocked(userId);
+                if (remoteViewsProto()) {
+                    setGeneratedPreviewsAsync(provider, widgetCategories, preview);
+                } else {
+                    provider.setGeneratedPreviewLocked(widgetCategories, preview);
+                    scheduleNotifyGroupHostsForProvidersChangedLocked(userId);
+                }
                 return true;
             }
             return false;
@@ -4979,11 +5037,361 @@
                 throw new IllegalArgumentException(
                         providerComponent + " is not a valid AppWidget provider");
             }
-            final boolean changed = provider.removeGeneratedPreviewLocked(widgetCategories);
-            if (changed) scheduleNotifyGroupHostsForProvidersChangedLocked(userId);
+
+            if (remoteViewsProto()) {
+                removeGeneratedPreviewsAsync(provider, widgetCategories);
+            } else {
+                final boolean changed = provider.removeGeneratedPreviewLocked(widgetCategories);
+                if (changed) scheduleNotifyGroupHostsForProvidersChangedLocked(userId);
+            }
         }
     }
 
+    /**
+     * Return previews for the specified provider from a background thread. The result of the future
+     * is nullable.
+     */
+    @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+    @NonNull
+    private AndroidFuture<RemoteViews> getGeneratedPreviewsAsync(
+            @NonNull Provider provider, @AppWidgetProviderInfo.CategoryFlags int widgetCategory) {
+        AndroidFuture<RemoteViews> result = new AndroidFuture<>();
+        mSavePreviewsHandler.post(() -> {
+            SparseArray<RemoteViews> previews = loadGeneratedPreviews(provider);
+            for (int i = 0; i < previews.size(); i++) {
+                if ((widgetCategory & previews.keyAt(i)) != 0) {
+                    result.complete(previews.valueAt(i));
+                    return;
+                }
+            }
+            result.complete(null);
+        });
+        return result;
+    }
+
+    /**
+     * Set previews for the specified provider on a background thread.
+     */
+    @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+    private void setGeneratedPreviewsAsync(@NonNull Provider provider, int widgetCategories,
+            @NonNull RemoteViews preview) {
+        mSavePreviewsHandler.post(() -> {
+            SparseArray<RemoteViews> previews = loadGeneratedPreviews(provider);
+            for (int flag : Provider.WIDGET_CATEGORY_FLAGS) {
+                if ((widgetCategories & flag) != 0) {
+                    previews.put(flag, preview);
+                }
+            }
+            saveGeneratedPreviews(provider, previews, /* notify= */ true);
+        });
+    }
+
+    /**
+     * Remove previews for the specified provider on a background thread.
+     */
+    @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+    private void removeGeneratedPreviewsAsync(@NonNull Provider provider, int widgetCategories) {
+        mSavePreviewsHandler.post(() -> {
+            SparseArray<RemoteViews> previews = loadGeneratedPreviews(provider);
+            boolean changed = false;
+            for (int flag : Provider.WIDGET_CATEGORY_FLAGS) {
+                if ((widgetCategories & flag) != 0) {
+                    changed |= previews.removeReturnOld(flag) != null;
+                }
+            }
+            if (changed) {
+                saveGeneratedPreviews(provider, previews, /* notify= */ true);
+            }
+        });
+    }
+
+    /**
+     * Clear previews for the specified provider on a background thread. Returns true if changed
+     * (i.e. there are previews to clear). If returns true, the caller should schedule a providers
+     * changed notification.
+     */
+    @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+    private boolean clearGeneratedPreviewsAsync(@NonNull Provider provider) {
+        mSavePreviewsHandler.post(() -> {
+            saveGeneratedPreviews(provider, /* previews= */ null, /* notify= */ false);
+        });
+        return provider.info.generatedPreviewCategories != 0;
+    }
+
+    private void checkSavePreviewsThread() {
+        if (DEBUG && !mSavePreviewsHandler.getLooper().isCurrentThread()) {
+            throw new IllegalStateException("Only modify previews on the background thread");
+        }
+    }
+
+    /**
+     * Load previews from file for the given provider. If there are no previews, returns an empty
+     * SparseArray. Else, returns a SparseArray of the previews mapped by widget category.
+     */
+    @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+    @NonNull
+    private SparseArray<RemoteViews> loadGeneratedPreviews(@NonNull Provider provider) {
+        checkSavePreviewsThread();
+        try {
+            AtomicFile previewsFile = getWidgetPreviewsFile(provider);
+            if (!previewsFile.exists()) {
+                return new SparseArray<>();
+            }
+            ProtoInputStream input = new ProtoInputStream(previewsFile.readFully());
+            SparseArray<RemoteViews> entries = readGeneratedPreviewsFromProto(input);
+            SparseArray<RemoteViews> singleCategoryKeyedEntries = new SparseArray<>();
+            for (int i = 0; i < entries.size(); i++) {
+                int widgetCategories = entries.keyAt(i);
+                RemoteViews preview = entries.valueAt(i);
+                for (int flag : Provider.WIDGET_CATEGORY_FLAGS) {
+                    if ((widgetCategories & flag) != 0) {
+                        singleCategoryKeyedEntries.put(flag, preview);
+                    }
+                }
+            }
+            return singleCategoryKeyedEntries;
+        } catch (IOException e) {
+            Slog.e(TAG, "Failed to load generated previews for " + provider, e);
+            return new SparseArray<>();
+        }
+    }
+
+    /**
+     * This is called when loading profile/group state to populate
+     * AppWidgetProviderInfo.generatedPreviewCategories based on what previews are saved.
+     *
+     * This is the only time previews are read while not on mSavePreviewsHandler. It happens once
+     * per profile during initialization, before any calls to get/set/removeWidgetPreviewAsync
+     * happen for that profile.
+     */
+    @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+    @GuardedBy("mLock")
+    private void loadGeneratedPreviewCategoriesLocked(int profileId) throws IOException {
+        for (Provider provider : mProviders) {
+            if (provider.id.getProfile().getIdentifier() != profileId) {
+                continue;
+            }
+            AtomicFile previewsFile = getWidgetPreviewsFile(provider);
+            if (!previewsFile.exists()) {
+                continue;
+            }
+            ProtoInputStream input = new ProtoInputStream(previewsFile.readFully());
+            provider.info.generatedPreviewCategories = readGeneratedPreviewCategoriesFromProto(
+                    input);
+            if (DEBUG) {
+                Slog.i(TAG, TextUtils.formatSimple(
+                        "loadGeneratedPreviewCategoriesLocked %d %s categories %d", profileId,
+                        provider, provider.info.generatedPreviewCategories));
+            }
+        }
+    }
+
+    /**
+     * Save the given previews into storage.
+     *
+     * @param provider Provider for which to save previews
+     * @param previews Previews to save. If null or empty, clears any saved previews for this
+     *                 provider.
+     * @param notify If true, then this function will notify hosts of updated provider info.
+     */
+    @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+    private void saveGeneratedPreviews(@NonNull Provider provider,
+            @Nullable SparseArray<RemoteViews> previews, boolean notify) {
+        checkSavePreviewsThread();
+        AtomicFile file = null;
+        FileOutputStream stream = null;
+        try {
+            file = getWidgetPreviewsFile(provider);
+            if (previews == null || previews.size() == 0) {
+                if (file.exists()) {
+                    if (DEBUG) {
+                        Slog.i(TAG, "Deleting widget preview file " + file);
+                    }
+                    file.delete();
+                }
+            } else {
+                if (DEBUG) {
+                    Slog.i(TAG, "Writing widget preview file " + file);
+                }
+                ProtoOutputStream out = new ProtoOutputStream();
+                writePreviewsToProto(out, previews);
+                stream = file.startWrite();
+                stream.write(out.getBytes());
+                file.finishWrite(stream);
+            }
+
+            synchronized (mLock) {
+                provider.updateGeneratedPreviewCategoriesLocked(previews);
+                if (notify) {
+                    scheduleNotifyGroupHostsForProvidersChangedLocked(provider.getUserId());
+                }
+            }
+        } catch (IOException e) {
+            if (file != null && stream != null) {
+                file.failWrite(stream);
+            }
+            Slog.w(TAG, "Failed to save widget previews for provider " + provider.id.componentName);
+        }
+    }
+
+
+    /**
+     * Write the given previews as a GeneratedPreviewsProto to the output stream.
+     */
+    @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+    private void writePreviewsToProto(@NonNull ProtoOutputStream out,
+            @NonNull SparseArray<RemoteViews> generatedPreviews) {
+        // Collect RemoteViews mapped by hashCode in order to avoid writing duplicates.
+        SparseArray<Pair<Integer, RemoteViews>> previewsToWrite = new SparseArray<>();
+        for (int i = 0; i < generatedPreviews.size(); i++) {
+            int widgetCategory = generatedPreviews.keyAt(i);
+            RemoteViews views = generatedPreviews.valueAt(i);
+            if (!previewsToWrite.contains(views.hashCode())) {
+                previewsToWrite.put(views.hashCode(), new Pair<>(widgetCategory, views));
+            } else {
+                Pair<Integer, RemoteViews> entry = previewsToWrite.get(views.hashCode());
+                previewsToWrite.put(views.hashCode(),
+                        Pair.create(entry.first | widgetCategory, views));
+            }
+        }
+
+        for (int i = 0; i < previewsToWrite.size(); i++) {
+            final long token = out.start(GeneratedPreviewsProto.PREVIEWS);
+            Pair<Integer, RemoteViews> entry = previewsToWrite.valueAt(i);
+            out.write(GeneratedPreviewsProto.Preview.WIDGET_CATEGORIES, entry.first);
+            final long viewsToken = out.start(GeneratedPreviewsProto.Preview.VIEWS);
+            entry.second.writePreviewToProto(mContext, out);
+            out.end(viewsToken);
+            out.end(token);
+        }
+    }
+
+    /**
+     * Read a GeneratedPreviewsProto message from the input stream.
+     */
+    @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+    @NonNull
+    private SparseArray<RemoteViews> readGeneratedPreviewsFromProto(@NonNull ProtoInputStream input)
+            throws IOException {
+        SparseArray<RemoteViews> entries = new SparseArray<>();
+        while (input.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+            switch (input.getFieldNumber()) {
+                case (int) GeneratedPreviewsProto.PREVIEWS:
+                    final long token = input.start(GeneratedPreviewsProto.PREVIEWS);
+                    Pair<Integer, RemoteViews> entry = readSinglePreviewFromProto(input,
+                            /* skipViews= */ false);
+                    entries.put(entry.first, entry.second);
+                    input.end(token);
+                    break;
+                default:
+                    Slog.w(TAG, "Unknown field while reading GeneratedPreviewsProto! "
+                            + ProtoUtils.currentFieldToString(input));
+            }
+        }
+        return entries;
+    }
+
+    /**
+     * Read the widget categories from GeneratedPreviewsProto and return an int representing the
+     * combined widget categories of all the previews.
+     */
+    @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+    @AppWidgetProviderInfo.CategoryFlags
+    private int readGeneratedPreviewCategoriesFromProto(@NonNull ProtoInputStream input)
+            throws IOException {
+        int widgetCategories = 0;
+        while (input.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+            switch (input.getFieldNumber()) {
+                case (int) GeneratedPreviewsProto.PREVIEWS:
+                    final long token = input.start(GeneratedPreviewsProto.PREVIEWS);
+                    Pair<Integer, RemoteViews> entry = readSinglePreviewFromProto(input,
+                            /* skipViews= */ true);
+                    widgetCategories |= entry.first;
+                    input.end(token);
+                    break;
+                default:
+                    Slog.w(TAG, "Unknown field while reading GeneratedPreviewsProto! "
+                            + ProtoUtils.currentFieldToString(input));
+            }
+        }
+        return widgetCategories;
+    }
+
+    /**
+     * Read a single GeneratedPreviewsProto.Preview message from the input stream, and returns a
+     * pair of widget category and corresponding RemoteViews. If skipViews is true, this function
+     * will only read widget categories and the returned RemoteViews will be null.
+     */
+    @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+    @NonNull
+    private Pair<Integer, RemoteViews> readSinglePreviewFromProto(@NonNull ProtoInputStream input,
+            boolean skipViews) throws IOException {
+        int widgetCategories = 0;
+        RemoteViews views = null;
+        while (input.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+            switch (input.getFieldNumber()) {
+                case (int) GeneratedPreviewsProto.Preview.VIEWS:
+                    if (skipViews)  {
+                        // ProtoInputStream will skip over the nested message when nextField() is
+                        // called.
+                        continue;
+                    }
+                    final long token = input.start(GeneratedPreviewsProto.Preview.VIEWS);
+                    try {
+                        views = RemoteViews.createPreviewFromProto(mContext, input);
+                    } catch (Exception e) {
+                        Slog.e(TAG, "Unable to deserialize RemoteViews", e);
+                    }
+                    input.end(token);
+                    break;
+                case (int) GeneratedPreviewsProto.Preview.WIDGET_CATEGORIES:
+                    widgetCategories = input.readInt(
+                            GeneratedPreviewsProto.Preview.WIDGET_CATEGORIES);
+                    break;
+                default:
+                    Slog.w(TAG, "Unknown field while reading GeneratedPreviewsProto! "
+                            + ProtoUtils.currentFieldToString(input));
+            }
+        }
+        return Pair.create(widgetCategories, views);
+    }
+
+    /**
+     * Returns the file in which all generated previews for this provider are stored. This will be
+     * a path of the form:
+     *  {@literal /data/system_ce/<userId>/appwidget/previews/<package>-<class>-<uid>.binpb}
+     *
+     * This function will not create the file if it does not already exist.
+     */
+    @NonNull
+    private static AtomicFile getWidgetPreviewsFile(@NonNull Provider provider) throws IOException {
+        int userId = provider.getUserId();
+        File previewsDirectory = getWidgetPreviewsDirectory(userId);
+        File providerPreviews = Environment.buildPath(previewsDirectory,
+                TextUtils.formatSimple("%s-%s-%d.binpb", provider.id.componentName.getPackageName(),
+                        provider.id.componentName.getClassName(), provider.id.uid));
+        return new AtomicFile(providerPreviews);
+    }
+
+    /**
+     * Returns the widget previews directory for the given user, creating it if it does not exist.
+     * This will be a path of the form:
+     *  {@literal /data/system_ce/<userId>/appwidget/previews}
+     */
+    @NonNull
+    private static File getWidgetPreviewsDirectory(int userId) throws IOException {
+        File dataSystemCeDirectory = Environment.getDataSystemCeDirectory(userId);
+        File previewsDirectory = Environment.buildPath(dataSystemCeDirectory,
+                APPWIDGET_CE_DATA_DIRNAME, WIDGET_PREVIEWS_DIRNAME);
+        if (!previewsDirectory.exists()) {
+            if (!previewsDirectory.mkdirs()) {
+                throw new IOException("Unable to create widget preview directory "
+                        + previewsDirectory.getPath());
+            }
+        }
+        return previewsDirectory;
+    }
+
     private static void ensureWidgetCategoryCombinationIsValid(int widgetCategories) {
         int validCategories = AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN
                 | AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD
@@ -5415,11 +5823,11 @@
                                 AppWidgetManager.META_DATA_APPWIDGET_PROVIDER);
                     }
                     if (newInfo != null) {
+                        newInfo.generatedPreviewCategories = info.generatedPreviewCategories;
                         info = newInfo;
                         if (DEBUG) {
                             Objects.requireNonNull(info);
                         }
-                        updateGeneratedPreviewCategoriesLocked();
                     }
                 }
                 mInfoParsed = true;
@@ -5476,7 +5884,7 @@
                     generatedPreviews.put(flag, preview);
                 }
             }
-            updateGeneratedPreviewCategoriesLocked();
+            updateGeneratedPreviewCategoriesLocked(generatedPreviews);
         }
 
         @GuardedBy("this.mLock")
@@ -5488,7 +5896,7 @@
                 }
             }
             if (changed) {
-                updateGeneratedPreviewCategoriesLocked();
+                updateGeneratedPreviewCategoriesLocked(generatedPreviews);
             }
             return changed;
         }
@@ -5497,17 +5905,19 @@
         public boolean clearGeneratedPreviewsLocked() {
             if (generatedPreviews.size() > 0) {
                 generatedPreviews.clear();
-                updateGeneratedPreviewCategoriesLocked();
+                updateGeneratedPreviewCategoriesLocked(generatedPreviews);
                 return true;
             }
             return false;
         }
-
         @GuardedBy("this.mLock")
-        private void updateGeneratedPreviewCategoriesLocked() {
+        private void updateGeneratedPreviewCategoriesLocked(
+                @Nullable SparseArray<RemoteViews> previews) {
             info.generatedPreviewCategories = 0;
-            for (int i = 0; i < generatedPreviews.size(); i++) {
-                info.generatedPreviewCategories |= generatedPreviews.keyAt(i);
+            if (previews != null) {
+                for (int i = 0; i < previews.size(); i++) {
+                    info.generatedPreviewCategories |= previews.keyAt(i);
+                }
             }
         }
 
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 87ce649..2a9cb43 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -134,6 +134,7 @@
 import static android.util.FeatureFlagUtils.SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS;
 import static android.view.Display.INVALID_DISPLAY;
 
+import static com.android.internal.util.FrameworkStatsLog.INTENT_CREATOR_TOKEN_ADDED;
 import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__NEW_MUTABLE_IMPLICIT_PENDING_INTENT_RETRIEVED;
 import static com.android.sdksandbox.flags.Flags.sdkSandboxInstrumentationInfo;
 import static com.android.server.am.ActiveServices.FGS_SAW_RESTRICTIONS;
@@ -2794,9 +2795,6 @@
                 addServiceToMap(mAppBindArgs, Context.POWER_SERVICE);
                 addServiceToMap(mAppBindArgs, "mount");
                 addServiceToMap(mAppBindArgs, Context.PLATFORM_COMPAT_SERVICE);
-                addServiceToMap(mAppBindArgs, "permissionmgr");
-                addServiceToMap(mAppBindArgs, Context.APP_OPS_SERVICE);
-                addServiceToMap(mAppBindArgs, Context.USER_SERVICE);
             }
             // See b/79378449
             // Getting the window service and package service binder from servicemanager
@@ -2804,6 +2802,9 @@
             // TODO: remove exception
             addServiceToMap(mAppBindArgs, "package");
             addServiceToMap(mAppBindArgs, Context.WINDOW_SERVICE);
+            addServiceToMap(mAppBindArgs, Context.USER_SERVICE);
+            addServiceToMap(mAppBindArgs, "permissionmgr");
+            addServiceToMap(mAppBindArgs, Context.APP_OPS_SERVICE);
         }
         return mAppBindArgs;
     }
@@ -19291,12 +19292,14 @@
                             + "} does not correspond to an intent in the extra bundle.");
                     continue;
                 }
-                Slog.wtf(TAG,
-                        "A creator token is added to an intent. creatorPackage: " + creatorPackage
-                                + "; intent: " + intent);
-                IBinder creatorToken = createIntentCreatorToken(extraIntent, creatorPackage);
+                IntentCreatorToken creatorToken = createIntentCreatorToken(extraIntent,
+                        creatorPackage);
                 if (creatorToken != null) {
                     extraIntent.setCreatorToken(creatorToken);
+                    Slog.wtf(TAG, "A creator token is added to an intent. creatorPackage: "
+                            + creatorPackage + "; intent: " + intent);
+                    FrameworkStatsLog.write(INTENT_CREATOR_TOKEN_ADDED,
+                            creatorToken.getCreatorUid());
                 }
             } catch (Exception e) {
                 Slog.wtf(TAG,
@@ -19307,7 +19310,7 @@
         }
     }
 
-    private IBinder createIntentCreatorToken(Intent intent, String creatorPackage) {
+    private IntentCreatorToken createIntentCreatorToken(Intent intent, String creatorPackage) {
         if (IntentCreatorToken.isValid(intent)) return null;
         int creatorUid = getCallingUid();
         IntentCreatorToken.Key key = new IntentCreatorToken.Key(creatorUid, creatorPackage, intent);
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 7afcb13..69102f1 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -192,6 +192,7 @@
         "make_pixel_haptics",
         "media_audio",
         "media_drm",
+        "media_projection",
         "media_reliability",
         "media_solutions",
         "media_tv",
@@ -204,6 +205,7 @@
         "pixel_bluetooth",
         "pixel_connectivity_gps",
         "pixel_continuity",
+        "pixel_perf",
         "pixel_sensors",
         "pixel_system_sw_video",
         "pixel_watch",
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index dbdc614..906e584 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -2641,8 +2641,8 @@
                 Log.w(TAG, "failed to broadcast ACTION_SPEAKERPHONE_STATE_CHANGED: " + e);
             }
         }
-        mAudioService.postUpdateRingerModeServiceInt();
         dispatchCommunicationDevice();
+        mAudioService.postUpdateRingerModeServiceInt();
     }
 
     @GuardedBy("mDeviceStateLock")
diff --git a/services/core/java/com/android/server/biometrics/PreAuthInfo.java b/services/core/java/com/android/server/biometrics/PreAuthInfo.java
index b2c616a..96c178a 100644
--- a/services/core/java/com/android/server/biometrics/PreAuthInfo.java
+++ b/services/core/java/com/android/server/biometrics/PreAuthInfo.java
@@ -112,7 +112,7 @@
             throws RemoteException {
 
         final boolean isOnlyMandatoryBiometricsRequested = promptInfo.getAuthenticators()
-                == BiometricManager.Authenticators.MANDATORY_BIOMETRICS;
+                == BiometricManager.Authenticators.IDENTITY_CHECK;
         boolean isMandatoryBiometricsAuthentication = false;
 
         if (dropCredentialFallback(promptInfo.getAuthenticators(),
@@ -180,8 +180,8 @@
     private static boolean dropCredentialFallback(int authenticators,
             boolean isMandatoryBiometricsEnabled, ITrustManager trustManager) {
         final boolean isMandatoryBiometricsRequested =
-                (authenticators & BiometricManager.Authenticators.MANDATORY_BIOMETRICS)
-                        == BiometricManager.Authenticators.MANDATORY_BIOMETRICS;
+                (authenticators & BiometricManager.Authenticators.IDENTITY_CHECK)
+                        == BiometricManager.Authenticators.IDENTITY_CHECK;
         if (Flags.mandatoryBiometrics() && isMandatoryBiometricsEnabled
                 && isMandatoryBiometricsRequested) {
             try {
diff --git a/services/core/java/com/android/server/biometrics/Utils.java b/services/core/java/com/android/server/biometrics/Utils.java
index 8734136..c1f8e2e 100644
--- a/services/core/java/com/android/server/biometrics/Utils.java
+++ b/services/core/java/com/android/server/biometrics/Utils.java
@@ -147,7 +147,7 @@
      * @return true if mandatory biometrics is requested
      */
     static boolean isMandatoryBiometricsRequested(@Authenticators.Types int authenticators) {
-        return (authenticators & Authenticators.MANDATORY_BIOMETRICS) != 0;
+        return (authenticators & Authenticators.IDENTITY_CHECK) != 0;
     }
 
     /**
@@ -257,7 +257,7 @@
         if (Flags.mandatoryBiometrics()) {
             testBits = ~(Authenticators.DEVICE_CREDENTIAL
                     | Authenticators.BIOMETRIC_MIN_STRENGTH
-                    | Authenticators.MANDATORY_BIOMETRICS);
+                    | Authenticators.IDENTITY_CHECK);
         } else {
             testBits = ~(Authenticators.DEVICE_CREDENTIAL
                     | Authenticators.BIOMETRIC_MIN_STRENGTH);
@@ -329,8 +329,8 @@
             case BiometricConstants.BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED:
                 biometricManagerCode = BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE;
                 break;
-            case BiometricConstants.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE:
-                biometricManagerCode = BiometricManager.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE;
+            case BiometricConstants.BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE:
+                biometricManagerCode = BiometricManager.BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE;
                 break;
             case BiometricConstants.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS:
                 biometricManagerCode = BiometricManager.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS;
@@ -397,7 +397,7 @@
             case BIOMETRIC_SENSOR_PRIVACY_ENABLED:
                 return BiometricConstants.BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED;
             case MANDATORY_BIOMETRIC_UNAVAILABLE_ERROR:
-                return BiometricConstants.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE;
+                return BiometricConstants.BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE;
             case BIOMETRIC_NOT_ENABLED_FOR_APPS:
                 if (Flags.mandatoryBiometrics()) {
                     return BiometricConstants.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS;
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/vibrator/ComposePwleV2VibratorStep.java b/services/core/java/com/android/server/vibrator/ComposePwleV2VibratorStep.java
new file mode 100644
index 0000000..d0d6071
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/ComposePwleV2VibratorStep.java
@@ -0,0 +1,153 @@
+/*
+ * 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.vibrator;
+
+import android.annotation.NonNull;
+import android.os.Trace;
+import android.os.VibrationEffect;
+import android.os.vibrator.Flags;
+import android.os.vibrator.PwleSegment;
+import android.os.vibrator.VibrationEffectSegment;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a step to turn the vibrator on using a composition of PWLE segments.
+ *
+ * <p>This step will use the maximum supported number of consecutive segments of type
+ * {@link PwleSegment}, starting at the current index.
+ */
+final class ComposePwleV2VibratorStep extends AbstractComposedVibratorStep {
+
+    ComposePwleV2VibratorStep(VibrationStepConductor conductor, long startTime,
+            VibratorController controller, VibrationEffect.Composed effect, int index,
+            long pendingVibratorOffDeadline) {
+        // This step should wait for the last vibration to finish (with the timeout) and for the
+        // intended step start time (to respect the effect delays).
+        super(conductor, Math.max(startTime, pendingVibratorOffDeadline), controller, effect,
+                index, pendingVibratorOffDeadline);
+    }
+
+    @NonNull
+    @Override
+    public List<Step> play() {
+        if (!Flags.normalizedPwleEffects()) {
+            // Skip this step and play the next one right away.
+            return nextSteps(/* segmentsPlayed= */ 1);
+        }
+
+        Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "ComposePwleV2Step");
+        try {
+            // Load the next PwleSegments to create a single composePwleV2 call to the vibrator,
+            // limited to the vibrator's maximum envelope effect size.
+            int limit = controller.getVibratorInfo().getMaxEnvelopeEffectSize();
+            List<PwleSegment> pwles = unrollPwleSegments(effect, segmentIndex, limit);
+
+            if (pwles.isEmpty()) {
+                Slog.w(VibrationThread.TAG, "Ignoring wrong segment for a ComposeEnvelopeStep: "
+                        + effect.getSegments().get(segmentIndex));
+                // Skip this step and play the next one right away.
+                return nextSteps(/* segmentsPlayed= */ 1);
+            }
+
+            if (VibrationThread.DEBUG) {
+                Slog.d(VibrationThread.TAG, "Compose " + pwles + " PWLEs on vibrator "
+                        + controller.getVibratorInfo().getId());
+            }
+            PwleSegment[] pwlesArray = pwles.toArray(new PwleSegment[pwles.size()]);
+            long vibratorOnResult = controller.on(pwlesArray, getVibration().id);
+            handleVibratorOnResult(vibratorOnResult);
+            getVibration().stats.reportComposePwle(vibratorOnResult, pwlesArray);
+
+            // The next start and off times will be calculated from mVibratorOnResult.
+            return nextSteps(/* segmentsPlayed= */ pwles.size());
+        } finally {
+            Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+        }
+    }
+
+    private List<PwleSegment> unrollPwleSegments(VibrationEffect.Composed effect, int startIndex,
+            int limit) {
+        List<PwleSegment> segments = new ArrayList<>(limit);
+        float bestBreakAmplitude = 1;
+        int bestBreakPosition = limit; // Exclusive index.
+
+        int segmentCount = effect.getSegments().size();
+        int repeatIndex = effect.getRepeatIndex();
+
+        // Loop once after reaching the limit to see if breaking it will really be necessary, then
+        // apply the best break position found, otherwise return the full list as it fits the limit.
+        for (int i = startIndex; segments.size() <= limit; i++) {
+            if (i == segmentCount) {
+                if (repeatIndex >= 0) {
+                    i = repeatIndex;
+                } else {
+                    // Non-repeating effect, stop collecting pwles.
+                    break;
+                }
+            }
+            VibrationEffectSegment segment = effect.getSegments().get(i);
+            if (segment instanceof PwleSegment pwleSegment) {
+                segments.add(pwleSegment);
+
+                if (isBetterBreakPosition(segments, bestBreakAmplitude, limit)) {
+                    // Mark this position as the best one so far to break a long waveform.
+                    bestBreakAmplitude = pwleSegment.getEndAmplitude();
+                    bestBreakPosition = segments.size(); // Break after this pwle ends.
+                }
+            } else {
+                // First non-pwle segment, stop collecting pwles.
+                break;
+            }
+        }
+
+        return segments.size() > limit
+                // Remove excessive segments, using the best breaking position recorded.
+                ? segments.subList(0, bestBreakPosition)
+                // Return all collected pwle segments.
+                : segments;
+    }
+
+    /**
+     * Returns true if the current segment list represents a better break position for a PWLE,
+     * given the current amplitude being used for breaking it at a smaller size and the size limit.
+     */
+    private boolean isBetterBreakPosition(List<PwleSegment> segments,
+            float currentBestBreakAmplitude, int limit) {
+        PwleSegment lastSegment = segments.get(segments.size() - 1);
+        float breakAmplitudeCandidate = lastSegment.getEndAmplitude();
+        int breakPositionCandidate = segments.size();
+
+        if (breakPositionCandidate > limit) {
+            // We're beyond limit, last break position found should be used.
+            return false;
+        }
+        if (breakAmplitudeCandidate == 0) {
+            // Breaking at amplitude zero at any position is always preferable.
+            return true;
+        }
+        if (breakPositionCandidate < limit / 2) {
+            // Avoid breaking at the first half of the allowed maximum size, even if amplitudes are
+            // lower, to avoid creating PWLEs that are too small unless it's to break at zero.
+            return false;
+        }
+        // Prefer lower amplitudes at a later position for breaking the PWLE in a more subtle way.
+        return breakAmplitudeCandidate <= currentBestBreakAmplitude;
+    }
+}
diff --git a/services/core/java/com/android/server/vibrator/DeviceAdapter.java b/services/core/java/com/android/server/vibrator/DeviceAdapter.java
index bd4fc07..751e83c 100644
--- a/services/core/java/com/android/server/vibrator/DeviceAdapter.java
+++ b/services/core/java/com/android/server/vibrator/DeviceAdapter.java
@@ -47,6 +47,11 @@
      * instance is created with the final segment list.
      */
     private final List<VibrationSegmentsAdapter> mSegmentAdapters;
+    /**
+     * The vibration segment validators that can validate VibrationEffectSegments entries based on
+     * the VibratorInfo.
+     */
+    private final List<VibrationSegmentsValidator> mSegmentsValidators;
 
     DeviceAdapter(VibrationSettings settings, SparseArray<VibratorController> vibrators) {
         mSegmentAdapters = Arrays.asList(
@@ -60,7 +65,13 @@
                 // Split segments based on their duration and device supported limits
                 new SplitSegmentsAdapter(),
                 // Clip amplitudes and frequencies of final segments based on device bandwidth curve
-                new ClippingAmplitudeAndFrequencyAdapter()
+                new ClippingAmplitudeAndFrequencyAdapter(),
+                // Split Pwle segments based on their duration and device supported limits
+                new SplitPwleSegmentsAdapter()
+        );
+        mSegmentsValidators = List.of(
+                // Validate Pwle segments base on the vibrators frequency range
+                new PwleSegmentsValidator()
         );
         mAvailableVibrators = vibrators;
         mAvailableVibratorIds = new int[vibrators.size()];
@@ -78,7 +89,6 @@
         return mAvailableVibratorIds;
     }
 
-    @NonNull
     @Override
     public VibrationEffect adaptToVibrator(int vibratorId, @NonNull VibrationEffect effect) {
         if (!(effect instanceof VibrationEffect.Composed composed)) {
@@ -102,6 +112,14 @@
                     mSegmentAdapters.get(i).adaptToVibrator(info, newSegments, newRepeatIndex);
         }
 
+        // Validate the vibration segments. If a segment is not supported, ignore the entire
+        // vibration effect.
+        for (int i = 0; i < mSegmentsValidators.size(); i++) {
+            if (!mSegmentsValidators.get(i).hasValidSegments(info, newSegments)) {
+                return null;
+            }
+        }
+
         return new VibrationEffect.Composed(newSegments, newRepeatIndex);
     }
 }
diff --git a/services/core/java/com/android/server/vibrator/HalVibration.java b/services/core/java/com/android/server/vibrator/HalVibration.java
index d192e64..c9f1e4b 100644
--- a/services/core/java/com/android/server/vibrator/HalVibration.java
+++ b/services/core/java/com/android/server/vibrator/HalVibration.java
@@ -124,12 +124,18 @@
      * @param deviceAdapter A {@link CombinedVibration.VibratorAdapter} that transforms vibration
      *                      effects to device vibrators based on its capabilities.
      */
-    public void adaptToDevice(CombinedVibration.VibratorAdapter deviceAdapter) {
-        CombinedVibration newEffect = mEffectToPlay.adapt(deviceAdapter);
-        if (!Objects.equals(mEffectToPlay, newEffect)) {
-            mEffectToPlay = newEffect;
+    public boolean adaptToDevice(CombinedVibration.VibratorAdapter deviceAdapter) {
+        CombinedVibration adaptedEffect = mEffectToPlay.adapt(deviceAdapter);
+        if (adaptedEffect == null) {
+            return false;
+        }
+
+        if (!mEffectToPlay.equals(adaptedEffect)) {
+            mEffectToPlay = adaptedEffect;
         }
         // No need to update fallback effects, they are already configured per device.
+
+        return true;
     }
 
     /** Return the effect that should be played by this vibration. */
diff --git a/services/core/java/com/android/server/vibrator/PwleSegmentsValidator.java b/services/core/java/com/android/server/vibrator/PwleSegmentsValidator.java
new file mode 100644
index 0000000..87369aa
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/PwleSegmentsValidator.java
@@ -0,0 +1,60 @@
+/*
+ * 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.vibrator;
+
+import android.hardware.vibrator.IVibrator;
+import android.os.VibratorInfo;
+import android.os.vibrator.PwleSegment;
+import android.os.vibrator.VibrationEffectSegment;
+
+import java.util.List;
+
+/**
+ * Validates Pwle segments to ensure they are compatible with the device's capabilities
+ * and adhere to frequency constraints.
+ *
+ * <p>The validator verifies that each segment's start and end frequencies fall within
+ * the supported range.
+ *
+ * <p>The segments will be considered invalid of the device does not have
+ * {@link IVibrator#CAP_COMPOSE_PWLE_EFFECTS_V2}.
+ */
+final class PwleSegmentsValidator implements VibrationSegmentsValidator {
+
+    @Override
+    public boolean hasValidSegments(VibratorInfo info, List<VibrationEffectSegment> segments) {
+
+        boolean hasPwleCapability = info.hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2);
+        float minFrequency = info.getFrequencyProfile().getMinFrequencyHz();
+        float maxFrequency = info.getFrequencyProfile().getMaxFrequencyHz();
+
+        for (VibrationEffectSegment segment : segments) {
+            if (!(segment instanceof PwleSegment pwleSegment)) {
+                continue;
+            }
+
+            if (!hasPwleCapability || pwleSegment.getStartFrequencyHz() < minFrequency
+                    || pwleSegment.getStartFrequencyHz() > maxFrequency
+                    || pwleSegment.getEndFrequencyHz() < minFrequency
+                    || pwleSegment.getEndFrequencyHz() > maxFrequency) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+}
diff --git a/services/core/java/com/android/server/vibrator/SplitPwleSegmentsAdapter.java b/services/core/java/com/android/server/vibrator/SplitPwleSegmentsAdapter.java
new file mode 100644
index 0000000..ad44227
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/SplitPwleSegmentsAdapter.java
@@ -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.server.vibrator;
+
+import android.hardware.vibrator.IVibrator;
+import android.os.VibratorInfo;
+import android.os.vibrator.PwleSegment;
+import android.os.vibrator.VibrationEffectSegment;
+import android.util.MathUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Adapter that splits Pwle segments with longer duration than the device capabilities.
+ *
+ * <p>This transformation replaces large {@link android.os.vibrator.PwleSegment} entries by a
+ * sequence of smaller segments that starts and ends at the same amplitudes/frequencies,
+ * interpolating the intermediate values.
+ *
+ * <p>The segments will not be changed if the device doesn't have
+ * {@link IVibrator#CAP_COMPOSE_PWLE_EFFECTS_V2}.
+ */
+final class SplitPwleSegmentsAdapter implements VibrationSegmentsAdapter {
+
+    @Override
+    public int adaptToVibrator(VibratorInfo info, List<VibrationEffectSegment> segments,
+            int repeatIndex) {
+        if (!info.hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2)) {
+            // The vibrator does not have PWLE v2 capability, so keep the segments unchanged.
+            return repeatIndex;
+        }
+        int maxPwleDuration = info.getMaxEnvelopeEffectDurationMillis();
+        if (maxPwleDuration <= 0) {
+            // No limit set to PWLE primitive duration.
+            return repeatIndex;
+        }
+
+        int segmentCount = segments.size();
+        for (int i = 0; i < segmentCount; i++) {
+            if (!(segments.get(i) instanceof PwleSegment pwleSegment)) {
+                continue;
+            }
+            int splits = ((int) pwleSegment.getDuration() + maxPwleDuration - 1) / maxPwleDuration;
+            if (splits <= 1) {
+                continue;
+            }
+            segments.remove(i);
+            segments.addAll(i, splitPwleSegment(pwleSegment, splits));
+            int addedSegments = splits - 1;
+            if (repeatIndex > i) {
+                repeatIndex += addedSegments;
+            }
+            i += addedSegments;
+            segmentCount += addedSegments;
+        }
+
+        return repeatIndex;
+    }
+
+    private static List<PwleSegment> splitPwleSegment(PwleSegment pwleSegment,
+            int splits) {
+        List<PwleSegment> pwleSegments = new ArrayList<>(splits);
+        float startFrequencyHz = pwleSegment.getStartFrequencyHz();
+        float endFrequencyHz = pwleSegment.getEndFrequencyHz();
+        long splitDuration = pwleSegment.getDuration() / splits;
+        float previousAmplitude = pwleSegment.getStartAmplitude();
+        float previousFrequencyHz = startFrequencyHz;
+        long accumulatedDuration = 0;
+
+        for (int i = 1; i < splits; i++) {
+            accumulatedDuration += splitDuration;
+            float durationRatio = (float) accumulatedDuration / pwleSegment.getDuration();
+            float interpolatedFrequency =
+                    MathUtils.lerp(startFrequencyHz, endFrequencyHz, durationRatio);
+            float interpolatedAmplitude = MathUtils.lerp(pwleSegment.getStartAmplitude(),
+                    pwleSegment.getEndAmplitude(), durationRatio);
+            PwleSegment pwleSplit = new PwleSegment(
+                    previousAmplitude, interpolatedAmplitude,
+                    previousFrequencyHz, interpolatedFrequency,
+                    (int) splitDuration);
+            pwleSegments.add(pwleSplit);
+            previousAmplitude = pwleSplit.getEndAmplitude();
+            previousFrequencyHz = pwleSplit.getEndFrequencyHz();
+        }
+
+        pwleSegments.add(
+                new PwleSegment(previousAmplitude, pwleSegment.getEndAmplitude(),
+                        previousFrequencyHz, endFrequencyHz,
+                        (int) (pwleSegment.getDuration() - accumulatedDuration)));
+
+        return pwleSegments;
+    }
+}
diff --git a/services/core/java/com/android/server/vibrator/VibrationSegmentsValidator.java b/services/core/java/com/android/server/vibrator/VibrationSegmentsValidator.java
new file mode 100644
index 0000000..75002bf
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/VibrationSegmentsValidator.java
@@ -0,0 +1,35 @@
+/*
+ * 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.vibrator;
+
+import android.os.VibratorInfo;
+import android.os.vibrator.VibrationEffectSegment;
+
+import java.util.List;
+
+/** Validates a sequence of {@link VibrationEffectSegment}s for a vibrator. */
+public interface VibrationSegmentsValidator {
+    /**
+     * Checks whether the vibrator can play the provided segments based on the given
+     * {@link VibratorInfo}.
+     *
+     * @param info     The vibrator info to be applied to the sequence of segments.
+     * @param segments List of {@link VibrationEffectSegment} to be checked.
+     * @return True if vibrator can play the effect, false otherwise.
+     */
+    boolean hasValidSegments(VibratorInfo info, List<VibrationEffectSegment> segments);
+}
diff --git a/services/core/java/com/android/server/vibrator/VibrationStats.java b/services/core/java/com/android/server/vibrator/VibrationStats.java
index 637a5a1..bc4dbe7 100644
--- a/services/core/java/com/android/server/vibrator/VibrationStats.java
+++ b/services/core/java/com/android/server/vibrator/VibrationStats.java
@@ -22,6 +22,7 @@
 import android.os.SystemClock;
 import android.os.vibrator.PrebakedSegment;
 import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.PwleSegment;
 import android.os.vibrator.RampSegment;
 import android.util.Slog;
 import android.util.SparseBooleanArray;
@@ -292,6 +293,25 @@
         }
     }
 
+    /** Report a call to vibrator method to trigger a vibration as a PWLE. */
+    void reportComposePwle(long halResult, PwleSegment[] segments) {
+        mVibratorComposePwleCount++;
+        mVibrationPwleTotalSize += segments.length;
+
+        if (halResult > 0) {
+            // If HAL result is positive then it represents the actual duration of the vibration.
+            // Remove the zero-amplitude segments to update the total time the vibrator was ON.
+            for (PwleSegment ramp : segments) {
+                if ((ramp.getStartAmplitude() == 0) && (ramp.getEndAmplitude() == 0)) {
+                    halResult -= ramp.getDuration();
+                }
+            }
+            if (halResult > 0) {
+                mVibratorOnTotalDurationMillis += (int) halResult;
+            }
+        }
+    }
+
     /**
      * Increment the stats for total number of times the {@code setExternalControl} method was
      * triggered in the vibrator HAL.
diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
index 4bb0c16..6a4790d 100644
--- a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
+++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
@@ -24,6 +24,7 @@
 import android.os.vibrator.Flags;
 import android.os.vibrator.PrebakedSegment;
 import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.PwleSegment;
 import android.os.vibrator.RampSegment;
 import android.os.vibrator.VibrationEffectSegment;
 import android.util.IntArray;
@@ -166,12 +167,20 @@
             return new ComposePwleVibratorStep(this, startTime, controller, effect, segmentIndex,
                     pendingVibratorOffDeadline);
         }
+        if (segment instanceof PwleSegment) {
+            return new ComposePwleV2VibratorStep(this, startTime, controller, effect,
+                    segmentIndex, pendingVibratorOffDeadline);
+        }
         return new SetAmplitudeVibratorStep(this, startTime, controller, effect, segmentIndex,
                 pendingVibratorOffDeadline);
     }
 
-    /** Called when this conductor is going to be started running by the VibrationThread. */
-    public void prepareToStart() {
+    /**
+     * Called when this conductor is going to be started running by the VibrationThread.
+     *
+     * @return True if the vibration effect can be played, false otherwise.
+     */
+    public boolean prepareToStart() {
         if (Build.IS_DEBUGGABLE) {
             expectIsVibrationThread(true);
         }
@@ -182,7 +191,11 @@
         // Scale resolves the default amplitudes from the effect before scaling them.
         mVibration.scaleEffects(mVibrationScaler);
 
-        mVibration.adaptToDevice(mDeviceAdapter);
+        if (!mVibration.adaptToDevice(mDeviceAdapter)) {
+            // Unable to adapt vibration effect for playback. This likely indicates the presence
+            // of unsupported segments. The original effect will be ignored.
+            return false;
+        }
         CombinedVibration.Sequential sequentialEffect = toSequential(mVibration.getEffectToPlay());
         mPendingVibrateSteps++;
         // This count is decremented at the completion of the step, so we don't subtract one.
@@ -191,6 +204,8 @@
         // Vibration will start playing in the Vibrator, following the effect timings and delays.
         // Report current time as the vibration start time, for debugging.
         mVibration.stats.reportStarted();
+
+        return true;
     }
 
     public HalVibration getVibration() {
diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java
index 5b22c10..cb9988f 100644
--- a/services/core/java/com/android/server/vibrator/VibrationThread.java
+++ b/services/core/java/com/android/server/vibrator/VibrationThread.java
@@ -263,7 +263,15 @@
     private void playVibration() {
         Trace.traceBegin(TRACE_TAG_VIBRATOR, "playVibration");
         try {
-            mExecutingConductor.prepareToStart();
+            if (!mExecutingConductor.prepareToStart()) {
+                // The effect cannot be played, start clean-up tasks and notify
+                // callback immediately.
+                clientVibrationCompleteIfNotAlready(
+                        new Vibration.EndInfo(Status.IGNORED_UNSUPPORTED));
+
+                return;
+            }
+
             while (!mExecutingConductor.isFinished()) {
                 boolean readyToRun = mExecutingConductor.waitUntilNextStepIsDue();
                 // If we waited, don't run the next step, but instead re-evaluate status.
diff --git a/services/core/java/com/android/server/vibrator/VibratorController.java b/services/core/java/com/android/server/vibrator/VibratorController.java
index 6aed00e..f78bff8 100644
--- a/services/core/java/com/android/server/vibrator/VibratorController.java
+++ b/services/core/java/com/android/server/vibrator/VibratorController.java
@@ -30,6 +30,7 @@
 import android.os.VibratorInfo;
 import android.os.vibrator.PrebakedSegment;
 import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.PwleSegment;
 import android.os.vibrator.RampSegment;
 import android.util.IndentingPrintWriter;
 import android.util.Slog;
@@ -414,6 +415,33 @@
     }
 
     /**
+     * Plays a composition of pwle v2 primitives, using {@code vibrationId} for completion callback
+     * to {@link OnVibrationCompleteListener}.
+     *
+     * <p>This will affect the state of {@link #isVibrating()}.
+     *
+     * @return The duration of the effect playing, or 0 if unsupported.
+     */
+    public long on(PwleSegment[] primitives, long vibrationId) {
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on (PWLE v2)");
+        try {
+            if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2)) {
+                return 0;
+            }
+            synchronized (mLock) {
+                long duration = mNativeWrapper.composePwleV2(primitives, vibrationId);
+                if (duration > 0) {
+                    mCurrentAmplitude = -1;
+                    updateStateAndNotifyListenersLocked(VibratorState.VIBRATING);
+                }
+                return duration;
+            }
+        } finally {
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
+        }
+    }
+
+    /**
      * Turns off the vibrator and disables completion callback to any pending vibration.
      *
      * <p>This will affect the state of {@link #isVibrating()}.
@@ -534,6 +562,9 @@
         private static native long performPwleEffect(long nativePtr, RampSegment[] effect,
                 int braking, long vibrationId);
 
+        private static native long performPwleV2Effect(long nativePtr, PwleSegment[] effect,
+                long vibrationId);
+
         private static native void setExternalControl(long nativePtr, boolean enabled);
 
         private static native void alwaysOnEnable(long nativePtr, long id, long effect,
@@ -600,6 +631,11 @@
             return performPwleEffect(mNativePtr, primitives, braking, vibrationId);
         }
 
+        /** Turns vibrator on to perform PWLE effect composed of given primitives. */
+        public long composePwleV2(PwleSegment[] primitives, long vibrationId) {
+            return performPwleV2Effect(mNativePtr, primitives, vibrationId);
+        }
+
         /** Enabled the device vibrator to be controlled by another service. */
         public void setExternalControl(boolean enabled) {
             setExternalControl(mNativePtr, enabled);
diff --git a/services/core/java/com/android/server/wm/OWNERS b/services/core/java/com/android/server/wm/OWNERS
index 5d6d8bc..e983edf 100644
--- a/services/core/java/com/android/server/wm/OWNERS
+++ b/services/core/java/com/android/server/wm/OWNERS
@@ -32,3 +32,7 @@
 # Files related to tracing
 per-file *TransitionTracer.java = file:platform/development:/tools/winscope/OWNERS
 per-file *WindowTracing* = file:platform/development:/tools/winscope/OWNERS
+
+# Files related to activity security
+per-file ActivityStarter.java = file:/ACTIVITY_SECURITY_OWNERS
+per-file ActivityTaskManagerService.java = file:/ACTIVITY_SECURITY_OWNERS
diff --git a/services/core/jni/com_android_server_vibrator_VibratorController.cpp b/services/core/jni/com_android_server_vibrator_VibratorController.cpp
index 903d892..59dbf28 100644
--- a/services/core/jni/com_android_server_vibrator_VibratorController.cpp
+++ b/services/core/jni/com_android_server_vibrator_VibratorController.cpp
@@ -73,6 +73,13 @@
     jfieldID endFrequencyHz;
     jfieldID duration;
 } sRampClassInfo;
+static struct {
+    jfieldID startAmplitude;
+    jfieldID endAmplitude;
+    jfieldID startFrequencyHz;
+    jfieldID endFrequencyHz;
+    jfieldID duration;
+} sPwleClassInfo;
 
 static_assert(static_cast<uint8_t>(V1_0::EffectStrength::LIGHT) ==
               static_cast<uint8_t>(Aidl::EffectStrength::LIGHT));
@@ -182,6 +189,15 @@
     return pwle;
 }
 
+static Aidl::PwleV2Primitive pwleV2PrimitiveFromJavaPrimitive(JNIEnv* env, jobject pwleObj) {
+    Aidl::PwleV2Primitive pwle;
+    pwle.amplitude = static_cast<float>(env->GetFloatField(pwleObj, sPwleClassInfo.endAmplitude));
+    pwle.frequencyHz =
+            static_cast<float>(env->GetFloatField(pwleObj, sPwleClassInfo.endFrequencyHz));
+    pwle.timeMillis = static_cast<int32_t>(env->GetIntField(pwleObj, sPwleClassInfo.duration));
+    return pwle;
+}
+
 /* Return true if braking is not NONE and the active PWLE starts and ends with zero amplitude. */
 static bool shouldBeReplacedWithBraking(Aidl::ActivePwle activePwle, Aidl::Braking braking) {
     return (braking != Aidl::Braking::NONE) && (activePwle.startAmplitude == 0) &&
@@ -399,6 +415,31 @@
     return result.isOk() ? totalDuration.count() : (result.isUnsupported() ? 0 : -1);
 }
 
+static jlong vibratorPerformPwleV2Effect(JNIEnv* env, jclass /* clazz */, jlong ptr,
+                                         jobjectArray waveform, jlong vibrationId) {
+    VibratorControllerWrapper* wrapper = reinterpret_cast<VibratorControllerWrapper*>(ptr);
+    if (wrapper == nullptr) {
+        ALOGE("vibratorPerformPwleV2Effect failed because native wrapper was not initialized");
+        return -1;
+    }
+    size_t size = env->GetArrayLength(waveform);
+    Aidl::CompositePwleV2 composite;
+    std::vector<Aidl::PwleV2Primitive> primitives;
+    for (size_t i = 0; i < size; i++) {
+        jobject element = env->GetObjectArrayElement(waveform, i);
+        Aidl::PwleV2Primitive pwle = pwleV2PrimitiveFromJavaPrimitive(env, element);
+        primitives.push_back(pwle);
+    }
+    composite.pwlePrimitives = primitives;
+
+    auto callback = wrapper->createCallback(vibrationId);
+    auto composePwleV2Fn = [&composite, &callback](vibrator::HalWrapper* hal) {
+        return hal->composePwleV2(composite, callback);
+    };
+    auto result = wrapper->halCall<void>(composePwleV2Fn, "composePwleV2");
+    return result.isOk();
+}
+
 static void vibratorAlwaysOnEnable(JNIEnv* env, jclass /* clazz */, jlong ptr, jlong id,
                                    jlong effect, jlong strength) {
     VibratorControllerWrapper* wrapper = reinterpret_cast<VibratorControllerWrapper*>(ptr);
@@ -579,6 +620,8 @@
          (void*)vibratorPerformComposedEffect},
         {"performPwleEffect", "(J[Landroid/os/vibrator/RampSegment;IJ)J",
          (void*)vibratorPerformPwleEffect},
+        {"performPwleV2Effect", "(J[Landroid/os/vibrator/PwleSegment;J)J",
+         (void*)vibratorPerformPwleV2Effect},
         {"setExternalControl", "(JZ)V", (void*)vibratorSetExternalControl},
         {"alwaysOnEnable", "(JJJJ)V", (void*)vibratorAlwaysOnEnable},
         {"alwaysOnDisable", "(JJ)V", (void*)vibratorAlwaysOnDisable},
@@ -604,6 +647,13 @@
     sRampClassInfo.endFrequencyHz = GetFieldIDOrDie(env, rampClass, "mEndFrequencyHz", "F");
     sRampClassInfo.duration = GetFieldIDOrDie(env, rampClass, "mDuration", "I");
 
+    jclass pwleClass = FindClassOrDie(env, "android/os/vibrator/PwleSegment");
+    sPwleClassInfo.startAmplitude = GetFieldIDOrDie(env, pwleClass, "mStartAmplitude", "F");
+    sPwleClassInfo.endAmplitude = GetFieldIDOrDie(env, pwleClass, "mEndAmplitude", "F");
+    sPwleClassInfo.startFrequencyHz = GetFieldIDOrDie(env, pwleClass, "mStartFrequencyHz", "F");
+    sPwleClassInfo.endFrequencyHz = GetFieldIDOrDie(env, pwleClass, "mEndFrequencyHz", "F");
+    sPwleClassInfo.duration = GetFieldIDOrDie(env, pwleClass, "mDuration", "I");
+
     jclass frequencyProfileLegacyClass =
             FindClassOrDie(env, "android/os/VibratorInfo$FrequencyProfileLegacy");
     sFrequencyProfileLegacyClass =
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/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
index b2fe138..d66bb00 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
@@ -1692,16 +1692,9 @@
                         3 * MINUTE_IN_MILLIS, 5), false);
         final long timeUntilQuotaConsumedMs = 7 * MINUTE_IN_MILLIS;
         JobStatus job = createJobStatus("testGetMaxJobExecutionTimeLocked", 0);
-        //noinspection deprecation
-        JobStatus jobDefIWF = createJobStatus("testGetMaxJobExecutionTimeLocked",
-                createJobInfoBuilder(1)
-                        .setImportantWhileForeground(true)
-                        .setPriority(JobInfo.PRIORITY_DEFAULT)
-                        .build());
         JobStatus jobHigh = createJobStatus("testGetMaxJobExecutionTimeLocked",
                 createJobInfoBuilder(2).setPriority(JobInfo.PRIORITY_HIGH).build());
         setStandbyBucket(RARE_INDEX, job);
-        setStandbyBucket(RARE_INDEX, jobDefIWF);
         setStandbyBucket(RARE_INDEX, jobHigh);
 
         setCharging();
@@ -1709,8 +1702,6 @@
             assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
                     mQuotaController.getMaxJobExecutionTimeMsLocked((job)));
             assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
-                    mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF)));
-            assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
                     mQuotaController.getMaxJobExecutionTimeMsLocked((jobHigh)));
         }
 
@@ -1720,8 +1711,6 @@
             assertEquals(timeUntilQuotaConsumedMs,
                     mQuotaController.getMaxJobExecutionTimeMsLocked((job)));
             assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
-                    mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF)));
-            assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
                     mQuotaController.getMaxJobExecutionTimeMsLocked((jobHigh)));
         }
 
@@ -1730,9 +1719,8 @@
         // Top-stared jobs are out of quota enforcement.
         setProcessState(ActivityManager.PROCESS_STATE_TOP);
         synchronized (mQuotaController.mLock) {
-            trackJobs(job, jobDefIWF, jobHigh);
+            trackJobs(job, jobHigh);
             mQuotaController.prepareForExecutionLocked(job);
-            mQuotaController.prepareForExecutionLocked(jobDefIWF);
             mQuotaController.prepareForExecutionLocked(jobHigh);
         }
         setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
@@ -1740,11 +1728,8 @@
             assertEquals(timeUntilQuotaConsumedMs,
                     mQuotaController.getMaxJobExecutionTimeMsLocked((job)));
             assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
-                    mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF)));
-            assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
                     mQuotaController.getMaxJobExecutionTimeMsLocked((jobHigh)));
             mQuotaController.maybeStopTrackingJobLocked(job, null);
-            mQuotaController.maybeStopTrackingJobLocked(jobDefIWF, null);
             mQuotaController.maybeStopTrackingJobLocked(jobHigh, null);
         }
 
@@ -1753,8 +1738,6 @@
             assertEquals(timeUntilQuotaConsumedMs,
                     mQuotaController.getMaxJobExecutionTimeMsLocked(job));
             assertEquals(timeUntilQuotaConsumedMs,
-                    mQuotaController.getMaxJobExecutionTimeMsLocked(jobDefIWF));
-            assertEquals(timeUntilQuotaConsumedMs,
                     mQuotaController.getMaxJobExecutionTimeMsLocked(jobHigh));
         }
 
@@ -1762,9 +1745,8 @@
         // Quota is enforced for top-started job after the process leaves TOP/BTOP state.
         setProcessState(ActivityManager.PROCESS_STATE_TOP);
         synchronized (mQuotaController.mLock) {
-            trackJobs(job, jobDefIWF, jobHigh);
+            trackJobs(job, jobHigh);
             mQuotaController.prepareForExecutionLocked(job);
-            mQuotaController.prepareForExecutionLocked(jobDefIWF);
             mQuotaController.prepareForExecutionLocked(jobHigh);
         }
         setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
@@ -1772,11 +1754,8 @@
             assertEquals(timeUntilQuotaConsumedMs,
                     mQuotaController.getMaxJobExecutionTimeMsLocked((job)));
             assertEquals(timeUntilQuotaConsumedMs,
-                    mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF)));
-            assertEquals(timeUntilQuotaConsumedMs,
                     mQuotaController.getMaxJobExecutionTimeMsLocked((jobHigh)));
             mQuotaController.maybeStopTrackingJobLocked(job, null);
-            mQuotaController.maybeStopTrackingJobLocked(jobDefIWF, null);
             mQuotaController.maybeStopTrackingJobLocked(jobHigh, null);
         }
 
@@ -1785,13 +1764,145 @@
             assertEquals(timeUntilQuotaConsumedMs,
                     mQuotaController.getMaxJobExecutionTimeMsLocked(job));
             assertEquals(timeUntilQuotaConsumedMs,
-                    mQuotaController.getMaxJobExecutionTimeMsLocked(jobDefIWF));
-            assertEquals(timeUntilQuotaConsumedMs,
                     mQuotaController.getMaxJobExecutionTimeMsLocked(jobHigh));
         }
     }
 
     @Test
+    public void testGetMaxJobExecutionTimeLocked_Regular_ImportantWhileForeground() {
+        mQuotaController.saveTimingSession(0, SOURCE_PACKAGE,
+                createTimingSession(sElapsedRealtimeClock.millis() - (6 * MINUTE_IN_MILLIS),
+                        3 * MINUTE_IN_MILLIS, 5), false);
+        final long timeUntilQuotaConsumedMs = 7 * MINUTE_IN_MILLIS;
+        JobStatus job = createJobStatus("testGetMaxJobExecutionTimeLocked", 0);
+        //noinspection deprecation
+        JobStatus jobDefIWF;
+        mSetFlagsRule.disableFlags(android.app.job.Flags.FLAG_IGNORE_IMPORTANT_WHILE_FOREGROUND);
+        jobDefIWF = createJobStatus("testGetMaxJobExecutionTimeLocked_IWF",
+                createJobInfoBuilder(1)
+                        .setImportantWhileForeground(true)
+                        .setPriority(JobInfo.PRIORITY_DEFAULT)
+                        .build());
+
+        setStandbyBucket(RARE_INDEX, jobDefIWF);
+        setCharging();
+        synchronized (mQuotaController.mLock) {
+            assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
+                    mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF)));
+        }
+
+        setDischarging();
+        setProcessState(getProcessStateQuotaFreeThreshold());
+        synchronized (mQuotaController.mLock) {
+            assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
+                    mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF)));
+        }
+
+        // Top-started job
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS);
+        // Top-stared jobs are out of quota enforcement.
+        setProcessState(ActivityManager.PROCESS_STATE_TOP);
+        synchronized (mQuotaController.mLock) {
+            trackJobs(jobDefIWF);
+            mQuotaController.prepareForExecutionLocked(jobDefIWF);
+        }
+        setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
+        synchronized (mQuotaController.mLock) {
+            assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
+                    mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF)));
+            mQuotaController.maybeStopTrackingJobLocked(jobDefIWF, null);
+        }
+
+        setProcessState(ActivityManager.PROCESS_STATE_RECEIVER);
+        synchronized (mQuotaController.mLock) {
+            assertEquals(timeUntilQuotaConsumedMs,
+                    mQuotaController.getMaxJobExecutionTimeMsLocked(jobDefIWF));
+        }
+
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS);
+        // Quota is enforced for top-started job after the process leaves TOP/BTOP state.
+        setProcessState(ActivityManager.PROCESS_STATE_TOP);
+        synchronized (mQuotaController.mLock) {
+            trackJobs(jobDefIWF);
+            mQuotaController.prepareForExecutionLocked(jobDefIWF);
+        }
+        setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
+        synchronized (mQuotaController.mLock) {
+            assertEquals(timeUntilQuotaConsumedMs,
+                    mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF)));
+            mQuotaController.maybeStopTrackingJobLocked(jobDefIWF, null);
+        }
+
+        setProcessState(ActivityManager.PROCESS_STATE_RECEIVER);
+        synchronized (mQuotaController.mLock) {
+            assertEquals(timeUntilQuotaConsumedMs,
+                    mQuotaController.getMaxJobExecutionTimeMsLocked(jobDefIWF));
+        }
+
+        mSetFlagsRule.enableFlags(android.app.job.Flags.FLAG_IGNORE_IMPORTANT_WHILE_FOREGROUND);
+        jobDefIWF = createJobStatus("testGetMaxJobExecutionTimeLocked_IWF",
+                createJobInfoBuilder(1)
+                        .setImportantWhileForeground(true)
+                        .setPriority(JobInfo.PRIORITY_DEFAULT)
+                        .build());
+
+        setStandbyBucket(RARE_INDEX, jobDefIWF);
+        setCharging();
+        synchronized (mQuotaController.mLock) {
+            assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
+                    mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF)));
+        }
+
+        setDischarging();
+        setProcessState(getProcessStateQuotaFreeThreshold());
+        synchronized (mQuotaController.mLock) {
+            assertEquals(timeUntilQuotaConsumedMs,
+                    mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF)));
+        }
+
+        // Top-started job
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS);
+        // Top-stared jobs are out of quota enforcement.
+        setProcessState(ActivityManager.PROCESS_STATE_TOP);
+        synchronized (mQuotaController.mLock) {
+            trackJobs(jobDefIWF);
+            mQuotaController.prepareForExecutionLocked(jobDefIWF);
+        }
+        setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
+        synchronized (mQuotaController.mLock) {
+            assertEquals(timeUntilQuotaConsumedMs,
+                    mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF)));
+            mQuotaController.maybeStopTrackingJobLocked(jobDefIWF, null);
+        }
+
+        setProcessState(ActivityManager.PROCESS_STATE_RECEIVER);
+        synchronized (mQuotaController.mLock) {
+            assertEquals(timeUntilQuotaConsumedMs,
+                    mQuotaController.getMaxJobExecutionTimeMsLocked(jobDefIWF));
+        }
+
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS);
+        // Quota is enforced for top-started job after the process leaves TOP/BTOP state.
+        setProcessState(ActivityManager.PROCESS_STATE_TOP);
+        synchronized (mQuotaController.mLock) {
+            trackJobs(jobDefIWF);
+            mQuotaController.prepareForExecutionLocked(jobDefIWF);
+        }
+        setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
+        synchronized (mQuotaController.mLock) {
+            assertEquals(timeUntilQuotaConsumedMs,
+                    mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF)));
+            mQuotaController.maybeStopTrackingJobLocked(jobDefIWF, null);
+        }
+
+        setProcessState(ActivityManager.PROCESS_STATE_RECEIVER);
+        synchronized (mQuotaController.mLock) {
+            assertEquals(timeUntilQuotaConsumedMs,
+                    mQuotaController.getMaxJobExecutionTimeMsLocked(jobDefIWF));
+        }
+    }
+
+    @Test
     public void testGetMaxJobExecutionTimeLocked_Regular_Active() {
         JobStatus job = createJobStatus("testGetMaxJobExecutionTimeLocked_Regular_Active", 0);
         setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_ACTIVE_MS,
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
index b4b3612..bc410d9 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
@@ -1580,12 +1580,12 @@
         setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
 
         assertEquals(BiometricManager.BIOMETRIC_SUCCESS,
-                invokeCanAuthenticate(mBiometricService, Authenticators.MANDATORY_BIOMETRICS));
+                invokeCanAuthenticate(mBiometricService, Authenticators.IDENTITY_CHECK));
 
         when(mTrustManager.isInSignificantPlace()).thenReturn(true);
 
-        assertEquals(BiometricManager.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE,
-                invokeCanAuthenticate(mBiometricService, Authenticators.MANDATORY_BIOMETRICS));
+        assertEquals(BiometricManager.BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE,
+                invokeCanAuthenticate(mBiometricService, Authenticators.IDENTITY_CHECK));
     }
 
     @Test
@@ -1603,13 +1603,13 @@
         setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
 
         assertEquals(BiometricManager.BIOMETRIC_SUCCESS,
-                invokeCanAuthenticate(mBiometricService, Authenticators.MANDATORY_BIOMETRICS
+                invokeCanAuthenticate(mBiometricService, Authenticators.IDENTITY_CHECK
                         | Authenticators.BIOMETRIC_STRONG));
 
         when(mTrustManager.isInSignificantPlace()).thenReturn(true);
 
         assertEquals(BiometricManager.BIOMETRIC_SUCCESS,
-                invokeCanAuthenticate(mBiometricService, Authenticators.MANDATORY_BIOMETRICS
+                invokeCanAuthenticate(mBiometricService, Authenticators.IDENTITY_CHECK
                         | Authenticators.BIOMETRIC_STRONG));
     }
 
@@ -1628,12 +1628,12 @@
         setupAuthForOnly(TYPE_CREDENTIAL, Authenticators.DEVICE_CREDENTIAL);
 
         assertEquals(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE,
-                invokeCanAuthenticate(mBiometricService, Authenticators.MANDATORY_BIOMETRICS));
+                invokeCanAuthenticate(mBiometricService, Authenticators.IDENTITY_CHECK));
 
         when(mTrustManager.isInSignificantPlace()).thenReturn(true);
 
         assertEquals(BiometricManager.BIOMETRIC_SUCCESS,
-                invokeCanAuthenticate(mBiometricService, Authenticators.MANDATORY_BIOMETRICS
+                invokeCanAuthenticate(mBiometricService, Authenticators.IDENTITY_CHECK
                         | Authenticators.DEVICE_CREDENTIAL));
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java
index b758f57..85e45f4 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java
@@ -207,7 +207,7 @@
 
         final BiometricSensor sensor = getFaceSensor();
         final PromptInfo promptInfo = new PromptInfo();
-        promptInfo.setAuthenticators(BiometricManager.Authenticators.MANDATORY_BIOMETRICS);
+        promptInfo.setAuthenticators(BiometricManager.Authenticators.IDENTITY_CHECK);
         final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
                 mSettingObserver, List.of(sensor), 0 /* userId */, promptInfo, TEST_PACKAGE_NAME,
                 false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager);
@@ -222,7 +222,7 @@
         when(mTrustManager.isInSignificantPlace()).thenReturn(false);
 
         final PromptInfo promptInfo = new PromptInfo();
-        promptInfo.setAuthenticators(BiometricManager.Authenticators.MANDATORY_BIOMETRICS);
+        promptInfo.setAuthenticators(BiometricManager.Authenticators.IDENTITY_CHECK);
         final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
                 mSettingObserver, List.of(), 0 /* userId */, promptInfo, TEST_PACKAGE_NAME,
                 false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager);
@@ -238,7 +238,7 @@
 
         final BiometricSensor sensor = getFaceSensor();
         final PromptInfo promptInfo = new PromptInfo();
-        promptInfo.setAuthenticators(BiometricManager.Authenticators.MANDATORY_BIOMETRICS
+        promptInfo.setAuthenticators(BiometricManager.Authenticators.IDENTITY_CHECK
                 | BiometricManager.Authenticators.BIOMETRIC_STRONG);
         final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
                 mSettingObserver, List.of(sensor), 0 /* userId */, promptInfo, TEST_PACKAGE_NAME,
@@ -255,13 +255,13 @@
 
         final BiometricSensor sensor = getFaceSensor();
         final PromptInfo promptInfo = new PromptInfo();
-        promptInfo.setAuthenticators(BiometricManager.Authenticators.MANDATORY_BIOMETRICS);
+        promptInfo.setAuthenticators(BiometricManager.Authenticators.IDENTITY_CHECK);
         final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
                 mSettingObserver, List.of(sensor), 0 /* userId */, promptInfo, TEST_PACKAGE_NAME,
                 false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager);
 
         assertThat(preAuthInfo.getCanAuthenticateResult()).isEqualTo(
-                BiometricManager.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE);
+                BiometricManager.BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE);
         assertThat(preAuthInfo.eligibleSensors).hasSize(0);
     }
 
@@ -296,7 +296,7 @@
 
         final BiometricSensor sensor = getFaceSensor();
         final PromptInfo promptInfo = new PromptInfo();
-        promptInfo.setAuthenticators(BiometricManager.Authenticators.MANDATORY_BIOMETRICS);
+        promptInfo.setAuthenticators(BiometricManager.Authenticators.IDENTITY_CHECK);
         promptInfo.setNegativeButtonText(TEST_PACKAGE_NAME);
         final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
                 mSettingObserver, List.of(sensor), 0 /* userId */, promptInfo, TEST_PACKAGE_NAME,
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java b/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java
index 1bea371..c4167d2 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java
@@ -212,24 +212,24 @@
                 mContext, Authenticators.BIOMETRIC_MIN_STRENGTH));
 
         assertThrows(SecurityException.class, () -> Utils.isValidAuthenticatorConfig(
-                        mContext, Authenticators.MANDATORY_BIOMETRICS));
+                        mContext, Authenticators.IDENTITY_CHECK));
 
         doNothing().when(mContext).enforceCallingOrSelfPermission(
                 eq(SET_BIOMETRIC_DIALOG_ADVANCED), any());
 
         if (Flags.mandatoryBiometrics()) {
             assertTrue(Utils.isValidAuthenticatorConfig(mContext,
-                    Authenticators.MANDATORY_BIOMETRICS));
+                    Authenticators.IDENTITY_CHECK));
         } else {
             assertFalse(Utils.isValidAuthenticatorConfig(mContext,
-                    Authenticators.MANDATORY_BIOMETRICS));
+                    Authenticators.IDENTITY_CHECK));
         }
 
         // The rest of the bits are not allowed to integrate with the public APIs
         for (int i = 8; i < 32; i++) {
             final int authenticator = 1 << i;
             if (authenticator == Authenticators.DEVICE_CREDENTIAL
-                    || authenticator == Authenticators.MANDATORY_BIOMETRICS) {
+                    || authenticator == Authenticators.IDENTITY_CHECK) {
                 continue;
             }
             assertFalse(Utils.isValidAuthenticatorConfig(mContext, 1 << i));
@@ -307,8 +307,8 @@
                         BiometricManager.BIOMETRIC_ERROR_LOCKOUT},
                 {BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT,
                         BiometricManager.BIOMETRIC_ERROR_LOCKOUT},
-                {BiometricConstants.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE,
-                        BiometricManager.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE}
+                {BiometricConstants.BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE,
+                        BiometricManager.BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE}
         };
 
         for (int i = 0; i < testCases.length; i++) {
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
index 1331ae1..b110ff6 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
@@ -160,6 +160,7 @@
 
             // Make a possibly-not-full-permission (i.e. partial) copy and check that it is correct.
             final UserProperties copy = new UserProperties(orig, exposeAll, hasManage, hasQuery);
+            assertThat(copy.toString()).isNotNull();
             verifyTestCopyLacksPermissions(orig, copy, exposeAll, hasManage, hasQuery);
             if (permLevel < 1) {
                 // PropertiesPresent should definitely be different since not all items were copied.
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/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java b/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java
index d7ae046..88ed615 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java
@@ -29,8 +29,10 @@
 import android.os.PersistableBundle;
 import android.os.VibrationEffect;
 import android.os.test.TestLooper;
+import android.os.vibrator.Flags;
 import android.os.vibrator.PrebakedSegment;
 import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.PwleSegment;
 import android.os.vibrator.RampSegment;
 import android.os.vibrator.StepSegment;
 import android.os.vibrator.VibrationConfig;
@@ -57,11 +59,20 @@
     private static final int EMPTY_VIBRATOR_ID = 1;
     private static final int PWLE_VIBRATOR_ID = 2;
     private static final int PWLE_WITHOUT_FREQUENCIES_VIBRATOR_ID = 3;
+    private static final int PWLE_V2_VIBRATOR_ID = 4;
     private static final float TEST_MIN_FREQUENCY = 50;
     private static final float TEST_RESONANT_FREQUENCY = 150;
     private static final float TEST_FREQUENCY_RESOLUTION = 25;
     private static final float[] TEST_AMPLITUDE_MAP = new float[]{
             /* 50Hz= */ 0.08f, 0.16f, 0.32f, 0.64f, /* 150Hz= */ 0.8f, 0.72f, /* 200Hz= */ 0.64f};
+    private static final int TEST_MAX_ENVELOPE_EFFECT_SIZE = 10;
+    private static final int TEST_MIN_ENVELOPE_EFFECT_CONTROL_POINT_DURATION_MILLIS = 20;
+    private static final float[] TEST_FREQUENCIES_HZ = new float[]{30f, 50f, 100f, 120f, 150f};
+    private static final float[] TEST_OUTPUT_ACCELERATIONS_GS =
+            new float[]{0.3f, 0.5f, 1.0f, 0.8f, 0.6f};
+    private static final float PWLE_V2_MIN_FREQUENCY = TEST_FREQUENCIES_HZ[0];
+    private static final float PWLE_V2_MAX_FREQUENCY =
+            TEST_FREQUENCIES_HZ[TEST_FREQUENCIES_HZ.length - 1];
 
     @Rule
     public MockitoRule mMockitoRule = MockitoJUnit.rule();
@@ -296,6 +307,77 @@
         assertThat(vibration.adapt(mAdapter)).isEqualTo(expected);
     }
 
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testPwleSegment_withoutPwleV2Capability_returnsNull() {
+        VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList(
+                new PrimitiveSegment(VibrationEffect.Composition.PRIMITIVE_SPIN, 0.5f, 100),
+                new PwleSegment(1, 0.2f, 30, 60, 20),
+                new PwleSegment(0.8f, 0.2f, 60, 100, 100),
+                new PwleSegment(0.65f, 0.65f, 100, 50, 50)),
+                /* repeatIndex= */ 1);
+
+        VibrationEffect.Composed adaptedEffect =
+                (VibrationEffect.Composed) mAdapter.adaptToVibrator(EMPTY_VIBRATOR_ID, effect);
+        assertThat(adaptedEffect).isNull();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testPwleSegment_withPwleV2Capability_returnsAdaptedSegments() {
+        VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList(
+                new PwleSegment(1, 0.2f, 30, 60, 20),
+                new PwleSegment(0.8f, 0.2f, 60, 100, 100),
+                new PwleSegment(0.65f, 0.65f, 100, 50, 50)),
+                /* repeatIndex= */ 1);
+
+        VibrationEffect.Composed expected = new VibrationEffect.Composed(Arrays.asList(
+                new PwleSegment(1, 0.2f, 30, 60, 20),
+                new PwleSegment(0.8f, 0.2f, 60, 100, 100),
+                new PwleSegment(0.65f, 0.65f, 100, 50, 50)),
+                /* repeatIndex= */ 1);
+
+        SparseArray<VibratorController> vibrators = new SparseArray<>();
+        vibrators.put(PWLE_V2_VIBRATOR_ID, createPwleV2VibratorController(PWLE_V2_VIBRATOR_ID));
+        DeviceAdapter adapter = new DeviceAdapter(mVibrationSettings, vibrators);
+
+        assertThat(adapter.adaptToVibrator(PWLE_V2_VIBRATOR_ID, effect)).isEqualTo(expected);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testPwleSegment_withFrequenciesBelowSupportedRange_returnsNull() {
+        float frequencyBelowSupportedRange = PWLE_V2_MIN_FREQUENCY - 1f;
+        VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList(
+                new PwleSegment(0, 0.2f, 30, 60, 20),
+                new PwleSegment(0.8f, 0.2f, 60, frequencyBelowSupportedRange, 100),
+                new PwleSegment(0.65f, 0.65f, frequencyBelowSupportedRange, 50, 50)),
+                /* repeatIndex= */ 1);
+
+        SparseArray<VibratorController> vibrators = new SparseArray<>();
+        vibrators.put(PWLE_V2_VIBRATOR_ID, createPwleV2VibratorController(PWLE_V2_VIBRATOR_ID));
+        DeviceAdapter adapter = new DeviceAdapter(mVibrationSettings, vibrators);
+
+        assertThat(adapter.adaptToVibrator(PWLE_V2_VIBRATOR_ID, effect)).isNull();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testPwleSegment_withFrequenciesAboveSupportedRange_returnsNull() {
+        float frequencyAboveSupportedRange = PWLE_V2_MAX_FREQUENCY + 1f;
+        VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList(
+                new PwleSegment(0, 0.2f, 30, frequencyAboveSupportedRange, 20),
+                new PwleSegment(0.8f, 0.2f, frequencyAboveSupportedRange, 100, 100),
+                new PwleSegment(0.65f, 0.65f, 100, 50, 50)),
+                /* repeatIndex= */ 1);
+
+        SparseArray<VibratorController> vibrators = new SparseArray<>();
+        vibrators.put(PWLE_V2_VIBRATOR_ID, createPwleV2VibratorController(PWLE_V2_VIBRATOR_ID));
+        DeviceAdapter adapter = new DeviceAdapter(mVibrationSettings, vibrators);
+
+        assertThat(adapter.adaptToVibrator(PWLE_V2_VIBRATOR_ID, effect)).isNull();
+    }
+
     private VibratorController createEmptyVibratorController(int vibratorId) {
         return new FakeVibratorControllerProvider(mTestLooper.getLooper())
                 .newVibratorController(vibratorId, (id, vibrationId)  -> {});
@@ -318,4 +400,18 @@
         provider.setMaxAmplitudes(TEST_AMPLITUDE_MAP);
         return provider.newVibratorController(vibratorId, (id, vibrationId)  -> {});
     }
+
+    private VibratorController createPwleV2VibratorController(int vibratorId) {
+        FakeVibratorControllerProvider provider = new FakeVibratorControllerProvider(
+                mTestLooper.getLooper());
+        provider.setCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2);
+        provider.setResonantFrequency(TEST_RESONANT_FREQUENCY);
+        provider.setFrequenciesHz(TEST_FREQUENCIES_HZ);
+        provider.setOutputAccelerationsGs(TEST_OUTPUT_ACCELERATIONS_GS);
+        provider.setMaxEnvelopeEffectSize(TEST_MAX_ENVELOPE_EFFECT_SIZE);
+        provider.setMinEnvelopeEffectControlPointDurationMillis(
+                TEST_MIN_ENVELOPE_EFFECT_CONTROL_POINT_DURATION_MILLIS);
+
+        return provider.newVibratorController(vibratorId, (id, vibrationId)  -> {});
+    }
 }
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
index 58a1e84..8aa8a84 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -59,11 +59,13 @@
 import android.os.vibrator.Flags;
 import android.os.vibrator.PrebakedSegment;
 import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.PwleSegment;
 import android.os.vibrator.RampSegment;
 import android.os.vibrator.StepSegment;
 import android.os.vibrator.VibrationConfig;
 import android.os.vibrator.VibrationEffectSegment;
 import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -875,6 +877,49 @@
     }
 
     @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void vibrate_singleVibratorPwle_runsComposePwleV2() {
+        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
+        fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2);
+        fakeVibrator.setResonantFrequency(150);
+        fakeVibrator.setFrequenciesHz(new float[]{30f, 50f, 100f, 120f, 150f});
+        fakeVibrator.setOutputAccelerationsGs(new float[]{0.3f, 0.5f, 1.0f, 0.8f, 0.6f});
+        fakeVibrator.setMaxEnvelopeEffectSize(10);
+        fakeVibrator.setMinEnvelopeEffectControlPointDurationMillis(20);
+
+        VibrationEffect effect = VibrationEffect.startWaveformEnvelope()
+                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 20)
+                .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 30)
+                .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 20)
+                .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 30)
+                .build();
+        HalVibration vibration = startThreadAndDispatcher(effect);
+        waitForCompletion();
+
+        verify(mManagerHooks).noteVibratorOn(eq(UID), eq(100L));
+        verify(mManagerHooks).noteVibratorOff(eq(UID));
+        verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+        verifyCallbacksTriggered(vibration, Status.FINISHED);
+        assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
+        assertEquals(Arrays.asList(
+                        expectedPwle(/* startAmplitude= */ 0.0f, /* endAmplitude= */ 0.0f,
+                                /* startFrequencyHz= */ 60f, /* endFrequencyHz= */ 60f,
+                                /* duration= */ 20),
+                        expectedPwle(/* startAmplitude= */ 0.0f, /* endAmplitude= */ 0.3f,
+                                /* startFrequencyHz= */ 60f, /* endFrequencyHz= */ 100f,
+                                /* duration= */ 30),
+                        expectedPwle(/* startAmplitude= */ 0.3f, /* endAmplitude= */ 0.4f,
+                                /* startFrequencyHz= */ 100f, /* endFrequencyHz= */ 120f,
+                                /* duration= */ 20),
+                        expectedPwle(/* startAmplitude= */ 0.4f, /* endAmplitude= */ 0.0f,
+                                /* startFrequencyHz= */ 120f, /* endFrequencyHz= */ 120f,
+                                /* duration= */ 30)
+                ),
+                fakeVibrator.getEffectSegments(vibration.id));
+
+    }
+
+    @Test
     @DisableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
     public void vibrate_singleVibratorPwle_runsComposePwle() {
         FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
@@ -1968,6 +2013,12 @@
                 duration);
     }
 
+    private VibrationEffectSegment expectedPwle(float startAmplitude, float endAmplitude,
+            float startFrequencyHz, float endFrequencyHz, int duration) {
+        return new PwleSegment(startAmplitude, endAmplitude, startFrequencyHz, endFrequencyHz,
+                duration);
+    }
+
     private List<Float> expectedAmplitudes(int... amplitudes) {
         return Arrays.stream(amplitudes)
                 .mapToObj(amplitude -> amplitude / 255f)
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java
index 8179369..bc8db3b 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java
@@ -44,6 +44,7 @@
 import android.os.test.TestLooper;
 import android.os.vibrator.PrebakedSegment;
 import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.PwleSegment;
 import android.os.vibrator.RampSegment;
 
 import androidx.test.InstrumentationRegistry;
@@ -268,6 +269,22 @@
     }
 
     @Test
+    public void on_withComposedPwleV2_performsEffect() {
+        mockVibratorCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2);
+        when(mNativeWrapperMock.composePwleV2(any(), anyLong())).thenReturn(15L);
+        VibratorController controller = createController();
+
+        PwleSegment[] primitives = new PwleSegment[]{
+                new PwleSegment(/* startAmplitude= */ 0, /* endAmplitude= */ 1,
+                        /* startFrequencyHz= */ 100, /* endFrequencyHz= */ 200, /* duration= */ 10)
+        };
+        assertEquals(15L, controller.on(primitives, 12));
+        assertTrue(controller.isVibrating());
+
+        verify(mNativeWrapperMock).composePwleV2(eq(primitives), eq(12L));
+    }
+
+    @Test
     public void off_turnsOffVibrator() {
         when(mNativeWrapperMock.on(anyLong(), anyLong())).thenAnswer(args -> args.getArgument(0));
         VibratorController controller = createController();
diff --git a/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java b/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java
index 75a9cedf..2c3e9b2 100644
--- a/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java
+++ b/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java
@@ -26,6 +26,7 @@
 import android.os.VibratorInfo;
 import android.os.vibrator.PrebakedSegment;
 import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.PwleSegment;
 import android.os.vibrator.RampSegment;
 import android.os.vibrator.StepSegment;
 import android.os.vibrator.VibrationEffectSegment;
@@ -194,6 +195,19 @@
         }
 
         @Override
+        public long composePwleV2(PwleSegment[] primitives, long vibrationId) {
+            long duration = 0;
+            for (PwleSegment primitive: primitives) {
+                duration += primitive.getDuration();
+                recordEffectSegment(vibrationId, primitive);
+            }
+            applyLatency(mOnLatency);
+            scheduleListener(duration, vibrationId);
+
+            return duration;
+        }
+
+        @Override
         public void setExternalControl(boolean enabled) {
             mExternalControlStates.add(enabled);
         }
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);