Merge "Add API comment for future potential behavior change" into main
diff --git a/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java b/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java
index 6c8af39..ae98fe1 100644
--- a/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java
+++ b/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java
@@ -77,6 +77,12 @@
             @NonNull String notificationChannel, int userId, @NonNull String packageName);
 
     /**
+     * @return {@code true} if the given package holds the
+     * {@link android.Manifest.permission.RUN_BACKUP_JOBS} permission.
+     */
+    boolean hasRunBackupJobsPermission(@NonNull String packageName, int packageUid);
+
+    /**
      * Report a snapshot of sync-related jobs back to the sync manager
      */
     JobStorePersistStats getPersistStats();
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 fc193d8..57467e3 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -4197,6 +4197,11 @@
         }
 
         @Override
+        public boolean hasRunBackupJobsPermission(@NonNull String packageName, int packageUid) {
+            return JobSchedulerService.this.hasRunBackupJobsPermission(packageName, packageUid);
+        }
+
+        @Override
         public JobStorePersistStats getPersistStats() {
             synchronized (mLock) {
                 return new JobStorePersistStats(mJobs.getPersistStats());
@@ -4359,6 +4364,22 @@
     }
 
     /**
+     * Returns whether the app holds the {@link Manifest.permission.RUN_BACKUP_JOBS} permission.
+     */
+    private boolean hasRunBackupJobsPermission(@NonNull String packageName, int packageUid) {
+        if (packageName == null) {
+            Slog.wtfStack(TAG,
+                    "Expected a non-null package name when calling hasRunBackupJobsPermission");
+            return false;
+        }
+
+        return PermissionChecker.checkPermissionForPreflight(getTestableContext(),
+                android.Manifest.permission.RUN_BACKUP_JOBS,
+                PermissionChecker.PID_UNKNOWN, packageUid, packageName)
+                    == PermissionChecker.PERMISSION_GRANTED;
+    }
+
+    /**
      * Binder stub trampoline implementation
      */
     final class JobSchedulerStub extends IJobScheduler.Stub {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index a4df5d8..2ea980d 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -1222,21 +1222,25 @@
             return ACTIVE_INDEX;
         }
 
-        final int bucketWithMediaExemption;
-        if (actualBucket != RESTRICTED_INDEX && actualBucket != NEVER_INDEX
-                && mHasMediaBackupExemption) {
+        final boolean isEligibleAsBackupJob = job.getTriggerContentUris() != null
+                && job.getRequiredNetwork() != null
+                && !job.hasLateConstraint()
+                && mJobSchedulerInternal.hasRunBackupJobsPermission(sourcePackageName, sourceUid);
+        final boolean isBackupExempt = mHasMediaBackupExemption || isEligibleAsBackupJob;
+        final int bucketWithBackupExemption;
+        if (actualBucket != RESTRICTED_INDEX && actualBucket != NEVER_INDEX && isBackupExempt) {
             // Treat it as if it's at most WORKING_INDEX (lower index grants higher quota) since
             // media backup jobs are important to the user, and the source package may not have
             // been used directly in a while.
-            bucketWithMediaExemption = Math.min(WORKING_INDEX, actualBucket);
+            bucketWithBackupExemption = Math.min(WORKING_INDEX, actualBucket);
         } else {
-            bucketWithMediaExemption = actualBucket;
+            bucketWithBackupExemption = actualBucket;
         }
 
         // If the app is considered buggy, but hasn't yet been put in the RESTRICTED bucket
         // (potentially because it's used frequently by the user), limit its effective bucket
         // so that it doesn't get to run as much as a normal ACTIVE app.
-        if (isBuggy && bucketWithMediaExemption < WORKING_INDEX) {
+        if (isBuggy && bucketWithBackupExemption < WORKING_INDEX) {
             if (!mIsDowngradedDueToBuggyApp) {
                 // Safety check to avoid logging multiple times for the same job.
                 Counter.logIncrementWithUid(
@@ -1246,7 +1250,7 @@
             }
             return WORKING_INDEX;
         }
-        return bucketWithMediaExemption;
+        return bucketWithBackupExemption;
     }
 
     /** Returns the real standby bucket of the job. */
diff --git a/core/api/current.txt b/core/api/current.txt
index 89c7728..55ea2f4 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -147,6 +147,7 @@
     field public static final String MANAGE_DEVICE_POLICY_CAMERA = "android.permission.MANAGE_DEVICE_POLICY_CAMERA";
     field public static final String MANAGE_DEVICE_POLICY_CERTIFICATES = "android.permission.MANAGE_DEVICE_POLICY_CERTIFICATES";
     field public static final String MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE = "android.permission.MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE";
+    field @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") public static final String MANAGE_DEVICE_POLICY_CONTENT_PROTECTION = "android.permission.MANAGE_DEVICE_POLICY_CONTENT_PROTECTION";
     field public static final String MANAGE_DEVICE_POLICY_DEBUGGING_FEATURES = "android.permission.MANAGE_DEVICE_POLICY_DEBUGGING_FEATURES";
     field public static final String MANAGE_DEVICE_POLICY_DEFAULT_SMS = "android.permission.MANAGE_DEVICE_POLICY_DEFAULT_SMS";
     field public static final String MANAGE_DEVICE_POLICY_DEVICE_IDENTIFIERS = "android.permission.MANAGE_DEVICE_POLICY_DEVICE_IDENTIFIERS";
@@ -8194,6 +8195,9 @@
     field public static final String ACTION_SET_NEW_PASSWORD = "android.app.action.SET_NEW_PASSWORD";
     field public static final String ACTION_START_ENCRYPTION = "android.app.action.START_ENCRYPTION";
     field public static final String ACTION_SYSTEM_UPDATE_POLICY_CHANGED = "android.app.action.SYSTEM_UPDATE_POLICY_CHANGED";
+    field @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") public static final int CONTENT_PROTECTION_DISABLED = 1; // 0x1
+    field @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") public static final int CONTENT_PROTECTION_ENABLED = 2; // 0x2
+    field @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") public static final int CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY = 0; // 0x0
     field public static final String DELEGATION_APP_RESTRICTIONS = "delegation-app-restrictions";
     field public static final String DELEGATION_BLOCK_UNINSTALL = "delegation-block-uninstall";
     field public static final String DELEGATION_CERT_INSTALL = "delegation-cert-install";
@@ -22371,6 +22375,7 @@
     method @NonNull public java.util.List<java.lang.String> getSupportedVendorParameters();
     method @Nullable public static android.media.Image mapHardwareBuffer(@NonNull android.hardware.HardwareBuffer);
     method public void queueInputBuffer(int, int, int, long, int) throws android.media.MediaCodec.CryptoException;
+    method @FlaggedApi("com.android.media.codec.flags.large_audio_frame") public void queueInputBuffers(int, @NonNull java.util.ArrayDeque<android.media.MediaCodec.BufferInfo>);
     method public void queueSecureInputBuffer(int, int, @NonNull android.media.MediaCodec.CryptoInfo, long, int) throws android.media.MediaCodec.CryptoException;
     method public void release();
     method public void releaseOutputBuffer(int, boolean);
@@ -22432,6 +22437,7 @@
     method public abstract void onError(@NonNull android.media.MediaCodec, @NonNull android.media.MediaCodec.CodecException);
     method public abstract void onInputBufferAvailable(@NonNull android.media.MediaCodec, int);
     method public abstract void onOutputBufferAvailable(@NonNull android.media.MediaCodec, int, @NonNull android.media.MediaCodec.BufferInfo);
+    method @FlaggedApi("com.android.media.codec.flags.large_audio_frame") public void onOutputBuffersAvailable(@NonNull android.media.MediaCodec, int, @NonNull java.util.ArrayDeque<android.media.MediaCodec.BufferInfo>);
     method public abstract void onOutputFormatChanged(@NonNull android.media.MediaCodec, @NonNull android.media.MediaFormat);
   }
 
@@ -22519,6 +22525,7 @@
   }
 
   public static final class MediaCodec.OutputFrame {
+    method @FlaggedApi("com.android.media.codec.flags.large_audio_frame") @NonNull public java.util.ArrayDeque<android.media.MediaCodec.BufferInfo> getBufferInfos();
     method @NonNull public java.util.Set<java.lang.String> getChangedKeys();
     method public int getFlags();
     method @NonNull public android.media.MediaFormat getFormat();
@@ -22534,6 +22541,7 @@
 
   public final class MediaCodec.QueueRequest {
     method public void queue();
+    method @FlaggedApi("com.android.media.codec.flags.large_audio_frame") @NonNull public android.media.MediaCodec.QueueRequest setBufferInfos(@NonNull java.util.ArrayDeque<android.media.MediaCodec.BufferInfo>);
     method @NonNull public android.media.MediaCodec.QueueRequest setByteBufferParameter(@NonNull String, @NonNull java.nio.ByteBuffer);
     method @NonNull public android.media.MediaCodec.QueueRequest setEncryptedLinearBlock(@NonNull android.media.MediaCodec.LinearBlock, int, int, @NonNull android.media.MediaCodec.CryptoInfo);
     method @NonNull public android.media.MediaCodec.QueueRequest setFlags(int);
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 94cde98..708f10e 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -561,7 +561,7 @@
 
   public class ActivityManager {
     method @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public void addOnUidImportanceListener(android.app.ActivityManager.OnUidImportanceListener, int);
-    method @FlaggedApi("android.app.uid_importance_listener_for_uids") @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public void addOnUidImportanceListener(@NonNull android.app.ActivityManager.OnUidImportanceListener, int, @Nullable int[]);
+    method @FlaggedApi("android.app.uid_importance_listener_for_uids") @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public void addOnUidImportanceListener(@NonNull android.app.ActivityManager.OnUidImportanceListener, int, @NonNull int[]);
     method @RequiresPermission(android.Manifest.permission.FORCE_STOP_PACKAGES) public void forceStopPackage(String);
     method @FlaggedApi("android.app.get_binding_uid_importance") @RequiresPermission(android.Manifest.permission.GET_BINDING_UID_IMPORTANCE) public int getBindingUidImportance(int);
     method @RequiresPermission(anyOf={"android.permission.INTERACT_ACROSS_USERS", "android.permission.INTERACT_ACROSS_USERS_FULL"}) public static int getCurrentUser();
@@ -6165,6 +6165,7 @@
     method public boolean containsKey(String);
     method public int describeContents();
     method @Deprecated public android.graphics.Bitmap getBitmap(String);
+    method @FlaggedApi("android.hardware.radio.hd_radio_improved") public int getBitmapId(@NonNull String);
     method public android.hardware.radio.RadioMetadata.Clock getClock(String);
     method public int getInt(String);
     method public String getString(String);
@@ -6228,6 +6229,7 @@
     method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract void close();
     method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int getConfiguration(android.hardware.radio.RadioManager.BandConfig[]);
     method @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public android.hardware.radio.ProgramList getDynamicProgramList(@Nullable android.hardware.radio.ProgramList.Filter);
+    method @FlaggedApi("android.hardware.radio.hd_radio_improved") @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public android.graphics.Bitmap getMetadataImage(int);
     method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract boolean getMute();
     method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public java.util.Map<java.lang.String,java.lang.String> getParameters(@NonNull java.util.List<java.lang.String>);
     method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int getProgramInformation(android.hardware.radio.RadioManager.ProgramInfo[]);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 42cf08f..1fa2b5c 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -195,8 +195,11 @@
     method public void setTaskOverlay(boolean, boolean);
   }
 
-  public static final class ActivityOptions.LaunchCookie {
+  public static final class ActivityOptions.LaunchCookie implements android.os.Parcelable {
     ctor public ActivityOptions.LaunchCookie();
+    method public int describeContents();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.ActivityOptions.LaunchCookie> CREATOR;
   }
 
   public static interface ActivityOptions.OnAnimationFinishedListener {
@@ -2108,6 +2111,14 @@
 
 }
 
+package android.media.projection {
+
+  public final class MediaProjectionManager {
+    method @NonNull public android.content.Intent createScreenCaptureIntent(@Nullable android.app.ActivityOptions.LaunchCookie);
+  }
+
+}
+
 package android.media.soundtrigger {
 
   public final class SoundTriggerInstrumentation {
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 9d20f3c..6285eb3 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -107,6 +107,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
+import java.util.Objects;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
@@ -4440,8 +4441,7 @@
      * is used here, you will receive a call each time a uids importance transitions between
      * being <= {@link RunningAppProcessInfo#IMPORTANCE_PERCEPTIBLE} and
      * > {@link RunningAppProcessInfo#IMPORTANCE_PERCEPTIBLE}.
-     * @param uids The UIDs that this listener is interested with. A {@code null} value means
-     * all UIDs will be monitored by this listener, this will be equivalent to the
+     * @param uids The UIDs that this listener is interested with.
      * {@link #addOnUidImportanceListener(OnUidImportanceListener, int)} in this case.
      *
      * <p>Calling this API with the same instance of {@code listener} without
@@ -4456,7 +4456,9 @@
     @SuppressLint("SamShouldBeLast")
     @RequiresPermission(Manifest.permission.PACKAGE_USAGE_STATS)
     public void addOnUidImportanceListener(@NonNull OnUidImportanceListener listener,
-            @RunningAppProcessInfo.Importance int importanceCutpoint, @Nullable int[] uids) {
+            @RunningAppProcessInfo.Importance int importanceCutpoint, @NonNull int[] uids) {
+        Objects.requireNonNull(listener);
+        Objects.requireNonNull(uids);
         addOnUidImportanceListenerInternal(listener, importanceCutpoint, uids);
     }
 
diff --git a/core/java/android/app/ActivityOptions.aidl b/core/java/android/app/ActivityOptions.aidl
index bd5cd88..2d4a85f 100644
--- a/core/java/android/app/ActivityOptions.aidl
+++ b/core/java/android/app/ActivityOptions.aidl
@@ -17,4 +17,7 @@
 package android.app;
 
 /** @hide */
-parcelable ActivityOptions.SceneTransitionInfo;
\ No newline at end of file
+parcelable ActivityOptions.SceneTransitionInfo;
+
+/** @hide */
+parcelable ActivityOptions.LaunchCookie;
\ No newline at end of file
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index 4a566db..111895e 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -1958,14 +1958,87 @@
      */
     @SuppressLint("UnflaggedApi")
     @TestApi
-    public static final class LaunchCookie {
+    public static final class LaunchCookie implements Parcelable {
         /** @hide */
-        public final IBinder binder = new Binder();
+        public final IBinder binder;
 
         /** @hide */
         @SuppressLint("UnflaggedApi")
         @TestApi
-        public LaunchCookie() {}
+        public LaunchCookie() {
+            binder = new Binder();
+        }
+
+        /** @hide */
+        public LaunchCookie(@Nullable String descriptor) {
+            binder = new Binder(descriptor);
+        }
+
+        private LaunchCookie(Parcel in) {
+            this.binder = in.readStrongBinder();
+        }
+
+        /** @hide */
+        @SuppressLint("UnflaggedApi")
+        @TestApi
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        /** @hide */
+        @SuppressLint("UnflaggedApi")
+        @TestApi
+        @Override
+        public void writeToParcel(@NonNull Parcel dest, int flags) {
+            dest.writeStrongBinder(binder);
+        }
+
+        /** @hide */
+        public static LaunchCookie readFromParcel(@NonNull Parcel in) {
+            return new LaunchCookie(in);
+        }
+
+        /** @hide */
+        public static void writeToParcel(@Nullable LaunchCookie launchCookie, Parcel out) {
+            if (launchCookie != null) {
+                launchCookie.writeToParcel(out, 0);
+            } else {
+                out.writeStrongBinder(null);
+            }
+        }
+
+        /** @hide */
+        @SuppressLint("UnflaggedApi")
+        @TestApi
+        @NonNull
+        public static final Parcelable.Creator<LaunchCookie> CREATOR =
+                new Parcelable.Creator<LaunchCookie>() {
+
+                    @Override
+                    public LaunchCookie createFromParcel(Parcel source) {
+                        return new LaunchCookie(source);
+                    }
+
+                    @Override
+                    public LaunchCookie[] newArray(int size) {
+                        return new LaunchCookie[size];
+                    }
+                };
+
+        @Override
+        public boolean equals(@Nullable Object obj) {
+            if (obj instanceof LaunchCookie) {
+                LaunchCookie other = (LaunchCookie) obj;
+                return binder == other.binder;
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return binder.hashCode();
+        }
     }
 
     /**
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index 2162e3a..68512b8 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -79,6 +79,7 @@
  * implementation is described to the system through an AndroidManifest.xml's
  * &lt;instrumentation&gt; tag.
  */
+@android.ravenwood.annotation.RavenwoodKeepPartialClass
 public class Instrumentation {
 
     /**
@@ -132,6 +133,7 @@
     private UiAutomation mUiAutomation;
     private final Object mAnimationCompleteLock = new Object();
 
+    @android.ravenwood.annotation.RavenwoodKeep
     public Instrumentation() {
     }
 
@@ -142,6 +144,7 @@
      * reflection, but it will serve as noticeable discouragement from
      * doing such a thing.
      */
+    @android.ravenwood.annotation.RavenwoodReplace
     private void checkInstrumenting(String method) {
         // Check if we have an instrumentation context, as init should only get called by
         // the system in startup processes that are being instrumented.
@@ -151,6 +154,11 @@
         }
     }
 
+    private void checkInstrumenting$ravenwood(String method) {
+        // At the moment, Ravenwood doesn't attach a Context, but we're only ever
+        // running code as part of tests, so we continue quietly
+    }
+
     /**
      * Returns if it is being called in an instrumentation environment.
      *
@@ -2504,6 +2512,7 @@
      * Takes control of the execution of messages on the specified looper until
      * {@link TestLooperManager#release} is called.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public TestLooperManager acquireLooperManager(Looper looper) {
         checkInstrumenting("acquireLooperManager");
         return new TestLooperManager(looper);
diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java
index 0261f0a..1ac08ac 100644
--- a/core/java/android/app/PendingIntent.java
+++ b/core/java/android/app/PendingIntent.java
@@ -179,6 +179,14 @@
     @Overridable
     public static final long BLOCK_MUTABLE_IMPLICIT_PENDING_INTENT = 236704164L;
 
+    /**
+     * Validate options passed in as bundle.
+     * @hide
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public static final long PENDING_INTENT_OPTIONS_CHECK = 320664730L;
+
     /** @hide */
     @IntDef(flag = true,
             value = {
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 5c42b0e..86d0125 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -53,6 +53,7 @@
 import static android.content.Intent.LOCAL_FLAG_FROM_SYSTEM;
 import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
 import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
+import static android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED;
 
 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
 
@@ -61,6 +62,7 @@
 import android.annotation.BroadcastBehavior;
 import android.annotation.CallbackExecutor;
 import android.annotation.ColorInt;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -4092,6 +4094,29 @@
         return MTE_NOT_CONTROLLED_BY_POLICY;
     }
 
+    /** Indicates that content protection is not controlled by policy, allowing user to choose. */
+    @FlaggedApi(FLAG_MANAGE_DEVICE_POLICY_ENABLED)
+    public static final int CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY = 0;
+
+    /** Indicates that content protection is controlled and disabled by a policy. */
+    @FlaggedApi(FLAG_MANAGE_DEVICE_POLICY_ENABLED)
+    public static final int CONTENT_PROTECTION_DISABLED = 1;
+
+    /** Indicates that content protection is controlled and enabled by a policy. */
+    @FlaggedApi(FLAG_MANAGE_DEVICE_POLICY_ENABLED)
+    public static final int CONTENT_PROTECTION_ENABLED = 2;
+
+    /** @hide */
+    @IntDef(
+            prefix = {"CONTENT_PROTECTION_"},
+            value = {
+                CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY,
+                CONTENT_PROTECTION_DISABLED,
+                CONTENT_PROTECTION_ENABLED,
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ContentProtectionPolicy {}
+
     /**
      * This object is a single place to tack on invalidation and disable calls.  All
      * binder caches in this class derive from this Config, so all can be invalidated or
diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java
index ec181da..1f19f81 100644
--- a/core/java/android/appwidget/AppWidgetHostView.java
+++ b/core/java/android/appwidget/AppWidgetHostView.java
@@ -907,7 +907,10 @@
 
     private InteractionHandler getHandler(InteractionHandler handler) {
         return (view, pendingIntent, response) -> {
-            AppWidgetManager.getInstance(mContext).noteAppWidgetTapped(mAppWidgetId);
+            AppWidgetManager manager = AppWidgetManager.getInstance(mContext);
+            if (manager != null) {
+                manager.noteAppWidgetTapped(mAppWidgetId);
+            }
             if (handler != null) {
                 return handler.onInteraction(view, pendingIntent, response);
             } else {
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index b1173a2..b8d7543 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -3657,8 +3657,8 @@
      * On Android {@link android.os.Build.VERSION_CODES#S} and later,
      * if the application is in a state where the service
      * can not be started (such as not in the foreground in a state when services are allowed),
-     * {@link android.app.BackgroundServiceStartNotAllowedException} is thrown
-     * This excemption extends {@link IllegalStateException}, so apps can
+     * {@link android.app.BackgroundServiceStartNotAllowedException} is thrown.
+     * This exception extends {@link IllegalStateException}, so apps can
      * use {@code catch (IllegalStateException)} to catch both.
      *
      * @see #startForegroundService(Intent)
diff --git a/core/java/android/content/res/Element.java b/core/java/android/content/res/Element.java
index 89f4985..6ff96f4 100644
--- a/core/java/android/content/res/Element.java
+++ b/core/java/android/content/res/Element.java
@@ -93,6 +93,7 @@
     protected static final String TAG_SUPPORTS_GL_TEXTURE = "supports-gl-texture";
     protected static final String TAG_SUPPORTS_INPUT = "supports-input";
     protected static final String TAG_SUPPORTS_SCREENS = "supports-screens";
+    protected static final String TAG_URI_RELATIVE_FILTER_GROUP = "uri-relative-filter-group";
     protected static final String TAG_USES_CONFIGURATION = "uses-configuration";
     protected static final String TAG_USES_FEATURE = "uses-feature";
     protected static final String TAG_USES_GL_TEXTURE = "uses-gl-texture";
@@ -106,6 +107,11 @@
 
     protected static final String TAG_ATTR_BACKUP_AGENT = "backupAgent";
     protected static final String TAG_ATTR_CATEGORY = "category";
+    protected static final String TAG_ATTR_FRAGMENT = "fragment";
+    protected static final String TAG_ATTR_FRAGMENT_ADVANCED_PATTERN = "fragmentAdvancedPattern";
+    protected static final String TAG_ATTR_FRAGMENT_PATTERN = "fragmentPattern";
+    protected static final String TAG_ATTR_FRAGMENT_PREFIX = "fragmentPrefix";
+    protected static final String TAG_ATTR_FRAGMENT_SUFFIX = "fragmentSuffix";
     protected static final String TAG_ATTR_HOST = "host";
     protected static final String TAG_ATTR_MANAGE_SPACE_ACTIVITY = "manageSpaceActivity";
     protected static final String TAG_ATTR_MIMETYPE = "mimeType";
@@ -122,6 +128,11 @@
     protected static final String TAG_ATTR_PERMISSION_GROUP = "permissionGroup";
     protected static final String TAG_ATTR_PORT = "port";
     protected static final String TAG_ATTR_PROCESS = "process";
+    protected static final String TAG_ATTR_QUERY = "query";
+    protected static final String TAG_ATTR_QUERY_ADVANCED_PATTERN = "queryAdvancedPattern";
+    protected static final String TAG_ATTR_QUERY_PATTERN = "queryPattern";
+    protected static final String TAG_ATTR_QUERY_PREFIX = "queryPrefix";
+    protected static final String TAG_ATTR_QUERY_SUFFIX = "querySuffix";
     protected static final String TAG_ATTR_READ_PERMISSION = "readPermission";
     protected static final String TAG_ATTR_REQUIRED_ACCOUNT_TYPE = "requiredAccountType";
     protected static final String TAG_ATTR_REQUIRED_SYSTEM_PROPERTY_NAME =
@@ -143,7 +154,7 @@
 
     // The length of mTagCounters corresponds to the number of tags defined in getCounterIdx. If new
     // tags are added then the size here should be increased to match.
-    private final TagCounter[] mTagCounters = new TagCounter[34];
+    private final TagCounter[] mTagCounters = new TagCounter[35];
 
     String mTag;
 
@@ -238,9 +249,11 @@
                 return 31;
             case TAG_INTENT:
                 return 32;
+            case TAG_URI_RELATIVE_FILTER_GROUP:
+                return 33;
             default:
                 // The size of the mTagCounters array should be equal to this value+1
-                return 33;
+                return 34;
         }
     }
 
@@ -276,6 +289,7 @@
             case TAG_SERVICE:
             case TAG_SUPPORTS_GL_TEXTURE:
             case TAG_SUPPORTS_SCREENS:
+            case TAG_URI_RELATIVE_FILTER_GROUP:
             case TAG_USES_CONFIGURATION:
             case TAG_USES_FEATURE:
             case TAG_USES_LIBRARY:
@@ -322,6 +336,7 @@
                 break;
             case TAG_INTENT:
             case TAG_INTENT_FILTER:
+                initializeCounter(TAG_URI_RELATIVE_FILTER_GROUP, 100);
                 initializeCounter(TAG_ACTION, 20000);
                 initializeCounter(TAG_CATEGORY, 40000);
                 initializeCounter(TAG_DATA, 40000);
@@ -354,6 +369,9 @@
                 initializeCounter(TAG_INTENT, 2000);
                 initializeCounter(TAG_PROVIDER, 8000);
                 break;
+            case TAG_URI_RELATIVE_FILTER_GROUP:
+                initializeCounter(TAG_DATA, 100);
+                break;
         }
     }
 
@@ -391,11 +409,21 @@
             case TAG_ATTR_VERSION_NAME:
             case TAG_ATTR_ZYGOTE_PRELOAD_NAME:
                 return MAX_ATTR_LEN_NAME;
+            case TAG_ATTR_FRAGMENT:
+            case TAG_ATTR_FRAGMENT_ADVANCED_PATTERN:
+            case TAG_ATTR_FRAGMENT_PATTERN:
+            case TAG_ATTR_FRAGMENT_PREFIX:
+            case TAG_ATTR_FRAGMENT_SUFFIX:
             case TAG_ATTR_PATH:
             case TAG_ATTR_PATH_ADVANCED_PATTERN:
             case TAG_ATTR_PATH_PATTERN:
             case TAG_ATTR_PATH_PREFIX:
             case TAG_ATTR_PATH_SUFFIX:
+            case TAG_ATTR_QUERY:
+            case TAG_ATTR_QUERY_ADVANCED_PATTERN:
+            case TAG_ATTR_QUERY_PATTERN:
+            case TAG_ATTR_QUERY_PREFIX:
+            case TAG_ATTR_QUERY_SUFFIX:
                 return MAX_ATTR_LEN_PATH;
             case TAG_ATTR_VALUE:
                 return MAX_ATTR_LEN_VALUE;
@@ -535,6 +563,16 @@
             case R.styleable.AndroidManifestData_pathPrefix:
             case R.styleable.AndroidManifestData_pathSuffix:
             case R.styleable.AndroidManifestData_pathAdvancedPattern:
+            case R.styleable.AndroidManifestData_query:
+            case R.styleable.AndroidManifestData_queryPattern:
+            case R.styleable.AndroidManifestData_queryPrefix:
+            case R.styleable.AndroidManifestData_querySuffix:
+            case R.styleable.AndroidManifestData_queryAdvancedPattern:
+            case R.styleable.AndroidManifestData_fragment:
+            case R.styleable.AndroidManifestData_fragmentPattern:
+            case R.styleable.AndroidManifestData_fragmentPrefix:
+            case R.styleable.AndroidManifestData_fragmentSuffix:
+            case R.styleable.AndroidManifestData_fragmentAdvancedPattern:
                 return MAX_ATTR_LEN_PATH;
             default:
                 return DEFAULT_MAX_STRING_ATTR_LENGTH;
diff --git a/core/java/android/hardware/camera2/extension/CameraOutputConfig.aidl b/core/java/android/hardware/camera2/extension/CameraOutputConfig.aidl
index 7c54a9b..509bcb8 100644
--- a/core/java/android/hardware/camera2/extension/CameraOutputConfig.aidl
+++ b/core/java/android/hardware/camera2/extension/CameraOutputConfig.aidl
@@ -26,6 +26,7 @@
     Surface surface;
     int imageFormat;
     int capacity;
+    long usage;
 
     const int TYPE_SURFACE = 0;
     const int TYPE_IMAGEREADER = 1;
diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
index 98bc311..f6c8f36 100644
--- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
@@ -1182,7 +1182,8 @@
                     return null;
                 }
                 ImageReader reader = ImageReader.newInstance(output.size.width,
-                        output.size.height, output.imageFormat, output.capacity);
+                        output.size.height, output.imageFormat, output.capacity,
+                        output.usage);
                 mReaderMap.put(output.outputId.id, reader);
                 return reader.getSurface();
             case CameraOutputConfig.TYPE_MULTIRES_IMAGEREADER:
diff --git a/core/java/android/hardware/radio/RadioMetadata.java b/core/java/android/hardware/radio/RadioMetadata.java
index db14c08..da6b9c2 100644
--- a/core/java/android/hardware/radio/RadioMetadata.java
+++ b/core/java/android/hardware/radio/RadioMetadata.java
@@ -507,10 +507,16 @@
      *
      * @param key The key the value is stored under.
      * @return a bitmap identifier or 0 if it's missing.
-     * @hide This API is not thoroughly elaborated yet
+     * @throws NullPointerException if metadata key is {@code null}
+     * @throws IllegalArgumentException if the metadata with the key is not found in
+     * metadata or the key is not of bitmap-key type
      */
+    @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
     public int getBitmapId(@NonNull String key) {
-        if (!METADATA_KEY_ICON.equals(key) && !METADATA_KEY_ART.equals(key)) return 0;
+        Objects.requireNonNull(key, "Metadata key can not be null");
+        if (!METADATA_KEY_ICON.equals(key) && !METADATA_KEY_ART.equals(key)) {
+            throw new IllegalArgumentException("Failed to retrieve key " + key + " as bitmap key");
+        }
         return getInt(key);
     }
 
diff --git a/core/java/android/hardware/radio/RadioTuner.java b/core/java/android/hardware/radio/RadioTuner.java
index 9b2bcde..7c5c003 100644
--- a/core/java/android/hardware/radio/RadioTuner.java
+++ b/core/java/android/hardware/radio/RadioTuner.java
@@ -17,6 +17,7 @@
 package android.hardware.radio;
 
 import android.Manifest;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -332,29 +333,30 @@
     public abstract int getProgramInformation(RadioManager.ProgramInfo[] info);
 
     /**
-     * Retrieves a {@link Bitmap} for the given image ID or null,
+     * Retrieves a {@link Bitmap} for the given image ID or throw {@link IllegalArgumentException},
      * if the image was missing from the tuner.
      *
      * <p>This involves doing a call to the tuner, so the bitmap should be cached
      * on the application side.
      *
-     * <p>If the method returns null for non-zero ID, it means the image was
-     * updated on the tuner side. There is a race conditon between fetching
-     * image for an old ID and tuner updating the image (and cleaning up the
+     * <p>If the method throws {@link IllegalArgumentException} for non-zero ID, it
+     * means the image was updated on the tuner side. There is a race condition between
+     * fetching image for an old ID and tuner updating the image (and cleaning up the
      * old image). In such case, a new ProgramInfo with updated image id will
      * be sent with a {@link Callback#onProgramInfoChanged(RadioManager.ProgramInfo)}
      * callback.
      *
      * @param id The image identifier, retrieved with
      *           {@link RadioMetadata#getBitmapId(String)}.
-     * @return A {@link Bitmap} or null.
-     * @throws IllegalArgumentException if id==0
-     * @hide This API is not thoroughly elaborated yet
+     * @return A {@link Bitmap} for the given image ID.
+     * @throws IllegalArgumentException if id is 0 or the referenced image id no longer exists.
      */
-    @SuppressWarnings("HiddenAbstractMethod")
+    @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED)
     @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
-    public abstract @Nullable Bitmap getMetadataImage(int id);
-
+    public @NonNull Bitmap getMetadataImage(int id) {
+        throw new UnsupportedOperationException(
+                "Getting metadata image must be implemented in child classes");
+    }
     /**
      * Initiates a background scan to update internally cached program list.
      *
diff --git a/core/java/android/hardware/radio/TunerAdapter.java b/core/java/android/hardware/radio/TunerAdapter.java
index ba31ca3..63b2d4c 100644
--- a/core/java/android/hardware/radio/TunerAdapter.java
+++ b/core/java/android/hardware/radio/TunerAdapter.java
@@ -16,6 +16,7 @@
 
 package android.hardware.radio;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.graphics.Bitmap;
 import android.os.RemoteException;
@@ -251,10 +252,18 @@
     }
 
     @Override
-    @Nullable
+    @NonNull
     public Bitmap getMetadataImage(int id) {
+        if (id == 0) {
+            throw new IllegalArgumentException("Invalid metadata image id 0");
+        }
         try {
-            return mTuner.getImage(id);
+            Bitmap bitmap = mTuner.getImage(id);
+            if (bitmap == null) {
+                throw new IllegalArgumentException("Metadata image with id " + id
+                        + " is not available");
+            }
+            return bitmap;
         } catch (RemoteException e) {
             throw new RuntimeException("Service died", e);
         }
diff --git a/core/java/android/os/HandlerThread.java b/core/java/android/os/HandlerThread.java
index fcd5731..36730cb 100644
--- a/core/java/android/os/HandlerThread.java
+++ b/core/java/android/os/HandlerThread.java
@@ -35,6 +35,7 @@
     public HandlerThread(String name) {
         super(name);
         mPriority = Process.THREAD_PRIORITY_DEFAULT;
+        onCreated();
     }
     
     /**
@@ -46,8 +47,21 @@
     public HandlerThread(String name, int priority) {
         super(name);
         mPriority = priority;
+        onCreated();
     }
-    
+
+    /** @hide */
+    @android.ravenwood.annotation.RavenwoodReplace
+    protected void onCreated() {
+    }
+
+    /** @hide */
+    protected void onCreated$ravenwood() {
+        // Mark ourselves as daemon to enable tests to terminate quickly when finished, despite
+        // any HandlerThread instances that may be lingering around
+        setDaemon(true);
+    }
+
     /**
      * Call back method that can be explicitly overridden if needed to execute some
      * setup before Looper loops.
diff --git a/core/java/android/os/TestLooperManager.java b/core/java/android/os/TestLooperManager.java
index 5e7549f..4b16c1d 100644
--- a/core/java/android/os/TestLooperManager.java
+++ b/core/java/android/os/TestLooperManager.java
@@ -28,6 +28,7 @@
  * The test code may use {@link #next()} to acquire messages that have been queued to this
  * {@link Looper}/{@link MessageQueue} and then {@link #execute} to run any that desires.
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public class TestLooperManager {
 
     private static final ArraySet<Looper> sHeldLoopers = new ArraySet<>();
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 11edcaf..e238080 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -4932,6 +4932,7 @@
          *
          * @hide
          */
+        @Readable
         public static final String DEFAULT_DEVICE_FONT_SCALE = "device_font_scale";
 
         /**
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 9d19ef6..d4a5356 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -2297,6 +2297,18 @@
                     component, configurationActivity, pkg, id, enabler, zenPolicy, modified);
         }
 
+        /** Returns a deep copy of the {@link ZenRule}. */
+        public ZenRule copy() {
+            final Parcel parcel = Parcel.obtain();
+            try {
+                writeToParcel(parcel, 0);
+                parcel.setDataPosition(0);
+                return new ZenRule(parcel);
+            } finally {
+                parcel.recycle();
+            }
+        }
+
         public boolean isAutomaticActive() {
             return enabled && !snoozing && getPkg() != null && isTrueOrUnknown();
         }
diff --git a/core/java/android/view/SurfaceControlViewHost.java b/core/java/android/view/SurfaceControlViewHost.java
index 4840f00..5249fd5 100644
--- a/core/java/android/view/SurfaceControlViewHost.java
+++ b/core/java/android/view/SurfaceControlViewHost.java
@@ -538,8 +538,8 @@
     }
 
     private void addWindowToken(WindowManager.LayoutParams attrs) {
-        final WindowManagerImpl wm =
-                (WindowManagerImpl) mViewRoot.mContext.getSystemService(Context.WINDOW_SERVICE);
+        final WindowManager wm =
+                (WindowManager) mViewRoot.mContext.getSystemService(Context.WINDOW_SERVICE);
         attrs.token = wm.getDefaultToken();
     }
 
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 427d053..c788261 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -6109,4 +6109,12 @@
         throw new UnsupportedOperationException(
                 "getSurfaceControlInputClientToken is not implemented");
     }
+
+    /**
+     * @hide
+     */
+    default @NonNull IBinder getDefaultToken() {
+        throw new UnsupportedOperationException(
+                "getDefaultToken is not implemented");
+    }
 }
diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java
index 41d181c..5072ad7 100644
--- a/core/java/android/view/WindowManagerImpl.java
+++ b/core/java/android/view/WindowManagerImpl.java
@@ -458,7 +458,9 @@
         return null;
     }
 
-    IBinder getDefaultToken() {
+    @Override
+    @NonNull
+    public IBinder getDefaultToken() {
         return mDefaultToken;
     }
 
diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java
index 7782fd7..ef1bf5a 100644
--- a/core/java/android/view/accessibility/AccessibilityManager.java
+++ b/core/java/android/view/accessibility/AccessibilityManager.java
@@ -62,6 +62,7 @@
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.IWindow;
+import android.view.SurfaceControl;
 import android.view.View;
 import android.view.accessibility.AccessibilityEvent.EventType;
 
@@ -2404,4 +2405,29 @@
             throw re.rethrowFromSystemServer();
         }
     }
+
+
+    /**
+     * Attaches a {@link android.view.SurfaceControl} containing an accessibility overlay to the
+     * specified display.
+     *
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.INTERNAL_SYSTEM_WINDOW)
+    public void attachAccessibilityOverlayToDisplay(
+            int displayId, @NonNull SurfaceControl surfaceControl) {
+        final IAccessibilityManager service;
+        synchronized (mLock) {
+            service = getServiceLocked();
+            if (service == null) {
+                return;
+            }
+        }
+        try {
+            service.attachAccessibilityOverlayToDisplay_enforcePermission(
+                    displayId, surfaceControl);
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
 }
diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl
index 9c04c27..1c5d29e 100644
--- a/core/java/android/view/accessibility/IAccessibilityManager.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl
@@ -31,6 +31,7 @@
 import android.view.InputEvent;
 import android.view.IWindow;
 import android.view.MagnificationSpec;
+import android.view.SurfaceControl;
 
 /**
  * Interface implemented by the AccessibilityManagerService called by
@@ -136,4 +137,7 @@
         MagnificationSpec magnificationSpec;
     }
     WindowTransformationSpec getWindowTransformationSpec(int windowId);
+
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.INTERNAL_SYSTEM_WINDOW)")
+    void attachAccessibilityOverlayToDisplay_enforcePermission(int displayId, in SurfaceControl surfaceControl);
 }
diff --git a/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig b/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig
index 2a3008a..5d3153c 100644
--- a/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig
+++ b/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig
@@ -34,3 +34,10 @@
     description: "If true, an appop is logged when a notification is rapidly cleared by a notification listener."
     bug: "289080543"
 }
+
+flag {
+    name: "manage_device_policy_enabled"
+    namespace: "content_protection"
+    description: "If true, the APIs to manage content protection device policy will be enabled."
+    bug: "319477846"
+}
diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java
index 7a79e0f..aa60cc9 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -490,7 +490,7 @@
      * Returns true if this instance only supports reading history.
      */
     public boolean isReadOnly() {
-        return mActiveFile == null || mHistoryDir == null;
+        return !mMutable || mActiveFile == null || mHistoryDir == null;
     }
 
     /**
@@ -508,6 +508,13 @@
      * create next history file.
      */
     public void startNextFile(long elapsedRealtimeMs) {
+        synchronized (this) {
+            startNextFileLocked(elapsedRealtimeMs);
+        }
+    }
+
+    @GuardedBy("this")
+    private void startNextFileLocked(long elapsedRealtimeMs) {
         if (mMaxHistoryFiles == 0) {
             Slog.wtf(TAG, "mMaxHistoryFiles should not be zero when writing history");
             return;
@@ -548,10 +555,7 @@
         }
 
         mWrittenPowerStatsDescriptors.clear();
-
-        synchronized (this) {
-            cleanupLocked();
-        }
+        cleanupLocked();
     }
 
     @GuardedBy("this")
@@ -599,27 +603,31 @@
      * number 0 again.
      */
     public void reset() {
-        if (DEBUG) Slog.i(TAG, "********** CLEARING HISTORY!");
-        for (BatteryHistoryFile file : mHistoryFiles) {
-            file.atomicFile.delete();
+        synchronized (this) {
+            if (DEBUG) Slog.i(TAG, "********** CLEARING HISTORY!");
+            for (BatteryHistoryFile file : mHistoryFiles) {
+                file.atomicFile.delete();
+            }
+            mHistoryFiles.clear();
+
+            BatteryHistoryFile name = makeBatteryHistoryFile();
+            mHistoryFiles.add(name);
+            setActiveFile(name);
+
+            initHistoryBuffer();
         }
-        mHistoryFiles.clear();
-
-        BatteryHistoryFile name = makeBatteryHistoryFile();
-        mHistoryFiles.add(name);
-        setActiveFile(name);
-
-        initHistoryBuffer();
     }
 
     /**
      * Returns the monotonic clock time when the available battery history collection started.
      */
     public long getStartTime() {
-        if (!mHistoryFiles.isEmpty()) {
-            return mHistoryFiles.get(0).monotonicTimeMs;
-        } else {
-            return mHistoryBufferStartTime;
+        synchronized (this) {
+            if (!mHistoryFiles.isEmpty()) {
+                return mHistoryFiles.get(0).monotonicTimeMs;
+            } else {
+                return mHistoryBufferStartTime;
+            }
         }
     }
 
@@ -633,11 +641,14 @@
      */
     @NonNull
     public BatteryStatsHistoryIterator iterate(long startTimeMs, long endTimeMs) {
+        if (mMutable) {
+            return copy().iterate(startTimeMs, endTimeMs);
+        }
+
         mCurrentFileIndex = 0;
         mCurrentParcel = null;
         mCurrentParcelEnd = 0;
         mParcelIndex = 0;
-        mMutable = false;
         if (mWritableHistory != null) {
             synchronized (mWritableHistory) {
                 mWritableHistory.setCleanupEnabledLocked(false);
@@ -650,14 +661,11 @@
      * Finish iterating history files and history buffer.
      */
     void iteratorFinished() {
-        // setDataPosition so mHistoryBuffer Parcel can be written.
         mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize());
         if (mWritableHistory != null) {
             synchronized (mWritableHistory) {
                 mWritableHistory.setCleanupEnabledLocked(true);
             }
-        } else {
-            mMutable = true;
         }
     }
 
@@ -671,6 +679,8 @@
      */
     @Nullable
     public Parcel getNextParcel(long startTimeMs, long endTimeMs) {
+        checkImmutable();
+
         // First iterate through all records in current parcel.
         if (mCurrentParcel != null) {
             if (mCurrentParcel.dataPosition() < mCurrentParcelEnd) {
@@ -754,6 +764,12 @@
         return mCurrentParcel;
     }
 
+    private void checkImmutable() {
+        if (mMutable) {
+            throw new IllegalStateException("Iterating over a mutable battery history");
+        }
+    }
+
     /**
      * Read history file into a parcel.
      *
@@ -863,8 +879,10 @@
      * @param out the output parcel
      */
     public void writeToParcel(Parcel out) {
-        writeHistoryBuffer(out);
-        writeToParcel(out, false /* useBlobs */);
+        synchronized (this) {
+            writeHistoryBuffer(out);
+            writeToParcel(out, false /* useBlobs */);
+        }
     }
 
     /**
@@ -874,8 +892,10 @@
      * @param out the output parcel
      */
     public void writeToBatteryUsageStatsParcel(Parcel out) {
-        out.writeBlob(mHistoryBuffer.marshall());
-        writeToParcel(out, true /* useBlobs */);
+        synchronized (this) {
+            out.writeBlob(mHistoryBuffer.marshall());
+            writeToParcel(out, true /* useBlobs */);
+        }
     }
 
     private void writeToParcel(Parcel out, boolean useBlobs) {
@@ -1022,14 +1042,18 @@
      * Enables/disables recording of history.  When disabled, all "record*" calls are a no-op.
      */
     public void setHistoryRecordingEnabled(boolean enabled) {
-        mRecordingHistory = enabled;
+        synchronized (this) {
+            mRecordingHistory = enabled;
+        }
     }
 
     /**
      * Returns true if history recording is enabled.
      */
     public boolean isRecordingHistory() {
-        return mRecordingHistory;
+        synchronized (this) {
+            return mRecordingHistory;
+        }
     }
 
     /**
@@ -1037,8 +1061,10 @@
      */
     @VisibleForTesting
     public void forceRecordAllHistory() {
-        mHaveBatteryLevel = true;
-        mRecordingHistory = true;
+        synchronized (this) {
+            mHaveBatteryLevel = true;
+            mRecordingHistory = true;
+        }
     }
 
     /**
@@ -1046,37 +1072,43 @@
      */
     public void startRecordingHistory(final long elapsedRealtimeMs, final long uptimeMs,
             boolean reset) {
-        mRecordingHistory = true;
-        mHistoryCur.currentTime = mClock.currentTimeMillis();
-        writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur,
-                reset ? HistoryItem.CMD_RESET : HistoryItem.CMD_CURRENT_TIME);
-        mHistoryCur.currentTime = 0;
+        synchronized (this) {
+            mRecordingHistory = true;
+            mHistoryCur.currentTime = mClock.currentTimeMillis();
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur,
+                    reset ? HistoryItem.CMD_RESET : HistoryItem.CMD_CURRENT_TIME);
+            mHistoryCur.currentTime = 0;
+        }
     }
 
     /**
      * Prepares to continue recording after restoring previous history from persistent storage.
      */
     public void continueRecordingHistory() {
-        if (mHistoryBuffer.dataPosition() <= 0 && mHistoryFiles.size() <= 1) {
-            return;
-        }
+        synchronized (this) {
+            if (mHistoryBuffer.dataPosition() <= 0 && mHistoryFiles.size() <= 1) {
+                return;
+            }
 
-        mRecordingHistory = true;
-        final long elapsedRealtimeMs = mClock.elapsedRealtime();
-        final long uptimeMs = mClock.uptimeMillis();
-        writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur, HistoryItem.CMD_START);
-        startRecordingHistory(elapsedRealtimeMs, uptimeMs, false);
+            mRecordingHistory = true;
+            final long elapsedRealtimeMs = mClock.elapsedRealtime();
+            final long uptimeMs = mClock.uptimeMillis();
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur, HistoryItem.CMD_START);
+            startRecordingHistory(elapsedRealtimeMs, uptimeMs, false);
+        }
     }
 
     /**
      * Notes the current battery state to be reflected in the next written history item.
      */
     public void setBatteryState(boolean charging, int status, int level, int chargeUah) {
-        mHaveBatteryLevel = true;
-        setChargingState(charging);
-        mHistoryCur.batteryStatus = (byte) status;
-        mHistoryCur.batteryLevel = (byte) level;
-        mHistoryCur.batteryChargeUah = chargeUah;
+        synchronized (this) {
+            mHaveBatteryLevel = true;
+            setChargingState(charging);
+            mHistoryCur.batteryStatus = (byte) status;
+            mHistoryCur.batteryLevel = (byte) level;
+            mHistoryCur.batteryChargeUah = chargeUah;
+        }
     }
 
     /**
@@ -1084,24 +1116,28 @@
      */
     public void setBatteryState(int status, int level, int health, int plugType, int temperature,
             int voltageMv, int chargeUah) {
-        mHaveBatteryLevel = true;
-        mHistoryCur.batteryStatus = (byte) status;
-        mHistoryCur.batteryLevel = (byte) level;
-        mHistoryCur.batteryHealth = (byte) health;
-        mHistoryCur.batteryPlugType = (byte) plugType;
-        mHistoryCur.batteryTemperature = (short) temperature;
-        mHistoryCur.batteryVoltage = (char) voltageMv;
-        mHistoryCur.batteryChargeUah = chargeUah;
+        synchronized (this) {
+            mHaveBatteryLevel = true;
+            mHistoryCur.batteryStatus = (byte) status;
+            mHistoryCur.batteryLevel = (byte) level;
+            mHistoryCur.batteryHealth = (byte) health;
+            mHistoryCur.batteryPlugType = (byte) plugType;
+            mHistoryCur.batteryTemperature = (short) temperature;
+            mHistoryCur.batteryVoltage = (char) voltageMv;
+            mHistoryCur.batteryChargeUah = chargeUah;
+        }
     }
 
     /**
      * Notes the current power plugged-in state to be reflected in the next written history item.
      */
     public void setPluggedInState(boolean pluggedIn) {
-        if (pluggedIn) {
-            mHistoryCur.states |= HistoryItem.STATE_BATTERY_PLUGGED_FLAG;
-        } else {
-            mHistoryCur.states &= ~HistoryItem.STATE_BATTERY_PLUGGED_FLAG;
+        synchronized (this) {
+            if (pluggedIn) {
+                mHistoryCur.states |= HistoryItem.STATE_BATTERY_PLUGGED_FLAG;
+            } else {
+                mHistoryCur.states &= ~HistoryItem.STATE_BATTERY_PLUGGED_FLAG;
+            }
         }
     }
 
@@ -1109,10 +1145,12 @@
      * Notes the current battery charging state to be reflected in the next written history item.
      */
     public void setChargingState(boolean charging) {
-        if (charging) {
-            mHistoryCur.states2 |= HistoryItem.STATE2_CHARGING_FLAG;
-        } else {
-            mHistoryCur.states2 &= ~HistoryItem.STATE2_CHARGING_FLAG;
+        synchronized (this) {
+            if (charging) {
+                mHistoryCur.states2 |= HistoryItem.STATE2_CHARGING_FLAG;
+            } else {
+                mHistoryCur.states2 &= ~HistoryItem.STATE2_CHARGING_FLAG;
+            }
         }
     }
 
@@ -1121,38 +1159,44 @@
      */
     public void recordEvent(long elapsedRealtimeMs, long uptimeMs, int code, String name,
             int uid) {
-        mHistoryCur.eventCode = code;
-        mHistoryCur.eventTag = mHistoryCur.localEventTag;
-        mHistoryCur.eventTag.string = name;
-        mHistoryCur.eventTag.uid = uid;
-        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        synchronized (this) {
+            mHistoryCur.eventCode = code;
+            mHistoryCur.eventTag = mHistoryCur.localEventTag;
+            mHistoryCur.eventTag.string = name;
+            mHistoryCur.eventTag.uid = uid;
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        }
     }
 
     /**
      * Records a time change event.
      */
     public void recordCurrentTimeChange(long elapsedRealtimeMs, long uptimeMs, long currentTimeMs) {
-        if (!mRecordingHistory) {
-            return;
-        }
+        synchronized (this) {
+            if (!mRecordingHistory) {
+                return;
+            }
 
-        mHistoryCur.currentTime = currentTimeMs;
-        writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur,
-                HistoryItem.CMD_CURRENT_TIME);
-        mHistoryCur.currentTime = 0;
+            mHistoryCur.currentTime = currentTimeMs;
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur,
+                    HistoryItem.CMD_CURRENT_TIME);
+            mHistoryCur.currentTime = 0;
+        }
     }
 
     /**
      * Records a system shutdown event.
      */
     public void recordShutdownEvent(long elapsedRealtimeMs, long uptimeMs, long currentTimeMs) {
-        if (!mRecordingHistory) {
-            return;
-        }
+        synchronized (this) {
+            if (!mRecordingHistory) {
+                return;
+            }
 
-        mHistoryCur.currentTime = currentTimeMs;
-        writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur, HistoryItem.CMD_SHUTDOWN);
-        mHistoryCur.currentTime = 0;
+            mHistoryCur.currentTime = currentTimeMs;
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur, HistoryItem.CMD_SHUTDOWN);
+            mHistoryCur.currentTime = 0;
+        }
     }
 
     /**
@@ -1160,13 +1204,15 @@
      */
     public void recordBatteryState(long elapsedRealtimeMs, long uptimeMs, int batteryLevel,
             boolean isPlugged) {
-        mHistoryCur.batteryLevel = (byte) batteryLevel;
-        setPluggedInState(isPlugged);
-        if (DEBUG) {
-            Slog.v(TAG, "Battery unplugged to: "
-                    + Integer.toHexString(mHistoryCur.states));
+        synchronized (this) {
+            mHistoryCur.batteryLevel = (byte) batteryLevel;
+            setPluggedInState(isPlugged);
+            if (DEBUG) {
+                Slog.v(TAG, "Battery unplugged to: "
+                        + Integer.toHexString(mHistoryCur.states));
+            }
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs);
         }
-        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
     }
 
     /**
@@ -1174,9 +1220,11 @@
      */
     public void recordPowerStats(long elapsedRealtimeMs, long uptimeMs,
             PowerStats powerStats) {
-        mHistoryCur.powerStats = powerStats;
-        mHistoryCur.states2 |= HistoryItem.STATE2_EXTENSIONS_FLAG;
-        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        synchronized (this) {
+            mHistoryCur.powerStats = powerStats;
+            mHistoryCur.states2 |= HistoryItem.STATE2_EXTENSIONS_FLAG;
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        }
     }
 
     /**
@@ -1184,11 +1232,13 @@
      */
     public void recordProcessStateChange(long elapsedRealtimeMs, long uptimeMs,
             int uid, @BatteryConsumer.ProcessState int processState) {
-        mHistoryCur.processStateChange = mHistoryCur.localProcessStateChange;
-        mHistoryCur.processStateChange.uid = uid;
-        mHistoryCur.processStateChange.processState = processState;
-        mHistoryCur.states2 |= HistoryItem.STATE2_EXTENSIONS_FLAG;
-        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        synchronized (this) {
+            mHistoryCur.processStateChange = mHistoryCur.localProcessStateChange;
+            mHistoryCur.processStateChange.uid = uid;
+            mHistoryCur.processStateChange.processState = processState;
+            mHistoryCur.states2 |= HistoryItem.STATE2_EXTENSIONS_FLAG;
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        }
     }
 
     /**
@@ -1197,8 +1247,10 @@
      */
     public void recordWifiConsumedCharge(long elapsedRealtimeMs, long uptimeMs,
             double monitoredRailChargeMah) {
-        mHistoryCur.wifiRailChargeMah += monitoredRailChargeMah;
-        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        synchronized (this) {
+            mHistoryCur.wifiRailChargeMah += monitoredRailChargeMah;
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        }
     }
 
     /**
@@ -1206,10 +1258,12 @@
      */
     public void recordWakelockStartEvent(long elapsedRealtimeMs, long uptimeMs, String historyName,
             int uid) {
-        mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag;
-        mHistoryCur.wakelockTag.string = historyName;
-        mHistoryCur.wakelockTag.uid = uid;
-        recordStateStartEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.STATE_WAKE_LOCK_FLAG);
+        synchronized (this) {
+            mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag;
+            mHistoryCur.wakelockTag.string = historyName;
+            mHistoryCur.wakelockTag.uid = uid;
+            recordStateStartEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.STATE_WAKE_LOCK_FLAG);
+        }
     }
 
     /**
@@ -1217,18 +1271,20 @@
      */
     public boolean maybeUpdateWakelockTag(long elapsedRealtimeMs, long uptimeMs, String historyName,
             int uid) {
-        if (mHistoryLastWritten.cmd != HistoryItem.CMD_UPDATE) {
-            return false;
+        synchronized (this) {
+            if (mHistoryLastWritten.cmd != HistoryItem.CMD_UPDATE) {
+                return false;
+            }
+            if (mHistoryLastWritten.wakelockTag != null) {
+                // We'll try to update the last tag.
+                mHistoryLastWritten.wakelockTag = null;
+                mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag;
+                mHistoryCur.wakelockTag.string = historyName;
+                mHistoryCur.wakelockTag.uid = uid;
+                writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+            }
+            return true;
         }
-        if (mHistoryLastWritten.wakelockTag != null) {
-            // We'll try to update the last tag.
-            mHistoryLastWritten.wakelockTag = null;
-            mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag;
-            mHistoryCur.wakelockTag.string = historyName;
-            mHistoryCur.wakelockTag.uid = uid;
-            writeHistoryItem(elapsedRealtimeMs, uptimeMs);
-        }
-        return true;
     }
 
     /**
@@ -1236,26 +1292,32 @@
      */
     public void recordWakelockStopEvent(long elapsedRealtimeMs, long uptimeMs, String historyName,
             int uid) {
-        mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag;
-        mHistoryCur.wakelockTag.string = historyName != null ? historyName : "";
-        mHistoryCur.wakelockTag.uid = uid;
-        recordStateStopEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.STATE_WAKE_LOCK_FLAG);
+        synchronized (this) {
+            mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag;
+            mHistoryCur.wakelockTag.string = historyName != null ? historyName : "";
+            mHistoryCur.wakelockTag.uid = uid;
+            recordStateStopEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.STATE_WAKE_LOCK_FLAG);
+        }
     }
 
     /**
      * Records an event when some state flag changes to true.
      */
     public void recordStateStartEvent(long elapsedRealtimeMs, long uptimeMs, int stateFlags) {
-        mHistoryCur.states |= stateFlags;
-        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        synchronized (this) {
+            mHistoryCur.states |= stateFlags;
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        }
     }
 
     /**
      * Records an event when some state flag changes to false.
      */
     public void recordStateStopEvent(long elapsedRealtimeMs, long uptimeMs, int stateFlags) {
-        mHistoryCur.states &= ~stateFlags;
-        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        synchronized (this) {
+            mHistoryCur.states &= ~stateFlags;
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        }
     }
 
     /**
@@ -1263,34 +1325,42 @@
      */
     public void recordStateChangeEvent(long elapsedRealtimeMs, long uptimeMs, int stateStartFlags,
             int stateStopFlags) {
-        mHistoryCur.states = (mHistoryCur.states | stateStartFlags) & ~stateStopFlags;
-        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        synchronized (this) {
+            mHistoryCur.states = (mHistoryCur.states | stateStartFlags) & ~stateStopFlags;
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        }
     }
 
     /**
      * Records an event when some state2 flag changes to true.
      */
     public void recordState2StartEvent(long elapsedRealtimeMs, long uptimeMs, int stateFlags) {
-        mHistoryCur.states2 |= stateFlags;
-        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        synchronized (this) {
+            mHistoryCur.states2 |= stateFlags;
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        }
     }
 
     /**
      * Records an event when some state2 flag changes to false.
      */
     public void recordState2StopEvent(long elapsedRealtimeMs, long uptimeMs, int stateFlags) {
-        mHistoryCur.states2 &= ~stateFlags;
-        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        synchronized (this) {
+            mHistoryCur.states2 &= ~stateFlags;
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        }
     }
 
     /**
      * Records an wakeup event.
      */
     public void recordWakeupEvent(long elapsedRealtimeMs, long uptimeMs, String reason) {
-        mHistoryCur.wakeReasonTag = mHistoryCur.localWakeReasonTag;
-        mHistoryCur.wakeReasonTag.string = reason;
-        mHistoryCur.wakeReasonTag.uid = 0;
-        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        synchronized (this) {
+            mHistoryCur.wakeReasonTag = mHistoryCur.localWakeReasonTag;
+            mHistoryCur.wakeReasonTag.string = reason;
+            mHistoryCur.wakeReasonTag.uid = 0;
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        }
     }
 
     /**
@@ -1298,10 +1368,12 @@
      */
     public void recordScreenBrightnessEvent(long elapsedRealtimeMs, long uptimeMs,
             int brightnessBin) {
-        mHistoryCur.states = setBitField(mHistoryCur.states, brightnessBin,
-                HistoryItem.STATE_BRIGHTNESS_SHIFT,
-                HistoryItem.STATE_BRIGHTNESS_MASK);
-        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        synchronized (this) {
+            mHistoryCur.states = setBitField(mHistoryCur.states, brightnessBin,
+                    HistoryItem.STATE_BRIGHTNESS_SHIFT,
+                    HistoryItem.STATE_BRIGHTNESS_MASK);
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        }
     }
 
     /**
@@ -1309,20 +1381,24 @@
      */
     public void recordGpsSignalQualityEvent(long elapsedRealtimeMs, long uptimeMs,
             int signalLevel) {
-        mHistoryCur.states2 = setBitField(mHistoryCur.states2, signalLevel,
-                HistoryItem.STATE2_GPS_SIGNAL_QUALITY_SHIFT,
-                HistoryItem.STATE2_GPS_SIGNAL_QUALITY_MASK);
-        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        synchronized (this) {
+            mHistoryCur.states2 = setBitField(mHistoryCur.states2, signalLevel,
+                    HistoryItem.STATE2_GPS_SIGNAL_QUALITY_SHIFT,
+                    HistoryItem.STATE2_GPS_SIGNAL_QUALITY_MASK);
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        }
     }
 
     /**
      * Records a device idle mode change event.
      */
     public void recordDeviceIdleEvent(long elapsedRealtimeMs, long uptimeMs, int mode) {
-        mHistoryCur.states2 = setBitField(mHistoryCur.states2, mode,
-                HistoryItem.STATE2_DEVICE_IDLE_SHIFT,
-                HistoryItem.STATE2_DEVICE_IDLE_MASK);
-        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        synchronized (this) {
+            mHistoryCur.states2 = setBitField(mHistoryCur.states2, mode,
+                    HistoryItem.STATE2_DEVICE_IDLE_SHIFT,
+                    HistoryItem.STATE2_DEVICE_IDLE_MASK);
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        }
     }
 
     /**
@@ -1330,20 +1406,22 @@
      */
     public void recordPhoneStateChangeEvent(long elapsedRealtimeMs, long uptimeMs, int addStateFlag,
             int removeStateFlag, int state, int signalStrength) {
-        mHistoryCur.states = (mHistoryCur.states | addStateFlag) & ~removeStateFlag;
-        if (state != -1) {
-            mHistoryCur.states =
-                    setBitField(mHistoryCur.states, state,
-                            HistoryItem.STATE_PHONE_STATE_SHIFT,
-                            HistoryItem.STATE_PHONE_STATE_MASK);
+        synchronized (this) {
+            mHistoryCur.states = (mHistoryCur.states | addStateFlag) & ~removeStateFlag;
+            if (state != -1) {
+                mHistoryCur.states =
+                        setBitField(mHistoryCur.states, state,
+                                HistoryItem.STATE_PHONE_STATE_SHIFT,
+                                HistoryItem.STATE_PHONE_STATE_MASK);
+            }
+            if (signalStrength != -1) {
+                mHistoryCur.states =
+                        setBitField(mHistoryCur.states, signalStrength,
+                                HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_SHIFT,
+                                HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_MASK);
+            }
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs);
         }
-        if (signalStrength != -1) {
-            mHistoryCur.states =
-                    setBitField(mHistoryCur.states, signalStrength,
-                            HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_SHIFT,
-                            HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_MASK);
-        }
-        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
     }
 
     /**
@@ -1351,10 +1429,12 @@
      */
     public void recordDataConnectionTypeChangeEvent(long elapsedRealtimeMs, long uptimeMs,
             int dataConnectionType) {
-        mHistoryCur.states = setBitField(mHistoryCur.states, dataConnectionType,
-                HistoryItem.STATE_DATA_CONNECTION_SHIFT,
-                HistoryItem.STATE_DATA_CONNECTION_MASK);
-        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        synchronized (this) {
+            mHistoryCur.states = setBitField(mHistoryCur.states, dataConnectionType,
+                    HistoryItem.STATE_DATA_CONNECTION_SHIFT,
+                    HistoryItem.STATE_DATA_CONNECTION_MASK);
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        }
     }
 
     /**
@@ -1362,10 +1442,12 @@
      */
     public void recordNrStateChangeEvent(long elapsedRealtimeMs, long uptimeMs,
             int nrState) {
-        mHistoryCur.states2 = setBitField(mHistoryCur.states2, nrState,
-                HistoryItem.STATE2_NR_STATE_SHIFT,
-                HistoryItem.STATE2_NR_STATE_MASK);
-        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        synchronized (this) {
+            mHistoryCur.states2 = setBitField(mHistoryCur.states2, nrState,
+                    HistoryItem.STATE2_NR_STATE_SHIFT,
+                    HistoryItem.STATE2_NR_STATE_MASK);
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        }
     }
 
     /**
@@ -1373,11 +1455,13 @@
      */
     public void recordWifiSupplicantStateChangeEvent(long elapsedRealtimeMs, long uptimeMs,
             int supplState) {
-        mHistoryCur.states2 =
-                setBitField(mHistoryCur.states2, supplState,
-                        HistoryItem.STATE2_WIFI_SUPPL_STATE_SHIFT,
-                        HistoryItem.STATE2_WIFI_SUPPL_STATE_MASK);
-        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        synchronized (this) {
+            mHistoryCur.states2 =
+                    setBitField(mHistoryCur.states2, supplState,
+                            HistoryItem.STATE2_WIFI_SUPPL_STATE_SHIFT,
+                            HistoryItem.STATE2_WIFI_SUPPL_STATE_MASK);
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        }
     }
 
     /**
@@ -1385,11 +1469,13 @@
      */
     public void recordWifiSignalStrengthChangeEvent(long elapsedRealtimeMs, long uptimeMs,
             int strengthBin) {
-        mHistoryCur.states2 =
-                setBitField(mHistoryCur.states2, strengthBin,
-                        HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_SHIFT,
-                        HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_MASK);
-        writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        synchronized (this) {
+            mHistoryCur.states2 =
+                    setBitField(mHistoryCur.states2, strengthBin,
+                            HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_SHIFT,
+                            HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_MASK);
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        }
     }
 
     /**
@@ -1446,25 +1532,30 @@
      * Writes the current history item to history.
      */
     public void writeHistoryItem(long elapsedRealtimeMs, long uptimeMs) {
-        if (mTrackRunningHistoryElapsedRealtimeMs != 0) {
-            final long diffElapsedMs = elapsedRealtimeMs - mTrackRunningHistoryElapsedRealtimeMs;
-            final long diffUptimeMs = uptimeMs - mTrackRunningHistoryUptimeMs;
-            if (diffUptimeMs < (diffElapsedMs - 20)) {
-                final long wakeElapsedTimeMs = elapsedRealtimeMs - (diffElapsedMs - diffUptimeMs);
-                mHistoryAddTmp.setTo(mHistoryLastWritten);
-                mHistoryAddTmp.wakelockTag = null;
-                mHistoryAddTmp.wakeReasonTag = null;
-                mHistoryAddTmp.eventCode = HistoryItem.EVENT_NONE;
-                mHistoryAddTmp.states &= ~HistoryItem.STATE_CPU_RUNNING_FLAG;
-                writeHistoryItem(wakeElapsedTimeMs, uptimeMs, mHistoryAddTmp);
+        synchronized (this) {
+            if (mTrackRunningHistoryElapsedRealtimeMs != 0) {
+                final long diffElapsedMs =
+                        elapsedRealtimeMs - mTrackRunningHistoryElapsedRealtimeMs;
+                final long diffUptimeMs = uptimeMs - mTrackRunningHistoryUptimeMs;
+                if (diffUptimeMs < (diffElapsedMs - 20)) {
+                    final long wakeElapsedTimeMs =
+                            elapsedRealtimeMs - (diffElapsedMs - diffUptimeMs);
+                    mHistoryAddTmp.setTo(mHistoryLastWritten);
+                    mHistoryAddTmp.wakelockTag = null;
+                    mHistoryAddTmp.wakeReasonTag = null;
+                    mHistoryAddTmp.eventCode = HistoryItem.EVENT_NONE;
+                    mHistoryAddTmp.states &= ~HistoryItem.STATE_CPU_RUNNING_FLAG;
+                    writeHistoryItem(wakeElapsedTimeMs, uptimeMs, mHistoryAddTmp);
+                }
             }
+            mHistoryCur.states |= HistoryItem.STATE_CPU_RUNNING_FLAG;
+            mTrackRunningHistoryElapsedRealtimeMs = elapsedRealtimeMs;
+            mTrackRunningHistoryUptimeMs = uptimeMs;
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur);
         }
-        mHistoryCur.states |= HistoryItem.STATE_CPU_RUNNING_FLAG;
-        mTrackRunningHistoryElapsedRealtimeMs = elapsedRealtimeMs;
-        mTrackRunningHistoryUptimeMs = uptimeMs;
-        writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur);
     }
 
+    @GuardedBy("this")
     private void writeHistoryItem(long elapsedRealtimeMs, long uptimeMs, HistoryItem cur) {
         if (mTracer != null && mTracer.tracingEnabled()) {
             recordTraceEvents(cur.eventCode, cur.eventTag);
@@ -1591,6 +1682,7 @@
         writeHistoryItem(elapsedRealtimeMs, uptimeMs, cur, HistoryItem.CMD_UPDATE);
     }
 
+    @GuardedBy("this")
     private void writeHistoryItem(long elapsedRealtimeMs,
             @SuppressWarnings("UnusedVariable") long uptimeMs, HistoryItem cur, byte cmd) {
         if (!mMutable) {
@@ -1701,7 +1793,8 @@
     /**
      * Writes the delta between the previous and current history items into history buffer.
      */
-    public void writeHistoryDelta(Parcel dest, HistoryItem cur, HistoryItem last) {
+    @GuardedBy("this")
+    private void writeHistoryDelta(Parcel dest, HistoryItem cur, HistoryItem last) {
         if (last == null || cur.cmd != HistoryItem.CMD_UPDATE) {
             dest.writeInt(BatteryStatsHistory.DELTA_TIME_ABS);
             cur.writeToParcel(dest, 0);
@@ -1921,6 +2014,7 @@
      * while writing the current history buffer, the method returns
      * <code>(index | TAG_FIRST_OCCURRENCE_FLAG)</code>
      */
+    @GuardedBy("this")
     private int writeHistoryTag(HistoryTag tag) {
         if (tag.string == null) {
             Slog.wtfStack(TAG, "writeHistoryTag called with null name");
@@ -1964,33 +2058,37 @@
      * Don't allow any more batching in to the current history event.
      */
     public void commitCurrentHistoryBatchLocked() {
-        mHistoryLastWritten.cmd = HistoryItem.CMD_NULL;
+        synchronized (this) {
+            mHistoryLastWritten.cmd = HistoryItem.CMD_NULL;
+        }
     }
 
     /**
      * Saves the accumulated history buffer in the active file, see {@link #getActiveFile()} .
      */
     public void writeHistory() {
-        if (isReadOnly()) {
-            Slog.w(TAG, "writeHistory: this instance instance is read-only");
-            return;
-        }
-
-        // Save the monotonic time first, so that even if the history write below fails,
-        // we still wouldn't end up with overlapping history timelines.
-        mMonotonicClock.write();
-
-        Parcel p = Parcel.obtain();
-        try {
-            final long start = SystemClock.uptimeMillis();
-            writeHistoryBuffer(p);
-            if (DEBUG) {
-                Slog.d(TAG, "writeHistoryBuffer duration ms:"
-                        + (SystemClock.uptimeMillis() - start) + " bytes:" + p.dataSize());
+        synchronized (this) {
+            if (isReadOnly()) {
+                Slog.w(TAG, "writeHistory: this instance instance is read-only");
+                return;
             }
-            writeParcelToFileLocked(p, mActiveFile);
-        } finally {
-            p.recycle();
+
+            // Save the monotonic time first, so that even if the history write below fails,
+            // we still wouldn't end up with overlapping history timelines.
+            mMonotonicClock.write();
+
+            Parcel p = Parcel.obtain();
+            try {
+                final long start = SystemClock.uptimeMillis();
+                writeHistoryBuffer(p);
+                if (DEBUG) {
+                    Slog.d(TAG, "writeHistoryBuffer duration ms:"
+                            + (SystemClock.uptimeMillis() - start) + " bytes:" + p.dataSize());
+                }
+                writeParcelToFileLocked(p, mActiveFile);
+            } finally {
+                p.recycle();
+            }
         }
     }
 
@@ -1998,35 +2096,38 @@
      * Reads history buffer from a persisted Parcel.
      */
     public void readHistoryBuffer(Parcel in) throws ParcelFormatException {
-        final int version = in.readInt();
-        if (version != BatteryStatsHistory.VERSION) {
-            Slog.w("BatteryStats", "readHistoryBuffer: version got " + version
-                    + ", expected " + BatteryStatsHistory.VERSION + "; erasing old stats");
-            return;
-        }
-
-        mHistoryBufferStartTime = in.readLong();
-        mHistoryBuffer.setDataSize(0);
-        mHistoryBuffer.setDataPosition(0);
-
-        int bufSize = in.readInt();
-        int curPos = in.dataPosition();
-        if (bufSize >= (mMaxHistoryBufferSize * 100)) {
-            throw new ParcelFormatException(
-                    "File corrupt: history data buffer too large " + bufSize);
-        } else if ((bufSize & ~3) != bufSize) {
-            throw new ParcelFormatException(
-                    "File corrupt: history data buffer not aligned " + bufSize);
-        } else {
-            if (DEBUG) {
-                Slog.i(TAG, "***************** READING NEW HISTORY: " + bufSize
-                        + " bytes at " + curPos);
+        synchronized (this) {
+            final int version = in.readInt();
+            if (version != BatteryStatsHistory.VERSION) {
+                Slog.w("BatteryStats", "readHistoryBuffer: version got " + version
+                        + ", expected " + BatteryStatsHistory.VERSION + "; erasing old stats");
+                return;
             }
-            mHistoryBuffer.appendFrom(in, curPos, bufSize);
-            in.setDataPosition(curPos + bufSize);
+
+            mHistoryBufferStartTime = in.readLong();
+            mHistoryBuffer.setDataSize(0);
+            mHistoryBuffer.setDataPosition(0);
+
+            int bufSize = in.readInt();
+            int curPos = in.dataPosition();
+            if (bufSize >= (mMaxHistoryBufferSize * 100)) {
+                throw new ParcelFormatException(
+                        "File corrupt: history data buffer too large " + bufSize);
+            } else if ((bufSize & ~3) != bufSize) {
+                throw new ParcelFormatException(
+                        "File corrupt: history data buffer not aligned " + bufSize);
+            } else {
+                if (DEBUG) {
+                    Slog.i(TAG, "***************** READING NEW HISTORY: " + bufSize
+                            + " bytes at " + curPos);
+                }
+                mHistoryBuffer.appendFrom(in, curPos, bufSize);
+                in.setDataPosition(curPos + bufSize);
+            }
         }
     }
 
+    @GuardedBy("this")
     private void writeHistoryBuffer(Parcel out) {
         out.writeInt(BatteryStatsHistory.VERSION);
         out.writeLong(mHistoryBufferStartTime);
@@ -2038,6 +2139,7 @@
         out.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize());
     }
 
+    @GuardedBy("this")
     private void writeParcelToFileLocked(Parcel p, AtomicFile file) {
         FileOutputStream fos = null;
         mWriteLock.lock();
@@ -2066,34 +2168,43 @@
      * Returns the total number of history tags in the tag pool.
      */
     public int getHistoryStringPoolSize() {
-        return mHistoryTagPool.size();
+        synchronized (this) {
+            return mHistoryTagPool.size();
+        }
     }
 
     /**
      * Returns the total number of bytes occupied by the history tag pool.
      */
     public int getHistoryStringPoolBytes() {
-        return mNumHistoryTagChars;
+        synchronized (this) {
+            return mNumHistoryTagChars;
+        }
     }
 
     /**
      * Returns the string held by the requested history tag.
      */
     public String getHistoryTagPoolString(int index) {
-        ensureHistoryTagArray();
-        HistoryTag historyTag = mHistoryTags.get(index);
-        return historyTag != null ? historyTag.string : null;
+        synchronized (this) {
+            ensureHistoryTagArray();
+            HistoryTag historyTag = mHistoryTags.get(index);
+            return historyTag != null ? historyTag.string : null;
+        }
     }
 
     /**
      * Returns the UID held by the requested history tag.
      */
     public int getHistoryTagPoolUid(int index) {
-        ensureHistoryTagArray();
-        HistoryTag historyTag = mHistoryTags.get(index);
-        return historyTag != null ? historyTag.uid : Process.INVALID_UID;
+        synchronized (this) {
+            ensureHistoryTagArray();
+            HistoryTag historyTag = mHistoryTags.get(index);
+            return historyTag != null ? historyTag.uid : Process.INVALID_UID;
+        }
     }
 
+    @GuardedBy("this")
     private void ensureHistoryTagArray() {
         if (mHistoryTags != null) {
             return;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index c4b5d81..0171f58 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3751,6 +3751,13 @@
     <permission android:name="android.permission.MANAGE_DEVICE_POLICY_DEVICE_IDENTIFIERS"
                 android:protectionLevel="internal|role" />
 
+    <!-- Allows an application to manage policy related to content protection.
+        <p>Protection level: internal|role
+        @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled")
+    -->
+    <permission android:name="android.permission.MANAGE_DEVICE_POLICY_CONTENT_PROTECTION"
+                android:protectionLevel="internal|role" />
+
     <!-- Allows an application to set device policies outside the current user
         that are critical for securing data within the current user.
         <p>Holding this permission allows the use of other held MANAGE_DEVICE_POLICY_*
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 23c78fd..ebb0d34 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -6908,4 +6908,7 @@
 
     <!-- Defines the minimum interval (in ms) between two input-based user-activity poke events. -->
     <integer name="config_minMillisBetweenInputUserActivityEvents">100</integer>
+
+    <!-- Name of the starting activity for DisplayCompat host. specific to automotive.-->
+    <string name="config_defaultDisplayCompatHostActivity" translatable="false"></string>
 </resources>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index bd3a5e4..558bae7 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5307,6 +5307,12 @@
     <!-- Content description of the expand button icon in the notification when expanded.-->
     <string name="expand_button_content_description_expanded">Collapse</string>
 
+    <!-- A11y announcement when a view is collapsed. -->
+    <string name="content_description_collapsed">Collapsed</string>
+
+    <!-- A11y announcement when a view is expanded. -->
+    <string name="content_description_expanded">Expanded</string>
+
     <!-- Accessibility action description on the expand button. -->
     <string name="expand_action_accessibility">toggle expansion</string>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 3c1a42f..dc2f74b 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3826,6 +3826,9 @@
   <java-symbol type="string" name="expand_button_content_description_collapsed" />
   <java-symbol type="string" name="expand_button_content_description_expanded" />
 
+  <java-symbol type="string" name="content_description_collapsed" />
+  <java-symbol type="string" name="content_description_expanded" />
+
   <!-- Colon separated list of package names that should be granted Notification Listener access -->
   <java-symbol type="string" name="config_defaultListenerAccessPackages" />
 
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/DefaultRadioTunerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/DefaultRadioTunerTest.java
index 63de759..ddf3615 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/DefaultRadioTunerTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/DefaultRadioTunerTest.java
@@ -20,8 +20,6 @@
 
 import static org.junit.Assert.assertThrows;
 
-import android.graphics.Bitmap;
-
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
@@ -88,12 +86,6 @@
             return 0;
         }
 
-        @Nullable
-        @Override
-        public Bitmap getMetadataImage(int id) {
-            return null;
-        }
-
         @Override
         public boolean startBackgroundScan() {
             return false;
@@ -138,6 +130,16 @@
     }
 
     @Test
+    public void getMetadataImage_forRadioTuner_throwsException() {
+        UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class,
+                () -> DEFAULT_RADIO_TUNER.getMetadataImage(/* id= */ 1));
+
+        assertWithMessage("Exception for getting metadata image from default radio tuner")
+                .that(thrown).hasMessageThat()
+                .contains("Getting metadata image must be implemented in child classes");
+    }
+
+    @Test
     public void getDynamicProgramList_forRadioTuner_returnsNull() {
         assertWithMessage("Dynamic program list obtained from default radio tuner")
                 .that(DEFAULT_RADIO_TUNER.getDynamicProgramList(new ProgramList.Filter())).isNull();
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java
index fddfd39..b3a0aba 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java
@@ -290,13 +290,29 @@
     }
 
     @Test
-    public void getBitmapId_withIllegalKey() {
+    public void getBitmapId_withIllegalKey_fails() {
         String illegalKey = RadioMetadata.METADATA_KEY_ARTIST;
         RadioMetadata metadata = mBuilder.putInt(RadioMetadata.METADATA_KEY_ART, INT_KEY_VALUE)
                 .build();
 
-        mExpect.withMessage("Bitmap id value with non-bitmap-id-type key %s", illegalKey)
-                .that(metadata.getBitmapId(illegalKey)).isEqualTo(0);
+        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
+            metadata.getBitmapId(illegalKey);
+        });
+
+        mExpect.withMessage("Exception for getting string array for non-bitmap-id type key %s",
+                illegalKey).that(thrown).hasMessageThat().contains("bitmap key");
+    }
+
+    @Test
+    public void getBitmapId_withNullKey_fails() {
+        RadioMetadata metadata = mBuilder.build();
+
+        NullPointerException thrown = assertThrows(NullPointerException.class, () -> {
+            metadata.getBitmapId(/* key= */ null);
+        });
+
+        mExpect.withMessage("Exception for getting bitmap identifier with null key")
+                .that(thrown).hasMessageThat().contains("can not be null");
     }
 
     @Test
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java
index 4cda26d..5aace81 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java
@@ -514,6 +514,26 @@
     }
 
     @Test
+    public void getMetadataImage_withImageIdUnavailable_fails() throws Exception {
+        int nonExistImageId = 2;
+        when(mTunerMock.getImage(nonExistImageId)).thenReturn(null);
+        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
+                () -> mRadioTuner.getMetadataImage(nonExistImageId));
+
+        assertWithMessage("Exception for getting metadata image with non-existing id")
+                .that(thrown).hasMessageThat().contains("is not available");
+    }
+
+    @Test
+    public void getMetadataImage_withInvalidId_fails() {
+        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
+                () -> mRadioTuner.getMetadataImage(/* id= */ 0));
+
+        assertWithMessage("Exception for getting metadata image for id 0").that(thrown)
+                .hasMessageThat().contains("Invalid metadata image id 0");
+    }
+
+    @Test
     public void getMetadataImage_whenServiceDied_fails() throws Exception {
         when(mTunerMock.getImage(anyInt())).thenThrow(new RemoteException());
 
diff --git a/core/tests/coretests/src/android/os/BundleTest.java b/core/tests/coretests/src/android/os/BundleTest.java
index e7b5dff6..93c2e0e 100644
--- a/core/tests/coretests/src/android/os/BundleTest.java
+++ b/core/tests/coretests/src/android/os/BundleTest.java
@@ -20,6 +20,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
@@ -121,6 +122,14 @@
     }
 
     @Test
+    public void testEmpty() throws Exception {
+        assertNotNull(Bundle.EMPTY);
+        assertEquals(0, Bundle.EMPTY.size());
+
+        new Bundle(Bundle.EMPTY);
+    }
+
+    @Test
     @IgnoreUnderRavenwood(blockedBy = ParcelFileDescriptor.class)
     public void testCreateFromParcel() throws Exception {
         boolean withFd;
diff --git a/core/tests/coretests/src/android/os/TestLooperManagerTest.java b/core/tests/coretests/src/android/os/TestLooperManagerTest.java
new file mode 100644
index 0000000..5959444
--- /dev/null
+++ b/core/tests/coretests/src/android/os/TestLooperManagerTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class TestLooperManagerTest {
+    private static final String TAG = "TestLooperManagerTest";
+
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
+    @Test
+    public void testMainThread() throws Exception {
+        doTest(Looper.getMainLooper());
+    }
+
+    @Test
+    public void testCustomThread() throws Exception {
+        final HandlerThread thread = new HandlerThread(TAG);
+        thread.start();
+        doTest(thread.getLooper());
+    }
+
+    private void doTest(Looper looper) throws Exception {
+        final TestLooperManager tlm =
+                InstrumentationRegistry.getInstrumentation().acquireLooperManager(looper);
+
+        final Handler handler = new Handler(looper);
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        assertFalse(tlm.hasMessages(handler, null, 42));
+
+        handler.sendEmptyMessage(42);
+        handler.post(() -> {
+            latch.countDown();
+        });
+        assertTrue(tlm.hasMessages(handler, null, 42));
+        assertFalse(latch.await(100, TimeUnit.MILLISECONDS));
+
+        final Message first = tlm.next();
+        assertEquals(42, first.what);
+        assertNull(first.callback);
+        tlm.execute(first);
+        assertFalse(tlm.hasMessages(handler, null, 42));
+        assertFalse(latch.await(100, TimeUnit.MILLISECONDS));
+        tlm.recycle(first);
+
+        final Message second = tlm.next();
+        assertNotNull(second.callback);
+        tlm.execute(second);
+        assertFalse(tlm.hasMessages(handler, null, 42));
+        assertTrue(latch.await(100, TimeUnit.MILLISECONDS));
+        tlm.recycle(second);
+
+        tlm.release();
+    }
+}
diff --git a/data/fonts/font_fallback.xml b/data/fonts/font_fallback.xml
index 02e032b..1bd182f 100644
--- a/data/fonts/font_fallback.xml
+++ b/data/fonts/font_fallback.xml
@@ -730,6 +730,16 @@
               postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc
         </font>
     </family>
+    <family lang="ja">
+        <font weight="400" style="normal">
+            NotoSerifHentaigana-EL.ttf
+            <axis tag="wght" stylevalue="400"/>
+        </font>
+        <font weight="700" style="normal">
+            NotoSerifHentaigana-EL.ttf
+          <axis tag="wght" stylevalue="700"/>
+        </font>
+    </family>
     <family lang="ko">
         <font weight="400" style="normal" index="1" postScriptName="NotoSansCJKjp-Regular">
             NotoSansCJK-Regular.ttc
diff --git a/data/fonts/font_fallback_cjkvf.xml b/data/fonts/font_fallback_cjkvf.xml
index 70474ba..75bc74e 100644
--- a/data/fonts/font_fallback_cjkvf.xml
+++ b/data/fonts/font_fallback_cjkvf.xml
@@ -829,6 +829,16 @@
               postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc
         </font>
     </family>
+    <family lang="ja">
+        <font weight="400" style="normal">
+            NotoSerifHentaigana-EL.ttf
+            <axis tag="wght" stylevalue="400"/>
+        </font>
+        <font weight="700" style="normal">
+            NotoSerifHentaigana-EL.ttf
+            <axis tag="wght" stylevalue="700"/>
+        </font>
+    </family>
     <family lang="ko">
         <font weight="100" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin">
             NotoSansCJK-Regular.ttc
diff --git a/data/fonts/fonts.xml b/data/fonts/fonts.xml
index 9320c14..b23f005 100644
--- a/data/fonts/fonts.xml
+++ b/data/fonts/fonts.xml
@@ -1432,6 +1432,16 @@
               postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc
         </font>
     </family>
+    <family lang="ja">
+        <font weight="400" style="normal">
+            NotoSerifHentaigana-EL.ttf
+            <axis tag="wght" stylevalue="400"/>
+        </font>
+        <font weight="700" style="normal">
+            NotoSerifHentaigana-EL.ttf
+            <axis tag="wght" stylevalue="700"/>
+        </font>
+    </family>
     <family lang="ko">
         <font weight="400" style="normal" index="1" postScriptName="NotoSansCJKjp-Regular">
             NotoSansCJK-Regular.ttc
diff --git a/data/fonts/fonts_cjkvf.xml b/data/fonts/fonts_cjkvf.xml
index 8d91edd..1ab71ae 100644
--- a/data/fonts/fonts_cjkvf.xml
+++ b/data/fonts/fonts_cjkvf.xml
@@ -1531,6 +1531,16 @@
               postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc
         </font>
     </family>
+    <family lang="ja">
+        <font weight="400" style="normal">
+            NotoSerifHentaigana-EL.ttf
+            <axis tag="wght" stylevalue="400"/>
+        </font>
+        <font weight="700" style="normal">
+            NotoSerifHentaigana-EL.ttf
+            <axis tag="wght" stylevalue="700"/>
+        </font>
+    </family>
     <family lang="ko">
         <font weight="100" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin">
             NotoSansCJK-Regular.ttc
diff --git a/keystore/java/android/security/AndroidProtectedConfirmation.java b/keystore/java/android/security/AndroidProtectedConfirmation.java
index dfe485a..268e0a5 100644
--- a/keystore/java/android/security/AndroidProtectedConfirmation.java
+++ b/keystore/java/android/security/AndroidProtectedConfirmation.java
@@ -59,6 +59,10 @@
 
     /**
      * Requests keystore call into the confirmationui HAL to display a prompt.
+     * @deprecated Android Protected Confirmation had a low adoption rate among Android device
+     *             makers and developers alike. Given the lack of devices supporting the
+     *             feature, it is deprecated. Developers can use auth-bound Keystore keys
+     *             as a partial replacement.
      *
      * @param listener the binder to use for callbacks.
      * @param promptText the prompt to display.
@@ -68,6 +72,7 @@
      * @return one of the {@code CONFIRMATIONUI_*} constants, for
      * example {@code KeyStore.CONFIRMATIONUI_OK}.
      */
+    @Deprecated
     public int presentConfirmationPrompt(IConfirmationCallback listener, String promptText,
                                          byte[] extraData, String locale, int uiOptionsAsFlags) {
         try {
@@ -84,11 +89,16 @@
 
     /**
      * Requests keystore call into the confirmationui HAL to cancel displaying a prompt.
+     * @deprecated Android Protected Confirmation had a low adoption rate among Android device
+     *             makers and developers alike. Given the lack of devices supporting the
+     *             feature, it is deprecated. Developers can use auth-bound Keystore keys
+     *             as a partial replacement.
      *
      * @param listener the binder passed to the {@link #presentConfirmationPrompt} method.
      * @return one of the {@code CONFIRMATIONUI_*} constants, for
      * example {@code KeyStore.CONFIRMATIONUI_OK}.
      */
+    @Deprecated
     public int cancelConfirmationPrompt(IConfirmationCallback listener) {
         try {
             getService().cancelPrompt(listener);
@@ -103,9 +113,14 @@
 
     /**
      * Requests keystore to check if the confirmationui HAL is available.
+     * @deprecated Android Protected Confirmation had a low adoption rate among Android device
+     *             makers and developers alike. Given the lack of devices supporting the
+     *             feature, it is deprecated. Developers can use auth-bound Keystore keys
+     *             as a partial replacement.
      *
      * @return whether the confirmationUI HAL is available.
      */
+    @Deprecated
     public boolean isConfirmationPromptSupported() {
         try {
             return getService().isSupported();
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
index 9c05a3a..83ddfc5 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
@@ -109,13 +109,29 @@
         }
     }
 
+    // For curve 25519, KeyMint uses the KM_ALGORITHM_EC constant, but in the Java layer we need
+    // to distinguish between Curve 25519 and other EC algorithms, so we use a different constant
+    // with a value that is outside the range of the enum used for KeyMint algorithms.
+    private static final int ALGORITHM_XDH = KeymasterDefs.KM_ALGORITHM_EC + 1200;
+    private static final int ALGORITHM_ED25519 = ALGORITHM_XDH + 1;
+
     /**
-     * XDH represents Curve 25519 providers.
+     * XDH represents Curve 25519 agreement key provider.
      */
     public static class XDH extends AndroidKeyStoreKeyPairGeneratorSpi {
         // XDH is treated as EC.
         public XDH() {
-            super(KeymasterDefs.KM_ALGORITHM_EC);
+            super(ALGORITHM_XDH);
+        }
+    }
+
+    /**
+     * ED25519 represents Curve 25519 signing key provider.
+     */
+    public static class ED25519 extends AndroidKeyStoreKeyPairGeneratorSpi {
+        // ED25519 is treated as EC.
+        public ED25519() {
+            super(ALGORITHM_ED25519);
         }
     }
 
@@ -241,7 +257,9 @@
 
             KeyGenParameterSpec spec;
             boolean encryptionAtRestRequired = false;
-            int keymasterAlgorithm = mOriginalKeymasterAlgorithm;
+            int keymasterAlgorithm = (mOriginalKeymasterAlgorithm == ALGORITHM_XDH
+                    || mOriginalKeymasterAlgorithm == ALGORITHM_ED25519)
+                    ? KeymasterDefs.KM_ALGORITHM_EC : mOriginalKeymasterAlgorithm;
             if (params instanceof KeyGenParameterSpec) {
                 spec = (KeyGenParameterSpec) params;
             } else if (params instanceof KeyPairGeneratorSpec) {
@@ -610,6 +628,15 @@
                 if (algSpecificSpec instanceof ECGenParameterSpec) {
                     ECGenParameterSpec ecSpec = (ECGenParameterSpec) algSpecificSpec;
                     mEcCurveName = ecSpec.getName();
+                    if (mOriginalKeymasterAlgorithm == ALGORITHM_XDH
+                            && !mEcCurveName.equalsIgnoreCase("x25519")) {
+                        throw new InvalidAlgorithmParameterException("XDH algorithm only supports"
+                                + " x25519 curve.");
+                    } else if (mOriginalKeymasterAlgorithm == ALGORITHM_ED25519
+                            && !mEcCurveName.equalsIgnoreCase("ed25519")) {
+                        throw new InvalidAlgorithmParameterException("Ed25519 algorithm only"
+                                + " supports ed25519 curve.");
+                    }
                     final Integer ecSpecKeySizeBits = SUPPORTED_EC_CURVE_NAME_TO_SIZE.get(
                             mEcCurveName.toLowerCase(Locale.US));
                     if (ecSpecKeySizeBits == null) {
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java b/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java
index 11278e8..d204f13 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java
@@ -86,11 +86,14 @@
         put("KeyPairGenerator.EC", PACKAGE_NAME + ".AndroidKeyStoreKeyPairGeneratorSpi$EC");
         put("KeyPairGenerator.RSA", PACKAGE_NAME +  ".AndroidKeyStoreKeyPairGeneratorSpi$RSA");
         put("KeyPairGenerator.XDH", PACKAGE_NAME +  ".AndroidKeyStoreKeyPairGeneratorSpi$XDH");
+        put("KeyPairGenerator.ED25519", PACKAGE_NAME
+                +  ".AndroidKeyStoreKeyPairGeneratorSpi$ED25519");
 
         // java.security.KeyFactory
         putKeyFactoryImpl("EC");
         putKeyFactoryImpl("RSA");
         putKeyFactoryImpl("XDH");
+        putKeyFactoryImpl("ED25519");
 
         // javax.crypto.KeyGenerator
         put("KeyGenerator.AES", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$AES");
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipMenuController.java
similarity index 88%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java
rename to libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipMenuController.java
index 0775f52..2f1189a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipMenuController.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.pip;
+package com.android.wm.shell.common.pip;
 
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
 import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY;
@@ -33,12 +33,13 @@
 import android.view.WindowManager;
 
 import com.android.wm.shell.R;
+import com.android.wm.shell.ShellTaskOrganizer;
 
 import java.util.List;
 
 /**
- *  Interface to allow {@link com.android.wm.shell.pip.PipTaskOrganizer} to call into
- *  PiP menu when certain events happen (task appear/vanish, PiP move, etc.)
+ *  Interface to interact with PiP menu when certain events happen
+ *  (task appear/vanish, PiP move, etc.).
  */
 public interface PipMenuController {
 
@@ -52,15 +53,15 @@
     float ALPHA_NO_CHANGE = -1f;
 
     /**
-     * Called when
-     * {@link PipTaskOrganizer#onTaskAppeared(RunningTaskInfo, SurfaceControl)}
+     * Called when out implementation of
+     * {@link ShellTaskOrganizer.TaskListener#onTaskAppeared(RunningTaskInfo, SurfaceControl)}
      * is called.
      */
     void attach(SurfaceControl leash);
 
     /**
-     * Called when
-     * {@link PipTaskOrganizer#onTaskVanished(RunningTaskInfo)} is called.
+     * Called when our implementation of
+     * {@link ShellTaskOrganizer.TaskListener#onTaskVanished(RunningTaskInfo)} is called.
      */
     void detach();
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
index 7b98fa6..8eecf1c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
@@ -18,18 +18,23 @@
 
 import android.annotation.NonNull;
 import android.content.Context;
+import android.os.Handler;
 
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SystemWindows;
 import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.common.pip.PipBoundsState;
 import com.android.wm.shell.common.pip.PipDisplayLayoutState;
+import com.android.wm.shell.common.pip.PipMediaController;
+import com.android.wm.shell.common.pip.PipUiEventLogger;
 import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.dagger.WMShellBaseModule;
 import com.android.wm.shell.dagger.WMSingleton;
+import com.android.wm.shell.pip2.phone.PhonePipMenuController;
 import com.android.wm.shell.pip2.phone.PipController;
 import com.android.wm.shell.pip2.phone.PipScheduler;
 import com.android.wm.shell.pip2.phone.PipTransition;
@@ -86,4 +91,16 @@
             @ShellMainThread ShellExecutor mainExecutor) {
         return new PipScheduler(context, pipBoundsState, mainExecutor);
     }
+
+    @WMSingleton
+    @Provides
+    static PhonePipMenuController providePipPhoneMenuController(Context context,
+            PipBoundsState pipBoundsState, PipMediaController pipMediaController,
+            SystemWindows systemWindows,
+            PipUiEventLogger pipUiEventLogger,
+            @ShellMainThread ShellExecutor mainExecutor,
+            @ShellMainThread Handler mainHandler) {
+        return new PhonePipMenuController(context, pipBoundsState, pipMediaController,
+                systemWindows, pipUiEventLogger, mainExecutor, mainHandler);
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
index da1ca8d..6250fc5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
@@ -17,6 +17,8 @@
 package com.android.wm.shell.desktopmode;
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 
 import static com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.FINAL_FREEFORM_SCALE;
 
@@ -86,7 +88,6 @@
         mTaskSurface = taskSurface;
         mRootTdaOrganizer = taskDisplayAreaOrganizer;
         mCurrentType = IndicatorType.NO_INDICATOR;
-        createView();
     }
 
     /**
@@ -127,34 +128,15 @@
         mView = new View(mContext);
         final SurfaceControl.Builder builder = new SurfaceControl.Builder();
         mRootTdaOrganizer.attachToDisplayArea(mTaskInfo.displayId, builder);
-        String description;
-        switch (mCurrentType) {
-            case TO_DESKTOP_INDICATOR:
-                description = "Desktop indicator";
-                break;
-            case TO_FULLSCREEN_INDICATOR:
-                description = "Fullscreen indicator";
-                break;
-            case TO_SPLIT_LEFT_INDICATOR:
-                description = "Split Left indicator";
-                break;
-            case TO_SPLIT_RIGHT_INDICATOR:
-                description = "Split Right indicator";
-                break;
-            default:
-                description = "Invalid indicator";
-                break;
-        }
         mLeash = builder
-                .setName(description)
+                .setName("Desktop Mode Visual Indicator")
                 .setContainerLayer()
                 .build();
         t.show(mLeash);
         final WindowManager.LayoutParams lp =
-                new WindowManager.LayoutParams(screenWidth, screenHeight,
-                        WindowManager.LayoutParams.TYPE_APPLICATION,
-                        WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT);
-        lp.setTitle(description + " for Task=" + mTaskInfo.taskId);
+                new WindowManager.LayoutParams(screenWidth, screenHeight, TYPE_APPLICATION,
+                        FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT);
+        lp.setTitle("Desktop Mode Visual Indicator");
         lp.setTrustedOverlay();
         final WindowlessWindowManager windowManager = new WindowlessWindowManager(
                 mTaskInfo.configuration, mLeash,
@@ -201,6 +183,9 @@
      */
     private void transitionIndicator(IndicatorType newType) {
         if (mCurrentType == newType) return;
+        if (mView == null) {
+            createView();
+        }
         if (mCurrentType == IndicatorType.NO_INDICATOR) {
             fadeInIndicator(newType);
         } else if (newType == IndicatorType.NO_INDICATOR) {
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 4f5c39a..28c06a4 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
@@ -919,22 +919,27 @@
         }
         if (taskBounds.top <= transitionAreaHeight) {
             moveToFullscreenWithAnimation(taskInfo, position)
+            return
         }
         if (inputCoordinate.x <= transitionAreaWidth) {
             releaseVisualIndicator()
-            var wct = WindowContainerTransaction()
+            val wct = WindowContainerTransaction()
             addMoveToSplitChanges(wct, taskInfo)
             splitScreenController.requestEnterSplitSelect(taskInfo, wct,
                 SPLIT_POSITION_TOP_OR_LEFT, taskBounds)
+            return
         }
         if (inputCoordinate.x >= (displayController.getDisplayLayout(taskInfo.displayId)?.width()
             ?.minus(transitionAreaWidth) ?: return)) {
             releaseVisualIndicator()
-            var wct = WindowContainerTransaction()
+            val wct = WindowContainerTransaction()
             addMoveToSplitChanges(wct, taskInfo)
             splitScreenController.requestEnterSplitSelect(taskInfo, wct,
                 SPLIT_POSITION_BOTTOM_OR_RIGHT, taskBounds)
+            return
         }
+        // A freeform drag-move ended, remove the indicator immediately.
+        releaseVisualIndicator()
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index 07b8f11..52a06e0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -86,6 +86,7 @@
 import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.common.pip.PipBoundsState;
 import com.android.wm.shell.common.pip.PipDisplayLayoutState;
+import com.android.wm.shell.common.pip.PipMenuController;
 import com.android.wm.shell.common.pip.PipUiEventLogger;
 import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.pip.phone.PipMotionHelper;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 8e375a9..e739266 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -68,6 +68,7 @@
 import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.common.pip.PipBoundsState;
 import com.android.wm.shell.common.pip.PipDisplayLayoutState;
+import com.android.wm.shell.common.pip.PipMenuController;
 import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.splitscreen.SplitScreenController;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
index 0e70736..d1fd207 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
@@ -40,6 +40,7 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipMenuController;
 import com.android.wm.shell.common.split.SplitScreenUtils;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
index 7606526..d8e8b58 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
@@ -41,8 +41,8 @@
 import com.android.wm.shell.common.pip.PipBoundsState;
 import com.android.wm.shell.common.pip.PipMediaController;
 import com.android.wm.shell.common.pip.PipMediaController.ActionListener;
+import com.android.wm.shell.common.pip.PipMenuController;
 import com.android.wm.shell.common.pip.PipUiEventLogger;
-import com.android.wm.shell.pip.PipMenuController;
 import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.splitscreen.SplitScreenController;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
index c6803f7..843c84a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
@@ -40,7 +40,7 @@
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.R;
 import com.android.wm.shell.common.SystemWindows;
-import com.android.wm.shell.pip.PipMenuController;
+import com.android.wm.shell.common.pip.PipMenuController;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 
 import java.util.List;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java
index 21223c9a..cac63eb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java
@@ -28,10 +28,10 @@
 import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.common.pip.PipBoundsState;
 import com.android.wm.shell.common.pip.PipDisplayLayoutState;
+import com.android.wm.shell.common.pip.PipMenuController;
 import com.android.wm.shell.common.pip.PipUiEventLogger;
 import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.pip.PipAnimationController;
-import com.android.wm.shell.pip.PipMenuController;
 import com.android.wm.shell.pip.PipParamsChangedForwarder;
 import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
 import com.android.wm.shell.pip.PipTaskOrganizer;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
index 571c839..d16a692 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
@@ -25,10 +25,10 @@
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.transitTypeToString;
 
+import static com.android.wm.shell.common.pip.PipMenuController.ALPHA_NO_CHANGE;
 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP;
 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK;
 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP;
-import static com.android.wm.shell.pip.PipMenuController.ALPHA_NO_CHANGE;
 import static com.android.wm.shell.pip.PipTransitionState.ENTERED_PIP;
 import static com.android.wm.shell.pip.PipTransitionState.ENTERING_PIP;
 import static com.android.wm.shell.pip.PipTransitionState.EXITING_PIP;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/PipSurfaceTransactionHelper.java
new file mode 100644
index 0000000..24077a3
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/PipSurfaceTransactionHelper.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2020 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;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.Choreographer;
+import android.view.SurfaceControl;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.transition.Transitions;
+
+/**
+ * Abstracts the common operations on {@link SurfaceControl.Transaction} for PiP transition.
+ */
+public class PipSurfaceTransactionHelper {
+    /** for {@link #scale(SurfaceControl.Transaction, SurfaceControl, Rect, Rect)} operation */
+    private final Matrix mTmpTransform = new Matrix();
+    private final float[] mTmpFloat9 = new float[9];
+    private final RectF mTmpSourceRectF = new RectF();
+    private final RectF mTmpDestinationRectF = new RectF();
+    private final Rect mTmpDestinationRect = new Rect();
+
+    private int mCornerRadius;
+    private int mShadowRadius;
+
+    public PipSurfaceTransactionHelper(Context context) {
+        onDensityOrFontScaleChanged(context);
+    }
+
+    /**
+     * Called when display size or font size of settings changed
+     *
+     * @param context the current context
+     */
+    public void onDensityOrFontScaleChanged(Context context) {
+        mCornerRadius = context.getResources().getDimensionPixelSize(R.dimen.pip_corner_radius);
+        mShadowRadius = context.getResources().getDimensionPixelSize(R.dimen.pip_shadow_radius);
+    }
+
+    /**
+     * Operates the alpha on a given transaction and leash
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper alpha(SurfaceControl.Transaction tx, SurfaceControl leash,
+            float alpha) {
+        tx.setAlpha(leash, alpha);
+        return this;
+    }
+
+    /**
+     * Operates the crop (and position) on a given transaction and leash
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper crop(SurfaceControl.Transaction tx, SurfaceControl leash,
+            Rect destinationBounds) {
+        tx.setWindowCrop(leash, destinationBounds.width(), destinationBounds.height())
+                .setPosition(leash, destinationBounds.left, destinationBounds.top);
+        return this;
+    }
+
+    /**
+     * Operates the scale (setMatrix) on a given transaction and leash
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
+            Rect sourceBounds, Rect destinationBounds) {
+        mTmpDestinationRectF.set(destinationBounds);
+        return scale(tx, leash, sourceBounds, mTmpDestinationRectF, 0 /* degrees */);
+    }
+
+    /**
+     * Operates the scale (setMatrix) on a given transaction and leash
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
+            Rect sourceBounds, RectF destinationBounds) {
+        return scale(tx, leash, sourceBounds, destinationBounds, 0 /* degrees */);
+    }
+
+    /**
+     * Operates the scale (setMatrix) on a given transaction and leash
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
+            Rect sourceBounds, Rect destinationBounds, float degrees) {
+        mTmpDestinationRectF.set(destinationBounds);
+        return scale(tx, leash, sourceBounds, mTmpDestinationRectF, degrees);
+    }
+
+    /**
+     * Operates the scale (setMatrix) on a given transaction and leash, along with a rotation.
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
+            Rect sourceBounds, RectF destinationBounds, float degrees) {
+        mTmpSourceRectF.set(sourceBounds);
+        // We want the matrix to position the surface relative to the screen coordinates so offset
+        // the source to 0,0
+        mTmpSourceRectF.offsetTo(0, 0);
+        mTmpDestinationRectF.set(destinationBounds);
+        mTmpTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL);
+        mTmpTransform.postRotate(degrees,
+                mTmpDestinationRectF.centerX(), mTmpDestinationRectF.centerY());
+        tx.setMatrix(leash, mTmpTransform, mTmpFloat9);
+        return this;
+    }
+
+    /**
+     * Operates the scale (setMatrix) on a given transaction and leash
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper scaleAndCrop(SurfaceControl.Transaction tx,
+            SurfaceControl leash, Rect sourceRectHint,
+            Rect sourceBounds, Rect destinationBounds, Rect insets,
+            boolean isInPipDirection, float fraction) {
+        mTmpDestinationRect.set(sourceBounds);
+        // Similar to {@link #scale}, we want to position the surface relative to the screen
+        // coordinates so offset the bounds to 0,0
+        mTmpDestinationRect.offsetTo(0, 0);
+        mTmpDestinationRect.inset(insets);
+        // Scale to the bounds no smaller than the destination and offset such that the top/left
+        // of the scaled inset source rect aligns with the top/left of the destination bounds
+        final float scale;
+        if (isInPipDirection
+                && sourceRectHint != null && sourceRectHint.width() < sourceBounds.width()) {
+            // scale by sourceRectHint if it's not edge-to-edge, for entering PiP transition only.
+            final float endScale = sourceBounds.width() <= sourceBounds.height()
+                    ? (float) destinationBounds.width() / sourceRectHint.width()
+                    : (float) destinationBounds.height() / sourceRectHint.height();
+            final float startScale = sourceBounds.width() <= sourceBounds.height()
+                    ? (float) destinationBounds.width() / sourceBounds.width()
+                    : (float) destinationBounds.height() / sourceBounds.height();
+            scale = (1 - fraction) * startScale + fraction * endScale;
+        } else {
+            scale = Math.max((float) destinationBounds.width() / sourceBounds.width(),
+                    (float) destinationBounds.height() / sourceBounds.height());
+        }
+        final float left = destinationBounds.left - insets.left * scale;
+        final float top = destinationBounds.top - insets.top * scale;
+        mTmpTransform.setScale(scale, scale);
+        tx.setMatrix(leash, mTmpTransform, mTmpFloat9)
+                .setCrop(leash, mTmpDestinationRect)
+                .setPosition(leash, left, top);
+        return this;
+    }
+
+    /**
+     * Operates the rotation according to the given degrees and scale (setMatrix) according to the
+     * source bounds and rotated destination bounds. The crop will be the unscaled source bounds.
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper rotateAndScaleWithCrop(SurfaceControl.Transaction tx,
+            SurfaceControl leash, Rect sourceBounds, Rect destinationBounds, Rect insets,
+            float degrees, float positionX, float positionY, boolean isExpanding,
+            boolean clockwise) {
+        mTmpDestinationRect.set(sourceBounds);
+        mTmpDestinationRect.inset(insets);
+        final int srcW = mTmpDestinationRect.width();
+        final int srcH = mTmpDestinationRect.height();
+        final int destW = destinationBounds.width();
+        final int destH = destinationBounds.height();
+        // Scale by the short side so there won't be empty area if the aspect ratio of source and
+        // destination are different.
+        final float scale = srcW <= srcH ? (float) destW / srcW : (float) destH / srcH;
+        final Rect crop = mTmpDestinationRect;
+        crop.set(0, 0, Transitions.SHELL_TRANSITIONS_ROTATION ? destH
+                : destW, Transitions.SHELL_TRANSITIONS_ROTATION ? destW : destH);
+        // Inverse scale for crop to fit in screen coordinates.
+        crop.scale(1 / scale);
+        crop.offset(insets.left, insets.top);
+        if (isExpanding) {
+            // Expand bounds (shrink insets) in source orientation.
+            positionX -= insets.left * scale;
+            positionY -= insets.top * scale;
+        } else {
+            // Shrink bounds (expand insets) in destination orientation.
+            if (clockwise) {
+                positionX -= insets.top * scale;
+                positionY += insets.left * scale;
+            } else {
+                positionX += insets.top * scale;
+                positionY -= insets.left * scale;
+            }
+        }
+        mTmpTransform.setScale(scale, scale);
+        mTmpTransform.postRotate(degrees);
+        mTmpTransform.postTranslate(positionX, positionY);
+        tx.setMatrix(leash, mTmpTransform, mTmpFloat9).setCrop(leash, crop);
+        return this;
+    }
+
+    /**
+     * Resets the scale (setMatrix) on a given transaction and leash if there's any
+     *
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper resetScale(SurfaceControl.Transaction tx,
+            SurfaceControl leash,
+            Rect destinationBounds) {
+        tx.setMatrix(leash, Matrix.IDENTITY_MATRIX, mTmpFloat9)
+                .setPosition(leash, destinationBounds.left, destinationBounds.top);
+        return this;
+    }
+
+    /**
+     * Operates the round corner radius on a given transaction and leash
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper round(SurfaceControl.Transaction tx, SurfaceControl leash,
+            boolean applyCornerRadius) {
+        tx.setCornerRadius(leash, applyCornerRadius ? mCornerRadius : 0);
+        return this;
+    }
+
+    /**
+     * Operates the round corner radius on a given transaction and leash, scaled by bounds
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper round(SurfaceControl.Transaction tx, SurfaceControl leash,
+            Rect fromBounds, Rect toBounds) {
+        final float scale = (float) (Math.hypot(fromBounds.width(), fromBounds.height())
+                / Math.hypot(toBounds.width(), toBounds.height()));
+        tx.setCornerRadius(leash, mCornerRadius * scale);
+        return this;
+    }
+
+    /**
+     * Operates the shadow radius on a given transaction and leash
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    public PipSurfaceTransactionHelper shadow(SurfaceControl.Transaction tx, SurfaceControl leash,
+            boolean applyShadowRadius) {
+        tx.setShadowRadius(leash, applyShadowRadius ? mShadowRadius : 0);
+        return this;
+    }
+
+    /**
+     * Interface to standardize {@link SurfaceControl.Transaction} generation across PiP.
+     */
+    public interface SurfaceControlTransactionFactory {
+        /**
+         * @return a new transaction to operate on.
+         */
+        SurfaceControl.Transaction getTransaction();
+    }
+
+    /**
+     * Implementation of {@link SurfaceControlTransactionFactory} that returns
+     * {@link SurfaceControl.Transaction} with VsyncId being set.
+     */
+    public static class VsyncSurfaceControlTransactionFactory
+            implements SurfaceControlTransactionFactory {
+        @Override
+        public SurfaceControl.Transaction getTransaction() {
+            final SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+            tx.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
+            return tx;
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java
new file mode 100644
index 0000000..2478252
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java
@@ -0,0 +1,608 @@
+/*
+ * Copyright (C) 2020 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.view.WindowManager.SHELL_ROOT_LAYER_PIP;
+
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.RemoteAction;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Debug;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.util.Size;
+import android.view.MotionEvent;
+import android.view.SurfaceControl;
+import android.view.View;
+import android.view.ViewRootImpl;
+import android.view.WindowManagerGlobal;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SystemWindows;
+import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipMediaController;
+import com.android.wm.shell.common.pip.PipMediaController.ActionListener;
+import com.android.wm.shell.common.pip.PipMenuController;
+import com.android.wm.shell.common.pip.PipUiEventLogger;
+import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Manages the PiP menu view which can show menu options or a scrim.
+ *
+ * The current media session provides actions whenever there are no valid actions provided by the
+ * current PiP activity. Otherwise, those actions always take precedence.
+ */
+public class PhonePipMenuController implements PipMenuController {
+
+    private static final String TAG = "PhonePipMenuController";
+    private static final boolean DEBUG = false;
+
+    public static final int MENU_STATE_NONE = 0;
+    public static final int MENU_STATE_FULL = 1;
+
+    /**
+     * A listener interface to receive notification on changes in PIP.
+     */
+    public interface Listener {
+        /**
+         * Called when the PIP menu visibility change has started.
+         *
+         * @param menuState the new, about-to-change state of the menu
+         * @param resize whether or not to resize the PiP with the state change
+         */
+        void onPipMenuStateChangeStart(int menuState, boolean resize, Runnable callback);
+
+        /**
+         * Called when the PIP menu state has finished changing/animating.
+         *
+         * @param menuState the new state of the menu.
+         */
+        void onPipMenuStateChangeFinish(int menuState);
+
+        /**
+         * Called when the PIP requested to be expanded.
+         */
+        void onPipExpand();
+
+        /**
+         * Called when the PIP requested to be dismissed.
+         */
+        void onPipDismiss();
+
+        /**
+         * Called when the PIP requested to show the menu.
+         */
+        void onPipShowMenu();
+
+        /**
+         * Called when the PIP requested to enter Split.
+         */
+        void onEnterSplit();
+    }
+
+    private final Matrix mMoveTransform = new Matrix();
+    private final Rect mTmpSourceBounds = new Rect();
+    private final RectF mTmpSourceRectF = new RectF();
+    private final RectF mTmpDestinationRectF = new RectF();
+    private final Context mContext;
+    private final PipBoundsState mPipBoundsState;
+    private final PipMediaController mMediaController;
+    private final ShellExecutor mMainExecutor;
+    private final Handler mMainHandler;
+
+    private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
+            mSurfaceControlTransactionFactory;
+    private final float[] mTmpTransform = new float[9];
+
+    private final ArrayList<Listener> mListeners = new ArrayList<>();
+    private final SystemWindows mSystemWindows;
+    private final PipUiEventLogger mPipUiEventLogger;
+
+    private List<RemoteAction> mAppActions;
+    private RemoteAction mCloseAction;
+    private List<RemoteAction> mMediaActions;
+
+    private int mMenuState;
+
+    private PipMenuView mPipMenuView;
+
+    private SurfaceControl mLeash;
+
+    private ActionListener mMediaActionListener = new ActionListener() {
+        @Override
+        public void onMediaActionsChanged(List<RemoteAction> mediaActions) {
+            mMediaActions = new ArrayList<>(mediaActions);
+            updateMenuActions();
+        }
+    };
+
+    public PhonePipMenuController(Context context, PipBoundsState pipBoundsState,
+            PipMediaController mediaController, SystemWindows systemWindows,
+            PipUiEventLogger pipUiEventLogger,
+            ShellExecutor mainExecutor, Handler mainHandler) {
+        mContext = context;
+        mPipBoundsState = pipBoundsState;
+        mMediaController = mediaController;
+        mSystemWindows = systemWindows;
+        mMainExecutor = mainExecutor;
+        mMainHandler = mainHandler;
+        mPipUiEventLogger = pipUiEventLogger;
+
+        mSurfaceControlTransactionFactory =
+                new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
+    }
+
+    public boolean isMenuVisible() {
+        return mPipMenuView != null && mMenuState != MENU_STATE_NONE;
+    }
+
+    /**
+     * Attach the menu when the PiP task first appears.
+     */
+    @Override
+    public void attach(SurfaceControl leash) {
+        mLeash = leash;
+        attachPipMenuView();
+    }
+
+    /**
+     * Detach the menu when the PiP task is gone.
+     */
+    @Override
+    public void detach() {
+        hideMenu();
+        detachPipMenuView();
+        mLeash = null;
+    }
+
+    void attachPipMenuView() {
+        // In case detach was not called (e.g. PIP unexpectedly closed)
+        if (mPipMenuView != null) {
+            detachPipMenuView();
+        }
+        mPipMenuView = new PipMenuView(mContext, this, mMainExecutor, mMainHandler,
+                mPipUiEventLogger);
+        mPipMenuView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
+            @Override
+            public void onViewAttachedToWindow(View v) {
+                v.getViewRootImpl().addSurfaceChangedCallback(
+                        new ViewRootImpl.SurfaceChangedCallback() {
+                            @Override
+                            public void surfaceCreated(SurfaceControl.Transaction t) {
+                                final SurfaceControl sc = getSurfaceControl();
+                                if (sc != null) {
+                                    t.reparent(sc, mLeash);
+                                    // make menu on top of the surface
+                                    t.setLayer(sc, Integer.MAX_VALUE);
+                                }
+                            }
+
+                            @Override
+                            public void surfaceReplaced(SurfaceControl.Transaction t) {
+                            }
+
+                            @Override
+                            public void surfaceDestroyed() {
+                            }
+                        });
+            }
+
+            @Override
+            public void onViewDetachedFromWindow(View v) {
+            }
+        });
+
+        mSystemWindows.addView(mPipMenuView,
+                getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */),
+                0, SHELL_ROOT_LAYER_PIP);
+        setShellRootAccessibilityWindow();
+
+        // Make sure the initial actions are set
+        updateMenuActions();
+    }
+
+    private void detachPipMenuView() {
+        if (mPipMenuView == null) {
+            return;
+        }
+
+        mSystemWindows.removeView(mPipMenuView);
+        mPipMenuView = null;
+    }
+
+    /**
+     * Updates the layout parameters of the menu.
+     * @param destinationBounds New Menu bounds.
+     */
+    @Override
+    public void updateMenuBounds(Rect destinationBounds) {
+        mSystemWindows.updateViewLayout(mPipMenuView,
+                getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, destinationBounds.width(),
+                        destinationBounds.height()));
+        updateMenuLayout(destinationBounds);
+    }
+
+    @Override
+    public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
+        if (mPipMenuView != null) {
+            mPipMenuView.onFocusTaskChanged(taskInfo);
+        }
+    }
+
+    /**
+     * Tries to grab a surface control from {@link PipMenuView}. If this isn't available for some
+     * reason (ie. the window isn't ready yet, thus {@link ViewRootImpl} is
+     * {@code null}), it will get the leash that the WindowlessWM has assigned to it.
+     */
+    public SurfaceControl getSurfaceControl() {
+        return mSystemWindows.getViewSurface(mPipMenuView);
+    }
+
+    /**
+     * Adds a new menu activity listener.
+     */
+    public void addListener(Listener listener) {
+        if (!mListeners.contains(listener)) {
+            mListeners.add(listener);
+        }
+    }
+
+    @Nullable
+    Size getEstimatedMinMenuSize() {
+        return mPipMenuView == null ? null : mPipMenuView.getEstimatedMinMenuSize();
+    }
+
+    /**
+     * When other components requests the menu controller directly to show the menu, we must
+     * first fire off the request to the other listeners who will then propagate the call
+     * back to the controller with the right parameters.
+     */
+    @Override
+    public void showMenu() {
+        mListeners.forEach(Listener::onPipShowMenu);
+    }
+
+    /**
+     * Similar to {@link #showMenu(int, Rect, boolean, boolean, boolean)} but only show the menu
+     * upon PiP window transition is finished.
+     */
+    public void showMenuWithPossibleDelay(int menuState, Rect stackBounds, boolean allowMenuTimeout,
+            boolean willResizeMenu, boolean showResizeHandle) {
+        if (willResizeMenu) {
+            // hide all visible controls including close button and etc. first, this is to ensure
+            // menu is totally invisible during the transition to eliminate unpleasant artifacts
+            fadeOutMenu();
+        }
+        showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu,
+                willResizeMenu /* withDelay=willResizeMenu here */, showResizeHandle);
+    }
+
+    /**
+     * Shows the menu activity immediately.
+     */
+    public void showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout,
+            boolean willResizeMenu, boolean showResizeHandle) {
+        showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu,
+                false /* withDelay */, showResizeHandle);
+    }
+
+    private void showMenuInternal(int menuState, Rect stackBounds, boolean allowMenuTimeout,
+            boolean willResizeMenu, boolean withDelay, boolean showResizeHandle) {
+        if (DEBUG) {
+            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: showMenu() state=%s"
+                            + " isMenuVisible=%s"
+                            + " allowMenuTimeout=%s"
+                            + " willResizeMenu=%s"
+                            + " withDelay=%s"
+                            + " showResizeHandle=%s"
+                            + " callers=\n%s", TAG, menuState, isMenuVisible(), allowMenuTimeout,
+                    willResizeMenu, withDelay, showResizeHandle, Debug.getCallers(5, "    "));
+        }
+
+        if (!checkPipMenuState()) {
+            return;
+        }
+
+        // Sync the menu bounds before showing it in case it is out of sync.
+        movePipMenu(null /* pipLeash */, null /* transaction */, stackBounds,
+                PipMenuController.ALPHA_NO_CHANGE);
+        updateMenuBounds(stackBounds);
+
+        mPipMenuView.showMenu(menuState, stackBounds, allowMenuTimeout, willResizeMenu, withDelay,
+                showResizeHandle);
+    }
+
+    /**
+     * Move the PiP menu, which does a translation and possibly a scale transformation.
+     */
+    @Override
+    public void movePipMenu(@Nullable SurfaceControl pipLeash,
+            @Nullable SurfaceControl.Transaction t,
+            Rect destinationBounds, float alpha) {
+        if (destinationBounds.isEmpty()) {
+            return;
+        }
+
+        if (!checkPipMenuState()) {
+            return;
+        }
+
+        // TODO(b/286307861) transaction should be applied outside of PiP menu controller
+        if (pipLeash != null && t != null) {
+            t.apply();
+        }
+    }
+
+    /**
+     * Does an immediate window crop of the PiP menu.
+     */
+    @Override
+    public void resizePipMenu(@Nullable SurfaceControl pipLeash,
+            @Nullable SurfaceControl.Transaction t,
+            Rect destinationBounds) {
+        if (destinationBounds.isEmpty()) {
+            return;
+        }
+
+        if (!checkPipMenuState()) {
+            return;
+        }
+
+        // TODO(b/286307861) transaction should be applied outside of PiP menu controller
+        if (pipLeash != null && t != null) {
+            t.apply();
+        }
+    }
+
+    private boolean checkPipMenuState() {
+        if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) {
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: Not going to move PiP, either menu or its parent is not created.", TAG);
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Pokes the menu, indicating that the user is interacting with it.
+     */
+    public void pokeMenu() {
+        final boolean isMenuVisible = isMenuVisible();
+        if (DEBUG) {
+            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: pokeMenu() isMenuVisible=%b", TAG, isMenuVisible);
+        }
+        if (isMenuVisible) {
+            mPipMenuView.pokeMenu();
+        }
+    }
+
+    private void fadeOutMenu() {
+        final boolean isMenuVisible = isMenuVisible();
+        if (DEBUG) {
+            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: fadeOutMenu() isMenuVisible=%b", TAG, isMenuVisible);
+        }
+        if (isMenuVisible) {
+            mPipMenuView.fadeOutMenu();
+        }
+    }
+
+    /**
+     * Hides the menu view.
+     */
+    public void hideMenu() {
+        final boolean isMenuVisible = isMenuVisible();
+        if (isMenuVisible) {
+            mPipMenuView.hideMenu();
+        }
+    }
+
+    /**
+     * Hides the menu view.
+     *
+     * @param animationType the animation type to use upon hiding the menu
+     * @param resize whether or not to resize the PiP with the state change
+     */
+    public void hideMenu(@PipMenuView.AnimationType int animationType, boolean resize) {
+        final boolean isMenuVisible = isMenuVisible();
+        if (DEBUG) {
+            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: hideMenu() state=%s"
+                            + " isMenuVisible=%s"
+                            + " animationType=%s"
+                            + " resize=%s"
+                            + " callers=\n%s", TAG, mMenuState, isMenuVisible,
+                    animationType, resize,
+                    Debug.getCallers(5, "    "));
+        }
+        if (isMenuVisible) {
+            mPipMenuView.hideMenu(resize, animationType);
+        }
+    }
+
+    /**
+     * Hides the menu activity.
+     */
+    public void hideMenu(Runnable onStartCallback, Runnable onEndCallback) {
+        if (isMenuVisible()) {
+            // If the menu is visible in either the closed or full state, then hide the menu and
+            // trigger the animation trigger afterwards
+            if (onStartCallback != null) {
+                onStartCallback.run();
+            }
+            mPipMenuView.hideMenu(onEndCallback);
+        }
+    }
+
+    /**
+     * Sets the menu actions to the actions provided by the current PiP menu.
+     */
+    @Override
+    public void setAppActions(List<RemoteAction> appActions,
+            RemoteAction closeAction) {
+        mAppActions = appActions;
+        mCloseAction = closeAction;
+        updateMenuActions();
+    }
+
+    void onPipExpand() {
+        mListeners.forEach(Listener::onPipExpand);
+    }
+
+    void onPipDismiss() {
+        mListeners.forEach(Listener::onPipDismiss);
+    }
+
+    void onEnterSplit() {
+        mListeners.forEach(Listener::onEnterSplit);
+    }
+
+    /**
+     * @return the best set of actions to show in the PiP menu.
+     */
+    private List<RemoteAction> resolveMenuActions() {
+        if (isValidActions(mAppActions)) {
+            return mAppActions;
+        }
+        return mMediaActions;
+    }
+
+    /**
+     * Updates the PiP menu with the best set of actions provided.
+     */
+    private void updateMenuActions() {
+        if (mPipMenuView != null) {
+            mPipMenuView.setActions(mPipBoundsState.getBounds(),
+                    resolveMenuActions(), mCloseAction);
+        }
+    }
+
+    /**
+     * Returns whether the set of actions are valid.
+     */
+    private static boolean isValidActions(List<?> actions) {
+        return actions != null && actions.size() > 0;
+    }
+
+    /**
+     * Handles changes in menu visibility.
+     */
+    void onMenuStateChangeStart(int menuState, boolean resize, Runnable callback) {
+        if (DEBUG) {
+            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: onMenuStateChangeStart() mMenuState=%s"
+                            + " menuState=%s resize=%s"
+                            + " callers=\n%s", TAG, mMenuState, menuState, resize,
+                    Debug.getCallers(5, "    "));
+        }
+
+        if (menuState != mMenuState) {
+            mListeners.forEach(l -> l.onPipMenuStateChangeStart(menuState, resize, callback));
+            if (menuState == MENU_STATE_FULL) {
+                // Once visible, start listening for media action changes. This call will trigger
+                // the menu actions to be updated again.
+                mMediaController.addActionListener(mMediaActionListener);
+            } else {
+                // Once hidden, stop listening for media action changes. This call will trigger
+                // the menu actions to be updated again.
+                mMediaController.removeActionListener(mMediaActionListener);
+            }
+
+            try {
+                WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null /* window */,
+                        mSystemWindows.getFocusGrantToken(mPipMenuView),
+                        menuState != MENU_STATE_NONE /* grantFocus */);
+            } catch (RemoteException e) {
+                ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                        "%s: Unable to update focus as menu appears/disappears, %s", TAG, e);
+            }
+        }
+    }
+
+    void onMenuStateChangeFinish(int menuState) {
+        if (menuState != mMenuState) {
+            mListeners.forEach(l -> l.onPipMenuStateChangeFinish(menuState));
+        }
+        mMenuState = menuState;
+        setShellRootAccessibilityWindow();
+    }
+
+    private void setShellRootAccessibilityWindow() {
+        switch (mMenuState) {
+            case MENU_STATE_NONE:
+                mSystemWindows.setShellRootAccessibilityWindow(0, SHELL_ROOT_LAYER_PIP, null);
+                break;
+            default:
+                mSystemWindows.setShellRootAccessibilityWindow(0, SHELL_ROOT_LAYER_PIP,
+                        mPipMenuView);
+                break;
+        }
+    }
+
+    /**
+     * Handles a pointer event sent from pip input consumer.
+     */
+    void handlePointerEvent(MotionEvent ev) {
+        if (mPipMenuView == null) {
+            return;
+        }
+
+        if (ev.isTouchEvent()) {
+            mPipMenuView.dispatchTouchEvent(ev);
+        } else {
+            mPipMenuView.dispatchGenericMotionEvent(ev);
+        }
+    }
+
+    /**
+     * Tell the PIP Menu to recalculate its layout given its current position on the display.
+     */
+    public void updateMenuLayout(Rect bounds) {
+        final boolean isMenuVisible = isMenuVisible();
+        if (DEBUG) {
+            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: updateMenuLayout() state=%s"
+                            + " isMenuVisible=%s"
+                            + " callers=\n%s", TAG, mMenuState, isMenuVisible,
+                    Debug.getCallers(5, "    "));
+        }
+        if (isMenuVisible) {
+            mPipMenuView.updateMenuLayout(bounds);
+        }
+    }
+
+    void dump(PrintWriter pw, String prefix) {
+        final String innerPrefix = prefix + "  ";
+        pw.println(prefix + TAG);
+        pw.println(innerPrefix + "mMenuState=" + mMenuState);
+        pw.println(innerPrefix + "mPipMenuView=" + mPipMenuView);
+        pw.println(innerPrefix + "mListeners=" + mListeners.size());
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuActionView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuActionView.java
new file mode 100644
index 0000000..7252675
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuActionView.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2021 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 android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import com.android.wm.shell.R;
+
+/**
+ * Container layout wraps single action image view drawn in PiP menu and can restrict the size of
+ * action image view (see pip_menu_action.xml).
+ */
+public class PipMenuActionView extends FrameLayout {
+    private ImageView mImageView;
+    private View mCustomCloseBackground;
+
+    public PipMenuActionView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mImageView = findViewById(R.id.image);
+        mCustomCloseBackground = findViewById(R.id.custom_close_bg);
+    }
+
+    /** pass through to internal {@link #mImageView} */
+    public void setImageDrawable(Drawable drawable) {
+        mImageView.setImageDrawable(drawable);
+    }
+
+    /** pass through to internal {@link #mCustomCloseBackground} */
+    public void setCustomCloseBackgroundVisibility(@Visibility int visibility) {
+        mCustomCloseBackground.setVisibility(visibility);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuIconsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuIconsAlgorithm.java
new file mode 100644
index 0000000..b5e575b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuIconsAlgorithm.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2020 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 android.content.Context;
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+/**
+ * Helper class to calculate and place the menu icons on the PIP Menu.
+ */
+public class PipMenuIconsAlgorithm {
+
+    private static final String TAG = "PipMenuIconsAlgorithm";
+
+    protected ViewGroup mViewRoot;
+    protected ViewGroup mTopEndContainer;
+    protected View mDragHandle;
+    protected View mEnterSplitButton;
+    protected View mSettingsButton;
+    protected View mDismissButton;
+
+    protected PipMenuIconsAlgorithm(Context context) {
+    }
+
+    /**
+     * Bind the necessary views.
+     */
+    public void bindViews(ViewGroup viewRoot, ViewGroup topEndContainer, View dragHandle,
+            View enterSplitButton, View settingsButton, View dismissButton) {
+        mViewRoot = viewRoot;
+        mTopEndContainer = topEndContainer;
+        mDragHandle = dragHandle;
+        mEnterSplitButton = enterSplitButton;
+        mSettingsButton = settingsButton;
+        mDismissButton = dismissButton;
+    }
+
+    /**
+     * Updates the position of the drag handle based on where the PIP window is on the screen.
+     */
+    public void onBoundsChanged(Rect bounds) {
+        // On phones, the menu icons are always static and will never move based on the PIP window
+        // position. No need to do anything here.
+    }
+
+    /**
+     * Set the gravity on the given view.
+     */
+    protected static void setLayoutGravity(View v, int gravity) {
+        if (v.getLayoutParams() instanceof FrameLayout.LayoutParams) {
+            FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) v.getLayoutParams();
+            params.gravity = gravity;
+            v.setLayoutParams(params);
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuView.java
new file mode 100644
index 0000000..a5b76c7
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuView.java
@@ -0,0 +1,630 @@
+/*
+ * Copyright (C) 2020 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.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.provider.Settings.ACTION_PICTURE_IN_PICTURE_SETTINGS;
+import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS;
+import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
+
+import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_FULL;
+import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_NONE;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.PendingIntent;
+import android.app.RemoteAction;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.util.Pair;
+import android.util.Size;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.R;
+import com.android.wm.shell.animation.Interpolators;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.pip.PipUiEventLogger;
+import com.android.wm.shell.common.pip.PipUtils;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Translucent window that gets started on top of a task in PIP to allow the user to control it.
+ */
+public class PipMenuView extends FrameLayout {
+
+    private static final String TAG = "PipMenuView";
+
+    private static final int ANIMATION_NONE_DURATION_MS = 0;
+    private static final int ANIMATION_HIDE_DURATION_MS = 125;
+
+    /** No animation performed during menu hide. */
+    public static final int ANIM_TYPE_NONE = 0;
+    /** Fade out the menu until it's invisible. Used when the PIP window remains visible.  */
+    public static final int ANIM_TYPE_HIDE = 1;
+    /** Fade out the menu in sync with the PIP window. */
+    public static final int ANIM_TYPE_DISMISS = 2;
+
+    @IntDef(prefix = { "ANIM_TYPE_" }, value = {
+            ANIM_TYPE_NONE,
+            ANIM_TYPE_HIDE,
+            ANIM_TYPE_DISMISS
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface AnimationType {}
+
+    private static final int INITIAL_DISMISS_DELAY = 3500;
+    private static final int POST_INTERACTION_DISMISS_DELAY = 2000;
+    private static final long MENU_SHOW_ON_EXPAND_START_DELAY = 30;
+
+    private static final float MENU_BACKGROUND_ALPHA = 0.54f;
+    private static final float DISABLED_ACTION_ALPHA = 0.54f;
+
+    private int mMenuState;
+    private boolean mAllowMenuTimeout = true;
+    private boolean mAllowTouches = true;
+    private int mDismissFadeOutDurationMs;
+    private final List<RemoteAction> mActions = new ArrayList<>();
+    private RemoteAction mCloseAction;
+
+    private AccessibilityManager mAccessibilityManager;
+    private Drawable mBackgroundDrawable;
+    private View mMenuContainer;
+    private LinearLayout mActionsGroup;
+    private int mBetweenActionPaddingLand;
+
+    private AnimatorSet mMenuContainerAnimator;
+    private final PhonePipMenuController mController;
+    private final PipUiEventLogger mPipUiEventLogger;
+
+    private ValueAnimator.AnimatorUpdateListener mMenuBgUpdateListener =
+            new ValueAnimator.AnimatorUpdateListener() {
+                @Override
+                public void onAnimationUpdate(ValueAnimator animation) {
+                    final float alpha = (float) animation.getAnimatedValue();
+                    mBackgroundDrawable.setAlpha((int) (MENU_BACKGROUND_ALPHA * alpha * 255));
+                }
+            };
+
+    private ShellExecutor mMainExecutor;
+    private Handler mMainHandler;
+
+    /**
+     * Whether the most recent showing of the menu caused a PIP resize, such as when PIP is too
+     * small and it is resized on menu show to fit the actions.
+     */
+    private boolean mDidLastShowMenuResize;
+    private final Runnable mHideMenuRunnable = this::hideMenu;
+
+    protected View mViewRoot;
+    protected View mSettingsButton;
+    protected View mDismissButton;
+    protected View mEnterSplitButton;
+    protected View mTopEndContainer;
+    protected PipMenuIconsAlgorithm mPipMenuIconsAlgorithm;
+
+    // How long the shell will wait for the app to close the PiP if a custom action is set.
+    private final int mPipForceCloseDelay;
+
+    public PipMenuView(Context context, PhonePipMenuController controller,
+            ShellExecutor mainExecutor, Handler mainHandler, PipUiEventLogger pipUiEventLogger) {
+        super(context, null, 0);
+        mContext = context;
+        mController = controller;
+        mMainExecutor = mainExecutor;
+        mMainHandler = mainHandler;
+        mPipUiEventLogger = pipUiEventLogger;
+
+        mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
+        inflate(context, R.layout.pip_menu, this);
+
+        mPipForceCloseDelay = context.getResources().getInteger(
+                R.integer.config_pipForceCloseDelay);
+
+        mBackgroundDrawable = mContext.getDrawable(R.drawable.pip_menu_background);
+        mBackgroundDrawable.setAlpha(0);
+        mViewRoot = findViewById(R.id.background);
+        mViewRoot.setBackground(mBackgroundDrawable);
+        mMenuContainer = findViewById(R.id.menu_container);
+        mMenuContainer.setAlpha(0);
+        mTopEndContainer = findViewById(R.id.top_end_container);
+        mSettingsButton = findViewById(R.id.settings);
+        mSettingsButton.setAlpha(0);
+        mSettingsButton.setOnClickListener((v) -> {
+            if (v.getAlpha() != 0) {
+                showSettings();
+            }
+        });
+        mDismissButton = findViewById(R.id.dismiss);
+        mDismissButton.setAlpha(0);
+        mDismissButton.setOnClickListener(v -> dismissPip());
+        findViewById(R.id.expand_button).setOnClickListener(v -> {
+            if (mMenuContainer.getAlpha() != 0) {
+                expandPip();
+            }
+        });
+
+        mEnterSplitButton = findViewById(R.id.enter_split);
+        mEnterSplitButton.setAlpha(0);
+        mEnterSplitButton.setOnClickListener(v -> {
+            if (mEnterSplitButton.getAlpha() != 0) {
+                enterSplit();
+            }
+        });
+
+        // this disables the ripples
+        mEnterSplitButton.setEnabled(false);
+
+        findViewById(R.id.resize_handle).setAlpha(0);
+
+        mActionsGroup = findViewById(R.id.actions_group);
+        mBetweenActionPaddingLand = getResources().getDimensionPixelSize(
+                R.dimen.pip_between_action_padding_land);
+        mPipMenuIconsAlgorithm = new PipMenuIconsAlgorithm(mContext);
+        mPipMenuIconsAlgorithm.bindViews((ViewGroup) mViewRoot, (ViewGroup) mTopEndContainer,
+                findViewById(R.id.resize_handle), mEnterSplitButton, mSettingsButton,
+                mDismissButton);
+        mDismissFadeOutDurationMs = context.getResources()
+                .getInteger(R.integer.config_pipExitAnimationDuration);
+
+        initAccessibility();
+    }
+
+    private void initAccessibility() {
+        this.setAccessibilityDelegate(new AccessibilityDelegate() {
+            @Override
+            public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+                super.onInitializeAccessibilityNodeInfo(host, info);
+                String label = getResources().getString(R.string.pip_menu_title);
+                info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK, label));
+            }
+
+            @Override
+            public boolean performAccessibilityAction(View host, int action, Bundle args) {
+                if (action == ACTION_CLICK && mMenuState != MENU_STATE_FULL) {
+                    mController.showMenu();
+                }
+                return super.performAccessibilityAction(host, action, args);
+            }
+        });
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (keyCode == KeyEvent.KEYCODE_ESCAPE) {
+            hideMenu();
+            return true;
+        }
+        return super.onKeyUp(keyCode, event);
+    }
+
+    @Override
+    public boolean shouldDelayChildPressedState() {
+        return true;
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        if (!mAllowTouches) {
+            return false;
+        }
+
+        if (mAllowMenuTimeout) {
+            repostDelayedHide(POST_INTERACTION_DISMISS_DELAY);
+        }
+
+        return super.dispatchTouchEvent(ev);
+    }
+
+    @Override
+    public boolean dispatchGenericMotionEvent(MotionEvent event) {
+        if (mAllowMenuTimeout) {
+            repostDelayedHide(POST_INTERACTION_DISMISS_DELAY);
+        }
+
+        return super.dispatchGenericMotionEvent(event);
+    }
+
+    void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {}
+
+    void showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout,
+            boolean resizeMenuOnShow, boolean withDelay, boolean showResizeHandle) {
+        mAllowMenuTimeout = allowMenuTimeout;
+        mDidLastShowMenuResize = resizeMenuOnShow;
+        final boolean enableEnterSplit =
+                mContext.getResources().getBoolean(R.bool.config_pipEnableEnterSplitButton);
+        if (mMenuState != menuState) {
+            // Disallow touches if the menu needs to resize while showing, and we are transitioning
+            // to/from a full menu state.
+            boolean disallowTouchesUntilAnimationEnd = resizeMenuOnShow
+                    && (mMenuState == MENU_STATE_FULL || menuState == MENU_STATE_FULL);
+            mAllowTouches = !disallowTouchesUntilAnimationEnd;
+            cancelDelayedHide();
+            if (mMenuContainerAnimator != null) {
+                mMenuContainerAnimator.cancel();
+            }
+            mMenuContainerAnimator = new AnimatorSet();
+            ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA,
+                    mMenuContainer.getAlpha(), 1f);
+            menuAnim.addUpdateListener(mMenuBgUpdateListener);
+            ObjectAnimator settingsAnim = ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA,
+                    mSettingsButton.getAlpha(), 1f);
+            ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA,
+                    mDismissButton.getAlpha(), 1f);
+            if (menuState == MENU_STATE_FULL) {
+                mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim);
+            }
+            mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_IN);
+            mMenuContainerAnimator.setDuration(ANIMATION_HIDE_DURATION_MS);
+            mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mAllowTouches = true;
+                    notifyMenuStateChangeFinish(menuState);
+                    if (allowMenuTimeout) {
+                        repostDelayedHide(INITIAL_DISMISS_DELAY);
+                    }
+                }
+
+                @Override
+                public void onAnimationCancel(Animator animation) {
+                    mAllowTouches = true;
+                }
+            });
+            if (withDelay) {
+                // starts the menu container animation after window expansion is completed
+                notifyMenuStateChangeStart(menuState, resizeMenuOnShow, () -> {
+                    if (mMenuContainerAnimator == null) {
+                        return;
+                    }
+                    mMenuContainerAnimator.setStartDelay(MENU_SHOW_ON_EXPAND_START_DELAY);
+                    setVisibility(VISIBLE);
+                    mMenuContainerAnimator.start();
+                });
+            } else {
+                notifyMenuStateChangeStart(menuState, resizeMenuOnShow, null);
+                setVisibility(VISIBLE);
+                mMenuContainerAnimator.start();
+            }
+            updateActionViews(menuState, stackBounds);
+        } else {
+            // If we are already visible, then just start the delayed dismiss and unregister any
+            // existing input consumers from the previous drag
+            if (allowMenuTimeout) {
+                repostDelayedHide(POST_INTERACTION_DISMISS_DELAY);
+            }
+        }
+    }
+
+    /**
+     * Different from {@link #hideMenu()}, this function does not try to finish this menu activity
+     * and instead, it fades out the controls by setting the alpha to 0 directly without menu
+     * visibility callbacks invoked.
+     */
+    void fadeOutMenu() {
+        mMenuContainer.setAlpha(0f);
+        mSettingsButton.setAlpha(0f);
+        mDismissButton.setAlpha(0f);
+        mEnterSplitButton.setAlpha(0f);
+    }
+
+    void pokeMenu() {
+        cancelDelayedHide();
+    }
+
+    void updateMenuLayout(Rect bounds) {
+        mPipMenuIconsAlgorithm.onBoundsChanged(bounds);
+    }
+
+    void hideMenu() {
+        hideMenu(null);
+    }
+
+    void hideMenu(Runnable animationEndCallback) {
+        hideMenu(animationEndCallback, true /* notifyMenuVisibility */, mDidLastShowMenuResize,
+                ANIM_TYPE_HIDE);
+    }
+
+    void hideMenu(boolean resize, @AnimationType int animationType) {
+        hideMenu(null /* animationFinishedRunnable */, true /* notifyMenuVisibility */, resize,
+                animationType);
+    }
+
+    void hideMenu(final Runnable animationFinishedRunnable, boolean notifyMenuVisibility,
+            boolean resize, @AnimationType int animationType) {
+        if (mMenuState != MENU_STATE_NONE) {
+            cancelDelayedHide();
+            if (notifyMenuVisibility) {
+                notifyMenuStateChangeStart(MENU_STATE_NONE, resize, null);
+            }
+            mMenuContainerAnimator = new AnimatorSet();
+            ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA,
+                    mMenuContainer.getAlpha(), 0f);
+            menuAnim.addUpdateListener(mMenuBgUpdateListener);
+            ObjectAnimator settingsAnim = ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA,
+                    mSettingsButton.getAlpha(), 0f);
+            ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA,
+                    mDismissButton.getAlpha(), 0f);
+            ObjectAnimator enterSplitAnim = ObjectAnimator.ofFloat(mEnterSplitButton, View.ALPHA,
+                    mEnterSplitButton.getAlpha(), 0f);
+            mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim,
+                    enterSplitAnim);
+            mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_OUT);
+            mMenuContainerAnimator.setDuration(getFadeOutDuration(animationType));
+            mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    setVisibility(GONE);
+                    if (notifyMenuVisibility) {
+                        notifyMenuStateChangeFinish(MENU_STATE_NONE);
+                    }
+                    if (animationFinishedRunnable != null) {
+                        animationFinishedRunnable.run();
+                    }
+                }
+            });
+            mMenuContainerAnimator.start();
+        }
+    }
+
+    /**
+     * @return Estimated minimum {@link Size} to hold the actions.
+     * See also {@link #updateActionViews(Rect)}
+     */
+    Size getEstimatedMinMenuSize() {
+        final int pipActionSize = getResources().getDimensionPixelSize(R.dimen.pip_action_size);
+        // the minimum width would be (2 * pipActionSize) since we have settings and dismiss button
+        // on the top action container.
+        final int width = Math.max(2, mActions.size()) * pipActionSize;
+        final int height = getResources().getDimensionPixelSize(R.dimen.pip_expand_action_size)
+                + getResources().getDimensionPixelSize(R.dimen.pip_action_padding)
+                + getResources().getDimensionPixelSize(R.dimen.pip_expand_container_edge_margin);
+        return new Size(width, height);
+    }
+
+    void setActions(Rect stackBounds, @Nullable List<RemoteAction> actions,
+            @Nullable RemoteAction closeAction) {
+        mActions.clear();
+        if (actions != null && !actions.isEmpty()) {
+            mActions.addAll(actions);
+        }
+        mCloseAction = closeAction;
+        if (mMenuState == MENU_STATE_FULL) {
+            updateActionViews(mMenuState, stackBounds);
+        }
+    }
+
+    private void updateActionViews(int menuState, Rect stackBounds) {
+        ViewGroup expandContainer = findViewById(R.id.expand_container);
+        ViewGroup actionsContainer = findViewById(R.id.actions_container);
+        actionsContainer.setOnTouchListener((v, ev) -> {
+            // Do nothing, prevent click through to parent
+            return true;
+        });
+
+        // Update the expand button only if it should show with the menu
+        expandContainer.setVisibility(menuState == MENU_STATE_FULL
+                ? View.VISIBLE
+                : View.INVISIBLE);
+
+        LayoutParams expandedLp =
+                (LayoutParams) expandContainer.getLayoutParams();
+        if (mActions.isEmpty() || menuState == MENU_STATE_NONE) {
+            actionsContainer.setVisibility(View.INVISIBLE);
+
+            // Update the expand container margin to adjust the center of the expand button to
+            // account for the existence of the action container
+            expandedLp.topMargin = 0;
+            expandedLp.bottomMargin = 0;
+        } else {
+            actionsContainer.setVisibility(View.VISIBLE);
+            if (mActionsGroup != null) {
+                // Ensure we have as many buttons as actions
+                final LayoutInflater inflater = LayoutInflater.from(mContext);
+                while (mActionsGroup.getChildCount() < mActions.size()) {
+                    final PipMenuActionView actionView = (PipMenuActionView) inflater.inflate(
+                            R.layout.pip_menu_action, mActionsGroup, false);
+                    mActionsGroup.addView(actionView);
+                }
+
+                // Update the visibility of all views
+                for (int i = 0; i < mActionsGroup.getChildCount(); i++) {
+                    mActionsGroup.getChildAt(i).setVisibility(i < mActions.size()
+                            ? View.VISIBLE
+                            : View.GONE);
+                }
+
+                // Recreate the layout
+                final boolean isLandscapePip = stackBounds != null
+                        && (stackBounds.width() > stackBounds.height());
+                for (int i = 0; i < mActions.size(); i++) {
+                    final RemoteAction action = mActions.get(i);
+                    final PipMenuActionView actionView =
+                            (PipMenuActionView) mActionsGroup.getChildAt(i);
+                    final boolean isCloseAction = mCloseAction != null && Objects.equals(
+                            mCloseAction.getActionIntent(), action.getActionIntent());
+
+                    final int iconType = action.getIcon().getType();
+                    if (iconType == Icon.TYPE_URI || iconType == Icon.TYPE_URI_ADAPTIVE_BITMAP) {
+                        // Disallow loading icon from content URI
+                        actionView.setImageDrawable(null);
+                    } else {
+                        // TODO: Check if the action drawable has changed before we reload it
+                        action.getIcon().loadDrawableAsync(mContext, d -> {
+                            if (d != null) {
+                                d.setTint(Color.WHITE);
+                                actionView.setImageDrawable(d);
+                            }
+                        }, mMainHandler);
+                    }
+                    actionView.setCustomCloseBackgroundVisibility(
+                            isCloseAction ? View.VISIBLE : View.GONE);
+                    actionView.setContentDescription(action.getContentDescription());
+                    if (action.isEnabled()) {
+                        actionView.setOnClickListener(
+                                v -> onActionViewClicked(action.getActionIntent(), isCloseAction));
+                    }
+                    actionView.setEnabled(action.isEnabled());
+                    actionView.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA);
+
+                    // Update the margin between actions
+                    LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
+                            actionView.getLayoutParams();
+                    lp.leftMargin = (isLandscapePip && i > 0) ? mBetweenActionPaddingLand : 0;
+                }
+            }
+
+            // Update the expand container margin to adjust the center of the expand button to
+            // account for the existence of the action container
+            expandedLp.topMargin = getResources().getDimensionPixelSize(
+                    R.dimen.pip_action_padding);
+            expandedLp.bottomMargin = getResources().getDimensionPixelSize(
+                    R.dimen.pip_expand_container_edge_margin);
+        }
+        expandContainer.requestLayout();
+    }
+
+    private void notifyMenuStateChangeStart(int menuState, boolean resize, Runnable callback) {
+        mController.onMenuStateChangeStart(menuState, resize, callback);
+    }
+
+    private void notifyMenuStateChangeFinish(int menuState) {
+        mMenuState = menuState;
+        mController.onMenuStateChangeFinish(menuState);
+    }
+
+    private void expandPip() {
+        // Do not notify menu visibility when hiding the menu, the controller will do this when it
+        // handles the message
+        hideMenu(mController::onPipExpand, false /* notifyMenuVisibility */, true /* resize */,
+                ANIM_TYPE_HIDE);
+        mPipUiEventLogger.log(
+                PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_EXPAND_TO_FULLSCREEN);
+    }
+
+    private void dismissPip() {
+        if (mMenuState != MENU_STATE_NONE) {
+            // Do not call hideMenu() directly. Instead, let the menu controller handle it just as
+            // any other dismissal that will update the touch state and fade out the PIP task
+            // and the menu view at the same time.
+            mController.onPipDismiss();
+            mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_TAP_TO_REMOVE);
+        }
+    }
+
+    /**
+     * Execute the {@link PendingIntent} attached to the {@link PipMenuActionView}.
+     * If the given {@link PendingIntent} matches {@link #mCloseAction}, we need to make sure
+     * the PiP is removed after a certain timeout in case the app does not respond in a
+     * timely manner.
+     */
+    private void onActionViewClicked(@NonNull PendingIntent intent, boolean isCloseAction) {
+        try {
+            intent.send();
+        } catch (PendingIntent.CanceledException e) {
+            ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: Failed to send action, %s", TAG, e);
+        }
+        if (isCloseAction) {
+            mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_CUSTOM_CLOSE);
+            mAllowTouches = false;
+            mMainExecutor.executeDelayed(() -> {
+                hideMenu();
+                // TODO: it's unsafe to call onPipDismiss with a delay here since
+                // we may have a different PiP by the time this runnable is executed.
+                mController.onPipDismiss();
+                mAllowTouches = true;
+            }, mPipForceCloseDelay);
+        }
+    }
+
+    private void enterSplit() {
+        // Do not notify menu visibility when hiding the menu, the controller will do this when it
+        // handles the message
+        hideMenu(mController::onEnterSplit, false /* notifyMenuVisibility */, true /* resize */,
+                ANIM_TYPE_HIDE);
+    }
+
+
+    private void showSettings() {
+        final Pair<ComponentName, Integer> topPipActivityInfo =
+                PipUtils.getTopPipActivity(mContext);
+        if (topPipActivityInfo.first != null) {
+            final Intent settingsIntent = new Intent(ACTION_PICTURE_IN_PICTURE_SETTINGS,
+                    Uri.fromParts("package", topPipActivityInfo.first.getPackageName(), null));
+            settingsIntent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
+            mContext.startActivityAsUser(settingsIntent, UserHandle.of(topPipActivityInfo.second));
+            mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_SHOW_SETTINGS);
+        }
+    }
+
+    private void cancelDelayedHide() {
+        mMainExecutor.removeCallbacks(mHideMenuRunnable);
+    }
+
+    private void repostDelayedHide(int delay) {
+        int recommendedTimeout = mAccessibilityManager.getRecommendedTimeoutMillis(delay,
+                FLAG_CONTENT_ICONS | FLAG_CONTENT_CONTROLS);
+        mMainExecutor.removeCallbacks(mHideMenuRunnable);
+        mMainExecutor.executeDelayed(mHideMenuRunnable, recommendedTimeout);
+    }
+
+    private long getFadeOutDuration(@AnimationType int animationType) {
+        switch (animationType) {
+            case ANIM_TYPE_NONE:
+                return ANIMATION_NONE_DURATION_MS;
+            case ANIM_TYPE_HIDE:
+                return ANIMATION_HIDE_DURATION_MS;
+            case ANIM_TYPE_DISMISS:
+                return mDismissFadeOutDurationMs;
+            default:
+                throw new IllegalStateException("Invalid animation type " + animationType);
+        }
+    }
+}
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 f3d178a..fbf4d13 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
@@ -42,8 +42,8 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipMenuController;
 import com.android.wm.shell.common.pip.PipUtils;
-import com.android.wm.shell.pip.PipMenuController;
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
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 e6faa63..96eaa1e 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
@@ -287,7 +287,7 @@
     }
 
     boolean isHandlingDragResize() {
-        return mDragResizeListener.isHandlingDragResize();
+        return mDragResizeListener != null && mDragResizeListener.isHandlingDragResize();
     }
 
     private void closeDragResizeListener() {
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 4ba05ce..1f7cc5a 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
@@ -345,7 +345,7 @@
                 mTaskOperations.injectBackKey();
             } else if (id == R.id.caption_handle || id == R.id.open_menu_button) {
                 if (!decoration.isHandleMenuActive()) {
-                    moveTaskToFront(mTaskOrganizer.getRunningTaskInfo(mTaskId));
+                    moveTaskToFront(decoration.mTaskInfo);
                     decoration.createHandleMenu();
                 } else {
                     decoration.closeHandleMenu();
@@ -419,10 +419,10 @@
                     && id != R.id.maximize_window) {
                 return false;
             }
-            moveTaskToFront(mTaskOrganizer.getRunningTaskInfo(mTaskId));
+            final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
+            moveTaskToFront(decoration.mTaskInfo);
 
             if (!mHasLongClicked && id != R.id.maximize_window) {
-                final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
                 decoration.closeMaximizeMenuIfNeeded(e);
             }
 
@@ -466,7 +466,8 @@
          */
         @Override
         public boolean handleMotionEvent(@Nullable View v, MotionEvent e) {
-            final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
+            final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
+            final RunningTaskInfo taskInfo = decoration.mTaskInfo;
             if (DesktopModeStatus.isEnabled()
                     && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
                 return false;
@@ -492,8 +493,6 @@
                 }
                 case MotionEvent.ACTION_MOVE: {
                     mShouldClick = false;
-                    final DesktopModeWindowDecoration decoration =
-                            mWindowDecorByTaskId.get(mTaskId);
                     // If a decor's resize drag zone is active, don't also try to reposition it.
                     if (decoration.isHandlingDragResize()) break;
                     decoration.closeMaximizeMenu();
@@ -557,9 +556,10 @@
                     && action != MotionEvent.ACTION_CANCEL)) {
                 return false;
             }
-            final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
-            mDesktopTasksController.ifPresent(c -> c.toggleDesktopTaskSize(taskInfo,
-                    mWindowDecorByTaskId.get(taskInfo.taskId)));
+            mDesktopTasksController.ifPresent(c -> {
+                final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
+                c.toggleDesktopTaskSize(decoration.mTaskInfo, decoration);
+            });
             return true;
         }
     }
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 2023333..3f0a281 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
@@ -36,7 +36,6 @@
 import android.graphics.Region;
 import android.graphics.drawable.Drawable;
 import android.os.Handler;
-import android.util.Log;
 import android.view.Choreographer;
 import android.view.MotionEvent;
 import android.view.SurfaceControl;
@@ -388,27 +387,20 @@
     }
 
     boolean isHandlingDragResize() {
-        return mDragResizeListener.isHandlingDragResize();
+        return mDragResizeListener != null && mDragResizeListener.isHandlingDragResize();
     }
 
     private void loadAppInfo() {
-        String packageName = mTaskInfo.realActivity.getPackageName();
         PackageManager pm = mContext.getApplicationContext().getPackageManager();
-        try {
-            final IconProvider provider = new IconProvider(mContext);
-            mAppIconDrawable = provider.getIcon(pm.getActivityInfo(mTaskInfo.baseActivity,
-                    PackageManager.ComponentInfoFlags.of(0)));
-            final Resources resources = mContext.getResources();
-            final BaseIconFactory factory = new BaseIconFactory(mContext,
-                    resources.getDisplayMetrics().densityDpi,
-                    resources.getDimensionPixelSize(R.dimen.desktop_mode_caption_icon_radius));
-            mAppIconBitmap = factory.createScaledBitmap(mAppIconDrawable, MODE_DEFAULT);
-            final ApplicationInfo applicationInfo = pm.getApplicationInfo(packageName,
-                    PackageManager.ApplicationInfoFlags.of(0));
-            mAppName = pm.getApplicationLabel(applicationInfo);
-        } catch (PackageManager.NameNotFoundException e) {
-            Log.w(TAG, "Package not found: " + packageName, e);
-        }
+        final IconProvider provider = new IconProvider(mContext);
+        mAppIconDrawable = provider.getIcon(mTaskInfo.topActivityInfo);
+        final Resources resources = mContext.getResources();
+        final BaseIconFactory factory = new BaseIconFactory(mContext,
+                resources.getDisplayMetrics().densityDpi,
+                resources.getDimensionPixelSize(R.dimen.desktop_mode_caption_icon_radius));
+        mAppIconBitmap = factory.createScaledBitmap(mAppIconDrawable, MODE_DEFAULT);
+        final ApplicationInfo applicationInfo = mTaskInfo.topActivityInfo.applicationInfo;
+        mAppName = pm.getApplicationLabel(applicationInfo);
     }
 
     private void closeDragResizeListener() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
index 8b38f99..d902621 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
@@ -353,6 +353,7 @@
         private boolean mShouldHandleEvents;
         private int mLastCursorType = PointerIcon.TYPE_DEFAULT;
         private Rect mDragStartTaskBounds;
+        private final Rect mTmpRect = new Rect();
 
         private TaskResizeInputEventReceiver(
                 InputChannel inputChannel, Handler handler, Choreographer choreographer) {
@@ -477,14 +478,15 @@
         }
 
         private void updateInputSinkRegionForDrag(Rect taskBounds) {
+            mTmpRect.set(taskBounds);
             final DisplayLayout layout = mDisplayController.getDisplayLayout(mDisplayId);
             final Region dragTouchRegion = new Region(-taskBounds.left,
                     -taskBounds.top,
                     -taskBounds.left + layout.width(),
                     -taskBounds.top + layout.height());
             // Remove the localized task bounds from the touch region.
-            taskBounds.offsetTo(0, 0);
-            dragTouchRegion.op(taskBounds, Region.Op.DIFFERENCE);
+            mTmpRect.offsetTo(0, 0);
+            dragTouchRegion.op(mTmpRect, Region.Op.DIFFERENCE);
             updateSinkInputChannel(dragTouchRegion);
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java
index 368231e..b0d3b50 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java
@@ -125,6 +125,7 @@
 
         relayout(taskBounds, t);
         if (fadeIn) {
+            cancelAnimation();
             mVeilAnimator = new ValueAnimator();
             mVeilAnimator.setFloatValues(0f, 1f);
             mVeilAnimator.setDuration(RESIZE_ALPHA_DURATION);
@@ -210,15 +211,16 @@
      * Animate veil's alpha to 0, fading it out.
      */
     public void hideVeil() {
-        final ValueAnimator animator = new ValueAnimator();
-        animator.setFloatValues(1, 0);
-        animator.setDuration(RESIZE_ALPHA_DURATION);
-        animator.addUpdateListener(animation -> {
+        cancelAnimation();
+        mVeilAnimator = new ValueAnimator();
+        mVeilAnimator.setFloatValues(1, 0);
+        mVeilAnimator.setDuration(RESIZE_ALPHA_DURATION);
+        mVeilAnimator.addUpdateListener(animation -> {
             SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
-            t.setAlpha(mVeilSurface, 1 - animator.getAnimatedFraction());
+            t.setAlpha(mVeilSurface, 1 - mVeilAnimator.getAnimatedFraction());
             t.apply();
         });
-        animator.addListener(new AnimatorListenerAdapter() {
+        mVeilAnimator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
                 SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
@@ -226,7 +228,7 @@
                 t.apply();
             }
         });
-        animator.start();
+        mVeilAnimator.start();
     }
 
     @ColorRes
@@ -240,10 +242,20 @@
         }
     }
 
+    private void cancelAnimation() {
+        if (mVeilAnimator != null) {
+            mVeilAnimator.removeAllUpdateListeners();
+            mVeilAnimator.cancel();
+        }
+    }
+
     /**
      * Dispose of veil when it is no longer needed, likely on close of its container decor.
      */
     void dispose() {
+        cancelAnimation();
+        mVeilAnimator = null;
+
         if (mViewHost != null) {
             mViewHost.release();
             mViewHost = null;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index 193f16d..40e61dd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -27,6 +27,8 @@
 
 import android.app.ActivityManager;
 import android.content.ComponentName;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
@@ -202,6 +204,8 @@
                 .setTaskDescriptionBuilder(taskDescriptionBuilder)
                 .setVisible(visible)
                 .build();
+        taskInfo.topActivityInfo = new ActivityInfo();
+        taskInfo.topActivityInfo.applicationInfo = new ApplicationInfo();
         taskInfo.realActivity = new ComponentName("com.android.wm.shell.windowdecor",
                 "DesktopModeWindowDecorationTests");
         taskInfo.baseActivity = new ComponentName("com.android.wm.shell.windowdecor",
diff --git a/location/java/android/location/flags/gnss.aconfig b/location/java/android/location/flags/location.aconfig
similarity index 88%
rename from location/java/android/location/flags/gnss.aconfig
rename to location/java/android/location/flags/location.aconfig
index 8c7c871..32ad09c 100644
--- a/location/java/android/location/flags/gnss.aconfig
+++ b/location/java/android/location/flags/location.aconfig
@@ -1,6 +1,13 @@
 package: "android.location.flags"
 
 flag {
+    name: "fix_service_watcher"
+    namespace: "location"
+    description: "Enable null explicit services in ServiceWatcher"
+    bug: "311210517"
+}
+
+flag {
     name: "gnss_api_navic_l1"
     namespace: "location"
     description: "Flag for GNSS API for NavIC L1"
diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java
index 470a8ac..bfb4b42 100644
--- a/media/java/android/media/MediaCodec.java
+++ b/media/java/android/media/MediaCodec.java
@@ -17,6 +17,7 @@
 package android.media;
 
 import android.Manifest;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -43,21 +44,25 @@
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.nio.ReadOnlyBufferException;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.BitSet;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 
+import static com.android.media.codec.flags.Flags.FLAG_LARGE_AUDIO_FRAME;
 /**
  MediaCodec class can be used to access low-level media codecs, i.e. encoder/decoder components.
  It is part of the Android low-level multimedia support infrastructure (normally used together
@@ -1824,6 +1829,7 @@
     private static final String EOS_AND_DECODE_ONLY_ERROR_MESSAGE = "An input buffer cannot have "
             + "both BUFFER_FLAG_END_OF_STREAM and BUFFER_FLAG_DECODE_ONLY flags";
     private static final int CB_CRYPTO_ERROR = 6;
+    private static final int CB_LARGE_FRAME_OUTPUT_AVAILABLE = 7;
 
     private class EventHandler extends Handler {
         private MediaCodec mCodec;
@@ -1945,6 +1951,39 @@
                     break;
                 }
 
+                case CB_LARGE_FRAME_OUTPUT_AVAILABLE:
+                {
+                    int index = msg.arg2;
+                    ArrayDeque<BufferInfo> infos = (ArrayDeque<BufferInfo>)msg.obj;
+                    synchronized(mBufferLock) {
+                        switch (mBufferMode) {
+                            case BUFFER_MODE_LEGACY:
+                                validateOutputByteBuffersLocked(mCachedOutputBuffers,
+                                        index, infos);
+                                break;
+                            case BUFFER_MODE_BLOCK:
+                                while (mOutputFrames.size() <= index) {
+                                    mOutputFrames.add(null);
+                                }
+                                OutputFrame frame = mOutputFrames.get(index);
+                                if (frame == null) {
+                                    frame = new OutputFrame(index);
+                                    mOutputFrames.set(index, frame);
+                                }
+                                frame.setBufferInfos(infos);
+                                frame.setAccessible(true);
+                                break;
+                            default:
+                                throw new IllegalArgumentException(
+                                        "Unrecognized buffer mode: for large frame output");
+                        }
+                    }
+                    mCallback.onOutputBuffersAvailable(
+                            mCodec, index, infos);
+
+                    break;
+                }
+
                 case CB_ERROR:
                 {
                     mCallback.onError(mCodec, (MediaCodec.CodecException) msg.obj);
@@ -2836,11 +2875,72 @@
         }
     }
 
+    /**
+     * Submit multiple access units to the codec along with multiple
+     * {@link MediaCodec.BufferInfo} describing the contents of the buffer. This method
+     * is supported only in asynchronous mode. While this method can be used for all codecs,
+     * it is meant for buffer batching, which is only supported by codecs that advertise
+     * FEATURE_MultipleFrames. Other codecs will not output large output buffers via
+     * onOutputBuffersAvailable, and instead will output single-access-unit output via
+     * onOutputBufferAvailable.
+     * <p>
+     * Output buffer size can be configured using the following MediaFormat keys.
+     * {@link MediaFormat#KEY_BUFFER_BATCH_MAX_OUTPUT_SIZE} and
+     * {@link MediaFormat#KEY_BUFFER_BATCH_THRESHOLD_OUTPUT_SIZE}.
+     * Details for each access unit present in the buffer should be described using
+     * {@link MediaCodec.BufferInfo}. Access units must be laid out contiguously (without any gaps)
+     * and in order. Multiple access units in the output if present, will be available in
+     * {@link Callback#onOutputBuffersAvailable} or {@link Callback#onOutputBufferAvailable}
+     * in case of single-access-unit output or when output does not contain any buffers,
+     * such as flags.
+     * <p>
+     * All other details for populating {@link MediaCodec.BufferInfo} is the same as described in
+     * {@link #queueInputBuffer}.
+     *
+     * @param index The index of a client-owned input buffer previously returned
+     *              in a call to {@link #dequeueInputBuffer}.
+     * @param bufferInfos ArrayDeque of {@link MediaCodec.BufferInfo} that describes the
+     *                    contents in the buffer. The ArrayDeque and the BufferInfo objects provided
+     *                    can be recycled by the caller for re-use.
+     * @throws IllegalStateException if not in the Executing state or not in asynchronous mode.
+     * @throws MediaCodec.CodecException upon codec error.
+     * @throws IllegalArgumentException upon if bufferInfos is empty, contains null, or if the
+     *                    access units are not contiguous.
+     * @throws CryptoException if a crypto object has been specified in
+     *         {@link #configure}
+     */
+    @FlaggedApi(FLAG_LARGE_AUDIO_FRAME)
+    public final void queueInputBuffers(
+            int index,
+            @NonNull ArrayDeque<BufferInfo> bufferInfos) {
+        synchronized(mBufferLock) {
+            if (mBufferMode == BUFFER_MODE_BLOCK) {
+                throw new IncompatibleWithBlockModelException("queueInputBuffers() "
+                        + "is not compatible with CONFIGURE_FLAG_USE_BLOCK_MODEL. "
+                        + "Please use getQueueRequest() to queue buffers");
+            }
+            invalidateByteBufferLocked(mCachedInputBuffers, index, true /* input */);
+            mDequeuedInputBuffers.remove(index);
+        }
+        try {
+            native_queueInputBuffers(
+                    index, bufferInfos.toArray());
+        } catch (CryptoException | IllegalStateException | IllegalArgumentException e) {
+            revalidateByteBuffer(mCachedInputBuffers, index, true /* input */);
+            throw e;
+        }
+    }
+
     private native final void native_queueInputBuffer(
             int index,
             int offset, int size, long presentationTimeUs, int flags)
         throws CryptoException;
 
+    private native final void native_queueInputBuffers(
+            int index,
+            @NonNull Object[] infos)
+        throws CryptoException, CodecException;
+
     public static final int CRYPTO_MODE_UNENCRYPTED = 0;
     public static final int CRYPTO_MODE_AES_CTR     = 1;
     public static final int CRYPTO_MODE_AES_CBC     = 2;
@@ -3464,6 +3564,26 @@
         }
 
         /**
+         * Sets MediaCodec.BufferInfo objects describing the access units
+         * contained in this queue request. Access units must be laid out
+         * contiguously without gaps and in order.
+         *
+         * @param infos Represents {@link MediaCodec.BufferInfo} objects to mark
+         *              individual access-unit boundaries and the timestamps associated with it.
+         *              The buffer is expected to contain the data in a continuous manner.
+         * @return this object
+         */
+        @FlaggedApi(FLAG_LARGE_AUDIO_FRAME)
+        public @NonNull QueueRequest setBufferInfos(@NonNull ArrayDeque<BufferInfo> infos) {
+            if (!isAccessible()) {
+                throw new IllegalStateException("The request is stale");
+            }
+            mBufferInfos.clear();
+            mBufferInfos.addAll(infos);
+            return this;
+        }
+
+        /**
          * Add an integer parameter.
          * See {@link MediaFormat} for an exhaustive list of supported keys with
          * values of type int, that can also be set with {@link MediaFormat#setInteger}.
@@ -3579,10 +3699,18 @@
                 throw new IllegalStateException("No block is set");
             }
             setAccessible(false);
+            if (mBufferInfos.isEmpty()) {
+                BufferInfo info = new BufferInfo();
+                info.size = mSize;
+                info.offset = mOffset;
+                info.presentationTimeUs = mPresentationTimeUs;
+                info.flags = mFlags;
+                mBufferInfos.add(info);
+            }
             if (mLinearBlock != null) {
                 mCodec.native_queueLinearBlock(
-                        mIndex, mLinearBlock, mOffset, mSize, mCryptoInfo,
-                        mPresentationTimeUs, mFlags,
+                        mIndex, mLinearBlock, mCryptoInfo,
+                        mBufferInfos.toArray(),
                         mTuningKeys, mTuningValues);
             } else if (mHardwareBuffer != null) {
                 mCodec.native_queueHardwareBuffer(
@@ -3600,6 +3728,7 @@
             mHardwareBuffer = null;
             mPresentationTimeUs = 0;
             mFlags = 0;
+            mBufferInfos.clear();
             mTuningKeys.clear();
             mTuningValues.clear();
             return this;
@@ -3623,6 +3752,7 @@
         private HardwareBuffer mHardwareBuffer = null;
         private long mPresentationTimeUs = 0;
         private @BufferFlag int mFlags = 0;
+        private final ArrayDeque<BufferInfo> mBufferInfos = new ArrayDeque<>();
         private final ArrayList<String> mTuningKeys = new ArrayList<>();
         private final ArrayList<Object> mTuningValues = new ArrayList<>();
 
@@ -3632,11 +3762,8 @@
     private native void native_queueLinearBlock(
             int index,
             @NonNull LinearBlock block,
-            int offset,
-            int size,
             @Nullable CryptoInfo cryptoInfo,
-            long presentationTimeUs,
-            int flags,
+            @NonNull Object[] bufferInfos,
             @NonNull ArrayList<String> keys,
             @NonNull ArrayList<Object> values);
 
@@ -4050,6 +4177,27 @@
         }
     }
 
+    private void validateOutputByteBuffersLocked(
+        @Nullable ByteBuffer[] buffers, int index, @NonNull ArrayDeque<BufferInfo> infoDeque) {
+        Optional<BufferInfo> minInfo = infoDeque.stream().min(
+                (info1, info2) -> Integer.compare(info1.offset, info2.offset));
+        Optional<BufferInfo> maxInfo = infoDeque.stream().max(
+                (info1, info2) -> Integer.compare(info1.offset, info2.offset));
+        if (buffers == null) {
+            if (index >= 0) {
+                mValidOutputIndices.set(index);
+            }
+        } else if (index >= 0 && index < buffers.length) {
+            ByteBuffer buffer = buffers[index];
+            if (buffer != null && minInfo.isPresent() && maxInfo.isPresent()) {
+                buffer.setAccessible(true);
+                buffer.limit(maxInfo.get().offset + maxInfo.get().size);
+                buffer.position(minInfo.get().offset);
+            }
+        }
+
+    }
+
     private void validateOutputByteBufferLocked(
             @Nullable ByteBuffer[] buffers, int index, @NonNull BufferInfo info) {
         if (buffers == null) {
@@ -4407,6 +4555,22 @@
             return mFlags;
         }
 
+        /*
+         * Returns the BufferInfos associated with this OutputFrame. These BufferInfos
+         * describes the access units present in the OutputFrame. Access units are laid
+         * out contiguously without gaps and in order.
+         */
+        @FlaggedApi(FLAG_LARGE_AUDIO_FRAME)
+        public @NonNull ArrayDeque<BufferInfo> getBufferInfos() {
+            if (mBufferInfos.isEmpty()) {
+                // single BufferInfo could be present.
+                BufferInfo bufferInfo = new BufferInfo();
+                bufferInfo.set(0, 0, mPresentationTimeUs, mFlags);
+                mBufferInfos.add(bufferInfo);
+            }
+            return mBufferInfos;
+        }
+
         /**
          * Returns a read-only {@link MediaFormat} for this frame. The returned
          * object is valid only until the client calls {@link MediaCodec#releaseOutputBuffer}.
@@ -4432,6 +4596,7 @@
             mLinearBlock = null;
             mHardwareBuffer = null;
             mFormat = null;
+            mBufferInfos.clear();
             mChangedKeys.clear();
             mKeySet.clear();
             mLoaded = false;
@@ -4450,6 +4615,11 @@
             mFlags = info.flags;
         }
 
+        void setBufferInfos(ArrayDeque<BufferInfo> infos) {
+            mBufferInfos.clear();
+            mBufferInfos.addAll(infos);
+        }
+
         boolean isLoaded() {
             return mLoaded;
         }
@@ -4464,6 +4634,7 @@
         private long mPresentationTimeUs = 0;
         private @BufferFlag int mFlags = 0;
         private MediaFormat mFormat = null;
+        private final ArrayDeque<BufferInfo> mBufferInfos = new ArrayDeque<>();
         private final ArrayList<String> mChangedKeys = new ArrayList<>();
         private final Set<String> mKeySet = new HashSet<>();
         private boolean mAccessible = false;
@@ -5172,6 +5343,32 @@
                 @NonNull MediaCodec codec, int index, @NonNull BufferInfo info);
 
         /**
+         * Called when multiple access-units are available in the output.
+         *
+         * @param codec The MediaCodec object.
+         * @param index The index of the available output buffer.
+         * @param infos Infos describing the available output buffer {@link MediaCodec.BufferInfo}.
+         *              Access units present in the output buffer are laid out contiguously
+         *              without gaps and in order.
+         */
+        @FlaggedApi(FLAG_LARGE_AUDIO_FRAME)
+        public void onOutputBuffersAvailable(
+                @NonNull MediaCodec codec, int index, @NonNull ArrayDeque<BufferInfo> infos) {
+            /*
+             * This callback returns multiple BufferInfos when codecs are configured to operate on
+             * large audio frame. Since at this point, we have a single large buffer, returning
+             * each BufferInfo using
+             * {@link Callback#onOutputBufferAvailable onOutputBufferAvailable} may cause the
+             * index to be released to the codec using {@link MediaCodec#releaseOutputBuffer}
+             * before all BuffersInfos can be returned to the client.
+             * Hence this callback is required to be implemented or else an exception is thrown.
+             */
+            throw new IllegalStateException(
+                    "Client must override onOutputBuffersAvailable when codec is " +
+                    "configured to operate with multiple access units");
+        }
+
+        /**
          * Called when the MediaCodec encountered an error
          *
          * @param codec The MediaCodec object.
diff --git a/media/java/android/media/MediaMuxer.java b/media/java/android/media/MediaMuxer.java
index 30f4562..80b606c 100644
--- a/media/java/android/media/MediaMuxer.java
+++ b/media/java/android/media/MediaMuxer.java
@@ -514,7 +514,7 @@
      * The following table summarizes support for specific format keys across android releases.
      * Keys marked with '+:' are required.
      *
-     * <table style="width: 0%">
+     * <table>
      *  <thead>
      *   <tr>
      *    <th rowspan=2>OS Version(s)</th>
@@ -583,7 +583,7 @@
      * <p>
      * The following table summarizes codec support for containers across android releases:
      *
-     * <table style="width: 0%">
+     * <table>
      *  <thead>
      *   <tr>
      *    <th rowspan=2>OS Version(s)</th>
diff --git a/media/java/android/media/MediaRecorder.java b/media/java/android/media/MediaRecorder.java
index aa30748..bdfa6301 100644
--- a/media/java/android/media/MediaRecorder.java
+++ b/media/java/android/media/MediaRecorder.java
@@ -65,7 +65,8 @@
  *
  * <p>A common case of using MediaRecorder to record audio works as follows:
  *
- * <pre>MediaRecorder recorder = new MediaRecorder();
+ * <pre>
+ * MediaRecorder recorder = new MediaRecorder(context);
  * recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
  * recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
  * recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
diff --git a/media/java/android/media/projection/IMediaProjection.aidl b/media/java/android/media/projection/IMediaProjection.aidl
index 388b2c5..2fb0af5 100644
--- a/media/java/android/media/projection/IMediaProjection.aidl
+++ b/media/java/android/media/projection/IMediaProjection.aidl
@@ -18,6 +18,7 @@
 
 import android.media.projection.IMediaProjectionCallback;
 import android.os.IBinder;
+import android.app.ActivityOptions.LaunchCookie;
 
 /** {@hide} */
 interface IMediaProjection {
@@ -38,22 +39,22 @@
     void unregisterCallback(IMediaProjectionCallback callback);
 
     /**
-     * Returns the {@link android.os.IBinder} identifying the task to record, or {@code null} if
+     * Returns the {@link LaunchCookie} identifying the task to record, or {@code null} if
      * there is none.
      */
     @EnforcePermission("MANAGE_MEDIA_PROJECTION")
     @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
             + ".permission.MANAGE_MEDIA_PROJECTION)")
-    IBinder getLaunchCookie();
+    LaunchCookie getLaunchCookie();
 
     /**
-     * Updates the {@link android.os.IBinder} identifying the task to record, or {@code null} if
+     * Updates the {@link LaunchCookie} identifying the task to record, or {@code null} if
      * there is none.
      */
     @EnforcePermission("MANAGE_MEDIA_PROJECTION")
     @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
             + ".permission.MANAGE_MEDIA_PROJECTION)")
-    void setLaunchCookie(in IBinder launchCookie);
+    void setLaunchCookie(in LaunchCookie launchCookie);
 
     /**
      * Returns {@code true} if this token is still valid. A token is valid as long as the token
diff --git a/media/java/android/media/projection/MediaProjectionInfo.java b/media/java/android/media/projection/MediaProjectionInfo.java
index c820392..cd0763d 100644
--- a/media/java/android/media/projection/MediaProjectionInfo.java
+++ b/media/java/android/media/projection/MediaProjectionInfo.java
@@ -16,7 +16,7 @@
 
 package android.media.projection;
 
-import android.os.IBinder;
+import android.app.ActivityOptions.LaunchCookie;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.UserHandle;
@@ -27,9 +27,9 @@
 public final class MediaProjectionInfo implements Parcelable {
     private final String mPackageName;
     private final UserHandle mUserHandle;
-    private final IBinder mLaunchCookie;
+    private final LaunchCookie mLaunchCookie;
 
-    public MediaProjectionInfo(String packageName, UserHandle handle, IBinder launchCookie) {
+    public MediaProjectionInfo(String packageName, UserHandle handle, LaunchCookie launchCookie) {
         mPackageName = packageName;
         mUserHandle = handle;
         mLaunchCookie = launchCookie;
@@ -38,7 +38,7 @@
     public MediaProjectionInfo(Parcel in) {
         mPackageName = in.readString();
         mUserHandle = UserHandle.readFromParcel(in);
-        mLaunchCookie = in.readStrongBinder();
+        mLaunchCookie = LaunchCookie.readFromParcel(in);
     }
 
     public String getPackageName() {
@@ -49,7 +49,7 @@
         return mUserHandle;
     }
 
-    public IBinder getLaunchCookie() {
+    public LaunchCookie getLaunchCookie() {
         return mLaunchCookie;
     }
 
@@ -72,7 +72,7 @@
     public String toString() {
         return "MediaProjectionInfo{mPackageName="
             + mPackageName + ", mUserHandle="
-            + mUserHandle + ", mLaunchCookie"
+            + mUserHandle + ", mLaunchCookie="
             + mLaunchCookie + "}";
     }
 
@@ -85,7 +85,7 @@
     public void writeToParcel(Parcel out, int flags) {
         out.writeString(mPackageName);
         UserHandle.writeToParcel(mUserHandle, out);
-        out.writeStrongBinder(mLaunchCookie);
+        LaunchCookie.writeToParcel(mLaunchCookie, out);
     }
 
     public static final @android.annotation.NonNull Parcelable.Creator<MediaProjectionInfo> CREATOR =
diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java
index 9790d02..e3290d6 100644
--- a/media/java/android/media/projection/MediaProjectionManager.java
+++ b/media/java/android/media/projection/MediaProjectionManager.java
@@ -18,8 +18,11 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.annotation.SystemService;
+import android.annotation.TestApi;
 import android.app.Activity;
+import android.app.ActivityOptions.LaunchCookie;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -73,6 +76,9 @@
     /** @hide */
     public static final String EXTRA_MEDIA_PROJECTION =
             "android.media.projection.extra.EXTRA_MEDIA_PROJECTION";
+    /** @hide */
+    public static final String EXTRA_LAUNCH_COOKIE =
+            "android.media.projection.extra.EXTRA_LAUNCH_COOKIE";
 
     /** @hide */
     public static final int TYPE_SCREEN_CAPTURE = 0;
@@ -158,17 +164,29 @@
      */
     @NonNull
     public Intent createScreenCaptureIntent(@NonNull MediaProjectionConfig config) {
-        Intent i = new Intent();
-        final ComponentName mediaProjectionPermissionDialogComponent =
-                ComponentName.unflattenFromString(mContext.getResources()
-                        .getString(com.android.internal.R.string
-                                .config_mediaProjectionPermissionDialogComponent));
-        i.setComponent(mediaProjectionPermissionDialogComponent);
+        Intent i = createScreenCaptureIntent();
         i.putExtra(EXTRA_MEDIA_PROJECTION_CONFIG, config);
         return i;
     }
 
     /**
+     * Returns an intent similar to {@link #createScreenCaptureIntent()} that will enable screen
+     * recording of the task with the specified launch cookie. This method should only be used for
+     * testing.
+     *
+     * @param launchCookie the launch cookie corresponding to the task to record.
+     * @hide
+     */
+    @SuppressLint("UnflaggedApi")
+    @TestApi
+    @NonNull
+    public Intent createScreenCaptureIntent(@Nullable LaunchCookie launchCookie) {
+        Intent i = createScreenCaptureIntent();
+        i.putExtra(EXTRA_LAUNCH_COOKIE, launchCookie);
+        return i;
+    }
+
+    /**
      * Retrieves the {@link MediaProjection} obtained from a successful screen
      * capture request. The result code and data from the request are provided by overriding
      * {@link Activity#onActivityResult(int, int, Intent) onActivityResult(int, int, Intent)},
diff --git a/media/java/android/media/tv/SignalingDataRequest.aidl b/media/java/android/media/tv/SignalingDataRequest.aidl
new file mode 100644
index 0000000..29e89fe
--- /dev/null
+++ b/media/java/android/media/tv/SignalingDataRequest.aidl
@@ -0,0 +1,3 @@
+package android.media.tv;
+
+parcelable SignalingDataRequest;
diff --git a/media/java/android/media/tv/SignalingDataRequest.java b/media/java/android/media/tv/SignalingDataRequest.java
new file mode 100644
index 0000000..dcf1d48
--- /dev/null
+++ b/media/java/android/media/tv/SignalingDataRequest.java
@@ -0,0 +1,170 @@
+/*
+ * 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.media.tv;
+
+import android.annotation.NonNull;
+import android.os.Parcelable;
+
+/**
+ * Request to retrieve the Low-level Signalling Tables (LLS) and Service-layer Signalling (SLS)
+ * metadata.
+ *
+ * <p>For more details on each type of metadata that can be requested, refer to the ATSC standard
+ * A/344:2023-5 9.2.10 - Query Signaling Data API.
+ *
+ * @hide
+ */
+public class SignalingDataRequest extends BroadcastInfoRequest implements Parcelable {
+    private static final @TvInputManager.BroadcastInfoType int REQUEST_TYPE =
+            TvInputManager.BROADCAST_INFO_TYPE_SIGNALING_DATA;
+
+    public static final @NonNull Parcelable.Creator<SignalingDataRequest> CREATOR =
+            new Parcelable.Creator<SignalingDataRequest>() {
+                @Override
+                public SignalingDataRequest[] newArray(int size) {
+                    return new SignalingDataRequest[size];
+                }
+
+                @Override
+                public SignalingDataRequest createFromParcel(@NonNull android.os.Parcel in) {
+                    return new SignalingDataRequest(in);
+                }
+            };
+
+    /** SLS Metadata: All metadata objects for the requested service(s) */
+    public static final int SLS_METADATA_ALL = 0x7FFFFFF;
+
+    /** SLS Metadata: APD for the requested service(s) */
+    public static final int SLS_METADATA_APD = 1;
+
+    /** SLS Metadata: USBD for the requested service(s) */
+    public static final int SLS_METADATA_USBD = 1 << 1;
+
+    /** SLS Metadata: S-TSID for the requested service(s) */
+    public static final int SLS_METADATA_STSID = 1 << 2;
+
+    /** SLS Metadata: DASH MPD for the requested service(s) */
+    public static final int SLS_METADATA_MPD = 1 << 3;
+
+    /** SLS Metadata: User Service Description for MMTP */
+    public static final int SLS_METADATA_USD = 1 << 4;
+
+    /** SLS Metadata: MMT Package Access Table for the requested service(s) */
+    public static final int SLS_METADATA_PAT = 1 << 5;
+
+    /** SLS Metadata: MMT Package Table for the requested service(s) */
+    public static final int SLS_METADATA_MPT = 1 << 6;
+
+    /** SLS Metadata: MMT Media Presentation Information Table for the requested service(s) */
+    public static final int SLS_METADATA_MPIT = 1 << 7;
+
+    /** SLS Metadata: MMT Clock Relation Information for the requested service(s) */
+    public static final int SLS_METADATA_CRIT = 1 << 8;
+
+    /** SLS Metadata: MMT Device Capabilities Information Table for the requested service(s) */
+    public static final int SLS_METADATA_DCIT = 1 << 9;
+
+    /** SLS Metadata: HTML Entry Pages Location Description for the requested service(s) */
+    public static final int SLS_METADATA_HELD = 1 << 10;
+
+    /** SLS Metadata: Distribution Window Desciription for the requested service(s) */
+    public static final int SLS_METADATA_DWD = 1 << 11;
+
+    /** SLS Metadata: MMT Application Event Information for the requested service(s) */
+    public static final int SLS_METADATA_AEI = 1 << 12;
+
+    /** SLS Metadata: Video Stream Properties Descriptor */
+    public static final int SLS_METADATA_VSPD = 1 << 13;
+
+    /** SLS Metadata: ATSC Staggercast Descriptor */
+    public static final int SLS_METADATA_ASD = 1 << 14;
+
+    /** SLS Metadata: Inband Event Descriptor */
+    public static final int SLS_METADATA_IED = 1 << 15;
+
+    /** SLS Metadata: Caption Asset Descriptor */
+    public static final int SLS_METADATA_CAD = 1 << 16;
+
+    /** SLS Metadata: Audio Stream Properties Descriptor */
+    public static final int SLS_METADATA_ASPD = 1 << 17;
+
+    /** SLS Metadata: Security Properties Descriptor */
+    public static final int SLS_METADATA_SSD = 1 << 18;
+
+    /** SLS Metadata: ROUTE/DASH Application Dynamic Event for the requested service(s) */
+    public static final int SLS_METADATA_EMSG = 1 << 19;
+
+    /** SLS Metadata: MMT Application Dynamic Event for the requested service(s) */
+    public static final int SLS_METADATA_EVTI = 1 << 20;
+
+    /** Regional Service Availability Table for the requested service(s) */
+    public static final int SLS_METADATA_RSAT = 1 << 21;
+
+    private final int mGroup;
+    private @NonNull final int[] mLlsTableIds;
+    private final int mSlsMetadataTypes;
+
+    SignalingDataRequest(
+            int requestId,
+            int option,
+            int group,
+            @NonNull int[] llsTableIds,
+            int slsMetadataTypes) {
+        super(REQUEST_TYPE, requestId, option);
+        mGroup = group;
+        mLlsTableIds = llsTableIds;
+        mSlsMetadataTypes = slsMetadataTypes;
+    }
+
+    SignalingDataRequest(@NonNull android.os.Parcel in) {
+        super(REQUEST_TYPE, in);
+
+        int group = in.readInt();
+        int[] llsTableIds = in.createIntArray();
+        int slsMetadataTypes = in.readInt();
+
+        this.mGroup = group;
+        this.mLlsTableIds = llsTableIds;
+        com.android.internal.util.AnnotationValidations.validate(NonNull.class, null, mLlsTableIds);
+        this.mSlsMetadataTypes = slsMetadataTypes;
+    }
+
+    public int getGroup() {
+        return mGroup;
+    }
+
+    public @NonNull int[] getLlsTableIds() {
+        return mLlsTableIds;
+    }
+
+    public int getSlsMetadataTypes() {
+        return mSlsMetadataTypes;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeInt(mGroup);
+        dest.writeIntArray(mLlsTableIds);
+        dest.writeInt(mSlsMetadataTypes);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+}
diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java
index 2b31bfe..be1b675 100644
--- a/media/java/android/media/tv/TvInputManager.java
+++ b/media/java/android/media/tv/TvInputManager.java
@@ -489,10 +489,19 @@
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
-    @IntDef(prefix = "BROADCAST_INFO_TYPE_", value =
-            {BROADCAST_INFO_TYPE_TS, BROADCAST_INFO_TYPE_TABLE, BROADCAST_INFO_TYPE_SECTION,
-            BROADCAST_INFO_TYPE_PES, BROADCAST_INFO_STREAM_EVENT, BROADCAST_INFO_TYPE_DSMCC,
-            BROADCAST_INFO_TYPE_COMMAND, BROADCAST_INFO_TYPE_TIMELINE})
+    @IntDef(
+            prefix = "BROADCAST_INFO_TYPE_",
+            value = {
+                BROADCAST_INFO_TYPE_TS,
+                BROADCAST_INFO_TYPE_TABLE,
+                BROADCAST_INFO_TYPE_SECTION,
+                BROADCAST_INFO_TYPE_PES,
+                BROADCAST_INFO_STREAM_EVENT,
+                BROADCAST_INFO_TYPE_DSMCC,
+                BROADCAST_INFO_TYPE_COMMAND,
+                BROADCAST_INFO_TYPE_TIMELINE,
+                BROADCAST_INFO_TYPE_SIGNALING_DATA
+            })
     public @interface BroadcastInfoType {}
 
     public static final int BROADCAST_INFO_TYPE_TS = 1;
@@ -505,6 +514,9 @@
     public static final int BROADCAST_INFO_TYPE_TIMELINE = 8;
 
     /** @hide */
+    public static final int BROADCAST_INFO_TYPE_SIGNALING_DATA = 9;
+
+    /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(prefix = "SIGNAL_STRENGTH_",
             value = {SIGNAL_STRENGTH_LOST, SIGNAL_STRENGTH_WEAK, SIGNAL_STRENGTH_STRONG})
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
index 7b58531..1404d7c 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
@@ -63,6 +63,8 @@
     void onRequestTvRecordingInfoList(in int type, int seq);
     void onRequestSigning(in String id, in String algorithm, in String alias, in byte[] data,
             int seq);
+    void onRequestSigning2(in String id, in String algorithm, in String host,
+            int port, in byte[] data, int seq);
     void onRequestCertificate(in String host, int port, int seq);
     void onAdRequest(in AdRequest request, int Seq);
 }
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
index cb89181..3c91a3e 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
@@ -61,6 +61,7 @@
     void onRequestTvRecordingInfo(in String recordingId);
     void onRequestTvRecordingInfoList(in int type);
     void onRequestSigning(in String id, in String algorithm, in String alias, in byte[] data);
+    void onRequestSigning2(in String id, in String algorithm, in String host, int port, in byte[] data);
     void onRequestCertificate(in String host, int port);
     void onAdRequest(in AdRequest request);
 }
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
index 011744f..498eec6 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
@@ -657,6 +657,19 @@
             }
 
             @Override
+            public void onRequestSigning2(
+                    String id, String algorithm, String host, int port, byte[] data, int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq " + seq);
+                        return;
+                    }
+                    record.postRequestSigning(id, algorithm, host, port, data);
+                }
+            }
+
+            @Override
             public void onRequestCertificate(String host, int port, int seq) {
                 synchronized (mSessionCallbackRecordMap) {
                     SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
@@ -2258,6 +2271,17 @@
             });
         }
 
+        void postRequestSigning(String id, String algorithm, String host, int port,
+                byte[] data) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mSessionCallback.onRequestSigning(mSession, id, algorithm, host,
+                            port, data);
+                }
+            });
+        }
+
         void postRequestCertificate(String host, int port) {
             mHandler.post(new Runnable() {
                 @Override
@@ -2609,6 +2633,25 @@
         }
 
         /**
+         * This is called when
+         * {@link TvInteractiveAppService.Session#requestSigning(String, String, String, int, byte[])} is
+         * called.
+         *
+         * @param session A {@link TvInteractiveAppService.Session} associated with this callback.
+         * @param signingId the ID to identify the request.
+         * @param algorithm the standard name of the signature algorithm requested, such as
+         *                  MD5withRSA, SHA256withDSA, etc.
+         * @param host The host of the SSL CLient Authentication Server
+         * @param port The port of the SSL Client Authentication Server
+         * @param data the original bytes to be signed.
+         * @hide
+         */
+        public void onRequestSigning(
+                Session session, String signingId, String algorithm, String host,
+                int port, byte[] data) {
+        }
+
+        /**
          * This is called when the service requests a SSL certificate for client validation.
          *
          * @param session A {@link TvInteractiveAppService.Session} associated with this callback.
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
index 054b272..7b6dc38 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
@@ -1645,6 +1645,50 @@
         }
 
         /**
+         * Requests signing of the given data.
+         *
+         * <p>This is used when the corresponding server of the broadcast-independent interactive
+         * app requires signing during handshaking, and the interactive app service doesn't have
+         * the built-in private key. The private key is provided by the content providers and
+         * pre-built in the related app, such as TV app.
+         *
+         * @param signingId the ID to identify the request. When a result is received, this ID can
+         *                  be used to correlate the result with the request.
+         * @param algorithm the standard name of the signature algorithm requested, such as
+         *                  MD5withRSA, SHA256withDSA, etc. The name is from standards like
+         *                  FIPS PUB 186-4 and PKCS #1.
+         * @param host the host of the SSL client authentication server.
+         * @param port the port of the SSL client authentication server.
+         * @param data the original bytes to be signed.
+         *
+         * @see #onSigningResult(String, byte[])
+         * @see TvInteractiveAppView#createBiInteractiveApp(Uri, Bundle)
+         * @see TvInteractiveAppView#BI_INTERACTIVE_APP_KEY_ALIAS
+         * @hide
+         */
+        @CallSuper
+        public void requestSigning(@NonNull String signingId, @NonNull String algorithm,
+                @NonNull String host, int port, @NonNull byte[] data) {
+            executeOrPostRunnableOnMainThread(new Runnable() {
+                @MainThread
+                @Override
+                public void run() {
+                    try {
+                        if (DEBUG) {
+                            Log.d(TAG, "requestSigning");
+                        }
+                        if (mSessionCallback != null) {
+                            mSessionCallback.onRequestSigning2(signingId, algorithm,
+                                    host, port, data);
+                        }
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "error in requestSigning", e);
+                    }
+                }
+            });
+        }
+
+        /**
          * Requests a SSL certificate for client validation.
          *
          * @param host the host name of the SSL authentication server.
diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp
index ef90bf9..8cdd59e 100644
--- a/media/jni/android_media_MediaCodec.cpp
+++ b/media/jni/android_media_MediaCodec.cpp
@@ -163,6 +163,13 @@
 static struct {
     jclass clazz;
     jmethodID ctorId;
+    jmethodID sizeId;
+    jmethodID addId;
+} gArrayDequeInfo;
+
+static struct {
+    jclass clazz;
+    jmethodID ctorId;
     jmethodID setInternalStateId;
     jfieldID contextId;
     jfieldID validId;
@@ -200,8 +207,14 @@
     jfieldID queueRequestIndexID;
     jfieldID outputFrameLinearBlockID;
     jfieldID outputFrameHardwareBufferID;
+    jfieldID outputFramebufferInfosID;
     jfieldID outputFrameChangedKeysID;
     jfieldID outputFrameFormatID;
+    jfieldID bufferInfoFlags;
+    jfieldID bufferInfoOffset;
+    jfieldID bufferInfoSize;
+    jfieldID bufferInfoPresentationTimeUs;
+
 };
 
 static fields_t gFields;
@@ -412,6 +425,22 @@
             index, offset, size, timeUs, flags, errorDetailMsg);
 }
 
+status_t JMediaCodec::queueInputBuffers(
+        size_t index,
+        size_t offset,
+        size_t size,
+        const sp<RefBase> &infos,
+        AString *errorDetailMsg) {
+
+    sp<BufferInfosWrapper> auInfo((BufferInfosWrapper *)infos.get());
+    return mCodec->queueInputBuffers(
+            index,
+            offset,
+            size,
+            auInfo,
+            errorDetailMsg);
+}
+
 status_t JMediaCodec::queueSecureInputBuffer(
         size_t index,
         size_t offset,
@@ -430,10 +459,11 @@
 }
 
 status_t JMediaCodec::queueBuffer(
-        size_t index, const std::shared_ptr<C2Buffer> &buffer, int64_t timeUs,
-        uint32_t flags, const sp<AMessage> &tunings, AString *errorDetailMsg) {
+        size_t index, const std::shared_ptr<C2Buffer> &buffer,
+        const sp<RefBase> &infos, const sp<AMessage> &tunings, AString *errorDetailMsg) {
+    sp<BufferInfosWrapper> auInfo((BufferInfosWrapper *)infos.get());
     return mCodec->queueBuffer(
-            index, buffer, timeUs, flags, tunings, errorDetailMsg);
+            index, buffer, auInfo, tunings, errorDetailMsg);
 }
 
 status_t JMediaCodec::queueEncryptedLinearBlock(
@@ -446,13 +476,13 @@
         const uint8_t iv[16],
         CryptoPlugin::Mode mode,
         const CryptoPlugin::Pattern &pattern,
-        int64_t presentationTimeUs,
-        uint32_t flags,
+        const sp<RefBase> &infos,
         const sp<AMessage> &tunings,
         AString *errorDetailMsg) {
+    sp<BufferInfosWrapper> auInfo((BufferInfosWrapper *)infos.get());
     return mCodec->queueEncryptedBuffer(
             index, buffer, offset, subSamples, numSubSamples, key, iv, mode, pattern,
-            presentationTimeUs, flags, tunings, errorDetailMsg);
+            auInfo, tunings, errorDetailMsg);
 }
 
 status_t JMediaCodec::dequeueInputBuffer(size_t *index, int64_t timeoutUs) {
@@ -722,6 +752,42 @@
     return OK;
 }
 
+void maybeSetBufferInfos(JNIEnv *env, jobject &frame, const sp<BufferInfosWrapper> &bufInfos) {
+    if (!bufInfos) {
+        return;
+    }
+    std::vector<AccessUnitInfo> &infos = bufInfos.get()->value;
+    if (infos.empty()) {
+        return;
+    }
+    ScopedLocalRef<jobject> dequeObj{env, env->NewObject(
+            gArrayDequeInfo.clazz, gArrayDequeInfo.ctorId)};
+    jint offset = 0;
+    std::vector<jobject> jObjectInfos;
+    for (int i = 0 ; i < infos.size(); i++) {
+        jobject bufferInfo = env->NewObject(
+                gBufferInfo.clazz, gBufferInfo.ctorId);
+        if (bufferInfo != NULL) {
+            env->CallVoidMethod(bufferInfo, gBufferInfo.setId,
+                    offset,
+                    (jint)(infos)[i].mSize,
+                    (infos)[i].mTimestamp,
+                    (infos)[i].mFlags);
+            (void)env->CallBooleanMethod(
+                    dequeObj.get(), gArrayDequeInfo.addId, bufferInfo);
+            offset += (infos)[i].mSize;
+            jObjectInfos.push_back(bufferInfo);
+        }
+    }
+    env->SetObjectField(
+            frame,
+            gFields.outputFramebufferInfosID,
+            dequeObj.get());
+    for (int i = 0; i < jObjectInfos.size(); i++) {
+        env->DeleteLocalRef(jObjectInfos[i]);
+    }
+}
+
 status_t JMediaCodec::getOutputFrame(
         JNIEnv *env, jobject frame, size_t index) const {
     sp<MediaCodecBuffer> buffer;
@@ -732,6 +798,11 @@
     }
 
     if (buffer->size() > 0) {
+        sp<RefBase> obj;
+        sp<BufferInfosWrapper> bufInfos;
+        if (buffer->meta()->findObject("accessUnitInfo", &obj)) {
+            bufInfos = std::move(((decltype(bufInfos.get()))obj.get()));
+        }
         std::shared_ptr<C2Buffer> c2Buffer = buffer->asC2Buffer();
         if (c2Buffer) {
             switch (c2Buffer->data().type()) {
@@ -747,6 +818,7 @@
                             (jlong)context.release(),
                             true);
                     env->SetObjectField(frame, gFields.outputFrameLinearBlockID, linearBlock.get());
+                    maybeSetBufferInfos(env, frame, bufInfos);
                     break;
                 }
                 case C2BufferData::GRAPHIC: {
@@ -787,6 +859,7 @@
                         (jlong)context.release(),
                         true);
                 env->SetObjectField(frame, gFields.outputFrameLinearBlockID, linearBlock.get());
+                maybeSetBufferInfos(env, frame, bufInfos);
             } else {
                 // No-op.
             }
@@ -1250,6 +1323,7 @@
 void JMediaCodec::handleCallback(const sp<AMessage> &msg) {
     int32_t arg1, arg2 = 0;
     jobject obj = NULL;
+    std::vector<jobject> jObjectInfos;
     CHECK(msg->findInt32("callbackID", &arg1));
     JNIEnv *env = AndroidRuntime::getJNIEnv();
 
@@ -1287,6 +1361,35 @@
             break;
         }
 
+        case MediaCodec::CB_LARGE_FRAME_OUTPUT_AVAILABLE:
+        {
+            sp<RefBase> spobj = nullptr;
+            CHECK(msg->findInt32("index", &arg2));
+            CHECK(msg->findObject("accessUnitInfo", &spobj));
+            if (spobj != nullptr) {
+                sp<BufferInfosWrapper> bufferInfoParamsWrapper {
+                        (BufferInfosWrapper *)spobj.get()};
+                std::vector<AccessUnitInfo> &bufferInfoParams =
+                        bufferInfoParamsWrapper.get()->value;
+                obj = env->NewObject(gArrayDequeInfo.clazz, gArrayDequeInfo.ctorId);
+                jint offset = 0;
+                for (int i = 0 ; i < bufferInfoParams.size(); i++) {
+                    jobject bufferInfo = env->NewObject(gBufferInfo.clazz, gBufferInfo.ctorId);
+                    if (bufferInfo != NULL) {
+                        env->CallVoidMethod(bufferInfo, gBufferInfo.setId,
+                                            offset,
+                                            (jint)(bufferInfoParams)[i].mSize,
+                                            (bufferInfoParams)[i].mTimestamp,
+                                            (bufferInfoParams)[i].mFlags);
+                        (void)env->CallBooleanMethod(obj, gArrayDequeInfo.addId, bufferInfo);
+                        offset += (bufferInfoParams)[i].mSize;
+                        jObjectInfos.push_back(bufferInfo);
+                    }
+                }
+            }
+            break;
+        }
+
         case MediaCodec::CB_CRYPTO_ERROR:
         {
             int32_t err, actionCode;
@@ -1346,6 +1449,9 @@
             arg2,
             obj);
 
+    for (int i = 0; i < jObjectInfos.size(); i++) {
+        env->DeleteLocalRef(jObjectInfos[i]);
+    }
     env->DeleteLocalRef(obj);
 }
 
@@ -1913,6 +2019,103 @@
             codec->getExceptionMessage(errorDetailMsg.c_str()).c_str());
 }
 
+static status_t extractInfosFromObject(
+        JNIEnv * const env,
+        jint * const initialOffset,
+        jint * const totalSize,
+        std::vector<AccessUnitInfo> * const infos,
+        const jobjectArray &objArray,
+        AString * const errorDetailMsg) {
+    if (totalSize == nullptr
+            || initialOffset == nullptr
+            || infos == nullptr) {
+        if (errorDetailMsg) {
+            *errorDetailMsg = "Error: Null arguments provided for extracting Access unit info";
+        }
+        return BAD_VALUE;
+    }
+    const jsize numEntries = env->GetArrayLength(objArray);
+    if (numEntries <= 0) {
+        if (errorDetailMsg) {
+            *errorDetailMsg = "Error: No BufferInfo found while queuing for large frame input";
+        }
+        return BAD_VALUE;
+    }
+    *initialOffset = 0;
+    *totalSize = 0;
+    for (jsize i = 0; i < numEntries; i++) {
+        jobject param = env->GetObjectArrayElement(objArray, i);
+        if (param == NULL) {
+            if (errorDetailMsg) {
+                *errorDetailMsg = "Error: Queuing a null BufferInfo";
+            }
+            return BAD_VALUE;
+        }
+        size_t offset = static_cast<size_t>(env->GetIntField(param, gFields.bufferInfoOffset));
+        size_t size = static_cast<size_t>(env->GetIntField(param, gFields.bufferInfoSize));
+        uint32_t flags = static_cast<uint32_t>(env->GetIntField(param, gFields.bufferInfoFlags));
+        if (flags == 0 && size == 0) {
+            if (errorDetailMsg) {
+                *errorDetailMsg = "Error: Queuing an empty BufferInfo";
+            }
+            return BAD_VALUE;
+        }
+        if (i == 0) {
+            *initialOffset = offset;
+        }
+        if (CC_UNLIKELY((offset >  UINT32_MAX)
+                || ((long)(offset + size) > UINT32_MAX)
+                || ((offset - *initialOffset) != *totalSize))) {
+            if (errorDetailMsg) {
+                *errorDetailMsg = "Error: offset/size in BufferInfo";
+            }
+            return BAD_VALUE;
+        }
+        infos->emplace_back(
+                flags,
+                size,
+                env->GetLongField(param, gFields.bufferInfoPresentationTimeUs));
+        *totalSize += size;
+    }
+    return OK;
+}
+
+static void android_media_MediaCodec_queueInputBuffers(
+        JNIEnv *env,
+        jobject thiz,
+        jint index,
+        jobjectArray objArray) {
+    ALOGV("android_media_MediaCodec_queueInputBuffers");
+    sp<JMediaCodec> codec = getMediaCodec(env, thiz);
+    if (codec == NULL || codec->initCheck() != OK || objArray == NULL) {
+        throwExceptionAsNecessary(env, INVALID_OPERATION, codec);
+        return;
+    }
+    sp<BufferInfosWrapper> infoObj =
+            new BufferInfosWrapper{decltype(infoObj->value)()};
+    AString errorDetailMsg;
+    jint initialOffset = 0;
+    jint totalSize = 0;
+    status_t err = extractInfosFromObject(
+            env,
+            &initialOffset,
+            &totalSize,
+            &infoObj->value,
+            objArray,
+            &errorDetailMsg);
+    if (err == OK) {
+        err = codec->queueInputBuffers(
+            index,
+            initialOffset,
+            totalSize,
+            infoObj,
+            &errorDetailMsg);
+    }
+    throwExceptionAsNecessary(
+            env, err, ACTION_CODE_FATAL,
+            codec->getExceptionMessage(errorDetailMsg.c_str()).c_str());
+}
+
 struct NativeCryptoInfo {
     NativeCryptoInfo(JNIEnv *env, jobject cryptoInfoObj)
         : mEnv{env},
@@ -2559,8 +2762,7 @@
 
 static void android_media_MediaCodec_native_queueLinearBlock(
         JNIEnv *env, jobject thiz, jint index, jobject bufferObj,
-        jint offset, jint size, jobject cryptoInfoObj,
-        jlong presentationTimeUs, jint flags, jobject keys, jobject values) {
+        jobject cryptoInfoObj, jobjectArray objArray, jobject keys, jobject values) {
     ALOGV("android_media_MediaCodec_native_queueLinearBlock");
 
     sp<JMediaCodec> codec = getMediaCodec(env, thiz);
@@ -2578,7 +2780,24 @@
                 "error occurred while converting tunings from Java to native");
         return;
     }
-
+    jint totalSize;
+    jint initialOffset;
+    std::vector<AccessUnitInfo> infoVec;
+    AString errorDetailMsg;
+    err = extractInfosFromObject(env,
+            &initialOffset,
+            &totalSize,
+            &infoVec,
+            objArray,
+            &errorDetailMsg);
+    if (err != OK) {
+        throwExceptionAsNecessary(
+                env, INVALID_OPERATION, ACTION_CODE_FATAL,
+                codec->getExceptionMessage(errorDetailMsg.c_str()).c_str());
+        return;
+    }
+    sp<BufferInfosWrapper> infos =
+            new BufferInfosWrapper{std::move(infoVec)};
     std::shared_ptr<C2Buffer> buffer;
     sp<hardware::HidlMemory> memory;
     ScopedLocalRef<jobject> lock{env, env->GetObjectField(bufferObj, gLinearBlockInfo.lockId)};
@@ -2587,10 +2806,10 @@
             JMediaCodecLinearBlock *context =
                 (JMediaCodecLinearBlock *)env->GetLongField(bufferObj, gLinearBlockInfo.contextId);
             if (codec->hasCryptoOrDescrambler()) {
-                extractMemoryFromContext(context, offset, size, &memory);
-                offset += context->mHidlMemoryOffset;
+                extractMemoryFromContext(context, initialOffset, totalSize, &memory);
+                initialOffset += context->mHidlMemoryOffset;
             } else {
-                extractBufferFromContext(context, offset, size, &buffer);
+                extractBufferFromContext(context, initialOffset, totalSize, &buffer);
             }
         }
         env->MonitorExit(lock.get());
@@ -2601,7 +2820,6 @@
         return;
     }
 
-    AString errorDetailMsg;
     if (codec->hasCryptoOrDescrambler()) {
         if (!memory) {
             // It means there was an unexpected failure in extractMemoryFromContext above
@@ -2615,7 +2833,7 @@
             return;
         }
         auto cryptoInfo =
-                cryptoInfoObj ? NativeCryptoInfo{env, cryptoInfoObj} : NativeCryptoInfo{size};
+                cryptoInfoObj ? NativeCryptoInfo{env, cryptoInfoObj} : NativeCryptoInfo{totalSize};
         if (env->ExceptionCheck()) {
             // Creation of cryptoInfo failed. Let the exception bubble up.
             return;
@@ -2623,13 +2841,12 @@
         err = codec->queueEncryptedLinearBlock(
                 index,
                 memory,
-                offset,
+                initialOffset,
                 cryptoInfo.mSubSamples, cryptoInfo.mNumSubSamples,
                 (const uint8_t *)cryptoInfo.mKey, (const uint8_t *)cryptoInfo.mIv,
                 cryptoInfo.mMode,
                 cryptoInfo.mPattern,
-                presentationTimeUs,
-                flags,
+                infos,
                 tunings,
                 &errorDetailMsg);
         ALOGI_IF(err != OK, "queueEncryptedLinearBlock returned err = %d", err);
@@ -2646,7 +2863,7 @@
             return;
         }
         err = codec->queueBuffer(
-                index, buffer, presentationTimeUs, flags, tunings, &errorDetailMsg);
+                index, buffer, infos, tunings, &errorDetailMsg);
     }
     throwExceptionAsNecessary(
             env, err, ACTION_CODE_FATAL,
@@ -2704,8 +2921,11 @@
     std::shared_ptr<C2Buffer> buffer = C2Buffer::CreateGraphicBuffer(block->share(
             block->crop(), C2Fence{}));
     AString errorDetailMsg;
+    sp<BufferInfosWrapper> infos =
+        new BufferInfosWrapper{decltype(infos->value)()};
+    infos->value.emplace_back(flags, 0 /*not used*/, presentationTimeUs);
     err = codec->queueBuffer(
-            index, buffer, presentationTimeUs, flags, tunings, &errorDetailMsg);
+            index, buffer, infos, tunings, &errorDetailMsg);
     throwExceptionAsNecessary(
             env, err, ACTION_CODE_FATAL,
             codec->getExceptionMessage(errorDetailMsg.c_str()).c_str());
@@ -3214,6 +3434,10 @@
         env->GetFieldID(clazz.get(), "mLinearBlock", "Landroid/media/MediaCodec$LinearBlock;");
     CHECK(gFields.outputFrameLinearBlockID != NULL);
 
+    gFields.outputFramebufferInfosID =
+        env->GetFieldID(clazz.get(), "mBufferInfos", "Ljava/util/ArrayDeque;");
+    CHECK(gFields.outputFramebufferInfosID != NULL);
+
     gFields.outputFrameHardwareBufferID =
         env->GetFieldID(clazz.get(), "mHardwareBuffer", "Landroid/hardware/HardwareBuffer;");
     CHECK(gFields.outputFrameHardwareBufferID != NULL);
@@ -3401,6 +3625,19 @@
     gArrayListInfo.addId = env->GetMethodID(clazz.get(), "add", "(Ljava/lang/Object;)Z");
     CHECK(gArrayListInfo.addId != NULL);
 
+    clazz.reset(env->FindClass("java/util/ArrayDeque"));
+    CHECK(clazz.get() != NULL);
+    gArrayDequeInfo.clazz = (jclass)env->NewGlobalRef(clazz.get());
+
+    gArrayDequeInfo.ctorId = env->GetMethodID(clazz.get(), "<init>", "()V");
+    CHECK(gArrayDequeInfo.ctorId != NULL);
+
+    gArrayDequeInfo.sizeId = env->GetMethodID(clazz.get(), "size", "()I");
+    CHECK(gArrayDequeInfo.sizeId != NULL);
+
+    gArrayDequeInfo.addId = env->GetMethodID(clazz.get(), "add", "(Ljava/lang/Object;)Z");
+    CHECK(gArrayDequeInfo.addId != NULL);
+
     clazz.reset(env->FindClass("android/media/MediaCodec$LinearBlock"));
     CHECK(clazz.get() != NULL);
 
@@ -3444,6 +3681,12 @@
 
     gBufferInfo.setId = env->GetMethodID(clazz.get(), "set", "(IIJI)V");
     CHECK(gBufferInfo.setId != NULL);
+
+    gFields.bufferInfoSize = env->GetFieldID(clazz.get(), "size", "I");
+    gFields.bufferInfoFlags = env->GetFieldID(clazz.get(), "flags", "I");
+    gFields.bufferInfoOffset = env->GetFieldID(clazz.get(), "offset", "I");
+    gFields.bufferInfoPresentationTimeUs =
+            env->GetFieldID(clazz.get(), "presentationTimeUs", "J");
 }
 
 static void android_media_MediaCodec_native_setup(
@@ -3701,6 +3944,9 @@
     { "native_queueInputBuffer", "(IIIJI)V",
       (void *)android_media_MediaCodec_queueInputBuffer },
 
+    { "native_queueInputBuffers", "(I[Ljava/lang/Object;)V",
+      (void *)android_media_MediaCodec_queueInputBuffers },
+
     { "native_queueSecureInputBuffer", "(IILandroid/media/MediaCodec$CryptoInfo;JI)V",
       (void *)android_media_MediaCodec_queueSecureInputBuffer },
 
@@ -3711,8 +3957,8 @@
     { "native_closeMediaImage", "(J)V", (void *)android_media_MediaCodec_closeMediaImage },
 
     { "native_queueLinearBlock",
-      "(ILandroid/media/MediaCodec$LinearBlock;IILandroid/media/MediaCodec$CryptoInfo;JI"
-      "Ljava/util/ArrayList;Ljava/util/ArrayList;)V",
+      "(ILandroid/media/MediaCodec$LinearBlock;Landroid/media/MediaCodec$CryptoInfo;"
+      "[Ljava/lang/Object;Ljava/util/ArrayList;Ljava/util/ArrayList;)V",
       (void *)android_media_MediaCodec_native_queueLinearBlock },
 
     { "native_queueHardwareBuffer",
diff --git a/media/jni/android_media_MediaCodec.h b/media/jni/android_media_MediaCodec.h
index fbaf64f..02708ef 100644
--- a/media/jni/android_media_MediaCodec.h
+++ b/media/jni/android_media_MediaCodec.h
@@ -35,6 +35,7 @@
 namespace android {
 
 struct ABuffer;
+struct AccessUnitInfo;
 struct ALooper;
 struct AMessage;
 struct AString;
@@ -93,6 +94,13 @@
             size_t offset, size_t size, int64_t timeUs, uint32_t flags,
             AString *errorDetailMsg);
 
+    status_t queueInputBuffers(
+            size_t index,
+            size_t offset,
+            size_t size,
+            const sp<RefBase> &auInfo,
+            AString *errorDetailMsg = NULL);
+
     status_t queueSecureInputBuffer(
             size_t index,
             size_t offset,
@@ -108,7 +116,7 @@
 
     status_t queueBuffer(
             size_t index, const std::shared_ptr<C2Buffer> &buffer,
-            int64_t timeUs, uint32_t flags, const sp<AMessage> &tunings,
+            const sp<RefBase> &infos, const sp<AMessage> &tunings,
             AString *errorDetailMsg);
 
     status_t queueEncryptedLinearBlock(
@@ -121,8 +129,7 @@
             const uint8_t iv[16],
             CryptoPlugin::Mode mode,
             const CryptoPlugin::Pattern &pattern,
-            int64_t presentationTimeUs,
-            uint32_t flags,
+            const sp<RefBase> &infos,
             const sp<AMessage> &tunings,
             AString *errorDetailMsg);
 
diff --git a/media/jni/soundpool/SoundDecoder.cpp b/media/jni/soundpool/SoundDecoder.cpp
index 5ed10b0..ae57634 100644
--- a/media/jni/soundpool/SoundDecoder.cpp
+++ b/media/jni/soundpool/SoundDecoder.cpp
@@ -29,14 +29,15 @@
 // before the SoundDecoder thread closes.
 static constexpr int32_t kWaitTimeBeforeCloseMs = 1000;
 
-SoundDecoder::SoundDecoder(SoundManager* soundManager, size_t threads)
+SoundDecoder::SoundDecoder(SoundManager* soundManager, size_t threads, int32_t threadPriority)
     : mSoundManager(soundManager)
 {
     ALOGV("%s(%p, %zu)", __func__, soundManager, threads);
     // ThreadPool is created, but we don't launch any threads.
     mThreadPool = std::make_unique<ThreadPool>(
             std::min(threads, (size_t)std::thread::hardware_concurrency()),
-            "SoundDecoder_");
+            "SoundDecoder_",
+            threadPriority);
 }
 
 SoundDecoder::~SoundDecoder()
diff --git a/media/jni/soundpool/SoundDecoder.h b/media/jni/soundpool/SoundDecoder.h
index 7b62114..3f44a0d 100644
--- a/media/jni/soundpool/SoundDecoder.h
+++ b/media/jni/soundpool/SoundDecoder.h
@@ -28,7 +28,7 @@
  */
 class SoundDecoder {
 public:
-    SoundDecoder(SoundManager* soundManager, size_t threads);
+    SoundDecoder(SoundManager* soundManager, size_t threads, int32_t threadPriority);
     ~SoundDecoder();
     void loadSound(int32_t soundID) NO_THREAD_SAFETY_ANALYSIS; // uses unique_lock
     void quit();
diff --git a/media/jni/soundpool/SoundManager.cpp b/media/jni/soundpool/SoundManager.cpp
index 5b16174..fa35813 100644
--- a/media/jni/soundpool/SoundManager.cpp
+++ b/media/jni/soundpool/SoundManager.cpp
@@ -29,7 +29,7 @@
 static const size_t kDecoderThreads = std::thread::hardware_concurrency() >= 4 ? 2 : 1;
 
 SoundManager::SoundManager()
-    : mDecoder{std::make_unique<SoundDecoder>(this, kDecoderThreads)}
+    : mDecoder{std::make_unique<SoundDecoder>(this, kDecoderThreads, ANDROID_PRIORITY_NORMAL)}
 {
     ALOGV("%s()", __func__);
 }
diff --git a/media/jni/soundpool/StreamManager.cpp b/media/jni/soundpool/StreamManager.cpp
index 66fec1c..e11ccbc 100644
--- a/media/jni/soundpool/StreamManager.cpp
+++ b/media/jni/soundpool/StreamManager.cpp
@@ -126,7 +126,8 @@
     mThreadPool = std::make_unique<ThreadPool>(
             std::min((size_t)streams,  // do not make more threads than streams to play
                     std::min(threads, (size_t)std::thread::hardware_concurrency())),
-            "SoundPool_");
+            "SoundPool_",
+            ANDROID_PRIORITY_AUDIO);
 }
 
 #pragma clang diagnostic pop
diff --git a/media/jni/soundpool/StreamManager.h b/media/jni/soundpool/StreamManager.h
index 340b49b..a4cb286 100644
--- a/media/jni/soundpool/StreamManager.h
+++ b/media/jni/soundpool/StreamManager.h
@@ -46,9 +46,9 @@
  */
 class JavaThread {
 public:
-    JavaThread(std::function<void()> f, const char *name)
+    JavaThread(std::function<void()> f, const char *name, int32_t threadPriority)
         : mF{std::move(f)} {
-        createThreadEtc(staticFunction, this, name, ANDROID_PRIORITY_AUDIO);
+        createThreadEtc(staticFunction, this, name, threadPriority);
     }
 
     JavaThread(JavaThread &&) = delete; // uses "this" ptr, not moveable.
@@ -109,9 +109,11 @@
  */
 class ThreadPool {
 public:
-    ThreadPool(size_t maxThreadCount, std::string name)
+    ThreadPool(size_t maxThreadCount, std::string name,
+            int32_t threadPriority = ANDROID_PRIORITY_NORMAL)
         : mMaxThreadCount(maxThreadCount)
-        , mName{std::move(name)} { }
+        , mName{std::move(name)}
+        , mThreadPriority(threadPriority) {}
 
     ~ThreadPool() { quit(); }
 
@@ -159,7 +161,8 @@
             const int32_t id = mNextThreadId;
             mThreads.emplace_back(std::make_unique<JavaThread>(
                     [this, id, mf = std::move(f)] { mf(id); --mActiveThreadCount; },
-                    (mName + std::to_string(id)).c_str()));
+                    (mName + std::to_string(id)).c_str(),
+                    mThreadPriority));
             ++mActiveThreadCount;
             return id;
         }
@@ -180,6 +183,7 @@
 private:
     const size_t            mMaxThreadCount;
     const std::string       mName;
+    const int32_t           mThreadPriority;
 
     std::atomic_size_t      mActiveThreadCount = 0;
 
diff --git a/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java b/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java
index 774de5f..0df36af 100644
--- a/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java
+++ b/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java
@@ -19,7 +19,7 @@
 import static android.Manifest.permission.MANAGE_MEDIA_PROJECTION;
 
 import android.annotation.EnforcePermission;
-import android.os.IBinder;
+import android.app.ActivityOptions.LaunchCookie;
 import android.os.PermissionEnforcer;
 import android.os.RemoteException;
 
@@ -29,7 +29,7 @@
  */
 public final class FakeIMediaProjection extends IMediaProjection.Stub {
     boolean mIsStarted = false;
-    IBinder mLaunchCookie = null;
+    LaunchCookie mLaunchCookie = null;
     IMediaProjectionCallback mIMediaProjectionCallback = null;
 
     FakeIMediaProjection(PermissionEnforcer enforcer) {
@@ -80,14 +80,14 @@
 
     @Override
     @EnforcePermission(MANAGE_MEDIA_PROJECTION)
-    public IBinder getLaunchCookie() throws RemoteException {
+    public LaunchCookie getLaunchCookie() throws RemoteException {
         getLaunchCookie_enforcePermission();
         return mLaunchCookie;
     }
 
     @Override
     @EnforcePermission(MANAGE_MEDIA_PROJECTION)
-    public void setLaunchCookie(IBinder launchCookie) throws RemoteException {
+    public void setLaunchCookie(LaunchCookie launchCookie) throws RemoteException {
         setLaunchCookie_enforcePermission();
         mLaunchCookie = launchCookie;
     }
diff --git a/media/tests/projection/src/android/media/projection/MediaProjectionTest.java b/media/tests/projection/src/android/media/projection/MediaProjectionTest.java
index 2e0396f..1323e89 100644
--- a/media/tests/projection/src/android/media/projection/MediaProjectionTest.java
+++ b/media/tests/projection/src/android/media/projection/MediaProjectionTest.java
@@ -31,15 +31,14 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
 
 import android.annotation.Nullable;
+import android.app.ActivityOptions.LaunchCookie;
 import android.compat.testing.PlatformCompatChangeRule;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.VirtualDisplay;
 import android.hardware.display.VirtualDisplayConfig;
 import android.os.Handler;
-import android.os.IBinder;
 import android.os.Looper;
 import android.os.RemoteException;
 import android.os.test.FakePermissionEnforcer;
@@ -117,7 +116,7 @@
         permissionEnforcer.grant(MANAGE_MEDIA_PROJECTION);
         // Support the MediaProjection instance.
         mFakeIMediaProjection = new FakeIMediaProjection(permissionEnforcer);
-        mFakeIMediaProjection.setLaunchCookie(mock(IBinder.class));
+        mFakeIMediaProjection.setLaunchCookie(new LaunchCookie());
         mMediaProjection = new MediaProjection(mTestableContext, mFakeIMediaProjection,
                 mDisplayManager);
 
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 90593d6..900a2f8 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -214,6 +214,8 @@
 
     javacflags: [
         "-Adagger.fastInit=enabled",
+        "-Adagger.explicitBindingConflictsWithInject=ERROR",
+        "-Adagger.strictMultibindingValidation=enabled",
         "-Aroom.schemaLocation=frameworks/base/packages/SystemUI/schemas",
     ],
     kotlincflags: ["-Xjvm-default=all"],
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index a2530d5..375fe13 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -371,3 +371,10 @@
     description: "Enables refactored logic for SysUI+WM unlock/occlusion code paths"
     bug: "278086361"
 }
+
+flag {
+   name: "enable_keyguard_compose"
+   namespace: "systemui"
+   description: "Enables the compose version of keyguard."
+   bug: "301968149"
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
index 9c46ebdc..8194055 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
@@ -644,7 +644,7 @@
             var candidate: RemoteAnimationTarget? = null
             for (it in apps) {
                 if (it.mode == RemoteAnimationTarget.MODE_OPENING) {
-                    if (it.taskInfo != null && !it.hasAnimatingParent) {
+                    if (!it.hasAnimatingParent) {
                         return it
                     }
                     if (candidate == null) {
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt b/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt
index bb2fbf7..a18b460 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt
@@ -17,6 +17,7 @@
 
 package com.android.compose
 
+import androidx.annotation.DrawableRes
 import androidx.compose.foundation.BorderStroke
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.RowScope
@@ -24,8 +25,13 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.ButtonColors
 import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.IconButtonColors
+import androidx.compose.material3.IconButtonDefaults
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import com.android.compose.theme.LocalAndroidColorScheme
@@ -89,6 +95,29 @@
     )
 }
 
+@Composable
+fun PlatformIconButton(
+    onClick: () -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    colors: IconButtonColors = iconButtonColors(),
+    @DrawableRes iconResource: Int,
+    contentDescription: String?,
+) {
+    IconButton(
+        modifier = modifier,
+        onClick = onClick,
+        enabled = enabled,
+        colors = colors,
+    ) {
+        Icon(
+            painter = painterResource(id = iconResource),
+            contentDescription = contentDescription,
+            tint = colors.contentColor,
+        )
+    }
+}
+
 private val DefaultPlatformButtonVerticalPadding = 6.dp
 private val ButtonPaddings = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
 
@@ -109,6 +138,13 @@
 }
 
 @Composable
+private fun iconButtonColors(): IconButtonColors {
+    return IconButtonDefaults.filledIconButtonColors(
+        contentColor = LocalAndroidColorScheme.current.onSurface,
+    )
+}
+
+@Composable
 private fun outlineButtonBorder(): BorderStroke {
     return BorderStroke(
         width = 1.dp,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
index e2beaee..b11edf7 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
@@ -58,6 +58,8 @@
 import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
 import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout
 import com.android.systemui.res.R
+import com.android.systemui.scene.ui.composable.QuickSettings
+import com.android.systemui.scene.ui.composable.Shade as ShadeKey
 import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
 import com.android.systemui.statusbar.phone.StatusBarIconController
 import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager
@@ -348,7 +350,7 @@
 }
 
 @Composable
-private fun StatusIcons(
+private fun SceneScope.StatusIcons(
     viewModel: ShadeHeaderViewModel,
     createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
     statusBarIconController: StatusBarIconController,
@@ -358,7 +360,6 @@
     val carrierIconSlots =
         listOf(stringResource(id = com.android.internal.R.string.status_bar_mobile))
     val isSingleCarrier by viewModel.isSingleCarrier.collectAsState()
-    val isTransitioning by viewModel.isTransitioning.collectAsState()
 
     AndroidView(
         factory = { context ->
@@ -373,7 +374,9 @@
             iconContainer
         },
         update = { iconContainer ->
-            iconContainer.setQsExpansionTransitioning(isTransitioning)
+            iconContainer.setQsExpansionTransitioning(
+                layoutState.isTransitioningBetween(ShadeKey, QuickSettings)
+            )
             if (isSingleCarrier || !useExpandedFormat) {
                 iconContainer.removeIgnoredSlots(carrierIconSlots)
             } else {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
index 8552aaf..5f615fd 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -40,12 +40,15 @@
 import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
 import androidx.compose.ui.node.DelegatingNode
 import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.ObserverModifierNode
 import androidx.compose.ui.node.PointerInputModifierNode
 import androidx.compose.ui.node.currentValueOf
+import androidx.compose.ui.node.observeReads
 import androidx.compose.ui.platform.LocalViewConfiguration
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.util.fastForEach
+import kotlin.math.sign
 
 /**
  * Make an element draggable in the given [orientation].
@@ -65,7 +68,7 @@
 internal fun Modifier.multiPointerDraggable(
     orientation: Orientation,
     enabled: () -> Boolean,
-    startDragImmediately: () -> Boolean,
+    startDragImmediately: (startedPosition: Offset) -> Boolean,
     onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit,
     onDragDelta: (delta: Float) -> Unit,
     onDragStopped: (velocity: Float) -> Unit,
@@ -84,7 +87,7 @@
 private data class MultiPointerDraggableElement(
     private val orientation: Orientation,
     private val enabled: () -> Boolean,
-    private val startDragImmediately: () -> Boolean,
+    private val startDragImmediately: (startedPosition: Offset) -> Boolean,
     private val onDragStarted:
         (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit,
     private val onDragDelta: (Float) -> Unit,
@@ -113,14 +116,19 @@
 internal class MultiPointerDraggableNode(
     orientation: Orientation,
     enabled: () -> Boolean,
-    var startDragImmediately: () -> Boolean,
+    var startDragImmediately: (startedPosition: Offset) -> Boolean,
     var onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit,
     var onDragDelta: (Float) -> Unit,
     var onDragStopped: (velocity: Float) -> Unit,
-) : PointerInputModifierNode, DelegatingNode(), CompositionLocalConsumerModifierNode {
+) :
+    PointerInputModifierNode,
+    DelegatingNode(),
+    CompositionLocalConsumerModifierNode,
+    ObserverModifierNode {
     private val pointerInputHandler: suspend PointerInputScope.() -> Unit = { pointerInput() }
     private val delegate = delegate(SuspendingPointerInputModifierNode(pointerInputHandler))
     private val velocityTracker = VelocityTracker()
+    private var previousEnabled: Boolean = false
 
     var enabled: () -> Boolean = enabled
         set(value) {
@@ -140,6 +148,21 @@
             }
         }
 
+    override fun onAttach() {
+        previousEnabled = enabled()
+        onObservedReadsChanged()
+    }
+
+    override fun onObservedReadsChanged() {
+        observeReads {
+            val newEnabled = enabled()
+            if (newEnabled != previousEnabled) {
+                delegate.resetPointerInputHandler()
+            }
+            previousEnabled = newEnabled
+        }
+    }
+
     override fun onCancelPointerInput() = delegate.onCancelPointerInput()
 
     override fun onPointerEvent(
@@ -201,7 +224,7 @@
  */
 private suspend fun PointerInputScope.detectDragGestures(
     orientation: Orientation,
-    startDragImmediately: () -> Boolean,
+    startDragImmediately: (startedPosition: Offset) -> Boolean,
     onDragStart: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit,
     onDragEnd: () -> Unit,
     onDragCancel: () -> Unit,
@@ -211,7 +234,7 @@
         val initialDown = awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
         var overSlop = 0f
         val drag =
-            if (startDragImmediately()) {
+            if (startDragImmediately(initialDown.position)) {
                 initialDown.consume()
                 initialDown
             } else {
@@ -223,12 +246,31 @@
 
                 // TODO(b/291055080): Replace by await[Orientation]PointerSlopOrCancellation once
                 // it is public.
-                when (orientation) {
-                    Orientation.Horizontal ->
-                        awaitHorizontalTouchSlopOrCancellation(down.id, onSlopReached)
-                    Orientation.Vertical ->
-                        awaitVerticalTouchSlopOrCancellation(down.id, onSlopReached)
+                val drag =
+                    when (orientation) {
+                        Orientation.Horizontal ->
+                            awaitHorizontalTouchSlopOrCancellation(down.id, onSlopReached)
+                        Orientation.Vertical ->
+                            awaitVerticalTouchSlopOrCancellation(down.id, onSlopReached)
+                    }
+
+                // Make sure that overSlop is not 0f. This can happen when the user drags by exactly
+                // the touch slop. However, the overSlop we pass to onDragStarted() is used to
+                // compute the direction we are dragging in, so overSlop should never be 0f unless
+                // we intercept an ongoing swipe transition (i.e. startDragImmediately() returned
+                // true).
+                if (drag != null && overSlop == 0f) {
+                    val deltaOffset = drag.position - initialDown.position
+                    val delta =
+                        when (orientation) {
+                            Orientation.Horizontal -> deltaOffset.y
+                            Orientation.Vertical -> deltaOffset.y
+                        }
+                    check(delta != 0f)
+                    overSlop = delta.sign
                 }
+
+                drag
             }
 
         if (drag != null) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
index aed04f6..58c3be24 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
@@ -20,8 +20,7 @@
 
 import android.util.Log
 import androidx.compose.animation.core.Animatable
-import androidx.compose.animation.core.Spring
-import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.SpringSpec
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableFloatStateOf
@@ -54,7 +53,20 @@
         }
 
     private fun updateTransition(newTransition: SwipeTransition, force: Boolean = false) {
-        if (isDrivingTransition || force) layoutState.startTransition(newTransition)
+        if (isDrivingTransition || force) {
+            layoutState.startTransition(newTransition)
+
+            // Initialize SwipeTransition.swipeSpec. Note that this must be called right after
+            // layoutState.startTransition() is called, because it computes the
+            // layoutState.transformationSpec().
+            newTransition.swipeSpec =
+                layoutState.transformationSpec.swipeSpec ?: layoutState.transitions.defaultSwipeSpec
+        } else {
+            // We were not driving the transition and we don't force the update, so the spec won't
+            // be used and it doesn't matter which one we set here.
+            newTransition.swipeSpec = SceneTransitions.DefaultSwipeSpec
+        }
+
         swipeTransition = newTransition
     }
 
@@ -84,8 +96,35 @@
     private var upOrLeftResult: UserActionResult? = null
     private var downOrRightResult: UserActionResult? = null
 
+    /**
+     * Whether we should immediately intercept a gesture.
+     *
+     * Note: if this returns true, then [onDragStarted] will be called with overSlop equal to 0f,
+     * indicating that the transition should be intercepted.
+     */
+    internal fun shouldImmediatelyIntercept(startedPosition: Offset?): Boolean {
+        // We don't intercept the touch if we are not currently driving the transition.
+        if (!isDrivingTransition) {
+            return false
+        }
+
+        // Only intercept the current transition if one of the 2 swipes results is also a transition
+        // between the same pair of scenes.
+        val fromScene = swipeTransition._currentScene
+        val swipes = computeSwipes(fromScene, startedPosition, pointersDown = 1)
+        val (upOrLeft, downOrRight) = computeSwipesResults(fromScene, swipes)
+        return (upOrLeft != null &&
+            swipeTransition.isTransitioningBetween(fromScene.key, upOrLeft.toScene)) ||
+            (downOrRight != null &&
+                swipeTransition.isTransitioningBetween(fromScene.key, downOrRight.toScene))
+    }
+
     internal fun onDragStarted(pointersDown: Int, startedPosition: Offset?, overSlop: Float) {
-        if (isDrivingTransition) {
+        if (overSlop == 0f) {
+            check(isDrivingTransition) {
+                "onDragStarted() called while isDrivingTransition=false overSlop=0f"
+            }
+
             // This [transition] was already driving the animation: simply take over it.
             // Stop animating and start from where the current offset.
             swipeTransition.cancelOffsetAnimation()
@@ -93,9 +132,6 @@
             return
         }
 
-        check(overSlop != 0f) {
-            "onDragStarted() called while isDrivingTransition=false overSlop=0f"
-        }
         val transitionState = layoutState.transitionState
         if (transitionState is TransitionState.Transition) {
             // TODO(b/290184746): Better handle interruptions here if state != idle.
@@ -211,7 +247,7 @@
 
     private fun updateSwipesResults(fromScene: Scene) {
         val (upOrLeftResult, downOrRightResult) =
-            swipesResults(
+            computeSwipesResults(
                 fromScene,
                 this.swipes ?: error("updateSwipes() should be called before updateSwipesResults()")
             )
@@ -220,7 +256,7 @@
         this.downOrRightResult = downOrRightResult
     }
 
-    private fun swipesResults(
+    private fun computeSwipesResults(
         fromScene: Scene,
         swipes: Swipes
     ): Pair<UserActionResult?, UserActionResult?> {
@@ -346,6 +382,11 @@
             return
         }
 
+        // Important: Make sure that all the code here references the current transition when
+        // [onDragStopped] is called, otherwise the callbacks (like onAnimationCompleted()) might
+        // incorrectly finish a new transition that replaced this one.
+        val swipeTransition = this.swipeTransition
+
         fun animateTo(targetScene: Scene, targetOffset: Float) {
             // If the effective current scene changed, it should be reflected right now in the
             // current scene state, even before the settle animation is ongoing. That way all the
@@ -462,7 +503,7 @@
          * The signed distance between [fromScene] and [toScene]. It is negative if [fromScene] is
          * above or to the left of [toScene].
          */
-        val distance: Float
+        val distance: Float,
     ) : TransitionState.Transition(_fromScene.key, _toScene.key) {
         var _currentScene by mutableStateOf(_fromScene)
         override val currentScene: SceneKey
@@ -495,6 +536,9 @@
         /** Job to check that there is at most one offset animation in progress. */
         private var offsetAnimationJob: Job? = null
 
+        /** The spec to use when animating this transition to either [fromScene] or [toScene]. */
+        lateinit var swipeSpec: SpringSpec<Float>
+
         /** Ends any previous [offsetAnimationJob] and runs the new [job]. */
         private fun startOffsetAnimation(job: () -> Job) {
             cancelOffsetAnimation()
@@ -516,13 +560,6 @@
             }
         }
 
-        // TODO(b/290184746): Make this spring spec configurable.
-        private val animationSpec =
-            spring(
-                stiffness = Spring.StiffnessMediumLow,
-                visibilityThreshold = OffsetVisibilityThreshold
-            )
-
         fun animateOffset(
             // TODO(b/317063114) The CoroutineScope should be removed.
             coroutineScope: CoroutineScope,
@@ -546,7 +583,7 @@
 
             offsetAnimatable.animateTo(
                 targetValue = targetOffset,
-                animationSpec = animationSpec,
+                animationSpec = swipeSpec,
                 initialVelocity = initialVelocity,
             )
 
@@ -651,6 +688,7 @@
         }
 
         val source = this
+        var isIntercepting = false
 
         return PriorityNestedScrollConnection(
             orientation = orientation,
@@ -658,7 +696,9 @@
                 canChangeScene = offsetBeforeStart == 0f
 
                 val canInterceptSwipeTransition =
-                    canChangeScene && gestureHandler.isDrivingTransition && offsetAvailable != 0f
+                    canChangeScene &&
+                        offsetAvailable != 0f &&
+                        gestureHandler.shouldImmediatelyIntercept(startedPosition = null)
                 if (!canInterceptSwipeTransition) return@PriorityNestedScrollConnection false
 
                 val swipeTransition = gestureHandler.swipeTransition
@@ -676,7 +716,12 @@
                 }
 
                 // Start only if we cannot consume this event
-                !shouldSnapToIdle
+                val canStart = !shouldSnapToIdle
+                if (canStart) {
+                    isIntercepting = true
+                }
+
+                canStart
             },
             canStartPostScroll = { offsetAvailable, offsetBeforeStart ->
                 val behavior: NestedScrollBehavior =
@@ -688,24 +733,31 @@
 
                 val isZeroOffset = offsetBeforeStart == 0f
 
-                when (behavior) {
-                    NestedScrollBehavior.DuringTransitionBetweenScenes -> {
-                        canChangeScene = false // unused: added for consistency
-                        false
+                val canStart =
+                    when (behavior) {
+                        NestedScrollBehavior.DuringTransitionBetweenScenes -> {
+                            canChangeScene = false // unused: added for consistency
+                            false
+                        }
+                        NestedScrollBehavior.EdgeNoPreview -> {
+                            canChangeScene = isZeroOffset
+                            isZeroOffset && hasNextScene(offsetAvailable)
+                        }
+                        NestedScrollBehavior.EdgeWithPreview -> {
+                            canChangeScene = isZeroOffset
+                            hasNextScene(offsetAvailable)
+                        }
+                        NestedScrollBehavior.EdgeAlways -> {
+                            canChangeScene = true
+                            hasNextScene(offsetAvailable)
+                        }
                     }
-                    NestedScrollBehavior.EdgeNoPreview -> {
-                        canChangeScene = isZeroOffset
-                        isZeroOffset && hasNextScene(offsetAvailable)
-                    }
-                    NestedScrollBehavior.EdgeWithPreview -> {
-                        canChangeScene = isZeroOffset
-                        hasNextScene(offsetAvailable)
-                    }
-                    NestedScrollBehavior.EdgeAlways -> {
-                        canChangeScene = true
-                        hasNextScene(offsetAvailable)
-                    }
+
+                if (canStart) {
+                    isIntercepting = false
                 }
+
+                canStart
             },
             canStartPostFling = { velocityAvailable ->
                 val behavior: NestedScrollBehavior =
@@ -717,7 +769,13 @@
 
                 // We could start an overscroll animation
                 canChangeScene = false
-                behavior.canStartOnPostFling && hasNextScene(velocityAvailable)
+
+                val canStart = behavior.canStartOnPostFling && hasNextScene(velocityAvailable)
+                if (canStart) {
+                    isIntercepting = false
+                }
+
+                canStart
             },
             canContinueScroll = { true },
             canScrollOnFling = false,
@@ -726,7 +784,7 @@
                 gestureHandler.onDragStarted(
                     pointersDown = 1,
                     startedPosition = null,
-                    overSlop = offsetAvailable,
+                    overSlop = if (isIntercepting) 0f else offsetAvailable,
                 )
             },
             onScroll = { offsetAvailable ->
@@ -761,4 +819,6 @@
  * The number of pixels below which there won't be a visible difference in the transition and from
  * which the animation can stop.
  */
-private const val OffsetVisibilityThreshold = 0.5f
+// TODO(b/290184746): Have a better default visibility threshold which takes the swipe distance into
+// account instead.
+internal const val OffsetVisibilityThreshold = 0.5f
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
index 3a55567..ac423b7 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
@@ -17,7 +17,10 @@
 package com.android.compose.animation.scene
 
 import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.SpringSpec
 import androidx.compose.animation.core.snap
+import androidx.compose.animation.core.spring
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.util.fastForEach
@@ -36,6 +39,7 @@
 /** The transitions configuration of a [SceneTransitionLayout]. */
 class SceneTransitions
 internal constructor(
+    internal val defaultSwipeSpec: SpringSpec<Float>,
     internal val transitionSpecs: List<TransitionSpecImpl>,
 ) {
     private val cache = mutableMapOf<SceneKey, MutableMap<SceneKey, TransitionSpecImpl>>()
@@ -91,7 +95,13 @@
         TransitionSpecImpl(from, to, TransformationSpec.EmptyProvider)
 
     companion object {
-        val Empty = SceneTransitions(transitionSpecs = emptyList())
+        internal val DefaultSwipeSpec =
+            spring(
+                stiffness = Spring.StiffnessMediumLow,
+                visibilityThreshold = OffsetVisibilityThreshold,
+            )
+
+        val Empty = SceneTransitions(DefaultSwipeSpec, transitionSpecs = emptyList())
     }
 }
 
@@ -125,15 +135,30 @@
 }
 
 interface TransformationSpec {
-    /** The [AnimationSpec] used to animate the associated transition progress. */
+    /**
+     * The [AnimationSpec] used to animate the associated transition progress from `0` to `1` when
+     * the transition is triggered (i.e. it is not gesture-based).
+     */
     val progressSpec: AnimationSpec<Float>
 
+    /**
+     * The [SpringSpec] used to animate the associated transition progress when the transition was
+     * started by a swipe and is now animating back to a scene because the user lifted their finger.
+     *
+     * If `null`, then the [SceneTransitions.defaultSwipeSpec] will be used.
+     */
+    val swipeSpec: SpringSpec<Float>?
+
     /** The list of [Transformation] applied to elements during this transition. */
     val transformations: List<Transformation>
 
     companion object {
         internal val Empty =
-            TransformationSpecImpl(progressSpec = snap(), transformations = emptyList())
+            TransformationSpecImpl(
+                progressSpec = snap(),
+                swipeSpec = null,
+                transformations = emptyList(),
+            )
         internal val EmptyProvider = { Empty }
     }
 }
@@ -151,6 +176,7 @@
                 val reverse = transformationSpec.invoke()
                 TransformationSpecImpl(
                     progressSpec = reverse.progressSpec,
+                    swipeSpec = reverse.swipeSpec,
                     transformations = reverse.transformations.map { it.reversed() }
                 )
             }
@@ -166,6 +192,7 @@
  */
 internal class TransformationSpecImpl(
     override val progressSpec: AnimationSpec<Float>,
+    override val swipeSpec: SpringSpec<Float>?,
     override val transformations: List<Transformation>,
 ) : TransformationSpec {
     private val cache = mutableMapOf<ElementKey, MutableMap<SceneKey, ElementTransformations>>()
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index b9c4ac0..61f4978 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.runtime.Stable
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
 import androidx.compose.ui.node.DelegatingNode
@@ -94,13 +95,10 @@
         return userActions.keys.any { it is Swipe && it.direction.orientation == orientation }
     }
 
-    private fun startDragImmediately(): Boolean {
-        // Immediately start the drag if this our transition is currently animating to a scene
-        // (i.e. the user released their input pointer after swiping in this orientation) and the
-        // user can't swipe in the other direction.
-        return gestureHandler.isDrivingTransition &&
-            gestureHandler.swipeTransition.isAnimatingOffset &&
-            !canOppositeSwipe()
+    private fun startDragImmediately(startedPosition: Offset): Boolean {
+        // Immediately start the drag if the user can't swipe in the other direction and the gesture
+        // handler can intercept it.
+        return !canOppositeSwipe() && gestureHandler.shouldImmediatelyIntercept(startedPosition)
     }
 
     private fun canOppositeSwipe(): Boolean {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
index a764a527..04e0937 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -17,6 +17,7 @@
 package com.android.compose.animation.scene
 
 import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.SpringSpec
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
@@ -31,6 +32,12 @@
 @TransitionDsl
 interface SceneTransitionsBuilder {
     /**
+     * The default [AnimationSpec] used when after the user lifts their finger after starting a
+     * swipe to transition, to animate back into one of the 2 scenes we are transitioning to.
+     */
+    var defaultSwipeSpec: SpringSpec<Float>
+
+    /**
      * Define the default animation to be played when transitioning [to] the specified scene, from
      * any scene. For the animation specification to apply only when transitioning between two
      * specific scenes, use [from] instead.
@@ -64,12 +71,20 @@
 @TransitionDsl
 interface TransitionBuilder : PropertyTransformationBuilder {
     /**
-     * The [AnimationSpec] used to animate the progress of this transition from `0` to `1` when
-     * performing programmatic (not input pointer tracking) animations.
+     * The [AnimationSpec] used to animate the associated transition progress from `0` to `1` when
+     * the transition is triggered (i.e. it is not gesture-based).
      */
     var spec: AnimationSpec<Float>
 
     /**
+     * The [SpringSpec] used to animate the associated transition progress when the transition was
+     * started by a swipe and is now animating back to a scene because the user lifted their finger.
+     *
+     * If `null`, then the [SceneTransitionsBuilder.defaultSwipeSpec] will be used.
+     */
+    var swipeSpec: SpringSpec<Float>?
+
+    /**
      * Define a progress-based range for the transformations inside [builder].
      *
      * For instance, the following will fade `Foo` during the first half of the transition then it
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
index b96f9be..df186a1 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
@@ -19,6 +19,7 @@
 import androidx.compose.animation.core.AnimationSpec
 import androidx.compose.animation.core.DurationBasedAnimationSpec
 import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.SpringSpec
 import androidx.compose.animation.core.VectorConverter
 import androidx.compose.animation.core.spring
 import androidx.compose.ui.geometry.Offset
@@ -40,10 +41,12 @@
     builder: SceneTransitionsBuilder.() -> Unit,
 ): SceneTransitions {
     val impl = SceneTransitionsBuilderImpl().apply(builder)
-    return SceneTransitions(impl.transitionSpecs)
+    return SceneTransitions(impl.defaultSwipeSpec, impl.transitionSpecs)
 }
 
 private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder {
+    override var defaultSwipeSpec: SpringSpec<Float> = SceneTransitions.DefaultSwipeSpec
+
     val transitionSpecs = mutableListOf<TransitionSpecImpl>()
 
     override fun to(to: SceneKey, builder: TransitionBuilder.() -> Unit): TransitionSpec {
@@ -67,6 +70,7 @@
             val impl = TransitionBuilderImpl().apply(builder)
             return TransformationSpecImpl(
                 progressSpec = impl.spec,
+                swipeSpec = impl.swipeSpec,
                 transformations = impl.transformations,
             )
         }
@@ -80,6 +84,7 @@
 internal class TransitionBuilderImpl : TransitionBuilder {
     val transformations = mutableListOf<Transformation>()
     override var spec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessLow)
+    override var swipeSpec: SpringSpec<Float>? = null
 
     private var range: TransformationRange? = null
     private var reversed = false
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
index 88363ad..e8cc0ec 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
@@ -21,7 +21,6 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.Velocity
@@ -118,9 +117,6 @@
         fun up(fractionOfScreen: Float) =
             if (fractionOfScreen < 0f) error("use down()") else -down(fractionOfScreen)
 
-        // Float tolerance for comparisons
-        val tolerance = 0.00001f
-
         // Offset y: 10% of the screen
         val offsetY10 = Offset(x = 0f, y = down(0.1f))
 
@@ -150,31 +146,42 @@
             fromScene: SceneKey? = null,
             toScene: SceneKey? = null,
             progress: Float? = null,
+            isUserInputOngoing: Boolean? = null
         ) {
+            val transition = transitionState
             assertWithMessage("transitionState must be Transition")
-                .that(transitionState is Transition)
+                .that(transition is Transition)
                 .isTrue()
+            transition as Transition
+
             if (currentScene != null)
                 assertWithMessage("currentScene does not match")
-                    .that(transitionState.currentScene)
+                    .that(transition.currentScene)
                     .isEqualTo(currentScene)
+
             if (fromScene != null)
                 assertWithMessage("fromScene does not match")
-                    .that((transitionState as? Transition)?.fromScene)
+                    .that(transition.fromScene)
                     .isEqualTo(fromScene)
+
             if (toScene != null)
                 assertWithMessage("toScene does not match")
-                    .that((transitionState as? Transition)?.toScene)
+                    .that(transition.toScene)
                     .isEqualTo(toScene)
+
             if (progress != null)
                 assertWithMessage("progress does not match")
-                    .that((transitionState as? Transition)?.progress)
-                    .isWithin(tolerance)
+                    .that(transition.progress)
+                    .isWithin(0f) // returns true when comparing 0.0f with -0.0f
                     .of(progress)
+
+            if (isUserInputOngoing != null)
+                assertWithMessage("isUserInputOngoing does not match")
+                    .that(transition.isUserInputOngoing)
+                    .isEqualTo(isUserInputOngoing)
         }
     }
 
-    @OptIn(ExperimentalTestApi::class)
     private fun runGestureTest(block: suspend TestGestureScope.() -> Unit) {
         runMonotonicClockTest { TestGestureScope(coroutineScope = this).block() }
     }
@@ -248,7 +255,7 @@
     @Test
     fun onDragReversedDirection_changeToScene() = runGestureTest {
         // Drag A -> B with progress 0.6
-        draggable.onDragStarted(overSlop = up(0.6f))
+        draggable.onDragStarted(overSlop = -60f)
         assertTransition(
             currentScene = SceneA,
             fromScene = SceneA,
@@ -257,7 +264,7 @@
         )
 
         // Reverse direction such that A -> C now with 0.4
-        draggable.onDelta(down(1f))
+        draggable.onDelta(100f)
         assertTransition(
             currentScene = SceneA,
             fromScene = SceneA,
@@ -287,7 +294,7 @@
         navigateToSceneC()
 
         // We are on SceneC which has no action in Down direction
-        draggable.onDragStarted(down(0.1f))
+        draggable.onDragStarted(10f)
         assertTransition(
             currentScene = SceneC,
             fromScene = SceneC,
@@ -296,7 +303,7 @@
         )
 
         // Reverse drag direction, it will consume the previous drag
-        draggable.onDelta(up(0.1f))
+        draggable.onDelta(-10f)
         assertTransition(
             currentScene = SceneC,
             fromScene = SceneC,
@@ -305,7 +312,7 @@
         )
 
         // Continue reverse drag direction, it should record progress to Scene B
-        draggable.onDelta(up(0.1f))
+        draggable.onDelta(-10f)
         assertTransition(
             currentScene = SceneC,
             fromScene = SceneC,
@@ -557,53 +564,53 @@
     ) {
         val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview)
         // start scene transition
-        nestedScroll.scroll(available = Offset(0f, SCREEN_SIZE * firstScroll))
+        nestedScroll.scroll(available = Offset(0f, firstScroll))
 
         // stop scene transition (start the "stop animation")
         nestedScroll.onPreFling(available = Velocity.Zero)
 
         // a pre scroll event, that could be intercepted by SceneGestureHandler
-        nestedScroll.onPreScroll(Offset(0f, SCREEN_SIZE * secondScroll), NestedScrollSource.Drag)
+        nestedScroll.onPreScroll(Offset(0f, secondScroll), NestedScrollSource.Drag)
     }
 
     @Test
     fun scrollAndFling_scrollLessThanInterceptable_goToIdleOnCurrentScene() = runGestureTest {
-        val first = transitionInterceptionThreshold - tolerance
-        val second = 0.01f
+        val firstScroll = (transitionInterceptionThreshold - 0.0001f) * SCREEN_SIZE
+        val secondScroll = 1f
 
-        preScrollAfterSceneTransition(firstScroll = first, secondScroll = second)
+        preScrollAfterSceneTransition(firstScroll = firstScroll, secondScroll = secondScroll)
 
         assertIdle(SceneA)
     }
 
     @Test
     fun scrollAndFling_scrollMinInterceptable_interceptPreScrollEvents() = runGestureTest {
-        val first = transitionInterceptionThreshold + tolerance
-        val second = 0.01f
+        val firstScroll = (transitionInterceptionThreshold + 0.0001f) * SCREEN_SIZE
+        val secondScroll = 1f
 
-        preScrollAfterSceneTransition(firstScroll = first, secondScroll = second)
+        preScrollAfterSceneTransition(firstScroll = firstScroll, secondScroll = secondScroll)
 
-        assertTransition(progress = first + second)
+        assertTransition(progress = (firstScroll + secondScroll) / SCREEN_SIZE)
     }
 
     @Test
     fun scrollAndFling_scrollMaxInterceptable_interceptPreScrollEvents() = runGestureTest {
-        val first = 1f - transitionInterceptionThreshold - tolerance
-        val second = 0.01f
+        val firstScroll = -(1f - transitionInterceptionThreshold - 0.0001f) * SCREEN_SIZE
+        val secondScroll = -1f
 
-        preScrollAfterSceneTransition(firstScroll = first, secondScroll = second)
+        preScrollAfterSceneTransition(firstScroll = firstScroll, secondScroll = secondScroll)
 
-        assertTransition(progress = first + second)
+        assertTransition(progress = -(firstScroll + secondScroll) / SCREEN_SIZE)
     }
 
     @Test
     fun scrollAndFling_scrollMoreThanInterceptable_goToIdleOnNextScene() = runGestureTest {
-        val first = 1f - transitionInterceptionThreshold + tolerance
-        val second = 0.01f
+        val firstScroll = -(1f - transitionInterceptionThreshold + 0.0001f) * SCREEN_SIZE
+        val secondScroll = -0.01f
 
-        preScrollAfterSceneTransition(firstScroll = first, secondScroll = second)
+        preScrollAfterSceneTransition(firstScroll = firstScroll, secondScroll = secondScroll)
 
-        assertIdle(SceneC)
+        assertIdle(SceneB)
     }
 
     @Test
@@ -745,29 +752,75 @@
     @Test
     fun startNestedScrollWhileDragging() = runGestureTest {
         val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways)
-        draggable.onDragStarted(overSlop = down(0.1f))
+
+        // Start a drag and then stop it, given that
+        draggable.onDragStarted(overSlop = up(0.1f))
+
         assertTransition(currentScene = SceneA)
         assertThat(progress).isEqualTo(0.1f)
 
         // now we can intercept the scroll events
-        nestedScroll.scroll(available = offsetY10)
+        nestedScroll.scroll(available = -offsetY10)
         assertThat(progress).isEqualTo(0.2f)
 
         // this should be ignored, we are scrolling now!
-        draggable.onDragStopped(velocityThreshold)
+        draggable.onDragStopped(-velocityThreshold)
         assertTransition(currentScene = SceneA)
 
-        nestedScroll.scroll(available = offsetY10)
+        nestedScroll.scroll(available = -offsetY10)
         assertThat(progress).isEqualTo(0.3f)
 
-        nestedScroll.scroll(available = offsetY10)
+        nestedScroll.scroll(available = -offsetY10)
         assertThat(progress).isEqualTo(0.4f)
 
-        nestedScroll.onPreFling(available = Velocity(0f, velocityThreshold))
-        assertTransition(currentScene = SceneC)
+        nestedScroll.onPreFling(available = Velocity(0f, -velocityThreshold))
+        assertTransition(currentScene = SceneB)
 
         // wait for the stop animation
         advanceUntilIdle()
-        assertIdle(currentScene = SceneC)
+        assertIdle(currentScene = SceneB)
+    }
+
+    @Test
+    fun interceptTransition() = runGestureTest {
+        // Start at scene C.
+        navigateToSceneC()
+
+        // Swipe up from the middle to transition to scene B.
+        val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)
+        draggable.onDragStarted(startedPosition = middle, overSlop = up(0.1f))
+        assertTransition(
+            currentScene = SceneC,
+            fromScene = SceneC,
+            toScene = SceneB,
+            isUserInputOngoing = true,
+        )
+
+        val firstTransition = transitionState
+
+        // During the current gesture, start a new gesture, still in the middle of the screen. We
+        // should intercept it. Because it is intercepted, the overSlop passed to onDragStarted()
+        // should be 0f.
+        assertThat(sceneGestureHandler.shouldImmediatelyIntercept(middle)).isTrue()
+        draggable.onDragStarted(startedPosition = middle, overSlop = 0f)
+
+        // We should have intercepted the transition, so the transition should be the same object.
+        assertTransition(currentScene = SceneC, fromScene = SceneC, toScene = SceneB)
+        assertThat(transitionState).isSameInstanceAs(firstTransition)
+
+        // Start a new gesture from the bottom of the screen. Because swiping up from the bottom of
+        // C leads to scene A (and not B), the previous transitions is *not* intercepted and we
+        // instead animate from C to A.
+        val bottom = Offset(SCREEN_SIZE / 2, SCREEN_SIZE)
+        assertThat(sceneGestureHandler.shouldImmediatelyIntercept(bottom)).isFalse()
+        draggable.onDragStarted(startedPosition = bottom, overSlop = up(0.1f))
+
+        assertTransition(
+            currentScene = SceneC,
+            fromScene = SceneC,
+            toScene = SceneA,
+            isUserInputOngoing = true,
+        )
+        assertThat(transitionState).isNotSameInstanceAs(firstTransition)
     }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index 9403358..d81631a 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -21,6 +21,9 @@
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.platform.LocalViewConfiguration
@@ -63,7 +66,10 @@
 
     /** The content under test. */
     @Composable
-    private fun TestContent(layoutState: SceneTransitionLayoutState) {
+    private fun TestContent(
+        layoutState: SceneTransitionLayoutState,
+        swipesEnabled: () -> Boolean = { true },
+    ) {
         SceneTransitionLayout(
             state = layoutState,
             modifier = Modifier.size(LayoutWidth, LayoutHeight).testTag(TestElements.Foo.debugName),
@@ -71,28 +77,35 @@
             scene(
                 TestScenes.SceneA,
                 userActions =
-                    mapOf(
-                        Swipe.Left to TestScenes.SceneB,
-                        Swipe.Down to TestScenes.SceneC,
-                    ),
+                    if (swipesEnabled())
+                        mapOf(
+                            Swipe.Left to TestScenes.SceneB,
+                            Swipe.Down to TestScenes.SceneC,
+                            Swipe.Up to TestScenes.SceneB,
+                        )
+                    else emptyMap(),
             ) {
                 Box(Modifier.fillMaxSize())
             }
             scene(
                 TestScenes.SceneB,
-                userActions = mapOf(Swipe.Right to TestScenes.SceneA),
+                userActions =
+                    if (swipesEnabled()) mapOf(Swipe.Right to TestScenes.SceneA) else emptyMap(),
             ) {
                 Box(Modifier.fillMaxSize())
             }
             scene(
                 TestScenes.SceneC,
                 userActions =
-                    mapOf(
-                        Swipe.Down to TestScenes.SceneA,
-                        Swipe(SwipeDirection.Down, pointerCount = 2) to TestScenes.SceneB,
-                        Swipe(SwipeDirection.Right, fromSource = Edge.Left) to TestScenes.SceneB,
-                        Swipe(SwipeDirection.Down, fromSource = Edge.Top) to TestScenes.SceneB,
-                    ),
+                    if (swipesEnabled())
+                        mapOf(
+                            Swipe.Down to TestScenes.SceneA,
+                            Swipe(SwipeDirection.Down, pointerCount = 2) to TestScenes.SceneB,
+                            Swipe(SwipeDirection.Right, fromSource = Edge.Left) to
+                                TestScenes.SceneB,
+                            Swipe(SwipeDirection.Down, fromSource = Edge.Top) to TestScenes.SceneB,
+                        )
+                    else emptyMap(),
             ) {
                 Box(Modifier.fillMaxSize())
             }
@@ -357,7 +370,7 @@
         // detected as a drag event.
         var touchSlop = 0f
 
-        val layoutState = MutableSceneTransitionLayoutState(TestScenes.SceneA)
+        val layoutState = layoutState()
         val verticalSwipeDistance = 50.dp
         assertThat(verticalSwipeDistance).isNotEqualTo(LayoutHeight)
 
@@ -392,4 +405,74 @@
         assertThat(transition).isNotNull()
         assertThat(transition!!.progress).isEqualTo(0.5f)
     }
+
+    @Test
+    fun swipeByTouchSlop() {
+        val layoutState = layoutState()
+        var touchSlop = 0f
+        rule.setContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            TestContent(layoutState)
+        }
+
+        // Swipe down by exactly touchSlop, so that the drag overSlop is 0f.
+        rule.onRoot().performTouchInput {
+            down(middle)
+            moveBy(Offset(0f, touchSlop), delayMillis = 1_000)
+        }
+
+        // We should still correctly compute that we are swiping down to scene C.
+        var transition = layoutState.currentTransition
+        assertThat(transition).isNotNull()
+        assertThat(transition?.toScene).isEqualTo(TestScenes.SceneC)
+
+        // Release the finger, animating back to scene A.
+        rule.onRoot().performTouchInput { up() }
+        rule.waitForIdle()
+        assertThat(layoutState.currentTransition).isNull()
+        assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+        // Swipe up by exactly touchSlop, so that the drag overSlop is 0f.
+        rule.onRoot().performTouchInput {
+            down(middle)
+            moveBy(Offset(0f, -touchSlop), delayMillis = 1_000)
+        }
+
+        // We should still correctly compute that we are swiping up to scene B.
+        transition = layoutState.currentTransition
+        assertThat(transition).isNotNull()
+        assertThat(transition?.toScene).isEqualTo(TestScenes.SceneB)
+    }
+
+    @Test
+    fun swipeEnabledLater() {
+        val layoutState = MutableSceneTransitionLayoutState(TestScenes.SceneA)
+        var swipesEnabled by mutableStateOf(false)
+        var touchSlop = 0f
+        rule.setContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            TestContent(layoutState, swipesEnabled = { swipesEnabled })
+        }
+
+        // Drag down from the middle. This should not do anything, because swipes are disabled.
+        rule.onRoot().performTouchInput {
+            down(middle)
+            moveBy(Offset(0f, touchSlop), delayMillis = 1_000)
+        }
+        assertThat(layoutState.currentTransition).isNull()
+
+        // Release finger.
+        rule.onRoot().performTouchInput { up() }
+
+        // Enable swipes.
+        swipesEnabled = true
+        rule.waitForIdle()
+
+        // Drag down from the middle. Now it should start a transition.
+        rule.onRoot().performTouchInput {
+            down(middle)
+            moveBy(Offset(0f, touchSlop), delayMillis = 1_000)
+        }
+        assertThat(layoutState.currentTransition).isNotNull()
+    }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
index ef72992..140baca 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.animation.core.SpringSpec
 import androidx.compose.animation.core.TweenSpec
+import androidx.compose.animation.core.spring
 import androidx.compose.animation.core.tween
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.compose.animation.scene.transformation.Transformation
@@ -188,6 +189,40 @@
             )
     }
 
+    @Test
+    fun springSpec() {
+        val defaultSpec = spring<Float>(stiffness = 1f)
+        val specFromAToC = spring<Float>(stiffness = 2f)
+        val transitions = transitions {
+            defaultSwipeSpec = defaultSpec
+
+            from(TestScenes.SceneA, to = TestScenes.SceneB) {
+                // Default swipe spec.
+            }
+            from(TestScenes.SceneA, to = TestScenes.SceneC) { swipeSpec = specFromAToC }
+        }
+
+        assertThat(transitions.defaultSwipeSpec).isSameInstanceAs(defaultSpec)
+
+        // A => B does not have a custom spec.
+        assertThat(
+                transitions
+                    .transitionSpec(from = TestScenes.SceneA, to = TestScenes.SceneB)
+                    .transformationSpec()
+                    .swipeSpec
+            )
+            .isNull()
+
+        // A => C has a custom swipe spec.
+        assertThat(
+                transitions
+                    .transitionSpec(from = TestScenes.SceneA, to = TestScenes.SceneC)
+                    .transformationSpec()
+                    .swipeSpec
+            )
+            .isSameInstanceAs(specFromAToC)
+    }
+
     companion object {
         private val TRANSFORMATION_RANGE =
             Correspondence.transforming<Transformation, TransformationRange?>(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
index 34f703b..db414b7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
@@ -27,6 +27,7 @@
 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.coroutines.collectValues
 import com.android.systemui.dock.DockManager
 import com.android.systemui.dock.DockManagerFake
 import com.android.systemui.flags.FakeFeatureFlags
@@ -49,16 +50,17 @@
 import com.android.systemui.res.R
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
 import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.testKosmos
 import com.android.systemui.util.FakeSharedPreferences
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.settings.FakeSettings
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
@@ -82,6 +84,7 @@
     @Mock private lateinit var activityStarter: ActivityStarter
     @Mock private lateinit var launchAnimator: DialogLaunchAnimator
     @Mock private lateinit var devicePolicyManager: DevicePolicyManager
+    @Mock private lateinit var shadeInteractor: ShadeInteractor
     @Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger
 
     private lateinit var underTest: KeyguardQuickAffordanceInteractor
@@ -95,8 +98,6 @@
     private lateinit var dockManager: DockManagerFake
     private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
 
-    private val kosmos = testKosmos()
-
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
@@ -183,7 +184,7 @@
         underTest =
             KeyguardQuickAffordanceInteractor(
                 keyguardInteractor = withDeps.keyguardInteractor,
-                shadeInteractor = kosmos.shadeInteractor,
+                shadeInteractor = shadeInteractor,
                 lockPatternUtils = lockPatternUtils,
                 keyguardStateController = keyguardStateController,
                 userTracker = userTracker,
@@ -198,6 +199,8 @@
                 backgroundDispatcher = testDispatcher,
                 appContext = context,
             )
+
+        whenever(shadeInteractor.anyExpansion).thenReturn(MutableStateFlow(0f))
     }
 
     @Test
@@ -344,6 +347,25 @@
         }
 
     @Test
+    fun quickAffordance_updateOncePerShadeExpansion() =
+        testScope.runTest {
+            val shadeExpansion = MutableStateFlow(0f)
+            whenever(shadeInteractor.anyExpansion).thenReturn(shadeExpansion)
+
+            val collectedValue by
+                collectValues(
+                    underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
+                )
+
+            val initialSize = collectedValue.size
+            for (i in 0..10) {
+                shadeExpansion.value = i / 10f
+            }
+
+            assertThat(collectedValue.size).isEqualTo(initialSize + 1)
+        }
+
+    @Test
     fun quickAffordanceAlwaysVisible_notVisible_restrictedByPolicyManager() =
         testScope.runTest {
             whenever(devicePolicyManager.getKeyguardDisabledFeatures(null, userTracker.userId))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index bf99a86..942fbc2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -174,105 +174,6 @@
         }
 
     @Test
-    fun transitioning_idle_false() =
-        testScope.runTest {
-            val transitionState =
-                MutableStateFlow<ObservableTransitionState>(
-                    ObservableTransitionState.Idle(SceneKey.Shade)
-                )
-            val transitioning by
-                collectLastValue(underTest.transitioning(SceneKey.Shade, SceneKey.Lockscreen))
-            underTest.setTransitionState(transitionState)
-
-            assertThat(transitioning).isFalse()
-        }
-
-    @Test
-    fun transitioning_wrongFromScene_false() =
-        testScope.runTest {
-            val transitionState =
-                MutableStateFlow<ObservableTransitionState>(
-                    ObservableTransitionState.Transition(
-                        fromScene = SceneKey.Gone,
-                        toScene = SceneKey.Lockscreen,
-                        progress = flowOf(0.5f),
-                        isInitiatedByUserInput = false,
-                        isUserInputOngoing = flowOf(false),
-                    )
-                )
-            val transitioning by
-                collectLastValue(underTest.transitioning(SceneKey.Shade, SceneKey.Lockscreen))
-            underTest.setTransitionState(transitionState)
-
-            assertThat(transitioning).isFalse()
-        }
-
-    @Test
-    fun transitioning_wrongToScene_false() =
-        testScope.runTest {
-            val transitionState =
-                MutableStateFlow<ObservableTransitionState>(
-                    ObservableTransitionState.Transition(
-                        fromScene = SceneKey.Shade,
-                        toScene = SceneKey.QuickSettings,
-                        progress = flowOf(0.5f),
-                        isInitiatedByUserInput = false,
-                        isUserInputOngoing = flowOf(false),
-                    )
-                )
-            underTest.setTransitionState(transitionState)
-
-            assertThat(underTest.transitioning(SceneKey.Shade, SceneKey.Lockscreen).value).isFalse()
-        }
-
-    @Test
-    fun transitioning_correctFromAndToScenes_true() =
-        testScope.runTest {
-            val transitionState =
-                MutableStateFlow<ObservableTransitionState>(
-                    ObservableTransitionState.Transition(
-                        fromScene = SceneKey.Shade,
-                        toScene = SceneKey.Lockscreen,
-                        progress = flowOf(0.5f),
-                        isInitiatedByUserInput = false,
-                        isUserInputOngoing = flowOf(false),
-                    )
-                )
-            val transitioning by
-                collectLastValue(underTest.transitioning(SceneKey.Shade, SceneKey.Lockscreen))
-            underTest.setTransitionState(transitionState)
-
-            assertThat(transitioning).isTrue()
-        }
-
-    @Test
-    fun transitioning_updates() =
-        testScope.runTest {
-            val transitionState =
-                MutableStateFlow<ObservableTransitionState>(
-                    ObservableTransitionState.Idle(SceneKey.Shade)
-                )
-            val transitioning by
-                collectLastValue(underTest.transitioning(SceneKey.Shade, SceneKey.Lockscreen))
-            underTest.setTransitionState(transitionState)
-
-            assertThat(transitioning).isFalse()
-
-            transitionState.value =
-                ObservableTransitionState.Transition(
-                    fromScene = SceneKey.Shade,
-                    toScene = SceneKey.Lockscreen,
-                    progress = flowOf(0.5f),
-                    isInitiatedByUserInput = false,
-                    isUserInputOngoing = flowOf(false),
-                )
-            assertThat(transitioning).isTrue()
-
-            transitionState.value = ObservableTransitionState.Idle(SceneKey.Lockscreen)
-            assertThat(transitioning).isFalse()
-        }
-
-    @Test
     fun isTransitionUserInputOngoing_idle_false() =
         testScope.runTest {
             val transitionState =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
index e9a2a3b..c0aaab3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
@@ -9,8 +9,6 @@
 import com.android.systemui.flags.Flags
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.scene.domain.interactor.sceneInteractor
-import com.android.systemui.scene.shared.model.ObservableTransitionState
-import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
 import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
@@ -22,8 +20,6 @@
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -74,74 +70,6 @@
     }
 
     @Test
-    fun isTransitioning_idle_false() =
-        testScope.runTest {
-            val isTransitioning by collectLastValue(underTest.isTransitioning)
-            sceneInteractor.setTransitionState(
-                MutableStateFlow(ObservableTransitionState.Idle(SceneKey.Shade))
-            )
-
-            assertThat(isTransitioning).isFalse()
-        }
-
-    @Test
-    fun isTransitioning_shadeToQs_true() =
-        testScope.runTest {
-            val isTransitioning by collectLastValue(underTest.isTransitioning)
-            sceneInteractor.setTransitionState(
-                MutableStateFlow(
-                    ObservableTransitionState.Transition(
-                        fromScene = SceneKey.Shade,
-                        toScene = SceneKey.QuickSettings,
-                        progress = MutableStateFlow(0.5f),
-                        isInitiatedByUserInput = false,
-                        isUserInputOngoing = flowOf(false),
-                    )
-                )
-            )
-
-            assertThat(isTransitioning).isTrue()
-        }
-
-    @Test
-    fun isTransitioning_qsToShade_true() =
-        testScope.runTest {
-            val isTransitioning by collectLastValue(underTest.isTransitioning)
-            sceneInteractor.setTransitionState(
-                MutableStateFlow(
-                    ObservableTransitionState.Transition(
-                        fromScene = SceneKey.QuickSettings,
-                        toScene = SceneKey.Shade,
-                        progress = MutableStateFlow(0.5f),
-                        isInitiatedByUserInput = false,
-                        isUserInputOngoing = flowOf(false),
-                    )
-                )
-            )
-
-            assertThat(isTransitioning).isTrue()
-        }
-
-    @Test
-    fun isTransitioning_otherTransition_false() =
-        testScope.runTest {
-            val isTransitioning by collectLastValue(underTest.isTransitioning)
-            sceneInteractor.setTransitionState(
-                MutableStateFlow(
-                    ObservableTransitionState.Transition(
-                        fromScene = SceneKey.Gone,
-                        toScene = SceneKey.Shade,
-                        progress = MutableStateFlow(0.5f),
-                        isInitiatedByUserInput = false,
-                        isUserInputOngoing = flowOf(false),
-                    )
-                )
-            )
-
-            assertThat(isTransitioning).isFalse()
-        }
-
-    @Test
     fun mobileSubIds_update() =
         testScope.runTest {
             val mobileSubIds by collectLastValue(underTest.mobileSubIds)
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 8fa4a9e..7943588 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1748,6 +1748,9 @@
     <!-- Notification: Snooze panel: Snooze undo button label. [CHAR LIMIT=50]-->
     <string name="snooze_undo">Undo</string>
 
+    <!-- Notification: Snooze panel: Snooze undo content description for a11y. [CHAR LIMIT=NONE]-->
+    <string name="snooze_undo_content_description">Undo notification snooze</string>
+
     <!-- Notification: Snooze panel: message indicating how long the notification was snoozed for. [CHAR LIMIT=100]-->
     <string name="snoozed_for_time">Snoozed for <xliff:g id="time_amount" example="15 minutes">%1$s</xliff:g></string>
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
index f094102..d4ac195 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
@@ -40,7 +40,15 @@
      */
     public static RemoteAnimationTarget[] wrapApps(TransitionInfo info,
             SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
-        return wrap(info, t, leashMap, new TransitionUtil.LeafTaskFilter());
+        // LeafTaskFilter is order-dependent, so the same object needs to be used for all Change
+        // objects. That's why it's constructed here and captured by the lambda instead of building
+        // a new one ad hoc every time.
+        TransitionUtil.LeafTaskFilter taskFilter = new TransitionUtil.LeafTaskFilter();
+        return wrap(info, t, leashMap, (change) -> {
+            // Intra-task activity -> activity transitions should be categorized as apps.
+            if (change.getActivityComponent() != null) return true;
+            return taskFilter.test(change);
+        });
     }
 
     /**
@@ -53,8 +61,12 @@
      */
     public static RemoteAnimationTarget[] wrapNonApps(TransitionInfo info, boolean wallpapers,
             SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
-        return wrap(info, t, leashMap, (change) -> (wallpapers
-                ? TransitionUtil.isWallpaper(change) : TransitionUtil.isNonApp(change)));
+        return wrap(info, t, leashMap, (change) -> {
+            // Intra-task activity -> activity transitions should be categorized as apps.
+            if (change.getActivityComponent() != null) return false;
+            return wallpapers
+                    ? TransitionUtil.isWallpaper(change) : TransitionUtil.isNonApp(change);
+        });
     }
 
     private static RemoteAnimationTarget[] wrap(TransitionInfo info,
diff --git a/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java b/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java
index 4372826..0f5f869 100644
--- a/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java
+++ b/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java
@@ -20,12 +20,11 @@
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.pm.UserInfo;
-import android.os.HandlerExecutor;
-import android.os.HandlerThread;
 import android.os.UserHandle;
 
 import androidx.annotation.NonNull;
 
+import com.android.systemui.res.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.GuestResetOrExitSessionReceiver.ResetSessionDialogFactory;
@@ -33,7 +32,6 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.qs.QSUserSwitcherEvent;
-import com.android.systemui.res.R;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
@@ -63,7 +61,6 @@
     private final SecureSettings mSecureSettings;
     private final ResetSessionDialogFactory mResetSessionDialogFactory;
     private final GuestSessionNotification mGuestSessionNotification;
-    private final HandlerThread mHandlerThread;
 
     @VisibleForTesting
     public final UserTracker.Callback mUserChangedCallback =
@@ -114,16 +111,13 @@
         mSecureSettings = secureSettings;
         mGuestSessionNotification = guestSessionNotification;
         mResetSessionDialogFactory = resetSessionDialogFactory;
-        mHandlerThread = new HandlerThread("GuestResumeSessionReceiver");
-        mHandlerThread.start();
     }
 
     /**
      * Register this receiver with the {@link BroadcastDispatcher}
      */
     public void register() {
-        mUserTracker.addCallback(mUserChangedCallback,
-                  new HandlerExecutor(mHandlerThread.getThreadHandler()));
+        mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
     }
 
     private void cancelDialog() {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java
index 3ca95e1..5171a1f 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java
@@ -19,6 +19,7 @@
 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW;
 import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
 
 import static com.android.systemui.accessibility.AccessibilityLogger.MagnificationSettingsEvent;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP;
@@ -28,10 +29,12 @@
 import android.content.Context;
 import android.graphics.Rect;
 import android.hardware.display.DisplayManager;
+import android.os.Binder;
 import android.os.Handler;
 import android.util.SparseArray;
 import android.view.Display;
 import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
 import android.view.WindowManagerGlobal;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.IMagnificationConnection;
@@ -40,6 +43,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
 import com.android.systemui.CoreStartable;
+import com.android.systemui.Flags;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.model.SysUiState;
@@ -49,6 +53,7 @@
 import com.android.systemui.util.settings.SecureSettings;
 
 import java.io.PrintWriter;
+import java.util.function.Supplier;
 
 import javax.inject.Inject;
 
@@ -101,19 +106,28 @@
         @Override
         protected WindowMagnificationController createInstance(Display display) {
             final Context windowContext = mContext.createWindowContext(display,
-                    TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, /* options */ null);
+                    Flags.createWindowlessWindowMagnifier()
+                            ? TYPE_ACCESSIBILITY_OVERLAY
+                            : TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY,
+                    /* options */ null);
             windowContext.setTheme(com.android.systemui.res.R.style.Theme_SystemUI);
+
+            Supplier<SurfaceControlViewHost> scvhSupplier = () ->
+                    Flags.createWindowlessWindowMagnifier() ? new SurfaceControlViewHost(mContext,
+                            mContext.getDisplay(), new Binder(), TAG) : null;
+
             return new WindowMagnificationController(
                     windowContext,
                     mHandler,
                     new WindowMagnificationAnimationController(windowContext),
-                    new SfVsyncFrameCallbackProvider(),
-                    null,
+                    /* mirrorWindowControl= */ null,
                     new SurfaceControl.Transaction(),
                     mWindowMagnifierCallback,
                     mSysUiState,
-                    WindowManagerGlobal::getWindowSession,
-                    mSecureSettings);
+                    mSecureSettings,
+                    scvhSupplier,
+                    new SfVsyncFrameCallbackProvider(),
+                    WindowManagerGlobal::getWindowSession);
         }
     }
 
@@ -140,7 +154,7 @@
         @Override
         protected MagnificationSettingsController createInstance(Display display) {
             final Context windowContext = mContext.createWindowContext(display,
-                    TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, /* options */ null);
+                    TYPE_ACCESSIBILITY_OVERLAY, /* options */ null);
             windowContext.setTheme(com.android.systemui.res.R.style.Theme_SystemUI);
             return new MagnificationSettingsController(
                     windowContext,
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
index dde9f48..33728ac 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
@@ -63,23 +63,27 @@
 import android.view.MotionEvent;
 import android.view.Surface;
 import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
 import android.view.View;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
 import android.view.WindowMetrics;
+import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 import android.view.accessibility.IRemoteMagnificationAnimationCallback;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 
+import androidx.annotation.UiThread;
 import androidx.core.math.MathUtils;
 
 import com.android.internal.accessibility.common.MagnificationConstants;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
+import com.android.systemui.Flags;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.res.R;
 import com.android.systemui.util.settings.SecureSettings;
@@ -158,6 +162,15 @@
     private int mMagnificationFrameOffsetX = 0;
     private int mMagnificationFrameOffsetY = 0;
 
+    @Nullable private Supplier<SurfaceControlViewHost> mScvhSupplier;
+
+    /**
+     * SurfaceControlViewHost is used to control the position of the window containing
+     * {@link #mMirrorView}. Using SurfaceControlViewHost instead of a regular window enables
+     * changing the window's position and setting {@link #mMirrorSurface}'s geometry atomically.
+     */
+    private SurfaceControlViewHost mSurfaceControlViewHost;
+
     // The root of the mirrored content
     private SurfaceControl mMirrorSurface;
 
@@ -236,21 +249,21 @@
             @UiContext Context context,
             @NonNull Handler handler,
             @NonNull WindowMagnificationAnimationController animationController,
-            SfVsyncFrameCallbackProvider sfVsyncFrameProvider,
             MirrorWindowControl mirrorWindowControl,
             SurfaceControl.Transaction transaction,
             @NonNull WindowMagnifierCallback callback,
             SysUiState sysUiState,
-            @NonNull Supplier<IWindowSession> globalWindowSessionSupplier,
-            SecureSettings secureSettings) {
+            SecureSettings secureSettings,
+            Supplier<SurfaceControlViewHost> scvhSupplier,
+            SfVsyncFrameCallbackProvider sfVsyncFrameProvider,
+            Supplier<IWindowSession> globalWindowSessionSupplier) {
         mContext = context;
         mHandler = handler;
         mAnimationController = animationController;
-        mGlobalWindowSessionSupplier = globalWindowSessionSupplier;
         mAnimationController.setWindowMagnificationController(this);
-        mSfVsyncFrameProvider = sfVsyncFrameProvider;
         mWindowMagnifierCallback = callback;
         mSysUiState = sysUiState;
+        mScvhSupplier = scvhSupplier;
         mConfiguration = new Configuration(context.getResources().getConfiguration());
         mWindowMagnificationSizePrefs = new WindowMagnificationSizePrefs(mContext);
 
@@ -288,22 +301,78 @@
         mTransaction = transaction;
         mGestureDetector =
                 new MagnificationGestureDetector(mContext, handler, this);
+        mWindowInsetChangeRunnable = this::onWindowInsetChanged;
+        mGlobalWindowSessionSupplier = globalWindowSessionSupplier;
+        mSfVsyncFrameProvider = sfVsyncFrameProvider;
 
         // Initialize listeners.
-        mMirrorViewRunnable = () -> {
-            if (mMirrorView != null) {
-                final Rect oldViewBounds = new Rect(mMirrorViewBounds);
-                mMirrorView.getBoundsOnScreen(mMirrorViewBounds);
-                if (oldViewBounds.width() != mMirrorViewBounds.width()
-                        || oldViewBounds.height() != mMirrorViewBounds.height()) {
-                    mMirrorView.setSystemGestureExclusionRects(Collections.singletonList(
-                            new Rect(0, 0, mMirrorViewBounds.width(), mMirrorViewBounds.height())));
+        if (Flags.createWindowlessWindowMagnifier()) {
+            mMirrorViewRunnable = new Runnable() {
+                final Rect mPreviousBounds = new Rect();
+
+                @Override
+                public void run() {
+                    if (mMirrorView != null) {
+                        if (mPreviousBounds.width() != mMirrorViewBounds.width()
+                                || mPreviousBounds.height() != mMirrorViewBounds.height()) {
+                            mMirrorView.setSystemGestureExclusionRects(Collections.singletonList(
+                                    new Rect(0, 0, mMirrorViewBounds.width(),
+                                            mMirrorViewBounds.height())));
+                            mPreviousBounds.set(mMirrorViewBounds);
+                        }
+                        updateSystemUIStateIfNeeded();
+                        mWindowMagnifierCallback.onWindowMagnifierBoundsChanged(
+                                mDisplayId, mMirrorViewBounds);
+                    }
                 }
-                updateSystemUIStateIfNeeded();
-                mWindowMagnifierCallback.onWindowMagnifierBoundsChanged(
-                        mDisplayId, mMirrorViewBounds);
-            }
-        };
+            };
+
+            mMirrorSurfaceViewLayoutChangeListener =
+                    (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
+                            mMirrorView.post(this::applyTapExcludeRegion);
+
+            mMirrorViewGeometryVsyncCallback = null;
+        } else {
+            mMirrorViewRunnable = () -> {
+                if (mMirrorView != null) {
+                    final Rect oldViewBounds = new Rect(mMirrorViewBounds);
+                    mMirrorView.getBoundsOnScreen(mMirrorViewBounds);
+                    if (oldViewBounds.width() != mMirrorViewBounds.width()
+                            || oldViewBounds.height() != mMirrorViewBounds.height()) {
+                        mMirrorView.setSystemGestureExclusionRects(Collections.singletonList(
+                                new Rect(0, 0,
+                                        mMirrorViewBounds.width(), mMirrorViewBounds.height())));
+                    }
+                    updateSystemUIStateIfNeeded();
+                    mWindowMagnifierCallback.onWindowMagnifierBoundsChanged(
+                            mDisplayId, mMirrorViewBounds);
+                }
+            };
+
+            mMirrorSurfaceViewLayoutChangeListener =
+                    (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
+                            mMirrorView.post(this::applyTapExcludeRegion);
+
+            mMirrorViewGeometryVsyncCallback =
+                    l -> {
+                        if (isActivated() && mMirrorSurface != null && calculateSourceBounds(
+                                mMagnificationFrame, mScale)) {
+                            // The final destination for the magnification surface should be at 0,0
+                            // since the ViewRootImpl's position will change
+                            mTmpRect.set(0, 0, mMagnificationFrame.width(),
+                                    mMagnificationFrame.height());
+                            mTransaction.setGeometry(mMirrorSurface, mSourceBounds, mTmpRect,
+                                    Surface.ROTATION_0).apply();
+
+                            // Notify source bounds change when the magnifier is not animating.
+                            if (!mAnimationController.isAnimating()) {
+                                mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId,
+                                        mSourceBounds);
+                            }
+                        }
+                    };
+        }
+
         mMirrorViewLayoutChangeListener =
                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
                     if (!mHandler.hasCallbacks(mMirrorViewRunnable)) {
@@ -311,34 +380,11 @@
                     }
                 };
 
-        mMirrorSurfaceViewLayoutChangeListener =
-                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
-                        mMirrorView.post(this::applyTapExcludeRegion);
-
-        mMirrorViewGeometryVsyncCallback =
-                l -> {
-                    if (isActivated() && mMirrorSurface != null && calculateSourceBounds(
-                            mMagnificationFrame, mScale)) {
-                        // The final destination for the magnification surface should be at 0,0
-                        // since the ViewRootImpl's position will change
-                        mTmpRect.set(0, 0, mMagnificationFrame.width(),
-                                mMagnificationFrame.height());
-                        mTransaction.setGeometry(mMirrorSurface, mSourceBounds, mTmpRect,
-                                Surface.ROTATION_0).apply();
-
-                        // Notify source bounds change when the magnifier is not animating.
-                        if (!mAnimationController.isAnimating()) {
-                            mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId,
-                                    mSourceBounds);
-                        }
-                    }
-                };
         mUpdateStateDescriptionRunnable = () -> {
             if (isActivated()) {
                 mMirrorView.setStateDescription(formatStateDescription(mScale));
             }
         };
-        mWindowInsetChangeRunnable = this::onWindowInsetChanged;
     }
 
     private void setupMagnificationSizeScaleOptions() {
@@ -448,13 +494,21 @@
         if (mMirrorView != null) {
             mHandler.removeCallbacks(mMirrorViewRunnable);
             mMirrorView.removeOnLayoutChangeListener(mMirrorViewLayoutChangeListener);
-            mWm.removeView(mMirrorView);
+            if (!Flags.createWindowlessWindowMagnifier()) {
+                mWm.removeView(mMirrorView);
+            }
             mMirrorView = null;
         }
 
         if (mMirrorWindowControl != null) {
             mMirrorWindowControl.destroyControl();
         }
+
+        if (mSurfaceControlViewHost != null) {
+            mSurfaceControlViewHost.release();
+            mSurfaceControlViewHost = null;
+        }
+
         mMirrorViewBounds.setEmpty();
         mSourceBounds.setEmpty();
         updateSystemUIStateIfNeeded();
@@ -551,7 +605,11 @@
         if (!isActivated()) return;
         LayoutParams params = (LayoutParams) mMirrorView.getLayoutParams();
         params.accessibilityTitle = getAccessibilityWindowTitle();
-        mWm.updateViewLayout(mMirrorView, params);
+        if (Flags.createWindowlessWindowMagnifier()) {
+            mSurfaceControlViewHost.relayout(params);
+        } else {
+            mWm.updateViewLayout(mMirrorView, params);
+        }
     }
 
     /**
@@ -602,6 +660,11 @@
     }
 
     private void createMirrorWindow() {
+        if (Flags.createWindowlessWindowMagnifier()) {
+            createWindowlessMirrorWindow();
+            return;
+        }
+
         // The window should be the size the mirrored surface will be but also add room for the
         // border and the drag handle.
         int windowWidth = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin;
@@ -652,6 +715,68 @@
         addDragTouchListeners();
     }
 
+    private void createWindowlessMirrorWindow() {
+        // The window should be the size the mirrored surface will be but also add room for the
+        // border and the drag handle.
+        int windowWidth = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin;
+        int windowHeight = mMagnificationFrame.height() + 2 * mMirrorSurfaceMargin;
+
+        // TODO delete TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, it shouldn't be needed anymore
+
+        LayoutParams params = new LayoutParams(
+                windowWidth, windowHeight,
+                LayoutParams.TYPE_ACCESSIBILITY_OVERLAY,
+                LayoutParams.FLAG_NOT_TOUCH_MODAL
+                        | LayoutParams.FLAG_NOT_FOCUSABLE,
+                PixelFormat.TRANSPARENT);
+        params.receiveInsetsIgnoringZOrder = true;
+        params.setTitle(mContext.getString(R.string.magnification_window_title));
+        params.accessibilityTitle = getAccessibilityWindowTitle();
+        params.setTrustedOverlay();
+
+        mMirrorView = LayoutInflater.from(mContext).inflate(R.layout.window_magnifier_view, null);
+        mMirrorSurfaceView = mMirrorView.findViewById(R.id.surface_view);
+
+        mMirrorBorderView = mMirrorView.findViewById(R.id.magnification_inner_border);
+
+        // Allow taps to go through to the mirror SurfaceView below.
+        mMirrorSurfaceView.addOnLayoutChangeListener(mMirrorSurfaceViewLayoutChangeListener);
+
+        mMirrorView.addOnLayoutChangeListener(mMirrorViewLayoutChangeListener);
+        mMirrorView.setAccessibilityDelegate(new MirrorWindowA11yDelegate());
+        mMirrorView.setOnApplyWindowInsetsListener((v, insets) -> {
+            if (!mHandler.hasCallbacks(mWindowInsetChangeRunnable)) {
+                mHandler.post(mWindowInsetChangeRunnable);
+            }
+            return v.onApplyWindowInsets(insets);
+        });
+
+        mSurfaceControlViewHost = mScvhSupplier.get();
+        mSurfaceControlViewHost.setView(mMirrorView, params);
+        SurfaceControl surfaceControl = mSurfaceControlViewHost
+                .getSurfacePackage().getSurfaceControl();
+
+        int x = mMagnificationFrame.left - mMirrorSurfaceMargin;
+        int y = mMagnificationFrame.top - mMirrorSurfaceMargin;
+        mTransaction
+                .setCrop(surfaceControl, new Rect(0, 0, windowWidth, windowHeight))
+                .setPosition(surfaceControl, x, y)
+                .setLayer(surfaceControl, Integer.MAX_VALUE)
+                .show(surfaceControl)
+                .apply();
+
+        mMirrorViewBounds.set(x, y, x + windowWidth, y + windowHeight);
+
+        AccessibilityManager accessibilityManager = mContext
+                .getSystemService(AccessibilityManager.class);
+        accessibilityManager.attachAccessibilityOverlayToDisplay(mDisplayId, surfaceControl);
+
+        SurfaceHolder holder = mMirrorSurfaceView.getHolder();
+        holder.addCallback(this);
+        holder.setFormat(PixelFormat.RGBA_8888);
+        addDragTouchListeners();
+    }
+
     private void onWindowInsetChanged() {
         if (updateSystemGestureInsetsTop()) {
             updateSystemUIStateIfNeeded();
@@ -659,6 +784,11 @@
     }
 
     private void applyTapExcludeRegion() {
+        if (Flags.createWindowlessWindowMagnifier()) {
+            applyTouchableRegion();
+            return;
+        }
+
         // Sometimes this can get posted and run after deleteWindowMagnification() is called.
         if (mMirrorView == null) return;
 
@@ -709,6 +839,51 @@
         return regionInsideDragBorder;
     }
 
+    private void applyTouchableRegion() {
+        // Sometimes this can get posted and run after deleteWindowMagnification() is called.
+        if (mMirrorView == null) return;
+
+        var surfaceControl = mSurfaceControlViewHost.getRootSurfaceControl();
+        surfaceControl.setTouchableRegion(calculateTouchableRegion());
+    }
+
+    private Region calculateTouchableRegion() {
+        Region touchableRegion = new Region(0, 0, mMirrorView.getWidth(), mMirrorView.getHeight());
+
+        Region regionInsideDragBorder = new Region(mBorderDragSize, mBorderDragSize,
+                mMirrorView.getWidth() - mBorderDragSize,
+                mMirrorView.getHeight() - mBorderDragSize);
+        touchableRegion.op(regionInsideDragBorder, Region.Op.DIFFERENCE);
+
+        Rect dragArea = new Rect();
+        mDragView.getHitRect(dragArea);
+
+        Rect topLeftArea = new Rect();
+        mTopLeftCornerView.getHitRect(topLeftArea);
+
+        Rect topRightArea = new Rect();
+        mTopRightCornerView.getHitRect(topRightArea);
+
+        Rect bottomLeftArea = new Rect();
+        mBottomLeftCornerView.getHitRect(bottomLeftArea);
+
+        Rect bottomRightArea = new Rect();
+        mBottomRightCornerView.getHitRect(bottomRightArea);
+
+        Rect closeArea = new Rect();
+        mCloseView.getHitRect(closeArea);
+
+        // Add touchable regions for drag and close
+        touchableRegion.op(dragArea, Region.Op.UNION);
+        touchableRegion.op(topLeftArea, Region.Op.UNION);
+        touchableRegion.op(topRightArea, Region.Op.UNION);
+        touchableRegion.op(bottomLeftArea, Region.Op.UNION);
+        touchableRegion.op(bottomRightArea, Region.Op.UNION);
+        touchableRegion.op(closeArea, Region.Op.UNION);
+
+        return touchableRegion;
+    }
+
     private String getAccessibilityWindowTitle() {
         return mResources.getString(com.android.internal.R.string.android_system_label);
     }
@@ -852,8 +1027,84 @@
      * {@link #mMagnificationFrame}.
      */
     private void modifyWindowMagnification(boolean computeWindowSize) {
-        mSfVsyncFrameProvider.postFrameCallback(mMirrorViewGeometryVsyncCallback);
-        updateMirrorViewLayout(computeWindowSize);
+        if (Flags.createWindowlessWindowMagnifier()) {
+            updateMirrorSurfaceGeometry();
+            updateWindowlessMirrorViewLayout(computeWindowSize);
+        } else {
+            mSfVsyncFrameProvider.postFrameCallback(mMirrorViewGeometryVsyncCallback);
+            updateMirrorViewLayout(computeWindowSize);
+        }
+    }
+
+    /**
+     * Updates {@link #mMirrorSurface}'s geometry. This modifies {@link #mTransaction} but does not
+     * apply it.
+     */
+    @UiThread
+    private void updateMirrorSurfaceGeometry() {
+        if (isActivated() && mMirrorSurface != null
+                && calculateSourceBounds(mMagnificationFrame, mScale)) {
+            // The final destination for the magnification surface should be at 0,0
+            // since the ViewRootImpl's position will change
+            mTmpRect.set(0, 0, mMagnificationFrame.width(), mMagnificationFrame.height());
+            mTransaction.setGeometry(mMirrorSurface, mSourceBounds, mTmpRect, Surface.ROTATION_0);
+
+            // Notify source bounds change when the magnifier is not animating.
+            if (!mAnimationController.isAnimating()) {
+                mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId, mSourceBounds);
+            }
+        }
+    }
+
+    /**
+     * Updates the position of {@link mSurfaceControlViewHost} and layout params of MirrorView based
+     * on the position and size of {@link #mMagnificationFrame}.
+     *
+     * @param computeWindowSize set to {@code true} to compute window size with
+     * {@link #mMagnificationFrame}.
+     */
+    @UiThread
+    private void updateWindowlessMirrorViewLayout(boolean computeWindowSize) {
+        if (!isActivated()) {
+            return;
+        }
+
+        final int width = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin;
+        final int height = mMagnificationFrame.height() + 2 * mMirrorSurfaceMargin;
+
+        final int minX = -mOuterBorderSize;
+        final int maxX = mWindowBounds.right - width + mOuterBorderSize;
+        final int x = MathUtils.clamp(mMagnificationFrame.left - mMirrorSurfaceMargin, minX, maxX);
+
+        final int minY = -mOuterBorderSize;
+        final int maxY = mWindowBounds.bottom - height + mOuterBorderSize;
+        final int y = MathUtils.clamp(mMagnificationFrame.top - mMirrorSurfaceMargin, minY, maxY);
+
+        if (computeWindowSize) {
+            LayoutParams params = (LayoutParams) mMirrorView.getLayoutParams();
+            params.width = width;
+            params.height = height;
+            mSurfaceControlViewHost.relayout(params);
+            mTransaction.setCrop(mSurfaceControlViewHost.getSurfacePackage().getSurfaceControl(),
+                    new Rect(0, 0, width, height));
+        }
+
+        mMirrorViewBounds.set(x, y, x + width, y + height);
+        mTransaction.setPosition(
+                mSurfaceControlViewHost.getSurfacePackage().getSurfaceControl(), x, y);
+        if (computeWindowSize) {
+            mSurfaceControlViewHost.getRootSurfaceControl().applyTransactionOnDraw(mTransaction);
+        } else {
+            mTransaction.apply();
+        }
+
+        // If they are not dragging the handle, we can move the drag handle immediately without
+        // disruption. But if they are dragging it, we avoid moving until the end of the drag.
+        if (!mIsDragging) {
+            mMirrorView.post(this::maybeRepositionButton);
+        }
+
+        mMirrorViewRunnable.run();
     }
 
     /**
@@ -1094,7 +1345,7 @@
 
     /**
      * Enables window magnification with specified parameters. If the given scale is <strong>less
-     * than or equal to 1.0f<strong>, then
+     * than or equal to 1.0f</strong>, then
      * {@link WindowMagnificationController#deleteWindowMagnification()} will be called instead to
      * be consistent with the behavior of display magnification.
      *
@@ -1110,7 +1361,7 @@
 
     /**
      * Enables window magnification with specified parameters. If the given scale is <strong>less
-     * than 1.0f<strong>, then
+     * than 1.0f</strong>, then
      * {@link WindowMagnificationController#deleteWindowMagnification()} will be called instead to
      * be consistent with the behavior of display magnification.
      *
@@ -1426,10 +1677,8 @@
         final FrameLayout.LayoutParams layoutParams =
                 (FrameLayout.LayoutParams) mDragView.getLayoutParams();
 
-        mMirrorView.getBoundsOnScreen(mTmpRect);
-
         final int newGravity;
-        if (mTmpRect.right >= screenEdgeX) {
+        if (mMirrorViewBounds.right >= screenEdgeX) {
             newGravity = Gravity.BOTTOM | Gravity.LEFT;
         } else {
             newGravity = Gravity.BOTTOM | Gravity.RIGHT;
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/GlobalModule.java b/packages/SystemUI/src/com/android/systemui/dagger/GlobalModule.java
index 5c38264..3072f74 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/GlobalModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/GlobalModule.java
@@ -24,6 +24,7 @@
 import com.android.systemui.plugins.PluginsModule;
 import com.android.systemui.unfold.UnfoldTransitionModule;
 import com.android.systemui.util.concurrency.GlobalConcurrencyModule;
+import com.android.systemui.util.kotlin.GlobalCoroutinesModule;
 
 import dagger.Module;
 import dagger.Provides;
@@ -47,6 +48,7 @@
         AndroidInternalsModule.class,
         FrameworkServicesModule.class,
         GlobalConcurrencyModule.class,
+        GlobalCoroutinesModule.class,
         UnfoldTransitionModule.class,
         PluginsModule.class,
 })
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index 50f861f..e9d1e94 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -63,6 +63,7 @@
 import com.android.systemui.statusbar.policy.SensorPrivacyController;
 import com.android.systemui.statusbar.policy.SensorPrivacyControllerImpl;
 import com.android.systemui.toast.ToastModule;
+import com.android.systemui.unfold.UnfoldTransitionModule;
 import com.android.systemui.volume.dagger.VolumeModule;
 import com.android.systemui.wallpapers.dagger.WallpaperModule;
 
@@ -107,6 +108,7 @@
         ShadeModule.class,
         StartCentralSurfacesModule.class,
         SceneContainerFrameworkModule.class,
+        UnfoldTransitionModule.Startables.class,
         ToastModule.class,
         VolumeModule.class,
         WallpaperModule.class
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index c1870c0..2587e2d 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -134,7 +134,7 @@
 import com.android.systemui.util.EventLogModule;
 import com.android.systemui.util.concurrency.SysUIConcurrencyModule;
 import com.android.systemui.util.dagger.UtilModule;
-import com.android.systemui.util.kotlin.CoroutinesModule;
+import com.android.systemui.util.kotlin.SysUICoroutinesModule;
 import com.android.systemui.util.reference.ReferenceModule;
 import com.android.systemui.util.sensors.SensorModule;
 import com.android.systemui.util.settings.SettingsUtilModule;
@@ -183,7 +183,6 @@
         ConfigurationControllerModule.class,
         ConnectivityModule.class,
         ControlsModule.class,
-        CoroutinesModule.class,
         DemoModeModule.class,
         DeviceEntryModule.class,
         DisableFlagsModule.class,
@@ -229,6 +228,7 @@
         StatusBarWindowModule.class,
         SystemPropertiesFlagsModule.class,
         SysUIConcurrencyModule.class,
+        SysUICoroutinesModule.class,
         SysUIUnfoldModule.class,
         TelephonyRepositoryModule.class,
         TemporaryDisplayModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
index 3888345..a1f9425 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
@@ -56,6 +56,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
@@ -102,10 +103,10 @@
             quickAffordanceAlwaysVisible(position),
             keyguardInteractor.isDozing,
             keyguardInteractor.isKeyguardShowing,
-            shadeInteractor.anyExpansion,
+            shadeInteractor.anyExpansion.map { it < 1.0f }.distinctUntilChanged(),
             biometricSettingsRepository.isCurrentUserInLockdown,
-        ) { affordance, isDozing, isKeyguardShowing, qsExpansion, isUserInLockdown ->
-            if (!isDozing && isKeyguardShowing && (qsExpansion < 1.0f) && !isUserInLockdown) {
+        ) { affordance, isDozing, isKeyguardShowing, isQuickSettingsVisible, isUserInLockdown ->
+            if (!isDozing && isKeyguardShowing && isQuickSettingsVisible && !isUserInLockdown) {
                 affordance
             } else {
                 KeyguardQuickAffordanceModel.Hidden
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/PreviewKeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/PreviewKeyguardBlueprintViewBinder.kt
deleted file mode 100644
index 2feaa2e..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/PreviewKeyguardBlueprintViewBinder.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.keyguard.ui.binder
-
-import android.os.Trace
-import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.constraintlayout.widget.ConstraintSet
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel
-import com.android.systemui.lifecycle.repeatWhenAttached
-import kotlinx.coroutines.DisposableHandle
-import kotlinx.coroutines.launch
-
-/**
- * Binds the existing blueprint to the constraint layout that previews keyguard.
- *
- * This view binder should only inflate and add relevant views and apply the constraints. Actual
- * data binding should be done in {@link KeyguardPreviewRenderer}
- */
-class PreviewKeyguardBlueprintViewBinder {
-    companion object {
-
-        /**
-         * Binds the existing blueprint to the constraint layout that previews keyguard.
-         *
-         * @param constraintLayout The root view to bind to
-         * @param viewModel The instance of the view model that contains flows we collect on.
-         * @param finishedAddViewCallback Called when we have finished inflating the views.
-         */
-        fun bind(
-            constraintLayout: ConstraintLayout,
-            viewModel: KeyguardBlueprintViewModel,
-            finishedAddViewCallback: () -> Unit
-        ): DisposableHandle {
-            return constraintLayout.repeatWhenAttached {
-                repeatOnLifecycle(Lifecycle.State.CREATED) {
-                    launch {
-                        viewModel.blueprint.collect { blueprint ->
-                            val prevBluePrint = viewModel.currentBluePrint
-                            Trace.beginSection("PreviewKeyguardBlueprint#applyBlueprint")
-
-                            ConstraintSet().apply {
-                                clone(constraintLayout)
-                                val emptyLayout = ConstraintSet.Layout()
-                                knownIds.forEach { getConstraint(it).layout.copyFrom(emptyLayout) }
-                                blueprint.applyConstraints(this)
-                                // Add and remove views of sections that are not contained by the
-                                // other.
-                                blueprint.replaceViews(
-                                    prevBluePrint,
-                                    constraintLayout,
-                                    bindData = false
-                                )
-                                applyTo(constraintLayout)
-                            }
-
-                            viewModel.currentBluePrint = blueprint
-                            finishedAddViewCallback.invoke()
-                            Trace.endSection()
-                        }
-                    }
-                }
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index 841bad4..a0c0095 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -41,6 +41,7 @@
 import android.widget.FrameLayout
 import android.widget.TextView
 import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
 import androidx.core.view.isInvisible
 import com.android.keyguard.ClockEventController
 import com.android.keyguard.KeyguardClockSwitch
@@ -54,15 +55,12 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.flags.FeatureFlagsClassic
-import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
 import com.android.systemui.keyguard.ui.binder.KeyguardPreviewClockViewBinder
 import com.android.systemui.keyguard.ui.binder.KeyguardPreviewSmartspaceViewBinder
 import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
 import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder
-import com.android.systemui.keyguard.ui.binder.PreviewKeyguardBlueprintViewBinder
 import com.android.systemui.keyguard.ui.view.KeyguardRootView
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel
+import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSection
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewClockViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewSmartspaceViewModel
@@ -124,19 +122,18 @@
     private val broadcastDispatcher: BroadcastDispatcher,
     private val lockscreenSmartspaceController: LockscreenSmartspaceController,
     private val udfpsOverlayInteractor: UdfpsOverlayInteractor,
-    private val featureFlags: FeatureFlagsClassic,
     private val falsingManager: FalsingManager,
     private val vibratorHelper: VibratorHelper,
     private val indicationController: KeyguardIndicationController,
     private val keyguardRootViewModel: KeyguardRootViewModel,
     @Assisted bundle: Bundle,
-    private val keyguardBlueprintViewModel: KeyguardBlueprintViewModel,
     private val occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel,
     private val chipbarCoordinator: ChipbarCoordinator,
     private val screenOffAnimationController: ScreenOffAnimationController,
     private val shadeInteractor: ShadeInteractor,
     private val secureSettings: SecureSettings,
     private val communalTutorialViewModel: CommunalTutorialIndicatorViewModel,
+    private val defaultShortcutsSection: DefaultShortcutsSection,
 ) {
     val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN)
     private val width: Int = bundle.getInt(KEY_VIEW_WIDTH)
@@ -393,32 +390,32 @@
 
         setUpUdfps(previewContext, rootView)
 
-        disposables.add(
-            PreviewKeyguardBlueprintViewBinder.bind(keyguardRootView, keyguardBlueprintViewModel) {
-                if (keyguardBottomAreaRefactor()) {
-                    setupShortcuts(keyguardRootView)
-                }
+        if (keyguardBottomAreaRefactor()) {
+            setupShortcuts(keyguardRootView)
+        }
 
-                if (!shouldHideClock) {
-                    setUpClock(previewContext, rootView)
-                    KeyguardPreviewClockViewBinder.bind(
-                        largeClockHostView,
-                        smallClockHostView,
-                        clockViewModel,
-                    )
-                }
+        if (!shouldHideClock) {
+            setUpClock(previewContext, rootView)
+            KeyguardPreviewClockViewBinder.bind(
+                largeClockHostView,
+                smallClockHostView,
+                clockViewModel,
+            )
+        }
 
-                setUpSmartspace(previewContext, rootView)
-                smartSpaceView?.let {
-                    KeyguardPreviewSmartspaceViewBinder.bind(it, smartspaceViewModel)
-                }
-
-                setupCommunalTutorialIndicator(keyguardRootView)
-            }
-        )
+        setUpSmartspace(previewContext, rootView)
+        smartSpaceView?.let { KeyguardPreviewSmartspaceViewBinder.bind(it, smartspaceViewModel) }
+        setupCommunalTutorialIndicator(keyguardRootView)
     }
 
     private fun setupShortcuts(keyguardRootView: ConstraintLayout) {
+        // Add shortcuts
+        val cs = ConstraintSet()
+        cs.clone(keyguardRootView)
+        defaultShortcutsSection.addViews(keyguardRootView)
+        defaultShortcutsSection.applyConstraints(cs)
+        cs.applyTo(keyguardRootView)
+
         keyguardRootView.findViewById<LaunchableImageView?>(R.id.start_button)?.let { imageView ->
             shortcutsBindings.add(
                 KeyguardQuickAffordanceViewBinder.bind(
@@ -476,53 +473,40 @@
     }
 
     private fun setUpClock(previewContext: Context, parentView: ViewGroup) {
-        largeClockHostView =
-            if (KeyguardShadeMigrationNssl.isEnabled) {
-                parentView.requireViewById<FrameLayout>(R.id.lockscreen_clock_view_large)
-            } else {
-                val hostView = FrameLayout(previewContext)
-                hostView.layoutParams =
-                    FrameLayout.LayoutParams(
-                        FrameLayout.LayoutParams.MATCH_PARENT,
-                        FrameLayout.LayoutParams.MATCH_PARENT,
-                    )
-                parentView.addView(hostView)
-                hostView
-            }
+        val resources = parentView.resources
+        largeClockHostView = FrameLayout(previewContext)
+        largeClockHostView.layoutParams =
+            FrameLayout.LayoutParams(
+                FrameLayout.LayoutParams.MATCH_PARENT,
+                FrameLayout.LayoutParams.MATCH_PARENT,
+            )
+        parentView.addView(largeClockHostView)
         largeClockHostView.isInvisible = true
 
-        smallClockHostView =
-            if (KeyguardShadeMigrationNssl.isEnabled) {
-                parentView.requireViewById<FrameLayout>(R.id.lockscreen_clock_view)
-            } else {
-                val resources = parentView.resources
-                val hostView = FrameLayout(previewContext)
-                val layoutParams =
-                    FrameLayout.LayoutParams(
-                        FrameLayout.LayoutParams.WRAP_CONTENT,
-                        resources.getDimensionPixelSize(
-                            com.android.systemui.customization.R.dimen.small_clock_height
-                        )
-                    )
-                layoutParams.topMargin =
-                    KeyguardPreviewSmartspaceViewModel.getStatusBarHeight(resources) +
-                        resources.getDimensionPixelSize(
-                            com.android.systemui.customization.R.dimen.small_clock_padding_top
-                        )
-                hostView.layoutParams = layoutParams
-
-                hostView.setPaddingRelative(
-                    resources.getDimensionPixelSize(
-                        com.android.systemui.customization.R.dimen.clock_padding_start
-                    ),
-                    0,
-                    0,
-                    0
+        smallClockHostView = FrameLayout(previewContext)
+        val layoutParams =
+            FrameLayout.LayoutParams(
+                FrameLayout.LayoutParams.WRAP_CONTENT,
+                resources.getDimensionPixelSize(
+                    com.android.systemui.customization.R.dimen.small_clock_height
                 )
-                hostView.clipChildren = false
-                parentView.addView(hostView)
-                hostView
-            }
+            )
+        layoutParams.topMargin =
+            KeyguardPreviewSmartspaceViewModel.getStatusBarHeight(resources) +
+                resources.getDimensionPixelSize(
+                    com.android.systemui.customization.R.dimen.small_clock_padding_top
+                )
+        smallClockHostView.layoutParams = layoutParams
+        smallClockHostView.setPaddingRelative(
+            resources.getDimensionPixelSize(
+                com.android.systemui.customization.R.dimen.clock_padding_start
+            ),
+            0,
+            0,
+            0
+        )
+        smallClockHostView.clipChildren = false
+        parentView.addView(smallClockHostView)
         smallClockHostView.isInvisible = true
 
         // TODO (b/283465254): Move the listeners to KeyguardClockRepository
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt
index a3029b2..23ee00d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt
@@ -146,7 +146,7 @@
                 null,
                 UserHandle.ALL
             )
-            userTracker.addCallback(userTrackerCallback, backgroundExecutor)
+            userTracker.addCallback(userTrackerCallback, mainExecutor)
             loadSavedComponents()
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionCaptureTarget.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionCaptureTarget.kt
index 11d0be5..a618490 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionCaptureTarget.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionCaptureTarget.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.mediaprojection
 
-import android.os.IBinder
+import android.app.ActivityOptions.LaunchCookie
 import android.os.Parcel
 import android.os.Parcelable
 
@@ -24,12 +24,12 @@
  * Class that represents an area that should be captured. Currently it has only a launch cookie that
  * represents a task but we potentially could add more identifiers e.g. for a pair of tasks.
  */
-data class MediaProjectionCaptureTarget(val launchCookie: IBinder?) : Parcelable {
+data class MediaProjectionCaptureTarget(val launchCookie: LaunchCookie?) : Parcelable {
 
-    constructor(parcel: Parcel) : this(parcel.readStrongBinder())
+    constructor(parcel: Parcel) : this(LaunchCookie.readFromParcel(parcel))
 
     override fun writeToParcel(dest: Parcel, flags: Int) {
-        dest.writeStrongBinder(launchCookie)
+        LaunchCookie.writeToParcel(launchCookie, dest)
     }
 
     override fun describeContents(): Int = 0
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt
index 50e9e751..4685c5a 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt
@@ -16,6 +16,7 @@
 package com.android.systemui.mediaprojection.appselector
 
 import android.app.ActivityOptions
+import android.app.ActivityOptions.LaunchCookie
 import android.content.Intent
 import android.content.res.Configuration
 import android.content.res.Resources
@@ -24,9 +25,7 @@
 import android.media.projection.MediaProjectionManager.EXTRA_MEDIA_PROJECTION
 import android.media.projection.ReviewGrantedConsentResult.RECORD_CANCEL
 import android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_TASK
-import android.os.Binder
 import android.os.Bundle
-import android.os.IBinder
 import android.os.ResultReceiver
 import android.os.UserHandle
 import android.util.Log
@@ -163,9 +162,9 @@
 
         val intent = createIntent(targetInfo)
 
-        val launchToken: IBinder = Binder("media_projection_launch_token")
+        val launchCookie = LaunchCookie("media_projection_launch_token")
         val activityOptions = ActivityOptions.makeBasic()
-        activityOptions.launchCookie = launchToken
+        activityOptions.setLaunchCookie(launchCookie)
 
         val userHandle = mMultiProfilePagerAdapter.activeListAdapter.userHandle
 
@@ -175,7 +174,7 @@
         // is created and ready to be captured.
         val activityStarted =
             activityLauncher.startActivityAsUser(intent, userHandle, activityOptions.toBundle()) {
-                returnSelectedApp(launchToken)
+                returnSelectedApp(launchCookie)
             }
 
         // Rely on the ActivityManager to pop up a dialog regarding app suspension
@@ -233,7 +232,7 @@
         }
     }
 
-    override fun returnSelectedApp(launchCookie: IBinder) {
+    override fun returnSelectedApp(launchCookie: LaunchCookie) {
         taskSelected = true
         if (intent.hasExtra(EXTRA_CAPTURE_REGION_RESULT_RECEIVER)) {
             // The client requested to return the result in the result receiver instead of
@@ -255,7 +254,7 @@
             val mediaProjectionBinder = intent.getIBinderExtra(EXTRA_MEDIA_PROJECTION)
             val projection = IMediaProjection.Stub.asInterface(mediaProjectionBinder)
 
-            projection.launchCookie = launchCookie
+            projection.setLaunchCookie(launchCookie)
 
             val intent = Intent()
             intent.putExtra(EXTRA_MEDIA_PROJECTION, projection.asBinder())
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorResultHandler.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorResultHandler.kt
index 93c3bce..f204b3e 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorResultHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorResultHandler.kt
@@ -1,6 +1,6 @@
 package com.android.systemui.mediaprojection.appselector
 
-import android.os.IBinder
+import android.app.ActivityOptions.LaunchCookie
 
 /**
  * Interface that allows to continue the media projection flow and return the selected app
@@ -11,5 +11,5 @@
      * Return selected app to the original caller of the media projection app picker.
      * @param launchCookie launch cookie of the launched activity of the target app
      */
-    fun returnSelectedApp(launchCookie: IBinder)
+    fun returnSelectedApp(launchCookie: LaunchCookie)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt
index ba837db..a811065 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt
@@ -17,10 +17,10 @@
 package com.android.systemui.mediaprojection.appselector.view
 
 import android.app.ActivityOptions
+import android.app.ActivityOptions.LaunchCookie
 import android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
 import android.app.IActivityTaskManager
 import android.graphics.Rect
-import android.os.Binder
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
@@ -121,7 +121,7 @@
     }
 
     override fun onRecentAppClicked(task: RecentTask, view: View) {
-        val launchCookie = Binder()
+        val launchCookie = LaunchCookie()
         val activityOptions =
             ActivityOptions.makeScaleUpAnimation(
                 view,
@@ -132,7 +132,7 @@
             )
         activityOptions.pendingIntentBackgroundActivityStartMode =
             MODE_BACKGROUND_ACTIVITY_START_ALLOWED
-        activityOptions.launchCookie = launchCookie
+        activityOptions.setLaunchCookie(launchCookie)
         activityOptions.launchDisplayId = task.displayId
 
         activityTaskManager.startActivityFromRecents(task.taskId, activityOptions.toBundle())
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
index 039372d..8b034b2 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
@@ -28,6 +28,7 @@
 import android.annotation.Nullable;
 import android.app.Activity;
 import android.app.ActivityManager;
+import android.app.ActivityOptions.LaunchCookie;
 import android.app.AlertDialog;
 import android.app.StatusBarManager;
 import android.content.Context;
@@ -146,6 +147,13 @@
                 final IMediaProjection projection =
                         MediaProjectionServiceHelper.createOrReuseProjection(mUid, mPackageName,
                                 mReviewGrantedConsentRequired);
+
+                LaunchCookie launchCookie = launchingIntent.getParcelableExtra(
+                        MediaProjectionManager.EXTRA_LAUNCH_COOKIE, LaunchCookie.class);
+                if (launchCookie != null) {
+                    projection.setLaunchCookie(launchCookie);
+                }
+
                 // Automatically grant consent if a system-privileged component is recording.
                 final Intent intent = new Intent();
                 intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION,
diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
index 21de185..958ace35 100644
--- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
+++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
@@ -26,8 +26,6 @@
 import android.database.ContentObserver;
 import android.os.BatteryManager;
 import android.os.Handler;
-import android.os.HandlerExecutor;
-import android.os.HandlerThread;
 import android.os.IThermalEventListener;
 import android.os.IThermalService;
 import android.os.PowerManager;
@@ -97,7 +95,6 @@
     private Future mLastShowWarningTask;
     private boolean mEnableSkinTemperatureWarning;
     private boolean mEnableUsbTemperatureAlarm;
-    private final HandlerThread mHandlerThread;
 
     private int mLowBatteryAlertCloseLevel;
     private final int[] mLowBatteryReminderLevels = new int[2];
@@ -170,8 +167,6 @@
         mPowerManager = powerManager;
         mWakefulnessLifecycle = wakefulnessLifecycle;
         mUserTracker = userTracker;
-        mHandlerThread = new HandlerThread("PowerUI");
-        mHandlerThread.start();
     }
 
     public void start() {
@@ -190,8 +185,7 @@
                 false, obs, UserHandle.USER_ALL);
         updateBatteryWarningLevels();
         mReceiver.init();
-        mUserTracker.addCallback(mUserChangedCallback,
-                    new HandlerExecutor(mHandlerThread.getThreadHandler()));
+        mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor());
         mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
 
         // Check to see if we need to let the user know that the phone previously shut down due
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
index b3d2e09..b9e9fe7 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
@@ -171,28 +171,6 @@
         return repository.setVisible(isVisible)
     }
 
-    /** True if there is a transition happening from and to the specified scenes. */
-    fun transitioning(from: SceneKey, to: SceneKey): StateFlow<Boolean> {
-        fun transitioning(
-            state: ObservableTransitionState,
-            from: SceneKey,
-            to: SceneKey,
-        ): Boolean {
-            return (state as? ObservableTransitionState.Transition)?.let {
-                it.fromScene == from && it.toScene == to
-            }
-                ?: false
-        }
-
-        return transitionState
-            .map { state -> transitioning(state, from, to) }
-            .stateIn(
-                scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = transitioning(transitionState.value, from, to),
-            )
-    }
-
     /**
      * Binds the given flow so the system remembers it.
      *
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
index 51276c6..314637e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
@@ -22,12 +22,11 @@
 import android.icu.text.DateFormat
 import android.icu.text.DisplayContext
 import android.os.UserHandle
-import com.android.systemui.res.R
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.res.R
 import com.android.systemui.scene.domain.interactor.SceneInteractor
-import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
 import java.util.Date
@@ -38,7 +37,6 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
@@ -57,16 +55,6 @@
     val mobileIconsViewModel: MobileIconsViewModel,
     broadcastDispatcher: BroadcastDispatcher,
 ) {
-    /** True if we are transitioning between Shade and QuickSettings scenes, in either direction. */
-    val isTransitioning =
-        combine(
-                sceneInteractor.transitioning(from = SceneKey.Shade, to = SceneKey.QuickSettings),
-                sceneInteractor.transitioning(from = SceneKey.QuickSettings, to = SceneKey.Shade)
-            ) { shadeToQuickSettings, quickSettingsToShade ->
-                shadeToQuickSettings || quickSettingsToShade
-            }
-            .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false)
-
     /** True if there is exactly one mobile connection. */
     val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
index 28d4457..fc84973 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
@@ -35,6 +35,7 @@
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.Looper;
 import android.provider.Settings;
 import android.telephony.CarrierConfigManager;
@@ -60,6 +61,7 @@
 import com.android.settingslib.mobile.TelephonyIcons;
 import com.android.settingslib.net.DataUsageController;
 import com.android.systemui.Dumpable;
+import com.android.systemui.res.R;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
@@ -71,7 +73,6 @@
 import com.android.systemui.log.core.LogLevel;
 import com.android.systemui.log.dagger.StatusBarNetworkControllerLog;
 import com.android.systemui.qs.tiles.dialog.InternetDialogFactory;
-import com.android.systemui.res.R;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -84,8 +85,6 @@
 
 import dalvik.annotation.optimization.NeverCompile;
 
-import kotlin.Unit;
-
 import java.io.PrintWriter;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
@@ -100,6 +99,8 @@
 
 import javax.inject.Inject;
 
+import kotlin.Unit;
+
 /** Platform implementation of the network controller. **/
 @SysUISingleton
 public class NetworkControllerImpl extends BroadcastReceiver
@@ -349,7 +350,7 @@
         // AIRPLANE_MODE_CHANGED is sent at boot; we've probably already missed it
         updateAirplaneMode(true /* force callback */);
         mUserTracker = userTracker;
-        mUserTracker.addCallback(mUserChangedCallback, mBgExecutor);
+        mUserTracker.addCallback(mUserChangedCallback, new HandlerExecutor(mMainHandler));
 
         deviceProvisionedController.addCallback(new DeviceProvisionedListener() {
             @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
index 342828c..aca8b64 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
@@ -6,7 +6,6 @@
 import android.net.Uri
 import android.os.Handler
 import android.os.HandlerExecutor
-import android.os.HandlerThread
 import android.os.UserHandle
 import android.provider.Settings
 import com.android.keyguard.KeyguardUpdateMonitor
@@ -88,7 +87,6 @@
             secureSettings.getUriFor(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS)
     private val onStateChangedListeners = ListenerSet<Consumer<String>>()
     private var hideSilentNotificationsOnLockscreen: Boolean = false
-    private val handlerThread: HandlerThread = HandlerThread("KeyguardNotificationVis")
 
     private val userTrackerCallback = object : UserTracker.Callback {
         override fun onUserChanged(newUser: Int, userContext: Context) {
@@ -156,9 +154,7 @@
                 notifyStateChanged("onStatusBarUpcomingStateChanged")
             }
         })
-        handlerThread.start()
-        userTracker.addCallback(userTrackerCallback,
-                   HandlerExecutor(handlerThread.getThreadHandler()))
+        userTracker.addCallback(userTrackerCallback, HandlerExecutor(handler))
     }
 
     override fun addOnStateChangedListener(listener: Consumer<String>) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSnooze.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSnooze.java
index 3443da1..99a6f6a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSnooze.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSnooze.java
@@ -45,13 +45,15 @@
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import androidx.annotation.NonNull;
+
 import com.android.app.animation.Interpolators;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.systemui.res.R;
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption;
+import com.android.systemui.res.R;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -77,6 +79,9 @@
     private static final LogMaker UNDO_LOG =
             new LogMaker(MetricsEvent.NOTIFICATION_UNDO_SNOOZE)
                     .setType(MetricsEvent.TYPE_ACTION);
+
+    private static final String PARAGRAPH_SEPARATOR = "\u2029";
+
     private NotificationGuts mGutsContainer;
     private NotificationSwipeActionHelper mSnoozeListener;
     private StatusBarNotification mSbn;
@@ -111,8 +116,7 @@
     }
 
     @VisibleForTesting
-    SnoozeOption getDefaultOption()
-    {
+    SnoozeOption getDefaultOption() {
         return mDefaultOption;
     }
 
@@ -130,6 +134,8 @@
         mSelectedOptionText = (TextView) findViewById(R.id.snooze_option_default);
         mUndoButton = (TextView) findViewById(R.id.undo);
         mUndoButton.setOnClickListener(this);
+        mUndoButton.setContentDescription(
+                getContext().getString(R.string.snooze_undo_content_description));
         mExpandButton = (ImageView) findViewById(R.id.expand_button);
         mDivider = findViewById(R.id.divider);
         mDivider.setAlpha(0f);
@@ -163,6 +169,46 @@
                 info.addAction(action);
             }
         }
+
+        mSnoozeView.setAccessibilityDelegate(new AccessibilityDelegate() {
+            @Override
+            public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+                super.onInitializeAccessibilityNodeInfo(host, info);
+                // Replace "Double tap to activate" prompt with "Double tap to expand/collapse"
+                AccessibilityAction customClick = new AccessibilityAction(
+                        AccessibilityNodeInfo.ACTION_CLICK, getExpandActionString());
+                info.addAction(customClick);
+            }
+        });
+    }
+
+    /**
+     * Update the content description of the snooze view based on the snooze option and whether the
+     * snooze options are expanded or not.
+     * For example, this will be something like "Collapsed\u2029Snooze for 1 hour". The paragraph
+     * separator is added to introduce a break in speech, to match what TalkBack does by default
+     * when you e.g. press on a notification.
+     */
+    private void updateContentDescription() {
+        mSnoozeView.setContentDescription(
+                getExpandStateString() + PARAGRAPH_SEPARATOR + mSelectedOptionText.getText());
+    }
+
+    /** Returns "collapse" if the snooze options are expanded, or "expand" otherwise. */
+    @NonNull
+    private String getExpandActionString() {
+        return mContext.getString(mExpanded
+                ? com.android.internal.R.string.expand_button_content_description_expanded
+                : com.android.internal.R.string.expand_button_content_description_collapsed);
+    }
+
+
+    /** Returns "expanded" if the snooze options are expanded, or "collapsed" otherwise. */
+    @NonNull
+    private String getExpandStateString() {
+        return mContext.getString(
+                (mExpanded ? com.android.internal.R.string.content_description_expanded
+                        : com.android.internal.R.string.content_description_collapsed));
     }
 
     @Override
@@ -179,6 +225,8 @@
             if (so.getAccessibilityAction() != null
                     && so.getAccessibilityAction().getId() == action) {
                 setSelected(so, true);
+                mSnoozeView.sendAccessibilityEvent(
+                        AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
                 return true;
             }
         }
@@ -290,11 +338,10 @@
         int drawableId = show ? com.android.internal.R.drawable.ic_collapse_notification
                 : com.android.internal.R.drawable.ic_expand_notification;
         mExpandButton.setImageResource(drawableId);
-        mExpandButton.setContentDescription(mContext.getString(show
-                ? com.android.internal.R.string.expand_button_content_description_expanded
-                : com.android.internal.R.string.expand_button_content_description_collapsed));
+        mExpandButton.setContentDescription(getExpandActionString());
         if (mExpanded != show) {
             mExpanded = show;
+            updateContentDescription();
             animateSnoozeOptions(show);
             if (mGutsContainer != null) {
                 mGutsContainer.onHeightChanged();
@@ -335,8 +382,11 @@
     }
 
     private void setSelected(SnoozeOption option, boolean userAction) {
-        mSelectedOption = option;
-        mSelectedOptionText.setText(option.getConfirmation());
+        if (option != mSelectedOption) {
+            mSelectedOption = option;
+            mSelectedOptionText.setText(option.getConfirmation());
+            updateContentDescription();
+        }
         showSnoozeOptions(false);
         hideSelectedOption();
         if (userAction) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index ade417d..64fcef5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -125,6 +125,7 @@
 import com.android.systemui.charging.WirelessChargingAnimation;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
+import com.android.systemui.communal.domain.interactor.CommunalInteractor;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.UiBackground;
@@ -245,6 +246,7 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.Executor;
+import java.util.function.Consumer;
 
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -551,6 +553,25 @@
     private final WakefulnessLifecycle mWakefulnessLifecycle;
     protected final PowerInteractor mPowerInteractor;
 
+    private final CommunalInteractor mCommunalInteractor;
+
+    /**
+     * True if the device is showing the glanceable hub. See
+     * {@link CommunalInteractor#isIdleOnCommunal()} for more details.
+     */
+    private boolean mIsIdleOnCommunal = false;
+    private final Consumer<Boolean> mIdleOnCommunalConsumer = (Boolean idleOnCommunal) -> {
+        if (idleOnCommunal == mIsIdleOnCommunal) {
+            // Ignore initial value coming through the flow.
+            return;
+        }
+
+        mIsIdleOnCommunal = idleOnCommunal;
+        // Trigger an update for the scrim state when we enter or exit glanceable hub, so that we
+        // can transition to/from ScrimState.GLANCEABLE_HUB if needed.
+        updateScrimController();
+    };
+
     private boolean mNoAnimationOnNextBarModeChange;
     private final SysuiStatusBarStateController mStatusBarStateController;
 
@@ -618,6 +639,7 @@
             ScreenLifecycle screenLifecycle,
             WakefulnessLifecycle wakefulnessLifecycle,
             PowerInteractor powerInteractor,
+            CommunalInteractor communalInteractor,
             SysuiStatusBarStateController statusBarStateController,
             Optional<Bubbles> bubblesOptional,
             Lazy<NoteTaskController> noteTaskControllerLazy,
@@ -722,6 +744,7 @@
         mScreenLifecycle = screenLifecycle;
         mWakefulnessLifecycle = wakefulnessLifecycle;
         mPowerInteractor = powerInteractor;
+        mCommunalInteractor = communalInteractor;
         mStatusBarStateController = statusBarStateController;
         mBubblesOptional = bubblesOptional;
         mNoteTaskControllerLazy = noteTaskControllerLazy;
@@ -1051,6 +1074,10 @@
         //TODO(b/264502026) move the rest of the listeners here.
         mDeviceStateManager.registerCallback(mMainExecutor,
                 new FoldStateListener(mContext, this::onFoldedStateChanged));
+
+        mJavaAdapter.alwaysCollectFlow(
+                mCommunalInteractor.isIdleOnCommunal(),
+                mIdleOnCommunalConsumer);
     }
 
     /**
@@ -2795,6 +2822,8 @@
             // This will cancel the keyguardFadingAway animation if it is running. We need to do
             // this as otherwise it can remain pending and leave keyguard in a weird state.
             mUnlockScrimCallback.onCancelled();
+        } else if (mIsIdleOnCommunal) {
+            mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB);
         } else if (mKeyguardStateController.isShowing()
                 && !mKeyguardStateController.isOccluded()
                 && !unlocking) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index 3f20eaf..6f78604 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -17,7 +17,9 @@
 package com.android.systemui.statusbar.phone;
 
 import static com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER;
+import static com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB;
 import static com.android.systemui.keyguard.shared.model.KeyguardState.GONE;
+import static com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN;
 import static com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER;
 import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
 
@@ -62,6 +64,7 @@
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.model.KeyguardState;
 import com.android.systemui.keyguard.shared.model.ScrimAlpha;
 import com.android.systemui.keyguard.shared.model.TransitionState;
 import com.android.systemui.keyguard.shared.model.TransitionStep;
@@ -292,6 +295,30 @@
                 mScrimBehind.setViewAlpha(mBehindAlpha);
             };
 
+    /**
+     * Consumer that fades the behind scrim in and out during the transition between the lock screen
+     * and the glanceable hub.
+     *
+     * While the lock screen is showing, the behind scrim is used to slightly darken the lock screen
+     * wallpaper underneath. Since the glanceable hub is under all of the scrims, we want to fade
+     * out the scrim so that the glanceable hub isn't darkened when it opens.
+     *
+     * {@link #applyState()} handles the scrim alphas once on the glanceable hub, this is only
+     * responsible for setting the behind alpha during the transition.
+     */
+    private final Consumer<TransitionStep> mGlanceableHubConsumer = (TransitionStep step) -> {
+        final float baseAlpha = ScrimState.KEYGUARD.getBehindAlpha();
+        final float transitionProgress = step.getValue();
+        if (step.getTo() == KeyguardState.LOCKSCREEN) {
+            // Transitioning back to lock screen, fade in behind scrim again.
+            mBehindAlpha = baseAlpha * transitionProgress;
+        } else if (step.getTo() == GLANCEABLE_HUB) {
+            // Transitioning to glanceable hub, fade out behind scrim.
+            mBehindAlpha = baseAlpha * (1 - transitionProgress);
+        }
+        mScrimBehind.setViewAlpha(mBehindAlpha);
+    };
+
     Consumer<TransitionStep> mBouncerToGoneTransition;
 
     @Inject
@@ -444,6 +471,14 @@
                 mBouncerToGoneTransition, mMainDispatcher);
         collectFlow(behindScrim, mAlternateBouncerToGoneTransitionViewModel.getScrimAlpha(),
                 mScrimAlphaConsumer, mMainDispatcher);
+
+        // LOCKSCREEN<->GLANCEABLE_HUB
+        collectFlow(behindScrim,
+                mKeyguardTransitionInteractor.transition(LOCKSCREEN, GLANCEABLE_HUB),
+                mGlanceableHubConsumer, mMainDispatcher);
+        collectFlow(behindScrim,
+                mKeyguardTransitionInteractor.transition(GLANCEABLE_HUB, LOCKSCREEN),
+                mGlanceableHubConsumer, mMainDispatcher);
     }
 
     // TODO(b/270984686) recompute scrim height accurately, based on shade contents.
@@ -815,9 +850,9 @@
             return;
         }
         mBouncerHiddenFraction = bouncerHiddenAmount;
-        if (mState == ScrimState.DREAMING) {
-            // Only the dreaming state requires this for the scrim calculation, so we should
-            // only trigger an update if dreaming.
+        if (mState == ScrimState.DREAMING || mState == ScrimState.GLANCEABLE_HUB) {
+            // The dreaming and glanceable hub states requires this for the scrim calculation, so we
+            // should only trigger an update in those states.
             applyAndDispatchState();
         }
     }
@@ -939,7 +974,7 @@
         } else if (mState == ScrimState.AUTH_SCRIMMED_SHADE) {
             mNotificationsAlpha = (float) Math.pow(getInterpolatedFraction(), 0.8f);
         } else if (mState == ScrimState.KEYGUARD || mState == ScrimState.SHADE_LOCKED
-                || mState == ScrimState.PULSING) {
+                || mState == ScrimState.PULSING || mState == ScrimState.GLANCEABLE_HUB) {
             Pair<Integer, Float> result = calculateBackStateForState(mState);
             int behindTint = result.first;
             float behindAlpha = result.second;
@@ -950,6 +985,11 @@
                         mTransitionToFullShadeProgress);
                 behindTint = ColorUtils.blendARGB(behindTint, shadeResult.first,
                         mTransitionToFullShadeProgress);
+            } else if (mState == ScrimState.GLANCEABLE_HUB && mTransitionToFullShadeProgress == 0.0f
+                    && mBouncerHiddenFraction == KeyguardBouncerConstants.EXPANSION_HIDDEN) {
+                // Behind scrim should not be visible when idle on the glanceable hub and neither
+                // bouncer nor shade are showing.
+                behindAlpha = 0f;
             }
             mInFrontAlpha = mState.getFrontAlpha();
             if (mClipsQsScrim) {
@@ -965,6 +1005,13 @@
                 } else if (mState == ScrimState.SHADE_LOCKED) {
                     // going from KEYGUARD to SHADE_LOCKED state
                     mNotificationsAlpha = getInterpolatedFraction();
+                } else if (mState == ScrimState.GLANCEABLE_HUB
+                        && mTransitionToFullShadeProgress == 0.0f) {
+                    // Notification scrim should not be visible on the glanceable hub unless the
+                    // shade is showing or transitioning in. Otherwise the notification scrim will
+                    // be visible as the bouncer transitions in or after the notification shade
+                    // closes.
+                    mNotificationsAlpha = 0;
                 } else {
                     mNotificationsAlpha = Math.max(1.0f - getInterpolatedFraction(), mQsExpansion);
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
index 61bd112..f2a649b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
@@ -296,6 +296,21 @@
                 updateScrimColor(mScrimBehind, 1f /* alpha */, mBackgroundColor);
             }
         }
+    },
+
+    /**
+     * Device is locked or on dream and user has swiped from the right edge to enter the glanceable
+     * hub UI. From this state, the user can swipe from the left edge to go back to the lock screen
+     * or dream, as well as swipe down for the notifications and up for the bouncer.
+     */
+    GLANCEABLE_HUB {
+        @Override
+        public void prepare(ScrimState previousState) {
+            // No scrims should be visible by default in this state.
+            mBehindAlpha = 0;
+            mNotifAlpha = 0;
+            mFrontAlpha = 0;
+        }
     };
 
     boolean mBlankScreen = false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
index a5b9dc1..f73d089 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
@@ -153,7 +153,14 @@
         if (satelliteManager != null) {
             // First, check that satellite is supported on this device
             scope.launch {
-                ensureMinUptime(systemClock, MIN_UPTIME)
+                val waitTime = ensureMinUptime(systemClock, MIN_UPTIME)
+                if (waitTime > 0) {
+                    logBuffer.i({ long1 = waitTime }) {
+                        "Waiting $long1 ms before checking for satellite support"
+                    }
+                    delay(waitTime)
+                }
+
                 satelliteSupport.value = satelliteManager.checkSatelliteSupported()
 
                 logBuffer.i(
@@ -259,7 +266,7 @@
                     }
 
                     override fun onResult(allowed: Boolean) {
-                        logBuffer.i { allowed.toString() }
+                        logBuffer.i { "isSatelliteAllowedForCurrentLocation: $allowed" }
                         isSatelliteAllowedForCurrentLocation.value = allowed
                     }
                 }
@@ -308,19 +315,14 @@
         // TTL for satellite polling is one hour
         const val POLLING_INTERVAL_MS: Long = 1000 * 60 * 60
 
-        // Let the system boot up and stabilize before we check for system support
-        const val MIN_UPTIME: Long = 1000 * 60
+        // Let the system boot up (5s) and stabilize before we check for system support
+        const val MIN_UPTIME: Long = 1000 * 5
 
         private const val TAG = "DeviceBasedSatelliteRepo"
 
-        /** If our process hasn't been up for at least MIN_UPTIME, delay until we reach that time */
-        private suspend fun ensureMinUptime(clock: SystemClock, uptime: Long) {
-            val timeTilMinUptime =
-                uptime - (clock.uptimeMillis() - android.os.Process.getStartUptimeMillis())
-            if (timeTilMinUptime > 0) {
-                delay(timeTilMinUptime)
-            }
-        }
+        /** Calculates how long we have to wait to reach MIN_UPTIME */
+        private fun ensureMinUptime(clock: SystemClock, uptime: Long): Long =
+            uptime - (clock.uptimeMillis() - android.os.Process.getStartUptimeMillis())
 
         /** A couple of convenience logging methods rather than a whole class */
         private fun LogBuffer.i(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
index 8779577..6e1114c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
@@ -79,7 +79,7 @@
             } else {
                 flowOf(false)
             }
-            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+            .stateIn(scope, SharingStarted.WhileSubscribed(), true)
 }
 
 /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
index a2d8d15..20d1fff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
@@ -28,8 +28,6 @@
 import android.icu.text.DateTimePatternGenerator;
 import android.os.Bundle;
 import android.os.Handler;
-import android.os.HandlerExecutor;
-import android.os.HandlerThread;
 import android.os.Parcelable;
 import android.os.SystemClock;
 import android.os.UserHandle;
@@ -50,11 +48,11 @@
 import com.android.settingslib.Utils;
 import com.android.systemui.Dependency;
 import com.android.systemui.FontSizeUtils;
+import com.android.systemui.res.R;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.demomode.DemoModeCommandReceiver;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
-import com.android.systemui.res.R;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
@@ -108,7 +106,6 @@
     private final int mAmPmStyle;
     private boolean mShowSeconds;
     private Handler mSecondsHandler;
-    private HandlerThread mHandlerThread;
 
     // Fields to cache the width so the clock remains at an approximately constant width
     private int mCharsAtCurrentWidth = -1;
@@ -149,8 +146,6 @@
         }
         mBroadcastDispatcher = Dependency.get(BroadcastDispatcher.class);
         mUserTracker = Dependency.get(UserTracker.class);
-        mHandlerThread = new HandlerThread("Clock");
-        mHandlerThread.start();
 
         setIncludeFontPadding(false);
     }
@@ -210,8 +205,7 @@
             Dependency.get(TunerService.class).addTunable(this, CLOCK_SECONDS,
                     StatusBarIconController.ICON_HIDE_LIST);
             mCommandQueue.addCallback(this);
-            mUserTracker.addCallback(mUserChangedCallback,
-                       new HandlerExecutor(mHandlerThread.getThreadHandler()));
+            mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor());
             mCurrentUserId = mUserTracker.getUserId();
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java
index a7440d6..b7d8ee3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java
@@ -21,8 +21,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.os.HandlerExecutor;
-import android.os.HandlerThread;
 import android.os.UserHandle;
 
 import androidx.annotation.NonNull;
@@ -53,7 +51,6 @@
     private final UserTracker mUserTracker;
     private AlarmManager mAlarmManager;
     private AlarmManager.AlarmClockInfo mNextAlarm;
-    private HandlerThread mHandlerThread;
 
     private final UserTracker.Callback mUserChangedCallback =
             new UserTracker.Callback() {
@@ -78,10 +75,7 @@
         IntentFilter filter = new IntentFilter();
         filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
         broadcastDispatcher.registerReceiver(this, filter, null, UserHandle.ALL);
-        mHandlerThread = new HandlerThread("NextAlarmControllerImpl");
-        mHandlerThread.start();
-        mUserTracker.addCallback(mUserChangedCallback,
-                    new HandlerExecutor(mHandlerThread.getThreadHandler()));
+        mUserTracker.addCallback(mUserChangedCallback, mainExecutor);
         updateNextAlarm();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
index 6a6efbc..9f4a906 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
@@ -157,7 +157,7 @@
         // TODO: re-register network callback on user change.
         mConnectivityManager.registerNetworkCallback(REQUEST, mNetworkCallback);
         onUserSwitched(mUserTracker.getUserId());
-        mUserTracker.addCallback(mUserChangedCallback, mBgExecutor);
+        mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
     }
 
     public void dump(PrintWriter pw, String[] args) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java
index 0bc0e88..2ed9d15 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java
@@ -36,9 +36,9 @@
 
 import com.android.internal.util.UserIcons;
 import com.android.settingslib.drawable.UserIconDrawable;
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.res.R;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.settings.UserTracker;
 
 import java.util.ArrayList;
@@ -66,11 +66,11 @@
     /**
      */
     @Inject
-    public UserInfoControllerImpl(Context context, @Background Executor bgExecutor,
+    public UserInfoControllerImpl(Context context, @Main Executor mainExecutor,
             UserTracker userTracker) {
         mContext = context;
         mUserTracker = userTracker;
-        mUserTracker.addCallback(mUserChangedCallback, bgExecutor);
+        mUserTracker.addCallback(mUserChangedCallback, mainExecutor);
 
         IntentFilter profileFilter = new IntentFilter();
         profileFilter.addAction(ContactsContract.Intents.ACTION_PROFILE_CHANGED);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
index f0b4930..df210b0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
@@ -29,7 +29,6 @@
 import android.net.Uri;
 import android.os.Handler;
 import android.os.HandlerExecutor;
-import android.os.HandlerThread;
 import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -82,7 +81,6 @@
     private volatile int mZenMode;
     private long mZenUpdateTime;
     private NotificationManager.Policy mConsolidatedNotificationPolicy;
-    private HandlerThread mHandlerThread;
 
     private final UserTracker.Callback mUserChangedCallback =
             new UserTracker.Callback() {
@@ -135,8 +133,6 @@
                 }
             }
         };
-        mHandlerThread = new HandlerThread("ZenModeControllerImpl");
-        mHandlerThread.start();
         mNoMan = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
         globalSettings.registerContentObserver(Global.ZEN_MODE, modeContentObserver);
         updateZenMode(getModeSettingValueFromProvider());
@@ -147,8 +143,7 @@
         mSetupObserver = new SetupObserver(handler);
         mSetupObserver.register();
         mUserManager = context.getSystemService(UserManager.class);
-        mUserTracker.addCallback(mUserChangedCallback,
-                new HandlerExecutor(mHandlerThread.getThreadHandler()));
+        mUserTracker.addCallback(mUserChangedCallback, new HandlerExecutor(handler));
         // This registers the alarm broadcast receiver for the current user
         mUserChangedCallback.onUserChanged(getCurrentUser(), context);
 
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index 77518db..2b9ad50 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -480,7 +480,7 @@
             return;
         }
 
-        mUserTracker.addCallback(mUserTrackerCallback, mBgExecutor);
+        mUserTracker.addCallback(mUserTrackerCallback, mMainExecutor);
 
         mDeviceProvisionedController.addCallback(mDeviceProvisionedListener);
 
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
index f5b4d17..550a65c 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
@@ -26,7 +26,6 @@
 import android.net.Uri;
 import android.os.Handler;
 import android.os.HandlerExecutor;
-import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.UserManager;
 import android.provider.Settings;
@@ -39,11 +38,11 @@
 
 import com.android.internal.util.ArrayUtils;
 import com.android.systemui.DejankUtils;
+import com.android.systemui.res.R;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.qs.QSHost;
-import com.android.systemui.res.R;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
@@ -99,7 +98,6 @@
     private UserTracker.Callback mCurrentUserTracker;
     private UserTracker mUserTracker;
     private final ComponentName mTunerComponent;
-    private HandlerThread mHandlerThread;
 
     /**
      */
@@ -119,8 +117,7 @@
         mDemoModeController = demoModeController;
         mUserTracker = userTracker;
         mTunerComponent = new ComponentName(mContext, TunerActivity.class);
-        mHandlerThread = new HandlerThread("TunerServiceImpl");
-        mHandlerThread.start();
+
         for (UserInfo user : UserManager.get(mContext).getUsers()) {
             mCurrentUser = user.getUserHandle().getIdentifier();
             if (getValue(TUNER_VERSION, 0) != CURRENT_TUNER_VERSION) {
@@ -138,7 +135,7 @@
             }
         };
         mUserTracker.addCallback(mCurrentUserTracker,
-                new HandlerExecutor(mHandlerThread.getThreadHandler()));
+                new HandlerExecutor(mainHandler));
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt
index 10fc83c..0016d95 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt
@@ -26,7 +26,6 @@
 import com.android.systemui.unfold.util.UnfoldKeyguardVisibilityManager
 import com.android.systemui.util.kotlin.getOrNull
 import dagger.BindsInstance
-import dagger.Lazy
 import dagger.Module
 import dagger.Provides
 import dagger.Subcomponent
@@ -57,7 +56,6 @@
         rotationProvider: Optional<NaturalRotationUnfoldProgressProvider>,
         @Named(UNFOLD_STATUS_BAR) scopedProvider: Optional<ScopedUnfoldTransitionProgressProvider>,
         @UnfoldBg bgProvider: Optional<UnfoldTransitionProgressProvider>,
-        unfoldLatencyTracker: Lazy<UnfoldLatencyTracker>,
         factory: SysUIUnfoldComponent.Factory
     ): Optional<SysUIUnfoldComponent> {
         val p1 = provider.getOrNull()
@@ -67,7 +65,7 @@
         return if (p1 == null || p2 == null || p3 == null || p4 == null) {
             Optional.empty()
         } else {
-            Optional.of(factory.create(p1, p2, p3, p4, unfoldLatencyTracker.get()))
+            Optional.of(factory.create(p1, p2, p3, p4))
         }
     }
 }
@@ -82,8 +80,7 @@
             @BindsInstance p1: UnfoldTransitionProgressProvider,
             @BindsInstance p2: NaturalRotationUnfoldProgressProvider,
             @BindsInstance p3: ScopedUnfoldTransitionProgressProvider,
-            @BindsInstance @UnfoldBg p4: UnfoldTransitionProgressProvider,
-            @BindsInstance p5: UnfoldLatencyTracker,
+            @BindsInstance @UnfoldBg p4: UnfoldTransitionProgressProvider
         ): SysUIUnfoldComponent
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt
index 8c66c2f..33fa9b8 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt
@@ -22,7 +22,6 @@
 import android.os.Trace
 import android.util.Log
 import com.android.internal.util.LatencyTracker
-import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.UiBackground
 import com.android.systemui.keyguard.ScreenLifecycle
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
@@ -42,7 +41,7 @@
  * For now, the focus is on the time the inner display is visible, but in the future, it is easily
  * possible to monitor the time to go from the inner screen to the outer.
  */
-@SysUISingleton
+@SysUIUnfoldScope
 class UnfoldLatencyTracker
 @Inject
 constructor(
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
index 8bef53c..9bd0e32 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
@@ -177,17 +177,20 @@
 
     @Module
     interface Bindings {
-        @Binds
-        @IntoMap
-        @ClassKey(UnfoldTraceLogger::class)
-        fun bindUnfoldTraceLogger(impl: UnfoldTraceLogger): CoreStartable
-
         @Binds fun bindRepository(impl: UnfoldTransitionRepositoryImpl): UnfoldTransitionRepository
 
         @Binds fun bindInteractor(impl: UnfoldTransitionInteractorImpl): UnfoldTransitionInteractor
 
         @Binds fun bindFoldStateRepository(impl: FoldStateRepositoryImpl): FoldStateRepository
     }
+
+    @Module
+    interface Startables {
+        @Binds
+        @IntoMap
+        @ClassKey(UnfoldTraceLogger::class)
+        fun bindUnfoldTraceLogger(impl: UnfoldTraceLogger): CoreStartable
+    }
 }
 
 const val UNFOLD_STATUS_BAR = "unfold_status_bar"
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
index 74e1339..cf76c0d 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
@@ -190,7 +190,7 @@
                         }
                     }
 
-                tracker.addCallback(callback, backgroundDispatcher.asExecutor())
+                tracker.addCallback(callback, mainDispatcher.asExecutor())
                 send(currentSelectionStatus)
 
                 awaitClose { tracker.removeCallback(callback) }
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/CoroutinesModule.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/CoroutinesModule.kt
deleted file mode 100644
index cf6b0d9..0000000
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/CoroutinesModule.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-package com.android.systemui.util.kotlin
-
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.dagger.qualifiers.Tracing
-import com.android.app.tracing.coroutines.createCoroutineTracingContext
-import dagger.Module
-import dagger.Provides
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.plus
-import kotlin.coroutines.CoroutineContext
-
-/** Providers for various coroutines-related constructs. */
-@Module
-class CoroutinesModule {
-    @Provides
-    @SysUISingleton
-    @Application
-    fun applicationScope(
-            @Main dispatcherContext: CoroutineContext,
-    ): CoroutineScope = CoroutineScope(dispatcherContext)
-
-    @Provides
-    @SysUISingleton
-    @Background
-    fun bgApplicationScope(
-            @Application applicationScope: CoroutineScope,
-            @Background coroutineContext: CoroutineContext,
-    ): CoroutineScope = applicationScope.plus(coroutineContext)
-
-    @Provides
-    @SysUISingleton
-    @Main
-    @Deprecated(
-        "Use @Main CoroutineContext instead",
-        ReplaceWith("mainCoroutineContext()", "kotlin.coroutines.CoroutineContext")
-    )
-    fun mainDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
-
-    @Provides
-    @SysUISingleton
-    @Main
-    fun mainCoroutineContext(@Tracing tracingCoroutineContext: CoroutineContext): CoroutineContext {
-        return Dispatchers.Main.immediate + tracingCoroutineContext
-    }
-
-    /**
-     * Provide a [CoroutineDispatcher] backed by a thread pool containing at most X threads, where
-     * X is the number of CPU cores available.
-     *
-     * Because there are multiple threads at play, there is no serialization order guarantee. You
-     * should use a [kotlinx.coroutines.channels.Channel] for serialization if necessary.
-     *
-     * @see Dispatchers.Default
-     */
-    @Provides
-    @SysUISingleton
-    @Background
-    @Deprecated(
-        "Use @Background CoroutineContext instead",
-        ReplaceWith("bgCoroutineContext()", "kotlin.coroutines.CoroutineContext")
-    )
-    fun bgDispatcher(): CoroutineDispatcher = Dispatchers.IO
-
-
-    @Provides
-    @Background
-    @SysUISingleton
-    fun bgCoroutineContext(@Tracing tracingCoroutineContext: CoroutineContext): CoroutineContext {
-        return Dispatchers.IO + tracingCoroutineContext
-    }
-
-    @OptIn(ExperimentalCoroutinesApi::class)
-    @Provides
-    @Tracing
-    @SysUISingleton
-    fun tracingCoroutineContext(): CoroutineContext {
-        return createCoroutineTracingContext()
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/GlobalCoroutinesModule.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/GlobalCoroutinesModule.kt
new file mode 100644
index 0000000..8ecf250
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/GlobalCoroutinesModule.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.util.kotlin
+
+import com.android.app.tracing.coroutines.createCoroutineTracingContext
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dagger.qualifiers.Tracing
+import dagger.Module
+import dagger.Provides
+import javax.inject.Singleton
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+/** Providers for various application-wide coroutines-related constructs. */
+@Module
+class GlobalCoroutinesModule {
+    @Provides
+    @Singleton
+    @Application
+    fun applicationScope(
+        @Main dispatcherContext: CoroutineContext,
+    ): CoroutineScope = CoroutineScope(dispatcherContext)
+
+    @Provides
+    @Singleton
+    @Main
+    @Deprecated(
+        "Use @Main CoroutineContext instead",
+        ReplaceWith("mainCoroutineContext()", "kotlin.coroutines.CoroutineContext")
+    )
+    fun mainDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
+
+    @Provides
+    @Singleton
+    @Main
+    fun mainCoroutineContext(@Tracing tracingCoroutineContext: CoroutineContext): CoroutineContext {
+        return Dispatchers.Main.immediate + tracingCoroutineContext
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Provides
+    @Tracing
+    @Singleton
+    fun tracingCoroutineContext(): CoroutineContext {
+        return createCoroutineTracingContext()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt
new file mode 100644
index 0000000..a13d85b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.util.kotlin
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Tracing
+import dagger.Module
+import dagger.Provides
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.plus
+
+/** Providers for various SystemIU specific coroutines-related constructs. */
+@Module
+class SysUICoroutinesModule {
+    @Provides
+    @SysUISingleton
+    @Background
+    fun bgApplicationScope(
+        @Application applicationScope: CoroutineScope,
+        @Background coroutineContext: CoroutineContext,
+    ): CoroutineScope = applicationScope.plus(coroutineContext)
+
+    /**
+     * Provide a [CoroutineDispatcher] backed by a thread pool containing at most X threads, where X
+     * is the number of CPU cores available.
+     *
+     * Because there are multiple threads at play, there is no serialization order guarantee. You
+     * should use a [kotlinx.coroutines.channels.Channel] for serialization if necessary.
+     *
+     * @see Dispatchers.Default
+     */
+    @Provides
+    @SysUISingleton
+    @Background
+    @Deprecated(
+        "Use @Background CoroutineContext instead",
+        ReplaceWith("bgCoroutineContext()", "kotlin.coroutines.CoroutineContext")
+    )
+    fun bgDispatcher(): CoroutineDispatcher = Dispatchers.IO
+
+    @Provides
+    @Background
+    @SysUISingleton
+    fun bgCoroutineContext(@Tracing tracingCoroutineContext: CoroutineContext): CoroutineContext {
+        return Dispatchers.IO + tracingCoroutineContext
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index 7c6ad23..1e801ae 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -32,8 +32,6 @@
 import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.inputmethodservice.InputMethodService;
-import android.os.HandlerExecutor;
-import android.os.HandlerThread;
 import android.os.IBinder;
 import android.util.Log;
 import android.view.Display;
@@ -127,7 +125,6 @@
     private final DisplayTracker mDisplayTracker;
     private final NoteTaskInitializer mNoteTaskInitializer;
     private final Executor mSysUiMainExecutor;
-    private HandlerThread mHandlerThread;
 
     // Listeners and callbacks. Note that we prefer member variable over anonymous class here to
     // avoid the situation that some implementations, like KeyguardUpdateMonitor, use WeakReference
@@ -209,8 +206,6 @@
         mDisplayTracker = displayTracker;
         mNoteTaskInitializer = noteTaskInitializer;
         mSysUiMainExecutor = sysUiMainExecutor;
-        mHandlerThread = new HandlerThread("WMShell");
-        mHandlerThread.start();
     }
 
     @Override
@@ -224,8 +219,7 @@
         mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
 
         // Subscribe to user changes
-        mUserTracker.addCallback(mUserChangedCallback,
-                     new HandlerExecutor(mHandlerThread.getThreadHandler()));
+        mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor());
 
         mCommandQueue.addCallback(this);
         mPipOptional.ifPresent(this::initPip);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java
index 44770fa..b23dfdc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java
@@ -16,8 +16,10 @@
 
 package com.android.systemui.accessibility;
 
+import android.annotation.NonNull;
 import android.graphics.Rect;
 import android.graphics.Region;
+import android.os.IBinder;
 import android.view.Display;
 import android.view.View;
 import android.view.ViewGroup;
@@ -89,6 +91,11 @@
         return mWindowManager.getMaximumWindowMetrics();
     }
 
+    @Override
+    public @NonNull IBinder getDefaultToken() {
+        return mWindowManager.getDefaultToken();
+    }
+
     public View getAttachedView() {
         return mView;
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
index d86d123..8299acb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
@@ -21,8 +21,10 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
@@ -33,10 +35,16 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.Rect;
+import android.os.Binder;
 import android.os.Handler;
 import android.os.RemoteException;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.AndroidTestingRunner;
 import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
 import android.view.View;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
@@ -46,6 +54,7 @@
 import androidx.test.filters.LargeTest;
 
 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
+import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.AnimatorTestRule;
 import com.android.systemui.model.SysUiState;
@@ -63,7 +72,10 @@
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
 
 @LargeTest
 @RunWith(AndroidTestingRunner.class)
@@ -71,6 +83,8 @@
 
     @Rule
     public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
     private static final float DEFAULT_SCALE = 4.0f;
     private static final float DEFAULT_CENTER_X = 400.0f;
     private static final float DEFAULT_CENTER_Y = 500.0f;
@@ -107,6 +121,13 @@
 
     private TestableWindowManager mWindowManager;
     private ValueAnimator mValueAnimator;
+    // This list contains all SurfaceControlViewHosts created during a given test. If the
+    // magnification window is recreated during a test, the list will contain more than a single
+    // element.
+    private List<SurfaceControlViewHost> mSurfaceControlViewHosts = new ArrayList<>();
+    // The most recently created SurfaceControlViewHost.
+    private SurfaceControlViewHost mSurfaceControlViewHost;
+    private SurfaceControl.Transaction mTransaction;
 
     @Before
     public void setUp() throws Exception {
@@ -123,10 +144,27 @@
         mValueAnimator = newValueAnimator();
         mWindowMagnificationAnimationController = new WindowMagnificationAnimationController(
                 mContext, mValueAnimator);
-        mController = new SpyWindowMagnificationController(mContext, mHandler,
+
+        Supplier<SurfaceControlViewHost> scvhSupplier = () -> {
+            mSurfaceControlViewHost = spy(new SurfaceControlViewHost(
+                    mContext, mContext.getDisplay(), new Binder(), "WindowMagnification"));
+            mSurfaceControlViewHosts.add(mSurfaceControlViewHost);
+            return mSurfaceControlViewHost;
+        };
+
+        mTransaction = spy(new SurfaceControl.Transaction());
+        mController = new SpyWindowMagnificationController(
+                mContext,
+                mHandler,
                 mWindowMagnificationAnimationController,
-                mSfVsyncFrameProvider, null, new SurfaceControl.Transaction(),
-                mWindowMagnifierCallback, mSysUiState, mSecureSettings);
+                /* mirrorWindowControl= */ null,
+                mTransaction,
+                mWindowMagnifierCallback,
+                mSysUiState,
+                mSecureSettings,
+                scvhSupplier,
+                mSfVsyncFrameProvider);
+
         mSpyController = mController.getSpyController();
     }
 
@@ -235,8 +273,52 @@
         verifyFinalSpec(DEFAULT_SCALE, DEFAULT_CENTER_X, DEFAULT_CENTER_Y);
     }
 
+    @RequiresFlagsEnabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER)
     @Test
-    public void enableWindowMagnificationWithScaleOne_enabled_AnimationAndInvokeCallback()
+    public void
+            enableWindowMagnificationScaleOne_enabledAndWindowlessFlagOn_AnimationAndCallbackTrue()
+            throws RemoteException {
+        enableWindowMagnificationWithoutAnimation();
+
+        // Wait for Rects updated.
+        waitForIdleSync();
+        View mirrorView = mSurfaceControlViewHost.getView();
+        final float targetScale = 1.0f;
+        // Move the magnifier to the top left corner, within the boundary
+        final float targetCenterX = mirrorView.getWidth() / 2.0f;
+        final float targetCenterY = mirrorView.getHeight() / 2.0f;
+
+        Mockito.reset(mSpyController);
+        getInstrumentation().runOnMainSync(() -> {
+            mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
+                    targetCenterX, targetCenterY, mAnimationCallback);
+            mCurrentScale.set(mController.getScale());
+            mCurrentCenterX.set(mController.getCenterX());
+            mCurrentCenterY.set(mController.getCenterY());
+            advanceTimeBy(mWaitAnimationDuration);
+        });
+
+        verify(mSpyController, atLeast(2)).enableWindowMagnificationInternal(
+                mScaleCaptor.capture(),
+                mCenterXCaptor.capture(), mCenterYCaptor.capture(),
+                mOffsetXCaptor.capture(), mOffsetYCaptor.capture());
+        verifyStartValue(mScaleCaptor, mCurrentScale.get());
+        verifyStartValue(mCenterXCaptor, mCurrentCenterX.get());
+        verifyStartValue(mCenterYCaptor, mCurrentCenterY.get());
+        verifyStartValue(mOffsetXCaptor, 0f);
+        verifyStartValue(mOffsetYCaptor, 0f);
+
+        verifyFinalSpec(targetScale, targetCenterX, targetCenterY);
+
+        verify(mAnimationCallback).onResult(true);
+        assertEquals(WindowMagnificationAnimationController.STATE_ENABLED,
+                mWindowMagnificationAnimationController.getState());
+    }
+
+    @RequiresFlagsDisabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER)
+    @Test
+    public void
+            enableWindowMagnificationScaleOne_enabledAndWindowlessFlagOff_AnimationAndCallbackTrue()
             throws RemoteException {
         enableWindowMagnificationWithoutAnimation();
 
@@ -475,8 +557,46 @@
         verify(mAnimationCallback2).onResult(true);
     }
 
+    @RequiresFlagsEnabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER)
     @Test
-    public void enableWindowMagnificationWithOffset_expectedValues() {
+    public void enableWindowMagnificationWithOffset_windowlessFlagOn_expectedValues() {
+        final float offsetRatio = -0.1f;
+        final Rect windowBounds = new Rect(mWindowManager.getCurrentWindowMetrics().getBounds());
+
+        Mockito.reset(mSpyController);
+        getInstrumentation().runOnMainSync(() -> {
+            mWindowMagnificationAnimationController.enableWindowMagnification(DEFAULT_SCALE,
+                    windowBounds.exactCenterX(), windowBounds.exactCenterY(),
+                    offsetRatio, offsetRatio, mAnimationCallback);
+            advanceTimeBy(mWaitAnimationDuration);
+        });
+        // Wait for Rects update
+        waitForIdleSync();
+
+        final int mirrorSurfaceMargin = mContext.getResources().getDimensionPixelSize(
+                R.dimen.magnification_mirror_surface_margin);
+        final int defaultMagnificationWindowSize =
+                mController.getMagnificationWindowSizeFromIndex(
+                        WindowMagnificationSettings.MagnificationSize.MEDIUM);
+        final int defaultMagnificationFrameSize =
+                defaultMagnificationWindowSize - 2 * mirrorSurfaceMargin;
+        final int expectedOffset = (int) (defaultMagnificationFrameSize / 2 * offsetRatio);
+
+        final float expectedX = (int) (windowBounds.exactCenterX() + expectedOffset
+                - defaultMagnificationWindowSize / 2);
+        final float expectedY = (int) (windowBounds.exactCenterY() + expectedOffset
+                - defaultMagnificationWindowSize / 2);
+
+        // This is called 4 times when (1) first creating WindowlessMirrorWindow (2) SurfaceView is
+        // created and we place the mirrored content as a child of the SurfaceView
+        // (3) the animation starts (4) the animation updates
+        verify(mTransaction, times(4))
+                .setPosition(any(SurfaceControl.class), eq(expectedX), eq(expectedY));
+    }
+
+    @RequiresFlagsDisabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER)
+    @Test
+    public void enableWindowMagnificationWithOffset_windowlessFlagOff_expectedValues() {
         final float offsetRatio = -0.1f;
         final Rect windowBounds = new Rect(mWindowManager.getCurrentWindowMetrics().getBounds());
 
@@ -876,23 +996,28 @@
     private static class SpyWindowMagnificationController extends WindowMagnificationController {
         private WindowMagnificationController mSpyController;
 
-        SpyWindowMagnificationController(Context context, Handler handler,
+        SpyWindowMagnificationController(Context context,
+                Handler handler,
                 WindowMagnificationAnimationController animationController,
-                SfVsyncFrameCallbackProvider sfVsyncFrameProvider,
-                MirrorWindowControl mirrorWindowControl, SurfaceControl.Transaction transaction,
-                WindowMagnifierCallback callback, SysUiState sysUiState,
-                SecureSettings secureSettings) {
+                MirrorWindowControl mirrorWindowControl,
+                SurfaceControl.Transaction transaction,
+                WindowMagnifierCallback callback,
+                SysUiState sysUiState,
+                SecureSettings secureSettings,
+                Supplier<SurfaceControlViewHost> scvhSupplier,
+                SfVsyncFrameCallbackProvider sfVsyncFrameProvider) {
             super(
                     context,
                     handler,
                     animationController,
-                    sfVsyncFrameProvider,
                     mirrorWindowControl,
                     transaction,
                     callback,
                     sysUiState,
-                    WindowManagerGlobal::getWindowSession,
-                    secureSettings);
+                    secureSettings,
+                    scvhSupplier,
+                    sfVsyncFrameProvider,
+                    WindowManagerGlobal::getWindowSession);
             mSpyController = Mockito.mock(WindowMagnificationController.class);
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
index 04aef82..2225ad6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
@@ -68,6 +68,9 @@
 import android.os.Handler;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.provider.Settings;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
@@ -90,6 +93,7 @@
 import androidx.test.filters.LargeTest;
 
 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
+import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.AnimatorTestRule;
 import com.android.systemui.kosmos.KosmosJavaAdapter;
@@ -121,10 +125,13 @@
 @LargeTest
 @TestableLooper.RunWithLooper
 @RunWith(AndroidTestingRunner.class)
+@RequiresFlagsDisabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER)
 public class WindowMagnificationControllerTest extends SysuiTestCase {
 
     @Rule
     public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
 
     private static final int LAYOUT_CHANGE_TIMEOUT_MS = 5000;
     @Mock
@@ -216,13 +223,14 @@
                         mContext,
                         mHandler,
                         mWindowMagnificationAnimationController,
-                        mSfVsyncFrameProvider,
                         mMirrorWindowControl,
                         mTransaction,
                         mWindowMagnifierCallback,
                         mSysUiState,
-                        () -> mWindowSessionSpy,
-                        mSecureSettings);
+                        mSecureSettings,
+                        /* scvhSupplier= */ () -> null,
+                        mSfVsyncFrameProvider,
+                        /* globalWindowSessionSupplier= */ () -> mWindowSessionSpy);
 
         verify(mMirrorWindowControl).setWindowDelegate(
                 any(MirrorWindowControl.MirrorWindowDelegate.class));
@@ -270,7 +278,7 @@
         mInstrumentation.runOnMainSync(
                 () -> mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN,
                         Float.NaN, /* magnificationFrameOffsetRatioX= */ 0,
-                /* magnificationFrameOffsetRatioY= */ 0, null));
+                        /* magnificationFrameOffsetRatioY= */ 0, null));
 
         // Waits for the surface created
         verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS)).onSourceBoundsChanged(
@@ -1415,7 +1423,7 @@
     }
 
     private MotionEvent obtainMotionEvent(long downTime, long eventTime, int action, float x,
-                                          float y) {
+            float y) {
         return mMotionEventHelper.obtainMotionEvent(downTime, eventTime, action, x, y);
     }
 
@@ -1474,4 +1482,4 @@
             });
         }
     }
-}
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java
new file mode 100644
index 0000000..66fb63b6c3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java
@@ -0,0 +1,1502 @@
+/*
+ * 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.accessibility;
+
+import static android.content.pm.PackageManager.FEATURE_WINDOW_MAGNIFICATION;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_UP;
+import static android.view.WindowInsets.Type.systemGestures;
+import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.hasItems;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.AdditionalAnswers.returnsSecondArg;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.animation.ValueAnimator;
+import android.annotation.IdRes;
+import android.annotation.Nullable;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Insets;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.provider.Settings;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.testing.TestableResources;
+import android.text.TextUtils;
+import android.util.Size;
+import android.view.AttachedSurfaceControl;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewRootImpl;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.IRemoteMagnificationAnimationCallback;
+import android.widget.FrameLayout;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+
+import com.android.systemui.Flags;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.AnimatorTestRule;
+import com.android.systemui.kosmos.KosmosJavaAdapter;
+import com.android.systemui.model.SysUiState;
+import com.android.systemui.res.R;
+import com.android.systemui.settings.FakeDisplayTracker;
+import com.android.systemui.util.leak.ReferenceTestUtils;
+import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.utils.os.FakeHandler;
+
+import com.google.common.util.concurrent.AtomicDouble;
+
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+
+@LargeTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner.class)
+@RequiresFlagsEnabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER)
+public class WindowMagnificationControllerWindowlessMagnifierTest extends SysuiTestCase {
+
+    @Rule
+    public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    private static final int LAYOUT_CHANGE_TIMEOUT_MS = 5000;
+    @Mock
+    private MirrorWindowControl mMirrorWindowControl;
+    @Mock
+    private WindowMagnifierCallback mWindowMagnifierCallback;
+    @Mock
+    IRemoteMagnificationAnimationCallback mAnimationCallback;
+    @Mock
+    IRemoteMagnificationAnimationCallback mAnimationCallback2;
+
+    private SurfaceControl.Transaction mTransaction;
+    @Mock
+    private SecureSettings mSecureSettings;
+
+    private long mWaitAnimationDuration;
+    private long mWaitBounceEffectDuration;
+
+    private Handler mHandler;
+    private TestableWindowManager mWindowManager;
+    private SysUiState mSysUiState;
+    private Resources mResources;
+    private WindowMagnificationAnimationController mWindowMagnificationAnimationController;
+    private WindowMagnificationController mWindowMagnificationController;
+    private Instrumentation mInstrumentation;
+    private final ValueAnimator mValueAnimator = ValueAnimator.ofFloat(0, 1.0f).setDuration(0);
+    private final FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
+
+    private View mSpyView;
+    private View.OnTouchListener mTouchListener;
+
+    private MotionEventHelper mMotionEventHelper = new MotionEventHelper();
+
+    // This list contains all SurfaceControlViewHosts created during a given test. If the
+    // magnification window is recreated during a test, the list will contain more than a single
+    // element.
+    private List<SurfaceControlViewHost> mSurfaceControlViewHosts = new ArrayList<>();
+    // The most recently created SurfaceControlViewHost.
+    private SurfaceControlViewHost mSurfaceControlViewHost;
+    private KosmosJavaAdapter mKosmos;
+
+    /**
+     *  return whether window magnification is supported for current test context.
+     */
+    private boolean isWindowModeSupported() {
+        return getContext().getPackageManager().hasSystemFeature(FEATURE_WINDOW_MAGNIFICATION);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mKosmos = new KosmosJavaAdapter(this);
+        mContext = Mockito.spy(getContext());
+        mHandler = new FakeHandler(TestableLooper.get(this).getLooper());
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        final WindowManager wm = mContext.getSystemService(WindowManager.class);
+        mWindowManager = spy(new TestableWindowManager(wm));
+
+        mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
+        mSysUiState = new SysUiState(mDisplayTracker, mKosmos.getSceneContainerPlugin());
+        mSysUiState.addCallback(Mockito.mock(SysUiState.SysUiStateCallback.class));
+        when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).then(
+                returnsSecondArg());
+        when(mSecureSettings.getFloatForUser(anyString(), anyFloat(), anyInt())).then(
+                returnsSecondArg());
+
+        mResources = getContext().getOrCreateTestableResources().getResources();
+        // prevent the config orientation from undefined, which may cause config.diff method
+        // neglecting the orientation update.
+        if (mResources.getConfiguration().orientation == ORIENTATION_UNDEFINED) {
+            mResources.getConfiguration().orientation = ORIENTATION_PORTRAIT;
+        }
+
+        // Using the animation duration in WindowMagnificationAnimationController for testing.
+        mWaitAnimationDuration = mResources.getInteger(
+                com.android.internal.R.integer.config_longAnimTime);
+        // Using the bounce effect duration in WindowMagnificationController for testing.
+        mWaitBounceEffectDuration = mResources.getInteger(
+                com.android.internal.R.integer.config_shortAnimTime);
+
+        mWindowMagnificationAnimationController = new WindowMagnificationAnimationController(
+                mContext, mValueAnimator);
+        Supplier<SurfaceControlViewHost> scvhSupplier = () -> {
+            mSurfaceControlViewHost = spy(new SurfaceControlViewHost(
+                    mContext, mContext.getDisplay(), new Binder(), "WindowMagnification"));
+            ViewRootImpl viewRoot = mock(ViewRootImpl.class);
+            when(mSurfaceControlViewHost.getRootSurfaceControl()).thenReturn(viewRoot);
+            mSurfaceControlViewHosts.add(mSurfaceControlViewHost);
+            return mSurfaceControlViewHost;
+        };
+        mTransaction = spy(new SurfaceControl.Transaction());
+        mWindowMagnificationController =
+                new WindowMagnificationController(
+                        mContext,
+                        mHandler,
+                        mWindowMagnificationAnimationController,
+                        mMirrorWindowControl,
+                        mTransaction,
+                        mWindowMagnifierCallback,
+                        mSysUiState,
+                        mSecureSettings,
+                        scvhSupplier,
+                        /* sfVsyncFrameProvider= */ null,
+                        /* globalWindowSessionSupplier= */ null);
+
+        verify(mMirrorWindowControl).setWindowDelegate(
+                any(MirrorWindowControl.MirrorWindowDelegate.class));
+        mSpyView = Mockito.spy(new View(mContext));
+        doAnswer((invocation) -> {
+            mTouchListener = invocation.getArgument(0);
+            return null;
+        }).when(mSpyView).setOnTouchListener(
+                any(View.OnTouchListener.class));
+
+        // skip test if window magnification is not supported to prevent fail results. (b/279820875)
+        Assume.assumeTrue(isWindowModeSupported());
+    }
+
+    @After
+    public void tearDown() {
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.deleteWindowMagnification());
+        mValueAnimator.cancel();
+    }
+
+    @Test
+    public void initWindowMagnificationController_checkAllowDiagonalScrollingWithSecureSettings() {
+        verify(mSecureSettings).getIntForUser(
+                eq(Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING),
+                /* def */ eq(1), /* userHandle= */ anyInt());
+        assertTrue(mWindowMagnificationController.isDiagonalScrollingEnabled());
+    }
+
+    @Test
+    public void enableWindowMagnification_showControlAndNotifyBoundsChanged() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+
+        verify(mMirrorWindowControl).showControl();
+        verify(mWindowMagnifierCallback,
+                timeout(LAYOUT_CHANGE_TIMEOUT_MS).atLeastOnce()).onWindowMagnifierBoundsChanged(
+                eq(mContext.getDisplayId()), any(Rect.class));
+    }
+
+    @Test
+    public void enableWindowMagnification_notifySourceBoundsChanged() {
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN,
+                        Float.NaN, /* magnificationFrameOffsetRatioX= */ 0,
+                        /* magnificationFrameOffsetRatioY= */ 0, null));
+
+        // Waits for the surface created
+        verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS)).onSourceBoundsChanged(
+                (eq(mContext.getDisplayId())), any());
+    }
+
+    @Test
+    public void enableWindowMagnification_disabled_notifySourceBoundsChanged() {
+        enableWindowMagnification_notifySourceBoundsChanged();
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.deleteWindowMagnification(null));
+        Mockito.reset(mWindowMagnifierCallback);
+
+        enableWindowMagnification_notifySourceBoundsChanged();
+    }
+
+    @Test
+    public void enableWindowMagnification_withAnimation_schedulesFrame() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnification(2.0f, 10,
+                    10, /* magnificationFrameOffsetRatioX= */ 0,
+                    /* magnificationFrameOffsetRatioY= */ 0,
+                    Mockito.mock(IRemoteMagnificationAnimationCallback.class));
+        });
+        advanceTimeBy(LAYOUT_CHANGE_TIMEOUT_MS);
+
+        verify(mTransaction, atLeastOnce()).setGeometry(any(), any(), any(),
+                eq(Surface.ROTATION_0));
+    }
+
+    @Test
+    public void moveWindowMagnifier_enabled_notifySourceBoundsChanged() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN,
+                    Float.NaN, 0, 0, null);
+        });
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.moveWindowMagnifier(10, 10);
+        });
+
+        final ArgumentCaptor<Rect> sourceBoundsCaptor = ArgumentCaptor.forClass(Rect.class);
+        verify(mWindowMagnifierCallback, atLeast(2)).onSourceBoundsChanged(
+                (eq(mContext.getDisplayId())), sourceBoundsCaptor.capture());
+        assertEquals(mWindowMagnificationController.getCenterX(),
+                sourceBoundsCaptor.getValue().exactCenterX(), 0);
+        assertEquals(mWindowMagnificationController.getCenterY(),
+                sourceBoundsCaptor.getValue().exactCenterY(), 0);
+    }
+
+    @Test
+    public void enableWindowMagnification_systemGestureExclusionRectsIsSet() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+        // Wait for Rects updated.
+        waitForIdleSync();
+
+        List<Rect> rects = mSurfaceControlViewHost.getView().getSystemGestureExclusionRects();
+        assertFalse(rects.isEmpty());
+    }
+
+    @Ignore("The default window size should be constrained after fixing b/288056772")
+    @Test
+    public void enableWindowMagnification_LargeScreen_windowSizeIsConstrained() {
+        final int screenSize = mWindowManager.getCurrentWindowMetrics().getBounds().width() * 10;
+        mWindowManager.setWindowBounds(new Rect(0, 0, screenSize, screenSize));
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+
+        final int halfScreenSize = screenSize / 2;
+        ViewGroup.LayoutParams params = mSurfaceControlViewHost.getView().getLayoutParams();
+        // The frame size should be the half of smaller value of window height/width unless it
+        //exceed the max frame size.
+        assertTrue(params.width < halfScreenSize);
+        assertTrue(params.height < halfScreenSize);
+    }
+
+    @Test
+    public void deleteWindowMagnification_destroyControlAndUnregisterComponentCallback() {
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                        Float.NaN,
+                        Float.NaN));
+
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.deleteWindowMagnification());
+
+        verify(mMirrorWindowControl).destroyControl();
+        verify(mContext).unregisterComponentCallbacks(mWindowMagnificationController);
+    }
+
+    @Test
+    public void deleteWindowMagnification_enableAtTheBottom_overlapFlagIsFalse() {
+        final WindowManager wm = mContext.getSystemService(WindowManager.class);
+        final Rect bounds = wm.getCurrentWindowMetrics().getBounds();
+        setSystemGestureInsets();
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    bounds.bottom);
+        });
+        ReferenceTestUtils.waitForCondition(this::hasMagnificationOverlapFlag);
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.deleteWindowMagnification();
+        });
+
+        verify(mMirrorWindowControl).destroyControl();
+        assertFalse(hasMagnificationOverlapFlag());
+    }
+
+    @Test
+    public void deleteWindowMagnification_notifySourceBoundsChanged() {
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                        Float.NaN,
+                        Float.NaN));
+
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.deleteWindowMagnification());
+
+        // The first time is for notifying magnification enabled and the second time is for
+        // notifying magnification disabled.
+        verify(mWindowMagnifierCallback, times(2)).onSourceBoundsChanged(
+                (eq(mContext.getDisplayId())), any());
+    }
+
+    @Test
+    public void moveMagnifier_schedulesFrame() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+
+        waitForIdleSync();
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.moveWindowMagnifier(100f, 100f));
+
+        verify(mTransaction, atLeastOnce()).setGeometry(any(), any(), any(),
+                eq(Surface.ROTATION_0));
+    }
+
+    @Test
+    public void moveWindowMagnifierToPositionWithAnimation_expectedValuesAndInvokeCallback()
+            throws RemoteException {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN,
+                    Float.NaN, 0, 0, null);
+        });
+
+        final ArgumentCaptor<Rect> sourceBoundsCaptor = ArgumentCaptor.forClass(Rect.class);
+        verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS))
+                .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture());
+        final float targetCenterX = sourceBoundsCaptor.getValue().exactCenterX() + 10;
+        final float targetCenterY = sourceBoundsCaptor.getValue().exactCenterY() + 10;
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.moveWindowMagnifierToPosition(
+                    targetCenterX, targetCenterY, mAnimationCallback);
+        });
+        advanceTimeBy(mWaitAnimationDuration);
+
+        verify(mAnimationCallback, times(1)).onResult(eq(true));
+        verify(mAnimationCallback, never()).onResult(eq(false));
+        verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS))
+                .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture());
+        assertEquals(mWindowMagnificationController.getCenterX(),
+                sourceBoundsCaptor.getValue().exactCenterX(), 0);
+        assertEquals(mWindowMagnificationController.getCenterY(),
+                sourceBoundsCaptor.getValue().exactCenterY(), 0);
+        assertEquals(mWindowMagnificationController.getCenterX(), targetCenterX, 0);
+        assertEquals(mWindowMagnificationController.getCenterY(), targetCenterY, 0);
+    }
+
+    @Test
+    public void moveWindowMagnifierToPositionMultipleTimes_expectedValuesAndInvokeCallback()
+            throws RemoteException {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN,
+                    Float.NaN, 0, 0, null);
+        });
+
+        final ArgumentCaptor<Rect> sourceBoundsCaptor = ArgumentCaptor.forClass(Rect.class);
+        verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS))
+                .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture());
+        final float centerX = sourceBoundsCaptor.getValue().exactCenterX();
+        final float centerY = sourceBoundsCaptor.getValue().exactCenterY();
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.moveWindowMagnifierToPosition(
+                    centerX + 10, centerY + 10, mAnimationCallback);
+            mWindowMagnificationController.moveWindowMagnifierToPosition(
+                    centerX + 20, centerY + 20, mAnimationCallback);
+            mWindowMagnificationController.moveWindowMagnifierToPosition(
+                    centerX + 30, centerY + 30, mAnimationCallback);
+            mWindowMagnificationController.moveWindowMagnifierToPosition(
+                    centerX + 40, centerY + 40, mAnimationCallback2);
+        });
+        advanceTimeBy(mWaitAnimationDuration);
+
+        // only the last one callback will return true
+        verify(mAnimationCallback2).onResult(eq(true));
+        // the others will return false
+        verify(mAnimationCallback, times(3)).onResult(eq(false));
+        verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS))
+                .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture());
+        assertEquals(mWindowMagnificationController.getCenterX(),
+                sourceBoundsCaptor.getValue().exactCenterX(), 0);
+        assertEquals(mWindowMagnificationController.getCenterY(),
+                sourceBoundsCaptor.getValue().exactCenterY(), 0);
+        assertEquals(mWindowMagnificationController.getCenterX(), centerX + 40, 0);
+        assertEquals(mWindowMagnificationController.getCenterY(), centerY + 40, 0);
+    }
+
+    @Test
+    public void setScale_enabled_expectedValueAndUpdateStateDescription() {
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.enableWindowMagnificationInternal(2.0f,
+                        Float.NaN, Float.NaN));
+
+        mInstrumentation.runOnMainSync(() -> mWindowMagnificationController.setScale(3.0f));
+
+        assertEquals(3.0f, mWindowMagnificationController.getScale(), 0);
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        assertNotNull(mirrorView);
+        assertThat(mirrorView.getStateDescription().toString(), containsString("300"));
+    }
+
+    @Test
+    public void onConfigurationChanged_disabled_withoutException() {
+        Display display = Mockito.spy(mContext.getDisplay());
+        when(display.getRotation()).thenReturn(Surface.ROTATION_90);
+        when(mContext.getDisplay()).thenReturn(display);
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_DENSITY);
+            mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_ORIENTATION);
+            mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_LOCALE);
+            mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_SCREEN_SIZE);
+        });
+    }
+
+    @Test
+    public void onOrientationChanged_enabled_updateDisplayRotationAndCenterStayAtSamePosition() {
+        final int newRotation = simulateRotateTheDevice();
+        final Rect windowBounds = new Rect(mWindowManager.getCurrentWindowMetrics().getBounds());
+        final float center = Math.min(windowBounds.exactCenterX(), windowBounds.exactCenterY());
+        final float displayWidth = windowBounds.width();
+        final PointF magnifiedCenter = new PointF(center, center + 5f);
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                    magnifiedCenter.x, magnifiedCenter.y);
+            // Get the center again in case the center we set is out of screen.
+            magnifiedCenter.set(mWindowMagnificationController.getCenterX(),
+                    mWindowMagnificationController.getCenterY());
+        });
+        // Rotate the window clockwise 90 degree.
+        windowBounds.set(windowBounds.top, windowBounds.left, windowBounds.bottom,
+                windowBounds.right);
+        mWindowManager.setWindowBounds(windowBounds);
+
+        mInstrumentation.runOnMainSync(() -> mWindowMagnificationController.onConfigurationChanged(
+                ActivityInfo.CONFIG_ORIENTATION));
+
+        assertEquals(newRotation, mWindowMagnificationController.mRotation);
+        final PointF expectedCenter = new PointF(magnifiedCenter.y,
+                displayWidth - magnifiedCenter.x);
+        final PointF actualCenter = new PointF(mWindowMagnificationController.getCenterX(),
+                mWindowMagnificationController.getCenterY());
+        assertEquals(expectedCenter, actualCenter);
+    }
+
+    @Test
+    public void onOrientationChanged_disabled_updateDisplayRotation() {
+        final Rect windowBounds = new Rect(mWindowManager.getCurrentWindowMetrics().getBounds());
+        // Rotate the window clockwise 90 degree.
+        windowBounds.set(windowBounds.top, windowBounds.left, windowBounds.bottom,
+                windowBounds.right);
+        mWindowManager.setWindowBounds(windowBounds);
+        final int newRotation = simulateRotateTheDevice();
+
+        mInstrumentation.runOnMainSync(() -> mWindowMagnificationController.onConfigurationChanged(
+                ActivityInfo.CONFIG_ORIENTATION));
+
+        assertEquals(newRotation, mWindowMagnificationController.mRotation);
+    }
+
+    @Test
+    public void onScreenSizeAndDensityChanged_enabledAtTheCenterOfScreen_keepSameWindowSizeRatio() {
+        // The default position is at the center of the screen.
+        final float expectedRatio = 0.5f;
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+
+        // Screen size and density change
+        mContext.getResources().getConfiguration().smallestScreenWidthDp =
+                mContext.getResources().getConfiguration().smallestScreenWidthDp * 2;
+        final Rect testWindowBounds = new Rect(
+                mWindowManager.getCurrentWindowMetrics().getBounds());
+        testWindowBounds.set(testWindowBounds.left, testWindowBounds.top,
+                testWindowBounds.right + 100, testWindowBounds.bottom + 100);
+        mWindowManager.setWindowBounds(testWindowBounds);
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_SCREEN_SIZE);
+        });
+
+        // The ratio of center to window size should be the same.
+        assertEquals(expectedRatio,
+                mWindowMagnificationController.getCenterX() / testWindowBounds.width(),
+                0);
+        assertEquals(expectedRatio,
+                mWindowMagnificationController.getCenterY() / testWindowBounds.height(),
+                0);
+    }
+
+    @Test
+    public void onScreenChangedToSavedDensity_enabled_restoreSavedMagnifierWindow() {
+        mContext.getResources().getConfiguration().smallestScreenWidthDp =
+                mContext.getResources().getConfiguration().smallestScreenWidthDp * 2;
+        int windowFrameSize = mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+        mWindowMagnificationController.mWindowMagnificationSizePrefs.saveSizeForCurrentDensity(
+                new Size(windowFrameSize, windowFrameSize));
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+
+        ViewGroup.LayoutParams params = mSurfaceControlViewHost.getView().getLayoutParams();
+        assertTrue(params.width == windowFrameSize);
+        assertTrue(params.height == windowFrameSize);
+    }
+
+    @Test
+    public void screenSizeIsChangedToLarge_enabled_defaultWindowSize() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+        final int screenSize = mWindowManager.getCurrentWindowMetrics().getBounds().width() * 10;
+        // Screen size and density change
+        mContext.getResources().getConfiguration().smallestScreenWidthDp =
+                mContext.getResources().getConfiguration().smallestScreenWidthDp * 2;
+        mWindowManager.setWindowBounds(new Rect(0, 0, screenSize, screenSize));
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_SCREEN_SIZE);
+        });
+
+        final int defaultWindowSize =
+                mWindowMagnificationController.getMagnificationWindowSizeFromIndex(
+                        WindowMagnificationSettings.MagnificationSize.MEDIUM);
+        ViewGroup.LayoutParams params = mSurfaceControlViewHost.getView().getLayoutParams();
+
+        assertTrue(params.width == defaultWindowSize);
+        assertTrue(params.height == defaultWindowSize);
+    }
+
+    @Test
+    public void onDensityChanged_enabled_updateDimensionsAndResetWindowMagnification() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+            Mockito.reset(mWindowManager);
+            Mockito.reset(mMirrorWindowControl);
+        });
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_DENSITY);
+        });
+
+        verify(mResources, atLeastOnce()).getDimensionPixelSize(anyInt());
+        verify(mSurfaceControlViewHosts.get(0)).release();
+        verify(mMirrorWindowControl).destroyControl();
+        verify(mSurfaceControlViewHosts.get(1)).setView(any(), any());
+        verify(mMirrorWindowControl).showControl();
+    }
+
+    @Test
+    public void onDensityChanged_disabled_updateDimensions() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_DENSITY);
+        });
+
+        verify(mResources, atLeastOnce()).getDimensionPixelSize(anyInt());
+    }
+
+    @Test
+    public void initializeA11yNode_enabled_expectedValues() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(2.5f, Float.NaN,
+                    Float.NaN);
+        });
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        assertNotNull(mirrorView);
+        final AccessibilityNodeInfo nodeInfo = new AccessibilityNodeInfo();
+
+        mirrorView.onInitializeAccessibilityNodeInfo(nodeInfo);
+
+        assertNotNull(nodeInfo.getContentDescription());
+        assertThat(nodeInfo.getStateDescription().toString(), containsString("250"));
+        assertThat(nodeInfo.getActionList(),
+                hasItems(new AccessibilityAction(R.id.accessibility_action_zoom_in, null),
+                        new AccessibilityAction(R.id.accessibility_action_zoom_out, null),
+                        new AccessibilityAction(R.id.accessibility_action_move_right, null),
+                        new AccessibilityAction(R.id.accessibility_action_move_left, null),
+                        new AccessibilityAction(R.id.accessibility_action_move_down, null),
+                        new AccessibilityAction(R.id.accessibility_action_move_up, null)));
+    }
+
+    @Test
+    public void performA11yActions_visible_expectedResults() {
+        final int displayId = mContext.getDisplayId();
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(1.5f, Float.NaN,
+                    Float.NaN);
+        });
+
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        assertTrue(
+                mirrorView.performAccessibilityAction(R.id.accessibility_action_zoom_out, null));
+        // Minimum scale is 1.0.
+        verify(mWindowMagnifierCallback).onPerformScaleAction(
+                eq(displayId), /* scale= */ eq(1.0f), /* updatePersistence= */ eq(true));
+
+        assertTrue(mirrorView.performAccessibilityAction(R.id.accessibility_action_zoom_in, null));
+        verify(mWindowMagnifierCallback).onPerformScaleAction(
+                eq(displayId), /* scale= */ eq(2.5f), /* updatePersistence= */ eq(true));
+
+        // TODO: Verify the final state when the mirror surface is visible.
+        assertTrue(mirrorView.performAccessibilityAction(R.id.accessibility_action_move_up, null));
+        assertTrue(
+                mirrorView.performAccessibilityAction(R.id.accessibility_action_move_down, null));
+        assertTrue(
+                mirrorView.performAccessibilityAction(R.id.accessibility_action_move_right, null));
+        assertTrue(
+                mirrorView.performAccessibilityAction(R.id.accessibility_action_move_left, null));
+        verify(mWindowMagnifierCallback, times(4)).onMove(eq(displayId));
+
+        assertTrue(mirrorView.performAccessibilityAction(
+                AccessibilityAction.ACTION_CLICK.getId(), null));
+        verify(mWindowMagnifierCallback).onClickSettingsButton(eq(displayId));
+    }
+
+    @Test
+    public void performA11yActions_visible_notifyAccessibilityActionPerformed() {
+        final int displayId = mContext.getDisplayId();
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(2.5f, Float.NaN,
+                    Float.NaN);
+        });
+
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        mirrorView.performAccessibilityAction(R.id.accessibility_action_move_up, null);
+
+        verify(mWindowMagnifierCallback).onAccessibilityActionPerformed(eq(displayId));
+    }
+
+    @Test
+    public void windowMagnifierEditMode_performA11yClickAction_exitEditMode() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+            mWindowMagnificationController.setEditMagnifierSizeMode(true);
+        });
+
+        View closeButton = getInternalView(R.id.close_button);
+        View bottomRightCorner = getInternalView(R.id.bottom_right_corner);
+        View bottomLeftCorner = getInternalView(R.id.bottom_left_corner);
+        View topRightCorner = getInternalView(R.id.top_right_corner);
+        View topLeftCorner = getInternalView(R.id.top_left_corner);
+
+        assertEquals(View.VISIBLE, closeButton.getVisibility());
+        assertEquals(View.VISIBLE, bottomRightCorner.getVisibility());
+        assertEquals(View.VISIBLE, bottomLeftCorner.getVisibility());
+        assertEquals(View.VISIBLE, topRightCorner.getVisibility());
+        assertEquals(View.VISIBLE, topLeftCorner.getVisibility());
+
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        mInstrumentation.runOnMainSync(() ->
+                mirrorView.performAccessibilityAction(AccessibilityAction.ACTION_CLICK.getId(),
+                        null));
+
+        assertEquals(View.GONE, closeButton.getVisibility());
+        assertEquals(View.GONE, bottomRightCorner.getVisibility());
+        assertEquals(View.GONE, bottomLeftCorner.getVisibility());
+        assertEquals(View.GONE, topRightCorner.getVisibility());
+        assertEquals(View.GONE, topLeftCorner.getVisibility());
+    }
+
+    @Test
+
+    public void windowWidthIsNotMax_performA11yActionIncreaseWidth_windowWidthIncreased() {
+        final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        final int startingWidth = (int) (windowBounds.width() * 0.8);
+        final int startingHeight = (int) (windowBounds.height() * 0.8);
+        final float changeWindowSizeAmount = mContext.getResources().getFraction(
+                R.fraction.magnification_resize_window_size_amount,
+                /* base= */ 1,
+                /* pbase= */ 1);
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+            mWindowMagnificationController.setWindowSize(startingWidth, startingHeight);
+            mWindowMagnificationController.setEditMagnifierSizeMode(true);
+        });
+
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mirrorView.performAccessibilityAction(
+                            R.id.accessibility_action_increase_window_width, null);
+                    actualWindowHeight.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().height);
+                    actualWindowWidth.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().width);
+                });
+
+        final int mirrorSurfaceMargin = mResources.getDimensionPixelSize(
+                R.dimen.magnification_mirror_surface_margin);
+        // Window width includes the magnifier frame and the margin. Increasing the window size
+        // will be increasing the amount of the frame size only.
+        int newWindowWidth =
+                (int) ((startingWidth - 2 * mirrorSurfaceMargin) * (1 + changeWindowSizeAmount))
+                        + 2 * mirrorSurfaceMargin;
+        assertEquals(newWindowWidth, actualWindowWidth.get());
+        assertEquals(startingHeight, actualWindowHeight.get());
+    }
+
+    @Test
+    public void windowHeightIsNotMax_performA11yActionIncreaseHeight_windowHeightIncreased() {
+        final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        final int startingWidth = (int) (windowBounds.width() * 0.8);
+        final int startingHeight = (int) (windowBounds.height() * 0.8);
+        final float changeWindowSizeAmount = mContext.getResources().getFraction(
+                R.fraction.magnification_resize_window_size_amount,
+                /* base= */ 1,
+                /* pbase= */ 1);
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+            mWindowMagnificationController.setWindowSize(startingWidth, startingHeight);
+            mWindowMagnificationController.setEditMagnifierSizeMode(true);
+        });
+
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mirrorView.performAccessibilityAction(
+                            R.id.accessibility_action_increase_window_height, null);
+                    actualWindowHeight.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().height);
+                    actualWindowWidth.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().width);
+                });
+
+        final int mirrorSurfaceMargin = mResources.getDimensionPixelSize(
+                R.dimen.magnification_mirror_surface_margin);
+        // Window height includes the magnifier frame and the margin. Increasing the window size
+        // will be increasing the amount of the frame size only.
+        int newWindowHeight =
+                (int) ((startingHeight - 2 * mirrorSurfaceMargin) * (1 + changeWindowSizeAmount))
+                        + 2 * mirrorSurfaceMargin;
+        assertEquals(startingWidth, actualWindowWidth.get());
+        assertEquals(newWindowHeight, actualWindowHeight.get());
+    }
+
+    @Test
+    public void windowWidthIsMax_noIncreaseWindowWidthA11yAction() {
+        final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        final int startingWidth = windowBounds.width();
+        final int startingHeight = windowBounds.height();
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+            mWindowMagnificationController.setWindowSize(startingWidth, startingHeight);
+            mWindowMagnificationController.setEditMagnifierSizeMode(true);
+        });
+
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        final AccessibilityNodeInfo accessibilityNodeInfo =
+                mirrorView.createAccessibilityNodeInfo();
+        assertFalse(accessibilityNodeInfo.getActionList().contains(
+                new AccessibilityAction(R.id.accessibility_action_increase_window_width, null)));
+    }
+
+    @Test
+    public void windowHeightIsMax_noIncreaseWindowHeightA11yAction() {
+        final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        final int startingWidth = windowBounds.width();
+        final int startingHeight = windowBounds.height();
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+            mWindowMagnificationController.setWindowSize(startingWidth, startingHeight);
+            mWindowMagnificationController.setEditMagnifierSizeMode(true);
+        });
+
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        final AccessibilityNodeInfo accessibilityNodeInfo =
+                mirrorView.createAccessibilityNodeInfo();
+        assertFalse(accessibilityNodeInfo.getActionList().contains(
+                new AccessibilityAction(R.id.accessibility_action_increase_window_height, null)));
+    }
+
+    @Test
+    public void windowWidthIsNotMin_performA11yActionDecreaseWidth_windowWidthDecreased() {
+        int mMinWindowSize = mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+        final int startingSize = (int) (mMinWindowSize * 1.1);
+        final float changeWindowSizeAmount = mContext.getResources().getFraction(
+                R.fraction.magnification_resize_window_size_amount,
+                /* base= */ 1,
+                /* pbase= */ 1);
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+            mWindowMagnificationController.setWindowSize(startingSize, startingSize);
+            mWindowMagnificationController.setEditMagnifierSizeMode(true);
+        });
+
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mirrorView.performAccessibilityAction(
+                            R.id.accessibility_action_decrease_window_width, null);
+                    actualWindowHeight.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().height);
+                    actualWindowWidth.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().width);
+                });
+
+        final int mirrorSurfaceMargin = mResources.getDimensionPixelSize(
+                R.dimen.magnification_mirror_surface_margin);
+        // Window width includes the magnifier frame and the margin. Decreasing the window size
+        // will be decreasing the amount of the frame size only.
+        int newWindowWidth =
+                (int) ((startingSize - 2 * mirrorSurfaceMargin) * (1 - changeWindowSizeAmount))
+                        + 2 * mirrorSurfaceMargin;
+        assertEquals(newWindowWidth, actualWindowWidth.get());
+        assertEquals(startingSize, actualWindowHeight.get());
+    }
+
+    @Test
+    public void windowHeightIsNotMin_performA11yActionDecreaseHeight_windowHeightDecreased() {
+        int mMinWindowSize = mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+        final int startingSize = (int) (mMinWindowSize * 1.1);
+        final float changeWindowSizeAmount = mContext.getResources().getFraction(
+                R.fraction.magnification_resize_window_size_amount,
+                /* base= */ 1,
+                /* pbase= */ 1);
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+            mWindowMagnificationController.setWindowSize(startingSize, startingSize);
+            mWindowMagnificationController.setEditMagnifierSizeMode(true);
+        });
+
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mirrorView.performAccessibilityAction(
+                            R.id.accessibility_action_decrease_window_height, null);
+                    actualWindowHeight.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().height);
+                    actualWindowWidth.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().width);
+                });
+
+        final int mirrorSurfaceMargin = mResources.getDimensionPixelSize(
+                R.dimen.magnification_mirror_surface_margin);
+        // Window height includes the magnifier frame and the margin. Decreasing the window size
+        // will be decreasing the amount of the frame size only.
+        int newWindowHeight =
+                (int) ((startingSize - 2 * mirrorSurfaceMargin) * (1 - changeWindowSizeAmount))
+                        + 2 * mirrorSurfaceMargin;
+        assertEquals(startingSize, actualWindowWidth.get());
+        assertEquals(newWindowHeight, actualWindowHeight.get());
+    }
+
+    @Test
+    public void windowWidthIsMin_noDecreaseWindowWidthA11yAction() {
+        int mMinWindowSize = mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+        final int startingSize = mMinWindowSize;
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+            mWindowMagnificationController.setWindowSize(startingSize, startingSize);
+            mWindowMagnificationController.setEditMagnifierSizeMode(true);
+        });
+
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        final AccessibilityNodeInfo accessibilityNodeInfo =
+                mirrorView.createAccessibilityNodeInfo();
+        assertFalse(accessibilityNodeInfo.getActionList().contains(
+                new AccessibilityAction(R.id.accessibility_action_decrease_window_width, null)));
+    }
+
+    @Test
+    public void windowHeightIsMin_noDecreaseWindowHeightA11yAction() {
+        int mMinWindowSize = mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+        final int startingSize = mMinWindowSize;
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+            mWindowMagnificationController.setWindowSize(startingSize, startingSize);
+            mWindowMagnificationController.setEditMagnifierSizeMode(true);
+        });
+
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        final AccessibilityNodeInfo accessibilityNodeInfo =
+                mirrorView.createAccessibilityNodeInfo();
+        assertFalse(accessibilityNodeInfo.getActionList().contains(
+                new AccessibilityAction(R.id.accessibility_action_decrease_window_height, null)));
+    }
+
+    @Test
+    public void enableWindowMagnification_hasA11yWindowTitle() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+
+        assertEquals(getContext().getResources().getString(
+                com.android.internal.R.string.android_system_label), getAccessibilityWindowTitle());
+    }
+
+    @Test
+    public void enableWindowMagnificationWithScaleLessThanOne_enabled_disabled() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(0.9f, Float.NaN,
+                    Float.NaN);
+        });
+
+        assertEquals(Float.NaN, mWindowMagnificationController.getScale(), 0);
+    }
+
+    @Test
+    public void enableWindowMagnification_rotationIsChanged_updateRotationValue() {
+        // the config orientation should not be undefined, since it would cause config.diff
+        // returning 0 and thus the orientation changed would not be detected
+        assertNotEquals(ORIENTATION_UNDEFINED, mResources.getConfiguration().orientation);
+
+        final Configuration config = mResources.getConfiguration();
+        config.orientation = config.orientation == ORIENTATION_LANDSCAPE ? ORIENTATION_PORTRAIT
+                : ORIENTATION_LANDSCAPE;
+        final int newRotation = simulateRotateTheDevice();
+
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                        Float.NaN, Float.NaN));
+
+        assertEquals(newRotation, mWindowMagnificationController.mRotation);
+    }
+
+    @Test
+    public void enableWindowMagnification_registerComponentCallback() {
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                        Float.NaN,
+                        Float.NaN));
+
+        verify(mContext).registerComponentCallbacks(mWindowMagnificationController);
+    }
+
+    @Test
+    public void onLocaleChanged_enabled_updateA11yWindowTitle() {
+        final String newA11yWindowTitle = "new a11y window title";
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+        final TestableResources testableResources = getContext().getOrCreateTestableResources();
+        testableResources.addOverride(com.android.internal.R.string.android_system_label,
+                newA11yWindowTitle);
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_LOCALE);
+        });
+
+        assertTrue(TextUtils.equals(newA11yWindowTitle, getAccessibilityWindowTitle()));
+    }
+
+    @Ignore("it's flaky in presubmit but works in abtd, filter for now. b/305654925")
+    @Test
+    public void onSingleTap_enabled_scaleAnimates() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.onSingleTap(mSpyView);
+        });
+
+        final View mirrorView = mSurfaceControlViewHost.getView();
+
+        final AtomicDouble maxScaleX = new AtomicDouble();
+        advanceTimeBy(mWaitBounceEffectDuration, /* runnableOnEachRefresh= */ () -> {
+            // For some reason the fancy way doesn't compile...
+            // maxScaleX.getAndAccumulate(mirrorView.getScaleX(), Math::max);
+            final double oldMax = maxScaleX.get();
+            final double newMax = Math.max(mirrorView.getScaleX(), oldMax);
+            assertTrue(maxScaleX.compareAndSet(oldMax, newMax));
+        });
+
+        assertTrue(maxScaleX.get() > 1.0);
+    }
+
+    @Test
+    public void moveWindowMagnificationToTheBottom_enabledWithGestureInset_overlapFlagIsTrue() {
+        final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        setSystemGestureInsets();
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.moveWindowMagnifier(0, bounds.height());
+        });
+
+        ReferenceTestUtils.waitForCondition(() -> hasMagnificationOverlapFlag());
+    }
+
+    @Test
+    public void moveWindowMagnificationToRightEdge_dragHandleMovesToLeftAndUpdatesTapExcludeRegion()
+            throws RemoteException {
+        final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        setSystemGestureInsets();
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mWindowMagnificationController.enableWindowMagnificationInternal(
+                            Float.NaN, Float.NaN, Float.NaN);
+                });
+        // Wait for Region updated.
+        waitForIdleSync();
+
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mWindowMagnificationController.moveWindowMagnifier(bounds.width(), 0);
+                });
+        // Wait for Region updated.
+        waitForIdleSync();
+
+        AttachedSurfaceControl viewRoot = mSurfaceControlViewHost.getRootSurfaceControl();
+        // Verifying two times in: (1) enable window magnification (2) reposition drag handle
+        verify(viewRoot, times(2)).setTouchableRegion(any());
+
+        View dragButton = getInternalView(R.id.drag_handle);
+        FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) dragButton.getLayoutParams();
+        assertEquals(Gravity.BOTTOM | Gravity.LEFT, params.gravity);
+    }
+
+    @Test
+    public void moveWindowMagnificationToLeftEdge_dragHandleMovesToRightAndUpdatesTapExcludeRegion()
+            throws RemoteException {
+        final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        setSystemGestureInsets();
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mWindowMagnificationController.enableWindowMagnificationInternal(
+                            Float.NaN, Float.NaN, Float.NaN);
+                });
+        // Wait for Region updated.
+        waitForIdleSync();
+
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mWindowMagnificationController.moveWindowMagnifier(-bounds.width(), 0);
+                });
+        // Wait for Region updated.
+        waitForIdleSync();
+
+        AttachedSurfaceControl viewRoot = mSurfaceControlViewHost.getRootSurfaceControl();
+        // Verifying one times in: (1) enable window magnification
+        verify(viewRoot).setTouchableRegion(any());
+
+        View dragButton = getInternalView(R.id.drag_handle);
+        FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) dragButton.getLayoutParams();
+        assertEquals(Gravity.BOTTOM | Gravity.RIGHT, params.gravity);
+    }
+
+    @Test
+    public void setMinimumWindowSize_enabled_expectedWindowSize() {
+        final int minimumWindowSize = mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+        final int  expectedWindowHeight = minimumWindowSize;
+        final int  expectedWindowWidth = minimumWindowSize;
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                        Float.NaN, Float.NaN));
+
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.setWindowSize(expectedWindowWidth, expectedWindowHeight);
+            actualWindowHeight.set(mSurfaceControlViewHost.getView().getLayoutParams().height);
+            actualWindowWidth.set(mSurfaceControlViewHost.getView().getLayoutParams().width);
+
+        });
+
+        assertEquals(expectedWindowHeight, actualWindowHeight.get());
+        assertEquals(expectedWindowWidth, actualWindowWidth.get());
+    }
+
+    @Test
+    public void setMinimumWindowSizeThenEnable_expectedWindowSize() {
+        final int minimumWindowSize = mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+        final int  expectedWindowHeight = minimumWindowSize;
+        final int  expectedWindowWidth = minimumWindowSize;
+
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.setWindowSize(expectedWindowWidth, expectedWindowHeight);
+            mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                    Float.NaN, Float.NaN);
+            actualWindowHeight.set(mSurfaceControlViewHost.getView().getLayoutParams().height);
+            actualWindowWidth.set(mSurfaceControlViewHost.getView().getLayoutParams().width);
+        });
+
+        assertEquals(expectedWindowHeight, actualWindowHeight.get());
+        assertEquals(expectedWindowWidth, actualWindowWidth.get());
+    }
+
+    @Test
+    public void setWindowSizeLessThanMin_enabled_minimumWindowSize() {
+        final int minimumWindowSize = mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                        Float.NaN, Float.NaN));
+
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.setWindowSize(minimumWindowSize - 10,
+                    minimumWindowSize - 10);
+            actualWindowHeight.set(mSurfaceControlViewHost.getView().getLayoutParams().height);
+            actualWindowWidth.set(mSurfaceControlViewHost.getView().getLayoutParams().width);
+        });
+
+        assertEquals(minimumWindowSize, actualWindowHeight.get());
+        assertEquals(minimumWindowSize, actualWindowWidth.get());
+    }
+
+    @Test
+    public void setWindowSizeLargerThanScreenSize_enabled_windowSizeIsScreenSize() {
+        final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                        Float.NaN, Float.NaN));
+
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.setWindowSize(bounds.width() + 10, bounds.height() + 10);
+            actualWindowHeight.set(mSurfaceControlViewHost.getView().getLayoutParams().height);
+            actualWindowWidth.set(mSurfaceControlViewHost.getView().getLayoutParams().width);
+        });
+
+        assertEquals(bounds.height(), actualWindowHeight.get());
+        assertEquals(bounds.width(), actualWindowWidth.get());
+    }
+
+    @Test
+    public void changeMagnificationSize_expectedWindowSize() {
+        final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+
+        final float magnificationScaleLarge = 2.5f;
+        final int initSize = Math.min(bounds.width(), bounds.height()) / 3;
+        final int magnificationSize = (int) (initSize * magnificationScaleLarge)
+                - (int) (initSize * magnificationScaleLarge) % 2;
+
+        final int expectedWindowHeight = magnificationSize;
+        final int expectedWindowWidth = magnificationSize;
+
+        mInstrumentation.runOnMainSync(
+                () ->
+                        mWindowMagnificationController.enableWindowMagnificationInternal(
+                                Float.NaN, Float.NaN, Float.NaN));
+
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mWindowMagnificationController.changeMagnificationSize(
+                            WindowMagnificationSettings.MagnificationSize.LARGE);
+                    actualWindowHeight.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().height);
+                    actualWindowWidth.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().width);
+                });
+
+        assertEquals(expectedWindowHeight, actualWindowHeight.get());
+        assertEquals(expectedWindowWidth, actualWindowWidth.get());
+    }
+
+    @Test
+    public void editModeOnDragCorner_resizesWindow() {
+        final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+
+        final int startingSize = (int) (bounds.width() / 2);
+
+        mInstrumentation.runOnMainSync(
+                () ->
+                        mWindowMagnificationController.enableWindowMagnificationInternal(
+                                Float.NaN, Float.NaN, Float.NaN));
+
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mWindowMagnificationController.setWindowSize(startingSize, startingSize);
+                    mWindowMagnificationController.setEditMagnifierSizeMode(true);
+                });
+
+        waitForIdleSync();
+
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mWindowMagnificationController
+                            .onDrag(getInternalView(R.id.bottom_right_corner), 2f, 1f);
+                    actualWindowHeight.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().height);
+                    actualWindowWidth.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().width);
+                });
+
+        assertEquals(startingSize + 1, actualWindowHeight.get());
+        assertEquals(startingSize + 2, actualWindowWidth.get());
+    }
+
+    @Test
+    public void editModeOnDragEdge_resizesWindowInOnlyOneDirection() {
+        final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+
+        final int startingSize = (int) (bounds.width() / 2f);
+
+        mInstrumentation.runOnMainSync(
+                () ->
+                        mWindowMagnificationController.enableWindowMagnificationInternal(
+                                Float.NaN, Float.NaN, Float.NaN));
+
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mWindowMagnificationController.setWindowSize(startingSize, startingSize);
+                    mWindowMagnificationController.setEditMagnifierSizeMode(true);
+                    mWindowMagnificationController
+                            .onDrag(getInternalView(R.id.bottom_handle), 2f, 1f);
+                    actualWindowHeight.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().height);
+                    actualWindowWidth.set(
+                            mSurfaceControlViewHost.getView().getLayoutParams().width);
+                });
+        assertEquals(startingSize + 1, actualWindowHeight.get());
+        assertEquals(startingSize, actualWindowWidth.get());
+    }
+
+    @Test
+    public void setWindowCenterOutOfScreen_enabled_magnificationCenterIsInsideTheScreen() {
+
+        final int minimumWindowSize = mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+        final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        mInstrumentation.runOnMainSync(
+                () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN,
+                        Float.NaN, Float.NaN));
+
+        final AtomicInteger magnificationCenterX = new AtomicInteger();
+        final AtomicInteger magnificationCenterY = new AtomicInteger();
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.setWindowSizeAndCenter(minimumWindowSize,
+                    minimumWindowSize, bounds.right, bounds.bottom);
+            magnificationCenterX.set((int) mWindowMagnificationController.getCenterX());
+            magnificationCenterY.set((int) mWindowMagnificationController.getCenterY());
+        });
+
+        assertTrue(magnificationCenterX.get() < bounds.right);
+        assertTrue(magnificationCenterY.get() < bounds.bottom);
+    }
+
+    @Test
+    public void performSingleTap_DragHandle() {
+        final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mWindowMagnificationController.enableWindowMagnificationInternal(
+                            1.5f, bounds.centerX(), bounds.centerY());
+                });
+        View dragButton = getInternalView(R.id.drag_handle);
+
+        // Perform a single-tap
+        final long downTime = SystemClock.uptimeMillis();
+        dragButton.dispatchTouchEvent(
+                obtainMotionEvent(downTime, 0, ACTION_DOWN, 100, 100));
+        dragButton.dispatchTouchEvent(
+                obtainMotionEvent(downTime, downTime, ACTION_UP, 100, 100));
+
+        verify(mSurfaceControlViewHost).setView(any(View.class), any());
+    }
+
+    private <T extends View> T getInternalView(@IdRes int idRes) {
+        View mirrorView = mSurfaceControlViewHost.getView();
+        T view = mirrorView.findViewById(idRes);
+        assertNotNull(view);
+        return view;
+    }
+
+    private MotionEvent obtainMotionEvent(long downTime, long eventTime, int action, float x,
+            float y) {
+        return mMotionEventHelper.obtainMotionEvent(downTime, eventTime, action, x, y);
+    }
+
+    private CharSequence getAccessibilityWindowTitle() {
+        final View mirrorView = mSurfaceControlViewHost.getView();
+        if (mirrorView == null) {
+            return null;
+        }
+        WindowManager.LayoutParams layoutParams =
+                (WindowManager.LayoutParams) mirrorView.getLayoutParams();
+        return layoutParams.accessibilityTitle;
+    }
+
+    private boolean hasMagnificationOverlapFlag() {
+        return (mSysUiState.getFlags() & SYSUI_STATE_MAGNIFICATION_OVERLAP) != 0;
+    }
+
+    private void setSystemGestureInsets() {
+        final WindowInsets testInsets = new WindowInsets.Builder()
+                .setInsets(systemGestures(), Insets.of(0, 0, 0, 10))
+                .build();
+        mWindowManager.setWindowInsets(testInsets);
+    }
+
+    private int updateMirrorSurfaceMarginDimension() {
+        return mContext.getResources().getDimensionPixelSize(
+                R.dimen.magnification_mirror_surface_margin);
+    }
+
+    @Surface.Rotation
+    private int simulateRotateTheDevice() {
+        final Display display = Mockito.spy(mContext.getDisplay());
+        final int currentRotation = display.getRotation();
+        final int newRotation = (currentRotation + 1) % 4;
+        when(display.getRotation()).thenReturn(newRotation);
+        when(mContext.getDisplay()).thenReturn(display);
+        return newRotation;
+    }
+
+    // advance time based on the device frame refresh rate
+    private void advanceTimeBy(long timeDelta) {
+        advanceTimeBy(timeDelta, /* runnableOnEachRefresh= */ null);
+    }
+
+    // advance time based on the device frame refresh rate, and trigger runnable on each refresh
+    private void advanceTimeBy(long timeDelta, @Nullable Runnable runnableOnEachRefresh) {
+        final float frameRate = mContext.getDisplay().getRefreshRate();
+        final int timeSlot = (int) (1000 / frameRate);
+        int round = (int) Math.ceil((double) timeDelta / timeSlot);
+        for (; round >= 0; round--) {
+            mInstrumentation.runOnMainSync(() -> {
+                mAnimatorTestRule.advanceTimeBy(timeSlot);
+                if (runnableOnEachRefresh != null) {
+                    runnableOnEachRefresh.run();
+                }
+            });
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
index a2aed98..9ce77e5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
@@ -28,10 +28,10 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
+import android.app.ActivityOptions.LaunchCookie;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.content.Intent;
-import android.os.Binder;
 import android.os.Handler;
 import android.os.RemoteException;
 import android.os.UserHandle;
@@ -146,7 +146,7 @@
 
     @Test
     public void testLogStartPartialRecording() {
-        MediaProjectionCaptureTarget target = new MediaProjectionCaptureTarget(new Binder());
+        MediaProjectionCaptureTarget target = new MediaProjectionCaptureTarget(new LaunchCookie());
         Intent startIntent = RecordingService.getStartIntent(mContext, 0, 0, false, target);
         mRecordingService.onStartCommand(startIntent, 0, 0);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 9c4984e..849a13b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -41,6 +41,8 @@
 
 import static java.util.Collections.emptySet;
 
+import static kotlinx.coroutines.flow.FlowKt.flowOf;
+
 import android.app.ActivityManager;
 import android.app.IWallpaperManager;
 import android.app.WallpaperManager;
@@ -77,7 +79,6 @@
 import com.android.internal.logging.testing.FakeMetricsLogger;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.keyguard.TestScopeProvider;
 import com.android.keyguard.ViewMediatorCallback;
 import com.android.systemui.InitController;
 import com.android.systemui.SysuiTestCase;
@@ -92,6 +93,10 @@
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
+import com.android.systemui.communal.data.repository.CommunalRepository;
+import com.android.systemui.communal.domain.interactor.CommunalInteractor;
+import com.android.systemui.communal.shared.model.CommunalSceneKey;
+import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FakeFeatureFlags;
@@ -102,6 +107,7 @@
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel;
+import com.android.systemui.kosmos.KosmosJavaAdapter;
 import com.android.systemui.log.LogBuffer;
 import com.android.systemui.navigationbar.NavigationBarController;
 import com.android.systemui.notetask.NoteTaskController;
@@ -195,6 +201,8 @@
 
 import javax.inject.Provider;
 
+import kotlinx.coroutines.test.TestScope;
+
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper(setAsMainLooper = true)
@@ -203,11 +211,17 @@
     private static final int FOLD_STATE_FOLDED = 0;
     private static final int FOLD_STATE_UNFOLDED = 1;
 
+    private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
+
     private CentralSurfacesImpl mCentralSurfaces;
     private FakeMetricsLogger mMetricsLogger;
     private PowerManager mPowerManager;
     private VisualInterruptionDecisionProvider mVisualInterruptionDecisionProvider;
 
+
+    private final TestScope mTestScope = mKosmos.getTestScope();
+    private final CommunalInteractor mCommunalInteractor = mKosmos.getCommunalInteractor();
+    private final CommunalRepository mCommunalRepository = mKosmos.getCommunalRepository();
     @Mock private NotificationsController mNotificationsController;
     @Mock private LightBarController mLightBarController;
     @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
@@ -461,7 +475,7 @@
                 new DisplayMetrics(),
                 mMetricsLogger,
                 mShadeLogger,
-                new JavaAdapter(TestScopeProvider.getTestScope()),
+                new JavaAdapter(mTestScope),
                 mUiBgExecutor,
                 mNotificationPanelViewController,
                 mNotificationMediaManager,
@@ -473,6 +487,7 @@
                 mScreenLifecycle,
                 mWakefulnessLifecycle,
                 mPowerInteractor,
+                mCommunalInteractor,
                 mStatusBarStateController,
                 Optional.of(mBubbles),
                 () -> mNoteTaskController,
@@ -821,6 +836,25 @@
     }
 
     @Test
+    public void testEnteringGlanceableHub_updatesScrim() {
+        // Transition to the glanceable hub.
+        mCommunalRepository.setTransitionState(flowOf(new ObservableCommunalTransitionState.Idle(
+                CommunalSceneKey.Communal.INSTANCE)));
+        mTestScope.getTestScheduler().runCurrent();
+
+        // ScrimState also transitions.
+        verify(mScrimController).transitionTo(ScrimState.GLANCEABLE_HUB);
+
+        // Transition away from the glanceable hub.
+        mCommunalRepository.setTransitionState(flowOf(new ObservableCommunalTransitionState.Idle(
+                CommunalSceneKey.Blank.INSTANCE)));
+        mTestScope.getTestScheduler().runCurrent();
+
+        // ScrimState goes back to UNLOCKED.
+        verify(mScrimController).transitionTo(eq(ScrimState.UNLOCKED), any());
+    }
+
+    @Test
     public void testShowKeyguardImplementation_setsState() {
         when(mLockscreenUserManager.getCurrentProfiles()).thenReturn(new SparseArray<>());
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
index 423cc84..3bde6e3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -51,6 +51,7 @@
 import android.graphics.Color;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.testing.ViewUtils;
 import android.util.MathUtils;
 import android.view.View;
 
@@ -59,13 +60,13 @@
 import com.android.internal.colorextraction.ColorExtractor.GradientColors;
 import com.android.keyguard.BouncerPanelExpansionCalculator;
 import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.keyguard.TestScopeProvider;
 import com.android.systemui.DejankUtils;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.ShadeInterpolation;
 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.keyguard.shared.model.KeyguardState;
@@ -73,6 +74,7 @@
 import com.android.systemui.keyguard.shared.model.TransitionStep;
 import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel;
+import com.android.systemui.kosmos.KosmosJavaAdapter;
 import com.android.systemui.scrim.ScrimView;
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
 import com.android.systemui.shade.transition.LinearLargeScreenShadeInterpolator;
@@ -103,7 +105,6 @@
 import java.util.HashSet;
 import java.util.Map;
 
-import kotlinx.coroutines.CoroutineDispatcher;
 import kotlinx.coroutines.test.TestScope;
 
 @RunWith(AndroidTestingRunner.class)
@@ -112,13 +113,14 @@
 public class ScrimControllerTest extends SysuiTestCase {
 
     @Rule public Expect mExpect = Expect.create();
+    private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
 
     private final FakeConfigurationController mConfigurationController =
             new FakeConfigurationController();
     private final LargeScreenShadeInterpolator
             mLinearLargeScreenShadeInterpolator = new LinearLargeScreenShadeInterpolator();
 
-    private final TestScope mTestScope = TestScopeProvider.getTestScope();
+    private final TestScope mTestScope = mKosmos.getTestScope();
     private final JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope());
 
     private ScrimController mScrimController;
@@ -145,10 +147,12 @@
     @Mock private PrimaryBouncerToGoneTransitionViewModel mPrimaryBouncerToGoneTransitionViewModel;
     @Mock private AlternateBouncerToGoneTransitionViewModel
             mAlternateBouncerToGoneTransitionViewModel;
-    @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+    private final KeyguardTransitionInteractor mKeyguardTransitionInteractor =
+            mKosmos.getKeyguardTransitionInteractor();
+    private final FakeKeyguardTransitionRepository mKeyguardTransitionRepository =
+            mKosmos.getKeyguardTransitionRepository();
     @Mock private KeyguardInteractor mKeyguardInteractor;
     private final FakeWallpaperRepository mWallpaperRepository = new FakeWallpaperRepository();
-    @Mock private CoroutineDispatcher mMainDispatcher;
     @Mock private TypedArray mMockTypedArray;
 
     // TODO(b/204991468): Use a real PanelExpansionStateManager object once this bug is fixed. (The
@@ -265,8 +269,6 @@
         when(mDelayedWakeLockFactory.create(any(String.class))).thenReturn(mWakeLock);
         when(mDockManager.isDocked()).thenReturn(false);
 
-        when(mKeyguardTransitionInteractor.transition(any(), any()))
-                .thenReturn(emptyFlow());
         when(mPrimaryBouncerToGoneTransitionViewModel.getScrimAlpha())
                 .thenReturn(emptyFlow());
         when(mAlternateBouncerToGoneTransitionViewModel.getScrimAlpha())
@@ -292,13 +294,16 @@
                 mKeyguardTransitionInteractor,
                 mKeyguardInteractor,
                 mWallpaperRepository,
-                mMainDispatcher,
+                mKosmos.getTestDispatcher(),
                 mLinearLargeScreenShadeInterpolator);
         mScrimController.start();
         mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible);
         mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront);
         mScrimController.setAnimatorListener(mAnimatorListener);
 
+        // Attach behind scrim so flows that are collecting on it start running.
+        ViewUtils.attachView(mScrimBehind);
+
         mScrimController.setHasBackdrop(false);
 
         mWallpaperRepository.getWallpaperSupportsAmbientMode().setValue(false);
@@ -629,6 +634,164 @@
     }
 
     @Test
+    public void lockscreenToHubTransition_setsBehindScrimAlpha() {
+        // Start on lockscreen.
+        mScrimController.transitionTo(ScrimState.KEYGUARD);
+        finishAnimationsImmediately();
+
+        // Behind scrim starts at default alpha.
+        final float transitionProgress = 0f;
+        float expectedAlpha = ScrimState.KEYGUARD.getBehindAlpha();
+        mKeyguardTransitionRepository.sendTransitionStepJava(mKosmos.getTestScope(),
+                new TransitionStep(
+                        KeyguardState.LOCKSCREEN,
+                        KeyguardState.GLANCEABLE_HUB,
+                        transitionProgress,
+                        TransitionState.STARTED
+                ), true);
+        mTestScope.getTestScheduler().runCurrent();
+        assertThat(mScrimBehind.getViewAlpha()).isEqualTo(expectedAlpha);
+
+        // Scrim fades out as transition runs.
+        final float runningProgress = 0.2f;
+        expectedAlpha = (1 - runningProgress) * ScrimState.KEYGUARD.getBehindAlpha();
+        mKeyguardTransitionRepository.sendTransitionStepJava(mKosmos.getTestScope(),
+                new TransitionStep(
+                        KeyguardState.LOCKSCREEN,
+                        KeyguardState.GLANCEABLE_HUB,
+                        runningProgress,
+                        TransitionState.RUNNING
+                ), true);
+        mTestScope.getTestScheduler().runCurrent();
+        assertThat(mScrimBehind.getViewAlpha()).isEqualTo(expectedAlpha);
+
+        // Scrim invisible at end of transition.
+        final float finishedProgress = 1f;
+        expectedAlpha = 0f;
+        mKeyguardTransitionRepository.sendTransitionStepJava(mKosmos.getTestScope(),
+                new TransitionStep(
+                        KeyguardState.LOCKSCREEN,
+                        KeyguardState.GLANCEABLE_HUB,
+                        finishedProgress,
+                        TransitionState.FINISHED
+                ), true);
+        mTestScope.getTestScheduler().runCurrent();
+        assertThat(mScrimBehind.getViewAlpha()).isEqualTo(expectedAlpha);
+    }
+
+    @Test
+    public void hubToLockscreenTransition_setsViewAlpha() {
+        // Start on glanceable hub.
+        mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB);
+        finishAnimationsImmediately();
+
+        // Behind scrim starts at 0 alpha.
+        final float transitionProgress = 0f;
+        float expectedAlpha = 0f;
+        mKeyguardTransitionRepository.sendTransitionStepJava(mKosmos.getTestScope(),
+                new TransitionStep(
+                        KeyguardState.GLANCEABLE_HUB,
+                        KeyguardState.LOCKSCREEN,
+                        transitionProgress,
+                        TransitionState.STARTED
+                ), true);
+        mTestScope.getTestScheduler().runCurrent();
+        assertThat(mScrimBehind.getViewAlpha()).isEqualTo(expectedAlpha);
+
+        // Scrim fades in as transition runs.
+        final float runningProgress = 0.2f;
+        expectedAlpha = runningProgress * ScrimState.KEYGUARD.getBehindAlpha();
+        mKeyguardTransitionRepository.sendTransitionStepJava(mKosmos.getTestScope(),
+                new TransitionStep(
+                        KeyguardState.GLANCEABLE_HUB,
+                        KeyguardState.LOCKSCREEN,
+                        runningProgress,
+                        TransitionState.RUNNING
+                ), true);
+        mTestScope.getTestScheduler().runCurrent();
+        assertThat(mScrimBehind.getViewAlpha()).isEqualTo(expectedAlpha);
+
+        // Scrim at default visibility at end of transition.
+        final float finishedProgress = 1f;
+        expectedAlpha = finishedProgress * ScrimState.KEYGUARD.getBehindAlpha();
+        mKeyguardTransitionRepository.sendTransitionStepJava(mKosmos.getTestScope(),
+                new TransitionStep(
+                        KeyguardState.GLANCEABLE_HUB,
+                        KeyguardState.LOCKSCREEN,
+                        finishedProgress,
+                        TransitionState.FINISHED
+                ), true);
+        mTestScope.getTestScheduler().runCurrent();
+        assertThat(mScrimBehind.getViewAlpha()).isEqualTo(expectedAlpha);
+    }
+
+    @Test
+    public void transitionToHub() {
+        mScrimController.setRawPanelExpansionFraction(0f);
+        mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN);
+        mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB);
+        finishAnimationsImmediately();
+
+        // All scrims transparent on the hub.
+        assertScrimAlpha(Map.of(
+                mScrimInFront, TRANSPARENT,
+                mNotificationsScrim, TRANSPARENT,
+                mScrimBehind, TRANSPARENT));
+    }
+
+    @Test
+    public void openBouncerOnHub() {
+        mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB);
+
+        // Open the bouncer.
+        mScrimController.setRawPanelExpansionFraction(0f);
+        mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_VISIBLE);
+        finishAnimationsImmediately();
+
+        // Only behind widget is visible.
+        assertScrimAlpha(Map.of(
+                mScrimInFront, TRANSPARENT,
+                mNotificationsScrim, TRANSPARENT,
+                mScrimBehind, OPAQUE));
+
+        // Bouncer is closed.
+        mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN);
+        mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB);
+        finishAnimationsImmediately();
+
+        // All scrims are transparent.
+        assertScrimAlpha(Map.of(
+                mScrimInFront, TRANSPARENT,
+                mNotificationsScrim, TRANSPARENT,
+                mScrimBehind, TRANSPARENT));
+    }
+
+    @Test
+    public void openShadeOnHub() {
+        mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB);
+
+        // Open the shade.
+        mScrimController.transitionTo(SHADE_LOCKED);
+        mScrimController.setQsPosition(1f, 0);
+        finishAnimationsImmediately();
+
+        // Shade scrims are visible.
+        assertScrimAlpha(Map.of(
+                mNotificationsScrim, OPAQUE,
+                mScrimInFront, TRANSPARENT,
+                mScrimBehind, OPAQUE));
+
+        mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB);
+        finishAnimationsImmediately();
+
+        // All scrims are transparent.
+        assertScrimAlpha(Map.of(
+                mScrimInFront, TRANSPARENT,
+                mNotificationsScrim, TRANSPARENT,
+                mScrimBehind, TRANSPARENT));
+    }
+
+    @Test
     public void onThemeChange_bouncerBehindTint_isUpdatedToSurfaceColor() {
         assertEquals(BOUNCER.getBehindTint(), 0x112233);
         mSurfaceColor = 0x223344;
@@ -1001,7 +1164,7 @@
                 mKeyguardTransitionInteractor,
                 mKeyguardInteractor,
                 mWallpaperRepository,
-                mMainDispatcher,
+                mKosmos.getTestDispatcher(),
                 mLinearLargeScreenShadeInterpolator);
         mScrimController.start();
         mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible);
@@ -1267,7 +1430,7 @@
                 ScrimState.UNINITIALIZED, ScrimState.KEYGUARD, BOUNCER,
                 ScrimState.DREAMING, ScrimState.BOUNCER_SCRIMMED, ScrimState.BRIGHTNESS_MIRROR,
                 ScrimState.UNLOCKED, SHADE_LOCKED, ScrimState.AUTH_SCRIMMED,
-                ScrimState.AUTH_SCRIMMED_SHADE));
+                ScrimState.AUTH_SCRIMMED_SHADE, ScrimState.GLANCEABLE_HUB));
 
         for (ScrimState state : ScrimState.values()) {
             if (!lowPowerModeStates.contains(state) && !regularStates.contains(state)) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
index e010b86..d465b47 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.pipeline.satellite.domain.interactor
 
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import androidx.test.filters.SmallTest
 import com.android.internal.telephony.flags.Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG
 import com.android.systemui.SysuiTestCase
@@ -49,8 +51,6 @@
 
     @Before
     fun setUp() {
-        mSetFlagsRule.enableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
-
         underTest =
             DeviceBasedSatelliteInteractor(
                 repo,
@@ -60,6 +60,7 @@
     }
 
     @Test
+    @EnableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
     fun isSatelliteAllowed_falseWhenNotAllowed() =
         testScope.runTest {
             val latest by collectLastValue(underTest.isSatelliteAllowed)
@@ -72,6 +73,7 @@
         }
 
     @Test
+    @EnableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
     fun isSatelliteAllowed_trueWhenAllowed() =
         testScope.runTest {
             val latest by collectLastValue(underTest.isSatelliteAllowed)
@@ -84,10 +86,10 @@
         }
 
     @Test
+    @DisableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
     fun isSatelliteAllowed_offWhenFlagIsOff() =
         testScope.runTest {
             // GIVEN feature is disabled
-            mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
 
             // Remake the interactor so the flag is read
             underTest =
@@ -107,6 +109,7 @@
         }
 
     @Test
+    @EnableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
     fun connectionState_matchesRepositoryValue() =
         testScope.runTest {
             val latest by collectLastValue(underTest.connectionState)
@@ -129,10 +132,10 @@
         }
 
     @Test
+    @DisableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
     fun connectionState_offWhenFeatureIsDisabled() =
         testScope.runTest {
             // GIVEN the flag is disabled
-            mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
 
             // Remake the interactor so the flag is read
             underTest =
@@ -164,6 +167,7 @@
         }
 
     @Test
+    @EnableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
     fun signalStrength_matchesRepo() =
         testScope.runTest {
             val latest by collectLastValue(underTest.signalStrength)
@@ -182,10 +186,10 @@
         }
 
     @Test
+    @DisableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
     fun signalStrength_zeroWhenDisabled() =
         testScope.runTest {
             // GIVEN the flag is enabled
-            mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
 
             // Remake the interactor so the flag is read
             underTest =
@@ -212,6 +216,19 @@
         }
 
     @Test
+    @EnableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
+    fun areAllConnectionsOutOfService_noConnections_yes() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
+
+            // GIVEN, 0 connections
+
+            // THEN the value is propagated to this interactor
+            assertThat(latest).isTrue()
+        }
+
+    @Test
+    @EnableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
     fun areAllConnectionsOutOfService_twoConnectionsOos_yes() =
         testScope.runTest {
             val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
@@ -229,6 +246,7 @@
         }
 
     @Test
+    @EnableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
     fun areAllConnectionsOutOfService_oneConnectionOos_yes() =
         testScope.runTest {
             val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
@@ -244,6 +262,7 @@
         }
 
     @Test
+    @EnableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
     fun areAllConnectionsOutOfService_oneConnectionInService_no() =
         testScope.runTest {
             val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
@@ -259,6 +278,7 @@
         }
 
     @Test
+    @EnableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
     fun areAllConnectionsOutOfService_twoConnectionsOneInService_no() =
         testScope.runTest {
             val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
@@ -276,6 +296,7 @@
         }
 
     @Test
+    @EnableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
     fun areAllConnectionsOutOfService_twoConnectionsInService_no() =
         testScope.runTest {
             val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
@@ -293,10 +314,10 @@
         }
 
     @Test
+    @DisableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
     fun areAllConnectionsOutOfService_falseWhenFlagIsOff() =
         testScope.runTest {
             // GIVEN the flag is disabled
-            mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
 
             // Remake the interactor so the flag is read
             underTest =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
index 457acd2..b58a41c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
@@ -190,7 +190,7 @@
 
         mWakefulnessLifecycle.dispatchFinishedWakingUp();
         mThemeOverlayController.start();
-        verify(mUserTracker).addCallback(mUserTrackerCallback.capture(), eq(mBgExecutor));
+        verify(mUserTracker).addCallback(mUserTrackerCallback.capture(), eq(mMainExecutor));
         verify(mWallpaperManager).addOnColorsChangedListener(mColorsListener.capture(), eq(null),
                 eq(UserHandle.USER_ALL));
         verify(mBroadcastDispatcher).registerReceiver(mBroadcastReceiver.capture(), any(),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index 0c1dbfe..e20a0ab 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -28,9 +28,12 @@
 import java.util.UUID
 import javax.inject.Inject
 import junit.framework.Assert.fail
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.channels.BufferOverflow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.TestCoroutineScheduler
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
@@ -150,6 +153,15 @@
         _transitions.emit(step)
     }
 
+    /** Version of [sendTransitionStep] that's usable from Java tests. */
+    fun sendTransitionStepJava(
+        coroutineScope: CoroutineScope,
+        step: TransitionStep,
+        validateStep: Boolean = true
+    ): Job {
+        return coroutineScope.launch { sendTransitionStep(step, validateStep) }
+    }
+
     suspend fun sendTransitionSteps(
         steps: List<TransitionStep>,
         testScope: TestScope,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
index 11f2938..083de10 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
@@ -32,6 +32,8 @@
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.jank.interactionJankMonitor
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.model.sceneContainerPlugin
 import com.android.systemui.plugins.statusbar.statusBarStateController
 import com.android.systemui.power.data.repository.fakePowerRepository
@@ -61,6 +63,8 @@
     val bouncerRepository by lazy { kosmos.bouncerRepository }
     val communalRepository by lazy { kosmos.fakeCommunalRepository }
     val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
+    val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
+    val keyguardTransitionInteractor by lazy { kosmos.keyguardTransitionInteractor }
     val powerRepository by lazy { kosmos.fakePowerRepository }
     val clock by lazy { kosmos.systemClock }
     val mobileConnectionsRepository by lazy { kosmos.fakeMobileConnectionsRepository }
diff --git a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
index 3aa9cc8..155c523 100644
--- a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
+++ b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
@@ -132,7 +132,7 @@
 
     private static final String CAMERA_EXTENSION_VERSION_NAME =
             "androidx.camera.extensions.impl.ExtensionVersionImpl";
-    private static final String LATEST_VERSION = "1.4.0";
+    private static final String LATEST_VERSION = "1.5.0";
     // No support for the init sequence
     private static final String NON_INIT_VERSION_PREFIX = "1.0";
     // Support advanced API and latency queries
@@ -1693,6 +1693,7 @@
         private final Size mSize;
         private final int mImageFormat;
         private final int mDataspace;
+        private final long mUsage;
 
         public OutputSurfaceImplStub(OutputSurface outputSurface) {
             mSurface = outputSurface.surface;
@@ -1700,8 +1701,10 @@
             mImageFormat = outputSurface.imageFormat;
             if (mSurface != null) {
                 mDataspace = SurfaceUtils.getSurfaceDataspace(mSurface);
+                mUsage = SurfaceUtils.getSurfaceUsage(mSurface);
             } else {
                 mDataspace = -1;
+                mUsage = 0;
             }
         }
 
@@ -1724,6 +1727,11 @@
         public int getDataspace() {
             return mDataspace;
         }
+
+        @Override
+        public long getUsage() {
+            return mUsage;
+        }
     }
 
     private class PreviewExtenderImplStub extends IPreviewExtenderImpl.Stub implements
@@ -2471,6 +2479,11 @@
             ret.size.height = imageReaderOutputConfig.getSize().getHeight();
             ret.imageFormat = imageReaderOutputConfig.getImageFormat();
             ret.capacity = imageReaderOutputConfig.getMaxImages();
+            if (EFV_SUPPORTED) {
+                ret.usage = imageReaderOutputConfig.getUsage();
+            } else {
+                ret.usage = 0;
+            }
         } else if (output instanceof MultiResolutionImageReaderOutputConfigImpl) {
             MultiResolutionImageReaderOutputConfigImpl multiResReaderConfig =
                     (MultiResolutionImageReaderOutputConfigImpl) output;
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index e013a3e..1ac69f6 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -30,6 +30,9 @@
         "junit-src/**/*.java",
         "junit-impl-src/**/*.java",
     ],
+    static_libs: [
+        "androidx.test.monitor-for-device",
+    ],
     libs: [
         "framework-minus-apex.ravenwood",
         "junit",
@@ -61,3 +64,17 @@
         "core-xml-for-host",
     ],
 }
+
+java_host_for_device {
+    name: "androidx.test.monitor-for-device",
+    libs: [
+        "androidx.test.monitor-for-host",
+    ],
+}
+
+java_device_for_host {
+    name: "androidx.test.monitor-for-host",
+    libs: [
+        "androidx.test.monitor",
+    ],
+}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
index b3dbcde..a797b1d 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
@@ -16,12 +16,35 @@
 
 package android.platform.test.ravenwood;
 
+import android.app.Instrumentation;
+import android.os.Bundle;
 import android.os.HandlerThread;
 import android.os.Looper;
 
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import java.io.PrintStream;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
 public class RavenwoodRuleImpl {
     private static final String MAIN_THREAD_NAME = "RavenwoodMain";
 
+    /**
+     * When enabled, attempt to dump all thread stacks just before we hit the
+     * overall Tradefed timeout, to aid in debugging deadlocks.
+     */
+    private static final boolean ENABLE_TIMEOUT_STACKS = false;
+    private static final int TIMEOUT_MILLIS = 9_000;
+
+    private static final ScheduledExecutorService sTimeoutExecutor =
+            Executors.newScheduledThreadPool(1);
+
+    private static ScheduledFuture<?> sPendingTimeout;
+
     public static boolean isOnRavenwood() {
         return true;
     }
@@ -41,9 +64,22 @@
             main.start();
             Looper.setMainLooperForTest(main.getLooper());
         }
+
+        InstrumentationRegistry.registerInstance(new Instrumentation(), Bundle.EMPTY);
+
+        if (ENABLE_TIMEOUT_STACKS) {
+            sPendingTimeout = sTimeoutExecutor.schedule(RavenwoodRuleImpl::dumpStacks,
+                    TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+        }
     }
 
     public static void reset(RavenwoodRule rule) {
+        if (ENABLE_TIMEOUT_STACKS) {
+            sPendingTimeout.cancel(false);
+        }
+
+        InstrumentationRegistry.registerInstance(null, Bundle.EMPTY);
+
         if (rule.mProvideMainThread) {
             Looper.getMainLooper().quit();
             Looper.clearMainLooperForTest();
@@ -55,4 +91,19 @@
         android.os.Binder.reset$ravenwood();
         android.os.Process.reset$ravenwood();
     }
+
+    private static void dumpStacks() {
+        final PrintStream out = System.err;
+        out.println("-----BEGIN ALL THREAD STACKS-----");
+        final Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces();
+        for (Map.Entry<Thread, StackTraceElement[]> stack : stacks.entrySet()) {
+            out.println();
+            Thread t = stack.getKey();
+            out.println(t.toString() + " ID=" + t.getId());
+            for (StackTraceElement e : stack.getValue()) {
+                out.println("\tat " + e);
+            }
+        }
+        out.println("-----END ALL THREAD STACKS-----");
+    }
 }
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java
index 68b163e..8d76970 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java
@@ -19,6 +19,7 @@
 import static android.platform.test.ravenwood.RavenwoodRule.ENABLE_PROBE_IGNORED;
 import static android.platform.test.ravenwood.RavenwoodRule.IS_ON_RAVENWOOD;
 import static android.platform.test.ravenwood.RavenwoodRule.shouldEnableOnRavenwood;
+import static android.platform.test.ravenwood.RavenwoodRule.shouldStillIgnoreInProbeIgnoreMode;
 
 import android.platform.test.annotations.DisabledOnRavenwood;
 import android.platform.test.annotations.EnabledOnRavenwood;
@@ -45,6 +46,7 @@
         }
 
         if (ENABLE_PROBE_IGNORED) {
+            Assume.assumeFalse(shouldStillIgnoreInProbeIgnoreMode(description));
             // Pass through to possible underlying RavenwoodRule for both environment
             // configuration and handling method-level annotations
             return base;
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
index 952ee0e..1e7cbf6 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
@@ -27,7 +27,9 @@
 import org.junit.runner.Description;
 import org.junit.runners.model.Statement;
 
+import java.util.Objects;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Pattern;
 
 /**
  * {@code @Rule} that configures the Ravenwood test environment. This rule has no effect when
@@ -55,6 +57,43 @@
     static final boolean ENABLE_PROBE_IGNORED = "1".equals(
             System.getenv("RAVENWOOD_RUN_DISABLED_TESTS"));
 
+    /**
+     * When using ENABLE_PROBE_IGNORED, you may still want to skip certain tests,
+     * for example because the test would crash the JVM.
+     *
+     * This regex defines the tests that should still be disabled even if ENABLE_PROBE_IGNORED
+     * is set.
+     *
+     * Before running each test class and method, we check if this pattern can be found in
+     * the full test name (either [class full name], or [class full name] + "#" + [method name]),
+     * and if so, we skip it.
+     *
+     * For example, if you want to skip an entire test class, use:
+     * RAVENWOOD_REALLY_DISABLE='\.CustomTileDefaultsRepositoryTest$'
+     *
+     * For example, if you want to skip an entire test class, use:
+     * RAVENWOOD_REALLY_DISABLE='\.CustomTileDefaultsRepositoryTest#testSimple$'
+     *
+     * To ignore multiple classes, use (...|...), for example:
+     * RAVENWOOD_REALLY_DISABLE='\.(ClassA|ClassB)$'
+     *
+     * Because we use a regex-find, setting "." would disable all tests.
+     */
+    private static final Pattern REALLY_DISABLE_PATTERN = Pattern.compile(
+            Objects.requireNonNullElse(System.getenv("RAVENWOOD_REALLY_DISABLE"), ""));
+
+    private static final boolean ENABLE_REALLY_DISABLE_PATTERN =
+            !REALLY_DISABLE_PATTERN.pattern().isEmpty();
+
+    static {
+        if (ENABLE_PROBE_IGNORED) {
+            System.out.println("$RAVENWOOD_RUN_DISABLED_TESTS enabled: force running all tests");
+            if (ENABLE_REALLY_DISABLE_PATTERN) {
+                System.out.println("$RAVENWOOD_REALLY_DISABLE=" + REALLY_DISABLE_PATTERN.pattern());
+            }
+        }
+    }
+
     private static final int SYSTEM_UID = 1000;
     private static final int NOBODY_UID = 9999;
     private static final int FIRST_APPLICATION_UID = 10000;
@@ -203,6 +242,21 @@
         return true;
     }
 
+    static boolean shouldStillIgnoreInProbeIgnoreMode(Description description) {
+        if (!ENABLE_REALLY_DISABLE_PATTERN) {
+            return false;
+        }
+
+        final var fullname = description.getTestClass().getName()
+                + (description.isTest() ? "#" + description.getMethodName() : "");
+
+        if (REALLY_DISABLE_PATTERN.matcher(fullname).find()) {
+            System.out.println("Still ignoring " + fullname);
+            return true;
+        }
+        return false;
+    }
+
     @Override
     public Statement apply(Statement base, Description description) {
         // No special treatment when running outside Ravenwood; run tests as-is
@@ -245,6 +299,7 @@
         return new Statement() {
             @Override
             public void evaluate() throws Throwable {
+                Assume.assumeFalse(shouldStillIgnoreInProbeIgnoreMode(description));
                 RavenwoodRuleImpl.init(RavenwoodRule.this);
                 try {
                     base.evaluate();
diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt
index eaf01a3..b775f9a 100644
--- a/ravenwood/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/ravenwood-annotation-allowed-classes.txt
@@ -72,6 +72,7 @@
 android.os.ServiceSpecificException
 android.os.SystemClock
 android.os.SystemProperties
+android.os.TestLooperManager
 android.os.ThreadLocalWorkSource
 android.os.TimestampedValue
 android.os.Trace
@@ -141,6 +142,8 @@
 
 android.content.ContentProvider
 
+android.app.Instrumentation
+
 android.metrics.LogMaker
 
 android.view.Display$HdrCapabilities
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 57c0539..63784ba 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -16,6 +16,7 @@
 
 package com.android.server.accessibility;
 
+import static android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
 import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON;
 import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_MANAGER;
 import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_MANAGER_CLIENT;
@@ -5724,6 +5725,21 @@
     }
 
     @Override
+    public void attachAccessibilityOverlayToDisplay_enforcePermission(
+            int displayId, SurfaceControl sc) {
+        mContext.enforceCallingPermission(
+                INTERNAL_SYSTEM_WINDOW, "attachAccessibilityOverlayToDisplay_enforcePermission");
+        mMainHandler.sendMessage(
+                obtainMessage(
+                        AccessibilityManagerService::attachAccessibilityOverlayToDisplayInternal,
+                        this,
+                        -1,
+                        displayId,
+                        sc,
+                        null));
+    }
+
+    @Override
     public void attachAccessibilityOverlayToDisplay(
             int interactionId,
             int displayId,
@@ -5759,12 +5775,15 @@
             t.close();
             result = AccessibilityService.OVERLAY_RESULT_SUCCESS;
         }
-        // Send the result back to the service.
-        try {
-            callback.sendAttachOverlayResult(result, interactionId);
-        } catch (RemoteException re) {
-            Slog.e(LOG_TAG, "Exception while attaching overlay.", re);
-            // the other side will time out
+
+        if (callback != null) {
+            // Send the result back to the service.
+            try {
+                callback.sendAttachOverlayResult(result, interactionId);
+            } catch (RemoteException re) {
+                Slog.e(LOG_TAG, "Exception while attaching overlay.", re);
+                // the other side will time out
+            }
         }
     }
 }
diff --git a/services/core/java/com/android/server/BinderCallsStatsService.java b/services/core/java/com/android/server/BinderCallsStatsService.java
index b87184a..416b36f 100644
--- a/services/core/java/com/android/server/BinderCallsStatsService.java
+++ b/services/core/java/com/android/server/BinderCallsStatsService.java
@@ -288,22 +288,23 @@
                         CachedDeviceState.Readonly.class);
                 mBinderCallsStats.setDeviceState(deviceState);
 
-                BatteryStatsInternal batteryStatsInternal = getLocalService(
-                        BatteryStatsInternal.class);
-                mBinderCallsStats.setCallStatsObserver(new BinderInternal.CallStatsObserver() {
-                    @Override
-                    public void noteCallStats(int workSourceUid, long incrementalCallCount,
-                            Collection<BinderCallsStats.CallStat> callStats) {
-                        batteryStatsInternal.noteBinderCallStats(workSourceUid,
-                                incrementalCallCount, callStats);
-                    }
+                if (!com.android.server.power.optimization.Flags.disableSystemServicePowerAttr()) {
+                    BatteryStatsInternal batteryStatsInternal = getLocalService(
+                            BatteryStatsInternal.class);
+                    mBinderCallsStats.setCallStatsObserver(new BinderInternal.CallStatsObserver() {
+                        @Override
+                        public void noteCallStats(int workSourceUid, long incrementalCallCount,
+                                Collection<BinderCallsStats.CallStat> callStats) {
+                            batteryStatsInternal.noteBinderCallStats(workSourceUid,
+                                    incrementalCallCount, callStats);
+                        }
 
-                    @Override
-                    public void noteBinderThreadNativeIds(int[] binderThreadNativeTids) {
-                        batteryStatsInternal.noteBinderThreadNativeIds(binderThreadNativeTids);
-                    }
-                });
-
+                        @Override
+                        public void noteBinderThreadNativeIds(int[] binderThreadNativeTids) {
+                            batteryStatsInternal.noteBinderThreadNativeIds(binderThreadNativeTids);
+                        }
+                    });
+                }
                 // It needs to be called before mService.systemReady to make sure the observer is
                 // initialized before installing it.
                 mWorkSourceProvider.systemReady(getContext());
diff --git a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
index c391642..f619ca3 100644
--- a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
+++ b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
@@ -27,6 +27,7 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.RemoteException;
+import android.os.Trace;
 import android.os.UserHandle;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.NotificationListenerService.RankingMap;
@@ -49,12 +50,12 @@
     private static final String TAG = "SensitiveContentProtect";
     private static final boolean DEBUG = false;
 
-    @VisibleForTesting
-    NotificationListener mNotificationListener;
+    @VisibleForTesting NotificationListener mNotificationListener;
     private @Nullable MediaProjectionManager mProjectionManager;
     private @Nullable WindowManagerInternal mWindowManager;
 
     final Object mSensitiveContentProtectionLock = new Object();
+
     @GuardedBy("mSensitiveContentProtectionLock")
     private boolean mProjectionActive = false;
 
@@ -63,13 +64,24 @@
                 @Override
                 public void onStart(MediaProjectionInfo info) {
                     if (DEBUG) Log.d(TAG, "onStart projection: " + info);
-                    onProjectionStart();
+                    Trace.beginSection(
+                            "SensitiveContentProtectionManagerService.onProjectionStart");
+                    try {
+                        onProjectionStart();
+                    } finally {
+                        Trace.endSection();
+                    }
                 }
 
                 @Override
                 public void onStop(MediaProjectionInfo info) {
                     if (DEBUG) Log.d(TAG, "onStop projection: " + info);
-                    onProjectionEnd();
+                    Trace.beginSection("SensitiveContentProtectionManagerService.onProjectionStop");
+                    try {
+                        onProjectionEnd();
+                    } finally {
+                        Trace.endSection();
+                    }
                 }
             };
 
@@ -94,8 +106,7 @@
     }
 
     @VisibleForTesting
-    void init(MediaProjectionManager projectionManager,
-            WindowManagerInternal windowManager) {
+    void init(MediaProjectionManager projectionManager, WindowManagerInternal windowManager) {
         if (DEBUG) Log.d(TAG, "init");
 
         checkNotNull(projectionManager, "Failed to get valid MediaProjectionManager");
@@ -109,7 +120,8 @@
         mProjectionManager.addCallback(mProjectionCallback, new Handler(Looper.getMainLooper()));
 
         try {
-            mNotificationListener.registerAsSystemService(getContext(),
+            mNotificationListener.registerAsSystemService(
+                    getContext(),
                     new ComponentName(getContext(), NotificationListener.class),
                     UserHandle.USER_ALL);
         } catch (RemoteException e) {
@@ -174,8 +186,8 @@
         }
 
         // notify windowmanager of any currently posted sensitive content notifications
-        ArraySet<PackageInfo> packageInfos = getSensitivePackagesFromNotifications(
-                notifications, rankingMap);
+        ArraySet<PackageInfo> packageInfos =
+                getSensitivePackagesFromNotifications(notifications, rankingMap);
 
         mWindowManager.addBlockScreenCaptureForApps(packageInfos);
     }
@@ -197,8 +209,8 @@
         return sensitivePackages;
     }
 
-    private PackageInfo getSensitivePackageFromNotification(StatusBarNotification sbn,
-            RankingMap rankingMap) {
+    private PackageInfo getSensitivePackageFromNotification(
+            StatusBarNotification sbn, RankingMap rankingMap) {
         if (sbn == null) {
             Log.w(TAG, "Unable to protect null notification");
             return null;
@@ -220,38 +232,55 @@
         @Override
         public void onListenerConnected() {
             super.onListenerConnected();
-            // Projection started before notification listener was connected
-            synchronized (mSensitiveContentProtectionLock) {
-                if (mProjectionActive) {
-                    updateAppsThatShouldBlockScreenCapture();
+            Trace.beginSection("SensitiveContentProtectionManagerService.onListenerConnected");
+            try {
+                // Projection started before notification listener was connected
+                synchronized (mSensitiveContentProtectionLock) {
+                    if (mProjectionActive) {
+                        updateAppsThatShouldBlockScreenCapture();
+                    }
                 }
+            } finally {
+                Trace.endSection();
             }
         }
 
         @Override
         public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
             super.onNotificationPosted(sbn, rankingMap);
-            synchronized (mSensitiveContentProtectionLock) {
-                if (!mProjectionActive) {
-                    return;
-                }
+            Trace.beginSection("SensitiveContentProtectionManagerService.onNotificationPosted");
+            try {
+                synchronized (mSensitiveContentProtectionLock) {
+                    if (!mProjectionActive) {
+                        return;
+                    }
 
-                // notify windowmanager of any currently posted sensitive content notifications
-                PackageInfo packageInfo = getSensitivePackageFromNotification(sbn, rankingMap);
+                    // notify windowmanager of any currently posted sensitive content notifications
+                    PackageInfo packageInfo = getSensitivePackageFromNotification(sbn, rankingMap);
 
-                if (packageInfo != null) {
-                    mWindowManager.addBlockScreenCaptureForApps(new ArraySet(Set.of(packageInfo)));
+                    if (packageInfo != null) {
+                        mWindowManager.addBlockScreenCaptureForApps(
+                                new ArraySet(Set.of(packageInfo)));
+                    }
                 }
+            } finally {
+                Trace.endSection();
             }
         }
 
         @Override
         public void onNotificationRankingUpdate(RankingMap rankingMap) {
             super.onNotificationRankingUpdate(rankingMap);
-            synchronized (mSensitiveContentProtectionLock) {
-                if (mProjectionActive) {
-                    updateAppsThatShouldBlockScreenCapture(rankingMap);
+            Trace.beginSection(
+                    "SensitiveContentProtectionManagerService.onNotificationRankingUpdate");
+            try {
+                synchronized (mSensitiveContentProtectionLock) {
+                    if (mProjectionActive) {
+                        updateAppsThatShouldBlockScreenCapture(rankingMap);
+                    }
                 }
+            } finally {
+                Trace.endSection();
             }
         }
     }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 74902f7..374a17a 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -135,6 +135,7 @@
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION;
 import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__INTERNAL_NON_EXPORTED_COMPONENT_MATCH;
 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.ActivityManagerDebugConfig.DEBUG_ALL;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_ALLOWLISTS;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BACKGROUND_CHECK;
@@ -16141,10 +16142,22 @@
         }
 
         final ApplicationInfo sdkSandboxInfo;
+        final String processName;
         try {
-            sdkSandboxInfo =
-                    sandboxManagerLocal.getSdkSandboxApplicationInfoForInstrumentation(
-                            sdkSandboxClientAppInfo, isSdkInSandbox);
+            if (sdkSandboxInstrumentationInfo()) {
+                sdkSandboxInfo =
+                        sandboxManagerLocal.getSdkSandboxApplicationInfoForInstrumentation(
+                                sdkSandboxClientAppInfo, isSdkInSandbox);
+                processName = sdkSandboxInfo.processName;
+            } else {
+                final PackageManager pm = mContext.getPackageManager();
+                sdkSandboxInfo =
+                        pm.getApplicationInfoAsUser(pm.getSdkSandboxPackageName(), 0, userId);
+                processName =
+                        sandboxManagerLocal.getSdkSandboxProcessNameForInstrumentation(
+                                sdkSandboxClientAppInfo);
+                sdkSandboxInfo.uid = Process.toSdkSandboxUid(sdkSandboxClientAppInfo.uid);
+            }
         } catch (NameNotFoundException e) {
             reportStartInstrumentationFailureLocked(
                     watcher, className, "Can't find SdkSandbox package");
@@ -16153,7 +16166,7 @@
 
         ActiveInstrumentation activeInstr = new ActiveInstrumentation(this);
         activeInstr.mClass = className;
-        activeInstr.mTargetProcesses = new String[]{sdkSandboxInfo.processName};
+        activeInstr.mTargetProcesses = new String[]{processName};
         activeInstr.mTargetInfo = sdkSandboxInfo;
         activeInstr.mIsSdkInSandbox = isSdkInSandbox;
         activeInstr.mProfileFile = profileFile;
@@ -16196,7 +16209,7 @@
 
                 ProcessRecord app = addAppLocked(
                         sdkSandboxInfo,
-                        sdkSandboxInfo.processName,
+                        processName,
                         /* isolated= */ false,
                         /* isSdkSandbox= */ true,
                         sdkSandboxInfo.uid,
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 3487ae3..4f46ecd 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -422,7 +422,9 @@
         mStats.setExternalStatsSyncLocked(mWorker);
         mStats.setRadioScanningTimeoutLocked(mContext.getResources().getInteger(
                 com.android.internal.R.integer.config_radioScanningTimeout) * 1000L);
-        mStats.startTrackingSystemServerCpuTime();
+        if (!Flags.disableSystemServicePowerAttr()) {
+            mStats.startTrackingSystemServerCpuTime();
+        }
 
         mAggregatedPowerStatsConfig = createAggregatedPowerStatsConfig();
         mPowerStatsStore = new PowerStatsStore(systemDir, mHandler, mAggregatedPowerStatsConfig);
diff --git a/services/core/java/com/android/server/am/PendingIntentController.java b/services/core/java/com/android/server/am/PendingIntentController.java
index a20623c..5df9107 100644
--- a/services/core/java/com/android/server/am/PendingIntentController.java
+++ b/services/core/java/com/android/server/am/PendingIntentController.java
@@ -30,6 +30,7 @@
 import android.app.AppGlobals;
 import android.app.PendingIntent;
 import android.app.PendingIntentStats;
+import android.app.compat.CompatChanges;
 import android.content.IIntentSender;
 import android.content.Intent;
 import android.os.Binder;
@@ -136,6 +137,11 @@
                         + "intent creator ("
                         + packageName
                         + ") because this option is meant for the pending intent sender");
+                if (CompatChanges.isChangeEnabled(PendingIntent.PENDING_INTENT_OPTIONS_CHECK,
+                        callingUid)) {
+                    throw new IllegalArgumentException("pendingIntentBackgroundActivityStartMode "
+                            + "must not be set when creating a PendingIntent");
+                }
                 opts.setPendingIntentBackgroundActivityStartMode(
                         ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED);
             }
diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java
index 10d5fd3..95e130e 100644
--- a/services/core/java/com/android/server/am/PendingIntentRecord.java
+++ b/services/core/java/com/android/server/am/PendingIntentRecord.java
@@ -406,6 +406,9 @@
             String resolvedType, IBinder allowlistToken, IIntentReceiver finishedReceiver,
             String requiredPermission, IBinder resultTo, String resultWho, int requestCode,
             int flagsMask, int flagsValues, Bundle options) {
+        final int callingUid = Binder.getCallingUid();
+        final int callingPid = Binder.getCallingPid();
+
         if (intent != null) intent.setDefusable(true);
         if (options != null) options.setDefusable(true);
 
@@ -458,6 +461,12 @@
                                     + key.packageName
                                     + ") because this option is meant for the pending intent "
                                     + "creator");
+                    if (CompatChanges.isChangeEnabled(PendingIntent.PENDING_INTENT_OPTIONS_CHECK,
+                            callingUid)) {
+                        throw new IllegalArgumentException(
+                                "pendingIntentCreatorBackgroundActivityStartMode "
+                                + "must not be set when sending a PendingIntent");
+                    }
                     opts.setPendingIntentCreatorBackgroundActivityStartMode(
                             ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED);
                 }
@@ -494,9 +503,6 @@
         }
         // We don't hold the controller lock beyond this point as we will be calling into AM and WM.
 
-        final int callingUid = Binder.getCallingUid();
-        final int callingPid = Binder.getCallingPid();
-
         // Only system senders can declare a broadcast to be alarm-originated.  We check
         // this here rather than in the general case handling below to fail before the other
         // invocation side effects such as allowlisting.
diff --git a/services/core/java/com/android/server/content/SyncManager.java b/services/core/java/com/android/server/content/SyncManager.java
index df179a9..5b23364c 100644
--- a/services/core/java/com/android/server/content/SyncManager.java
+++ b/services/core/java/com/android/server/content/SyncManager.java
@@ -138,6 +138,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.NoSuchElementException;
 import java.util.Objects;
 import java.util.Random;
 import java.util.Set;
@@ -3870,8 +3871,12 @@
             final SyncStorageEngine.EndPoint info = syncOperation.target;
 
             if (activeSyncContext.mIsLinkedToDeath) {
-                activeSyncContext.mSyncAdapter.asBinder().unlinkToDeath(activeSyncContext, 0);
-                activeSyncContext.mIsLinkedToDeath = false;
+                try {
+                    activeSyncContext.mSyncAdapter.asBinder().unlinkToDeath(activeSyncContext, 0);
+                    activeSyncContext.mIsLinkedToDeath = false;
+                } catch (NoSuchElementException e) {
+                    Slog.wtf(TAG, "Failed to unlink active sync adapter to death", e);
+                }
             }
             final long elapsedTime = SystemClock.elapsedRealtime() - activeSyncContext.mStartTime;
             String historyMessage;
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 245fcea..93addcd 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -1656,14 +1656,16 @@
             ContentRecordingSession session = null;
             try {
                 if (projection != null) {
-                    IBinder launchCookie = projection.getLaunchCookie();
-                    if (launchCookie == null) {
+                    IBinder taskWindowContainerToken = projection.getLaunchCookie() == null ? null
+                            : projection.getLaunchCookie().binder;
+                    if (taskWindowContainerToken == null) {
                         // Record a particular display.
                         session = ContentRecordingSession.createDisplaySession(
                                 virtualDisplayConfig.getDisplayIdToMirror());
                     } else {
                         // Record a single task indicated by the launch cookie.
-                        session = ContentRecordingSession.createTaskSession(launchCookie);
+                        session = ContentRecordingSession.createTaskSession(
+                                taskWindowContainerToken);
                     }
                 }
             } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
index 82461fa..8b4e1ff 100644
--- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
@@ -394,6 +394,8 @@
             if (mDisplayObserver.isExternalDisplayLocked(displayId)) {
                 primarySummary.maxRenderFrameRate = Math.max(baseMode.getRefreshRate(),
                         primarySummary.maxRenderFrameRate);
+                appRequestSummary.maxRenderFrameRate = Math.max(baseMode.getRefreshRate(),
+                        appRequestSummary.maxRenderFrameRate);
             }
 
             return new DesiredDisplayModeSpecs(baseMode.getModeId(),
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java
index ba4d320..731c78e 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java
@@ -514,6 +514,18 @@
     protected int handleRoutingInformation(HdmiCecMessage message) {
         assertRunOnServiceThread();
         int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
+        HdmiDeviceInfo sourceDevice = mService.getHdmiCecNetwork()
+                .getCecDeviceInfo(message.getSource());
+        // Ignore <Routing Information> messages pointing to the same physical address as the
+        // message sender. In this case, we shouldn't consider the sender to be the active source.
+        // See more b/321771821#comment7.
+        if (sourceDevice != null
+                && sourceDevice.getLogicalAddress() != Constants.ADDR_TV
+                && sourceDevice.getPhysicalAddress() == physicalAddress) {
+            Slog.d(TAG, "<Routing Information> is ignored, it is pointing to the same physical"
+                    + " address as the message sender");
+            return Constants.HANDLED;
+        }
         handleRoutingChangeAndInformation(physicalAddress, message);
         return Constants.HANDLED;
     }
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 34e75c0..46061a5 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -482,6 +482,22 @@
     @Override
     @ServiceThreadOnly
     @Constants.HandleMessageResult
+    protected int handleStandby(HdmiCecMessage message) {
+        assertRunOnServiceThread();
+
+        // Ignore <Standby> from non-active source device.
+        if (getActiveSource().logicalAddress != message.getSource()) {
+            Slog.d(TAG, "<Standby> was not sent by the current active source, ignoring."
+                    + " Current active source has logical address "
+                    + getActiveSource().logicalAddress);
+            return Constants.HANDLED;
+        }
+        return super.handleStandby(message);
+    }
+
+    @Override
+    @ServiceThreadOnly
+    @Constants.HandleMessageResult
     protected int handleInactiveSource(HdmiCecMessage message) {
         assertRunOnServiceThread();
         // Seq #10
diff --git a/services/core/java/com/android/server/inputmethod/ClientController.java b/services/core/java/com/android/server/inputmethod/ClientController.java
index ece236a..86f4db9 100644
--- a/services/core/java/com/android/server/inputmethod/ClientController.java
+++ b/services/core/java/com/android/server/inputmethod/ClientController.java
@@ -17,6 +17,7 @@
 package com.android.server.inputmethod;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.pm.PackageManagerInternal;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -29,6 +30,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Consumer;
 
 /**
  * Store and manage {@link InputMethodManagerService} clients. This class was designed to be a
@@ -62,7 +64,7 @@
 
     // TODO(b/314150112): Make this field private when breaking the cycle with IMMS.
     @GuardedBy("ImfLock.class")
-    final ArrayMap<IBinder, ClientState> mClients = new ArrayMap<>();
+    private final ArrayMap<IBinder, ClientState> mClients = new ArrayMap<>();
 
     @GuardedBy("ImfLock.class")
     private final List<ClientControllerCallback> mCallbacks = new ArrayList<>();
@@ -145,6 +147,19 @@
     }
 
     @GuardedBy("ImfLock.class")
+    @Nullable
+    ClientState getClient(IBinder binder) {
+        return mClients.get(binder);
+    }
+
+    @GuardedBy("ImfLock.class")
+    void forAllClients(Consumer<ClientState> consumer) {
+        for (int i = 0; i < mClients.size(); i++) {
+            consumer.accept(mClients.valueAt(i));
+        }
+    }
+
+    @GuardedBy("ImfLock.class")
     boolean verifyClientAndPackageMatch(
             @NonNull IInputMethodClient client, @NonNull String packageName) {
         final ClientState cs = mClients.get(client.asBinder());
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 4767ebd..f031b7b 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -205,6 +205,7 @@
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
 import java.util.function.IntConsumer;
 
 /**
@@ -270,7 +271,6 @@
     @NonNull
     private final String[] mNonPreemptibleInputMethods;
 
-    // TODO(b/314150112): Move this to ClientController.
     @UserIdInt
     private int mLastSwitchUserId;
 
@@ -1819,10 +1819,8 @@
         }
 
         mLastSwitchUserId = newUserId;
-
         if (mIsInteractive && clientToBeReset != null) {
-            final ClientState cs =
-                    mClientController.mClients.get(clientToBeReset.asBinder());
+            final ClientState cs = mClientController.getClient(clientToBeReset.asBinder());
             if (cs == null) {
                 // The client is already gone.
                 return;
@@ -2165,26 +2163,25 @@
     /**
      * Hide the IME if the removed user is the current user.
      */
+    @GuardedBy("ImfLock.class")
     private void onClientRemoved(ClientState client) {
-        synchronized (ImfLock.class) {
-            clearClientSessionLocked(client);
-            clearClientSessionForAccessibilityLocked(client);
-            if (mCurClient == client) {
-                hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
-                        null /* resultReceiver */, SoftInputShowHideReason.HIDE_REMOVE_CLIENT);
-                if (mBoundToMethod) {
-                    mBoundToMethod = false;
-                    IInputMethodInvoker curMethod = getCurMethodLocked();
-                    if (curMethod != null) {
-                        // When we unbind input, we are unbinding the client, so we always
-                        // unbind ime and a11y together.
-                        curMethod.unbindInput();
-                        AccessibilityManagerInternal.get().unbindInput();
-                    }
+        clearClientSessionLocked(client);
+        clearClientSessionForAccessibilityLocked(client);
+        if (mCurClient == client) {
+            hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+                    null /* resultReceiver */, SoftInputShowHideReason.HIDE_REMOVE_CLIENT);
+            if (mBoundToMethod) {
+                mBoundToMethod = false;
+                IInputMethodInvoker curMethod = getCurMethodLocked();
+                if (curMethod != null) {
+                    // When we unbind input, we are unbinding the client, so we always
+                    // unbind ime and a11y together.
+                    curMethod.unbindInput();
+                    AccessibilityManagerInternal.get().unbindInput();
                 }
-                mBoundToAccessibility = false;
-                mCurClient = null;
             }
+            mBoundToAccessibility = false;
+            mCurClient = null;
             if (mCurFocusedWindowClient == client) {
                 mCurFocusedWindowClient = null;
                 mCurFocusedWindowEditorInfo = null;
@@ -2192,7 +2189,6 @@
         }
     }
 
-    // TODO(b/314150112): Move this to ClientController.
     @GuardedBy("ImfLock.class")
     void unbindCurrentClientLocked(@UnbindReason int unbindClientReason) {
         if (mCurClient != null) {
@@ -2883,11 +2879,16 @@
     @GuardedBy("ImfLock.class")
     void clearClientSessionsLocked() {
         if (getCurMethodLocked() != null) {
-            final int numClients = mClientController.mClients.size();
-            for (int i = 0; i < numClients; ++i) {
-                clearClientSessionLocked(mClientController.mClients.valueAt(i));
-                clearClientSessionForAccessibilityLocked(mClientController.mClients.valueAt(i));
-            }
+            // TODO(b/322816970): Replace this with lambda.
+            mClientController.forAllClients(new Consumer<ClientState>() {
+
+                @GuardedBy("ImfLock.class")
+                @Override
+                public void accept(ClientState c) {
+                    clearClientSessionLocked(c);
+                    clearClientSessionForAccessibilityLocked(c);
+                }
+            });
 
             finishSessionLocked(mEnabledSession);
             for (int i = 0; i < mEnabledAccessibilitySessions.size(); i++) {
@@ -3732,9 +3733,9 @@
             return InputBindResult.INVALID_USER;
         }
 
-        final ClientState cs = mClientController.mClients.get(client.asBinder());
+        final ClientState cs = mClientController.getClient(client.asBinder());
         if (cs == null) {
-            throw new IllegalArgumentException("unknown client " + client.asBinder());
+            throw new IllegalArgumentException("Unknown client " + client.asBinder());
         }
 
         final int imeClientFocus = mWindowManagerInternal.hasInputMethodClientFocus(
@@ -3906,8 +3907,7 @@
             // We need to check if this is the current client with
             // focus in the window manager, to allow this call to
             // be made before input is started in it.
-            final ClientState cs =
-                    mClientController.mClients.get(client.asBinder());
+            final ClientState cs = mClientController.getClient(client.asBinder());
             if (cs == null) {
                 ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_CLIENT_KNOWN);
                 throw new IllegalArgumentException("unknown client " + client.asBinder());
@@ -4518,16 +4518,17 @@
     @Override
     public void startImeTrace() {
         super.startImeTrace_enforcePermission();
-
         ImeTracing.getInstance().startTrace(null /* printwriter */);
-        ArrayMap<IBinder, ClientState> clients;
         synchronized (ImfLock.class) {
-            clients = new ArrayMap<>(mClientController.mClients);
-        }
-        for (ClientState state : clients.values()) {
-            if (state != null) {
-                state.mClient.setImeTraceEnabled(true /* enabled */);
-            }
+            // TODO(b/322816970): Replace this with lambda.
+            mClientController.forAllClients(new Consumer<ClientState>() {
+
+                @GuardedBy("ImfLock.class")
+                @Override
+                public void accept(ClientState c) {
+                    c.mClient.setImeTraceEnabled(true /* enabled */);
+                }
+            });
         }
     }
 
@@ -4538,14 +4539,16 @@
         super.stopImeTrace_enforcePermission();
 
         ImeTracing.getInstance().stopTrace(null /* printwriter */);
-        ArrayMap<IBinder, ClientState> clients;
         synchronized (ImfLock.class) {
-            clients = new ArrayMap<>(mClientController.mClients);
-        }
-        for (ClientState state : clients.values()) {
-            if (state != null) {
-                state.mClient.setImeTraceEnabled(false /* enabled */);
-            }
+            // TODO(b/322816970): Replace this with lambda.
+            mClientController.forAllClients(new Consumer<ClientState>() {
+
+                @GuardedBy("ImfLock.class")
+                @Override
+                public void accept(ClientState c) {
+                    c.mClient.setImeTraceEnabled(false /* enabled */);
+                }
+            });
         }
     }
 
@@ -5779,11 +5782,15 @@
                 // We only have sessions when we bound to an input method. Remove this session
                 // from all clients.
                 if (getCurMethodLocked() != null) {
-                    final int numClients = mClientController.mClients.size();
-                    for (int i = 0; i < numClients; ++i) {
-                        clearClientSessionForAccessibilityLocked(
-                                mClientController.mClients.valueAt(i), accessibilityConnectionId);
-                    }
+                    // TODO(b/322816970): Replace this with lambda.
+                    mClientController.forAllClients(new Consumer<ClientState>() {
+
+                        @GuardedBy("ImfLock.class")
+                        @Override
+                        public void accept(ClientState c) {
+                            clearClientSessionForAccessibilityLocked(c, accessibilityConnectionId);
+                        }
+                    });
                     AccessibilitySessionState session = mEnabledAccessibilitySessions.get(
                             accessibilityConnectionId);
                     if (session != null) {
@@ -5967,19 +5974,26 @@
                 p.println("  InputMethod #" + i + ":");
                 info.dump(p, "    ");
             }
+            // Dump ClientController#mClients
             p.println("  ClientStates:");
-            // TODO(b/314150112): move client related dump info to ClientController#dump
-            final int numClients = mClientController.mClients.size();
-            for (int i = 0; i < numClients; ++i) {
-                final ClientState ci = mClientController.mClients.valueAt(i);
-                p.println("  " + ci + ":");
-                p.println("    client=" + ci.mClient);
-                p.println("    fallbackInputConnection=" + ci.mFallbackInputConnection);
-                p.println("    sessionRequested=" + ci.mSessionRequested);
-                p.println("    sessionRequestedForAccessibility="
-                        + ci.mSessionRequestedForAccessibility);
-                p.println("    curSession=" + ci.mCurSession);
-            }
+            // TODO(b/322816970): Replace this with lambda.
+            mClientController.forAllClients(new Consumer<ClientState>() {
+
+                @GuardedBy("ImfLock.class")
+                @Override
+                public void accept(ClientState c) {
+                    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("  mCurMethodId=" + getSelectedMethodIdLocked());
             client = mCurClient;
             p.println("  mCurClient=" + client + " mCurSeq=" + getSequenceNumberLocked());
@@ -6583,14 +6597,16 @@
             }
         }
         boolean isImeTraceEnabled = ImeTracing.getInstance().isEnabled();
-        ArrayMap<IBinder, ClientState> clients;
         synchronized (ImfLock.class) {
-            clients = new ArrayMap<>(mClientController.mClients);
-        }
-        for (ClientState state : clients.values()) {
-            if (state != null) {
-                state.mClient.setImeTraceEnabled(isImeTraceEnabled);
-            }
+            // TODO(b/322816970): Replace this with lambda.
+            mClientController.forAllClients(new Consumer<ClientState>() {
+
+                @GuardedBy("ImfLock.class")
+                @Override
+                public void accept(ClientState c) {
+                    c.mClient.setImeTraceEnabled(isImeTraceEnabled);
+                }
+            });
         }
         return ShellCommandResult.SUCCESS;
     }
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
index cc205d4..cc58f38 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -1541,8 +1541,14 @@
      */
     public @NonNull AuthenticationResult unlockTokenBasedProtector(
             IGateKeeperService gatekeeper, long protectorId, byte[] token, int userId) {
-        SyntheticPasswordBlob blob = SyntheticPasswordBlob.fromBytes(loadState(SP_BLOB_NAME,
-                    protectorId, userId));
+        byte[] data = loadState(SP_BLOB_NAME, protectorId, userId);
+        if (data == null) {
+            AuthenticationResult result = new AuthenticationResult();
+            result.gkResponse = VerifyCredentialResponse.ERROR;
+            Slogf.w(TAG, "spblob not found for protector %016x, user %d", protectorId, userId);
+            return result;
+        }
+        SyntheticPasswordBlob blob = SyntheticPasswordBlob.fromBytes(data);
         return unlockTokenBasedProtectorInternal(gatekeeper, protectorId, blob.mProtectorType,
                 token, userId);
     }
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 1d516e2..7fabdf2 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -34,6 +34,8 @@
 import android.app.KeyguardManager;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
+import android.app.usage.UsageStatsManager;
+import android.app.usage.UsageStatsManagerInternal;
 import android.content.ActivityNotFoundException;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -68,6 +70,7 @@
 import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.Message;
+import android.os.PersistableBundle;
 import android.os.PowerExemptionManager;
 import android.os.PowerManager;
 import android.os.Process;
@@ -100,7 +103,9 @@
 import java.lang.reflect.InvocationTargetException;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 /**
  * System implementation of MediaSessionManager
@@ -145,6 +150,8 @@
     private AudioManager mAudioManager;
     private boolean mHasFeatureLeanback;
     private ActivityManagerInternal mActivityManagerInternal;
+    private UsageStatsManagerInternal mUsageStatsManagerInternal;
+    private final Set<Integer> mUserEngagingSessions = new HashSet<>();
 
     // The FullUserRecord of the current users. (i.e. The foreground user that isn't a profile)
     // It's always not null after the MediaSessionService is started.
@@ -230,6 +237,7 @@
         mContext.registerReceiver(mNotificationListenerEnabledChangedReceiver, filter);
 
         mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
+        mUsageStatsManagerInternal = LocalServices.getService(UsageStatsManagerInternal.class);
     }
 
     @Override
@@ -582,12 +590,43 @@
         if (allowRunningInForeground) {
             mActivityManagerInternal.startForegroundServiceDelegate(
                     foregroundServiceDelegationOptions, /* connection= */ null);
+            reportMediaInteractionEvent(record, /* userEngaged= */ true);
         } else {
             mActivityManagerInternal.stopForegroundServiceDelegate(
                     foregroundServiceDelegationOptions);
+            reportMediaInteractionEvent(record, /* userEngaged= */ false);
         }
     }
 
+    private void reportMediaInteractionEvent(MediaSessionRecordImpl record, boolean userEngaged) {
+           if (!android.app.usage.Flags.userInteractionTypeApi()) {
+                return;
+            }
+
+            String packageName = record.getPackageName();
+            int sessionUid = record.getUid();
+            String actionToLog = null;
+            if (userEngaged) {
+                if (!mUserEngagingSessions.contains(sessionUid)) {
+                    actionToLog = "start";
+                }
+                mUserEngagingSessions.add(sessionUid);
+            } else {
+                if (mUserEngagingSessions.contains(sessionUid)) {
+                    actionToLog = "stop";
+                }
+                mUserEngagingSessions.remove(sessionUid);
+            }
+
+            if (actionToLog != null) {
+                PersistableBundle extras = new PersistableBundle();
+                extras.putString(UsageStatsManager.EXTRA_EVENT_CATEGORY, "android.media");
+                extras.putString(UsageStatsManager.EXTRA_EVENT_ACTION, actionToLog);
+                mUsageStatsManagerInternal.reportUserInteractionEvent(
+                        packageName, record.getUserId(), extras);
+            }
+    }
+
     void tempAllowlistTargetPkgIfPossible(int targetUid, String targetPackage,
             int callingPid, int callingUid, String callingPackage, String reason) {
         final long token = Binder.clearCallingIdentity();
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index 978f468..bbb19e3 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -35,6 +35,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManagerInternal;
+import android.app.ActivityOptions.LaunchCookie;
 import android.app.AppOpsManager;
 import android.app.IProcessObserver;
 import android.app.compat.CompatChanges;
@@ -548,8 +549,11 @@
                             DEFAULT_DISPLAY));
                     break;
                 case RECORD_CONTENT_TASK:
-                    setReviewedConsentSessionLocked(ContentRecordingSession.createTaskSession(
-                            mProjectionGrant.getLaunchCookie()));
+                    IBinder taskWindowContainerToken =
+                            mProjectionGrant.getLaunchCookie() == null ? null
+                                    : mProjectionGrant.getLaunchCookie().binder;
+                    setReviewedConsentSessionLocked(
+                            ContentRecordingSession.createTaskSession(taskWindowContainerToken));
                     break;
             }
         }
@@ -973,7 +977,7 @@
         private IBinder mToken;
         private IBinder.DeathRecipient mDeathEater;
         private boolean mRestoreSystemAlertWindow;
-        private IBinder mLaunchCookie = null;
+        private LaunchCookie mLaunchCookie = null;
 
         // Values for tracking token validity.
         // Timeout value to compare creation time against.
@@ -1186,14 +1190,14 @@
 
         @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_MEDIA_PROJECTION)
         @Override // Binder call
-        public void setLaunchCookie(IBinder launchCookie) {
+        public void setLaunchCookie(LaunchCookie launchCookie) {
             setLaunchCookie_enforcePermission();
             mLaunchCookie = launchCookie;
         }
 
         @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_MEDIA_PROJECTION)
         @Override // Binder call
-        public IBinder getLaunchCookie() {
+        public LaunchCookie getLaunchCookie() {
             getLaunchCookie_enforcePermission();
             return mLaunchCookie;
         }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 425659e..4da2cc9 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -10869,6 +10869,7 @@
             ArrayList<Notification.Action> smartActions = record.getSystemGeneratedSmartActions();
             ArrayList<CharSequence> smartReplies = record.getSmartReplies();
             if (redactSensitiveNotificationsFromUntrustedListeners()
+                    && info != null
                     && !mListeners.isUidTrusted(info.uid)
                     && mListeners.hasSensitiveContent(record)) {
                 smartActions = null;
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 923be56d..fb95632 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -840,9 +840,13 @@
                 && !PACKAGE_ANDROID.equals(ruleToRemove.pkg)) {
             String deletedKey = ZenModeConfig.deletedRuleKey(ruleToRemove);
             if (deletedKey != null) {
-                ruleToRemove.deletionInstant = Instant.now(mClock);
+                ZenRule deletedRule = ruleToRemove.copy();
+                deletedRule.deletionInstant = Instant.now(mClock);
+                // If the rule is restored it shouldn't be active (or snoozed).
+                deletedRule.snoozing = false;
+                deletedRule.condition = null;
                 // Overwrites a previously-deleted rule with the same conditionId, but that's okay.
-                config.deletedRules.put(deletedKey, ruleToRemove);
+                config.deletedRules.put(deletedKey, deletedRule);
             }
         }
     }
diff --git a/services/core/java/com/android/server/pm/AppsFilterBase.java b/services/core/java/com/android/server/pm/AppsFilterBase.java
index a5bc2c3..98b7c96 100644
--- a/services/core/java/com/android/server/pm/AppsFilterBase.java
+++ b/services/core/java/com/android/server/pm/AppsFilterBase.java
@@ -24,6 +24,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.content.pm.Flags;
 import android.content.pm.SigningDetails;
 import android.os.Binder;
 import android.os.Handler;
@@ -318,6 +319,11 @@
                 existingSettings.untrackedStorage());
     }
 
+    private static boolean isQueryableBySdkSandbox(int callingUid, int targetUid) {
+        return Flags.allowSdkSandboxQueryIntentActivities()
+                && targetUid == Process.getAppUidForSdkSandboxUid(callingUid);
+    }
+
     /**
      * See
      * {@link AppsFilterSnapshot#shouldFilterApplication(PackageDataSnapshot, int, Object,
@@ -338,9 +344,11 @@
             } else if (Process.isSdkSandboxUid(callingAppId)) {
                 final int targetAppId = targetPkgSetting.getAppId();
                 final int targetUid = UserHandle.getUid(userId, targetAppId);
-                // we only allow sdk sandbox processes access to forcequeryable packages
+                // we only allow sdk sandbox processes access to forcequeryable packages or
+                // if the target app is the sandbox's client app
                 return !isForceQueryable(targetPkgSetting.getAppId())
-                      && !isImplicitlyQueryable(callingUid, targetUid);
+                        && !isImplicitlyQueryable(callingUid, targetUid)
+                        && !isQueryableBySdkSandbox(callingUid, targetUid);
             }
             // use cache
             if (mCacheReady && mCacheEnabled) {
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index ada79ae..e70c5ea 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -1049,6 +1049,18 @@
                 } finally {
                     Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
                 }
+                if (Flags.improveInstallFreeze()) {
+                    // Postpone freezer until after reconcile
+                    for (ReconciledPackage reconciledPkg : reconciledPackages) {
+                        InstallRequest installRequest = reconciledPkg.mInstallRequest;
+                        String packageName = installRequest.getParsedPackage().getPackageName();
+                        PackageFreezer freezer = freezePackageForInstall(packageName,
+                                UserHandle.USER_ALL, installRequest.getInstallFlags(),
+                                "installPackageLI", ApplicationExitInfo.REASON_PACKAGE_UPDATED,
+                                installRequest);
+                        installRequest.setFreezer(freezer);
+                    }
+                }
                 try {
                     Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "commitPackages");
                     commitPackagesLocked(reconciledPackages, mPm.mUserManager.getUserIds());
@@ -1613,9 +1625,12 @@
             parsedPackage.setBaseApkPath(request.getApexInfo().modulePath);
         }
 
-        final PackageFreezer freezer =
-                freezePackageForInstall(pkgName, UserHandle.USER_ALL, installFlags,
-                        "installPackageLI", ApplicationExitInfo.REASON_PACKAGE_UPDATED, request);
+        PackageFreezer freezer = null;
+        if (!Flags.improveInstallFreeze()) {
+            freezer = freezePackageForInstall(pkgName, UserHandle.USER_ALL, installFlags,
+                    "installPackageLI", ApplicationExitInfo.REASON_PACKAGE_UPDATED, request);
+        }
+
 
         boolean shouldCloseFreezerBeforeReturn = true;
         try {
@@ -1865,9 +1880,11 @@
                     oldPackageState, parsedPackage, archivedPackage,
                     replace /* clearCodeCache */, sysPkg, ps, disabledPs);
         } finally {
-            request.setFreezer(freezer);
-            if (shouldCloseFreezerBeforeReturn) {
-                freezer.close();
+            if (freezer != null) {
+                request.setFreezer(freezer);
+                if (shouldCloseFreezerBeforeReturn) {
+                    freezer.close();
+                }
             }
         }
     }
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 135bd4f..9617098 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -7807,7 +7807,8 @@
             mResolveActivity.launchMode = ActivityInfo.LAUNCH_MULTIPLE;
             mResolveActivity.flags = ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS
                     | ActivityInfo.FLAG_FINISH_ON_CLOSE_SYSTEM_DIALOGS
-                    | ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES;
+                    | ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES
+                    | ActivityInfo.FLAG_HARDWARE_ACCELERATED;
             mResolveActivity.theme = 0;
             mResolveActivity.exported = true;
             mResolveActivity.enabled = true;
@@ -7841,7 +7842,8 @@
                 mResolveActivity.documentLaunchMode = ActivityInfo.DOCUMENT_LAUNCH_NEVER;
                 mResolveActivity.flags = ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS
                         | ActivityInfo.FLAG_RELINQUISH_TASK_IDENTITY
-                        | ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES;
+                        | ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES
+                        | ActivityInfo.FLAG_HARDWARE_ACCELERATED;
                 mResolveActivity.theme = R.style.Theme_Material_Dialog_Alert;
                 mResolveActivity.exported = true;
                 mResolveActivity.enabled = true;
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index 09b19e6..25e749f 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -138,6 +138,7 @@
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
 import com.android.net.module.util.NetworkCapabilitiesUtils;
+import com.android.server.power.optimization.Flags;
 import com.android.server.power.stats.SystemServerCpuThreadReader.SystemServiceCpuThreadTimes;
 
 import libcore.util.EmptyArray;
@@ -185,7 +186,8 @@
     // TODO: remove "tcp" from network methods, since we measure total stats.
 
     // Current on-disk Parcel version. Must be updated when the format of the parcelable changes
-    public static final int VERSION = 214;
+    public static final int VERSION =
+            !Flags.disableSystemServicePowerAttr() ? 214 : 215;
 
     // The maximum number of names wakelocks we will keep track of
     // per uid; once the limit is reached, we batch the remaining wakelocks
@@ -1753,7 +1755,9 @@
         mCpuUidActiveTimeReader = new KernelCpuUidActiveTimeReader(true, mClock);
         mCpuUidClusterTimeReader = new KernelCpuUidClusterTimeReader(true, mClock);
         mKernelWakelockReader = new KernelWakelockReader();
-        mSystemServerCpuThreadReader = SystemServerCpuThreadReader.create();
+        if (!Flags.disableSystemServicePowerAttr()) {
+            mSystemServerCpuThreadReader = SystemServerCpuThreadReader.create();
+        }
         mKernelMemoryBandwidthStats = new KernelMemoryBandwidthStats();
         mTmpRailStats = new RailStats();
     }
@@ -11459,7 +11463,7 @@
     @Override
     public BatteryStatsHistoryIterator iterateBatteryStatsHistory(long startTimeMs,
             long endTimeMs) {
-        return mHistory.copy().iterate(startTimeMs, endTimeMs);
+        return mHistory.iterate(startTimeMs, endTimeMs);
     }
 
     @Override
@@ -11702,7 +11706,9 @@
 
         EnergyConsumerStats.resetIfNotNull(mGlobalEnergyConsumerStats);
 
-        resetIfNotNull(mBinderThreadCpuTimesUs, false, elapsedRealtimeUs);
+        if (!Flags.disableSystemServicePowerAttr()) {
+            resetIfNotNull(mBinderThreadCpuTimesUs, false, elapsedRealtimeUs);
+        }
 
         mNumAllUidCpuTimeReads = 0;
         mNumUidsRemoved = 0;
@@ -13676,7 +13682,9 @@
                     mKernelCpuSpeedReaders[i].readDelta();
                 }
             }
-            mSystemServerCpuThreadReader.readDelta();
+            if (!Flags.disableSystemServicePowerAttr()) {
+                mSystemServerCpuThreadReader.readDelta();
+            }
             return;
         }
 
@@ -15696,23 +15704,25 @@
             }
         }
 
-        updateSystemServiceCallStats();
-        if (mBinderThreadCpuTimesUs != null) {
-            pw.println("Per UID System server binder time in ms:");
-            long[] systemServiceTimeAtCpuSpeeds = getSystemServiceTimeAtCpuSpeeds();
-            for (int i = 0; i < size; i++) {
-                int u = mUidStats.keyAt(i);
-                Uid uid = mUidStats.get(u);
-                double proportionalSystemServiceUsage = uid.getProportionalSystemServiceUsage();
-                long timeUs = 0;
-                for (int j = systemServiceTimeAtCpuSpeeds.length - 1; j >= 0; j--) {
-                    timeUs += systemServiceTimeAtCpuSpeeds[j] * proportionalSystemServiceUsage;
-                }
+        if (!Flags.disableSystemServicePowerAttr()) {
+            updateSystemServiceCallStats();
+            if (mBinderThreadCpuTimesUs != null) {
+                pw.println("Per UID System server binder time in ms:");
+                long[] systemServiceTimeAtCpuSpeeds = getSystemServiceTimeAtCpuSpeeds();
+                for (int i = 0; i < size; i++) {
+                    int u = mUidStats.keyAt(i);
+                    Uid uid = mUidStats.get(u);
+                    double proportionalSystemServiceUsage = uid.getProportionalSystemServiceUsage();
+                    long timeUs = 0;
+                    for (int j = systemServiceTimeAtCpuSpeeds.length - 1; j >= 0; j--) {
+                        timeUs += systemServiceTimeAtCpuSpeeds[j] * proportionalSystemServiceUsage;
+                    }
 
-                pw.print("  ");
-                pw.print(u);
-                pw.print(": ");
-                pw.println(timeUs / 1000);
+                    pw.print("  ");
+                    pw.print(u);
+                    pw.print(": ");
+                    pw.println(timeUs / 1000);
+                }
             }
         }
     }
@@ -16428,8 +16438,10 @@
             }
         }
 
-        mBinderThreadCpuTimesUs =
-                LongSamplingCounterArray.readSummaryFromParcelLocked(in, mOnBatteryTimeBase);
+        if (!Flags.disableSystemServicePowerAttr()) {
+            mBinderThreadCpuTimesUs =
+                    LongSamplingCounterArray.readSummaryFromParcelLocked(in, mOnBatteryTimeBase);
+        }
     }
 
     /**
@@ -16973,7 +16985,9 @@
             }
         }
 
-        LongSamplingCounterArray.writeSummaryToParcelLocked(out, mBinderThreadCpuTimesUs);
+        if (!Flags.disableSystemServicePowerAttr()) {
+            LongSamplingCounterArray.writeSummaryToParcelLocked(out, mBinderThreadCpuTimesUs);
+        }
     }
 
     @GuardedBy("this")
@@ -16985,7 +16999,9 @@
         // if we had originally pulled a time before the RTC was set.
         getStartClockTime();
 
-        updateSystemServiceCallStats();
+        if (!Flags.disableSystemServicePowerAttr()) {
+            updateSystemServiceCallStats();
+        }
     }
 
     @GuardedBy("this")
diff --git a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
index c3221e4..30b80ae 100644
--- a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
+++ b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
@@ -96,11 +96,13 @@
                 mPowerCalculators.add(new CustomEnergyConsumerPowerCalculator(mPowerProfile));
                 mPowerCalculators.add(new UserPowerCalculator());
 
-                // It is important that SystemServicePowerCalculator be applied last,
-                // because it re-attributes some of the power estimated by the other
-                // calculators.
-                mPowerCalculators.add(
-                        new SystemServicePowerCalculator(mCpuScalingPolicies, mPowerProfile));
+                if (!com.android.server.power.optimization.Flags.disableSystemServicePowerAttr()) {
+                    // It is important that SystemServicePowerCalculator be applied last,
+                    // because it re-attributes some of the power estimated by the other
+                    // calculators.
+                    mPowerCalculators.add(
+                            new SystemServicePowerCalculator(mCpuScalingPolicies, mPowerProfile));
+                }
             }
         }
         return mPowerCalculators;
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java b/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java
index cd3db36..ba4c127 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java
@@ -74,8 +74,7 @@
             boolean clockUpdateAdded = false;
             long baseTime = startTimeMs > 0 ? startTimeMs : UNINITIALIZED;
             long lastTime = 0;
-            try (BatteryStatsHistoryIterator iterator =
-                         mHistory.copy().iterate(startTimeMs, endTimeMs)) {
+            try (BatteryStatsHistoryIterator iterator = mHistory.iterate(startTimeMs, endTimeMs)) {
                 while (iterator.hasNext()) {
                     BatteryStats.HistoryItem item = iterator.next();
 
diff --git a/services/core/java/com/android/server/power/stats/flags.aconfig b/services/core/java/com/android/server/power/stats/flags.aconfig
index 0f13571..6546646 100644
--- a/services/core/java/com/android/server/power/stats/flags.aconfig
+++ b/services/core/java/com/android/server/power/stats/flags.aconfig
@@ -13,3 +13,11 @@
     description: "Feature flag for streamlined battery stats"
     bug: "285646152"
 }
+
+flag {
+    name: "disable_system_service_power_attr"
+    namespace: "backstage_power"
+    description: "Deprecation of system service power re-attribution"
+    bug: "311793616"
+    is_fixed_read_only: true
+}
diff --git a/services/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java b/services/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java
index 6677e7e..1526230 100644
--- a/services/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java
+++ b/services/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java
@@ -36,10 +36,12 @@
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
 import android.content.res.Resources;
+import android.location.flags.Flags;
 import android.os.Bundle;
 import android.os.Process;
 import android.os.UserHandle;
 import android.util.Log;
+import android.util.TypedValue;
 
 import com.android.internal.util.Preconditions;
 import com.android.server.FgThread;
@@ -70,6 +72,10 @@
     private static final String EXTRA_SERVICE_VERSION = "serviceVersion";
     private static final String EXTRA_SERVICE_IS_MULTIUSER = "serviceIsMultiuser";
 
+    // a package value that will never match against any package (we can't use null since this will
+    // match against any package).
+    private static final String NO_MATCH_PACKAGE = "";
+
     private static final Comparator<BoundServiceInfo> sBoundServiceInfoComparator = (o1, o2) -> {
         if (o1 == o2) {
             return 0;
@@ -196,7 +202,19 @@
         Resources resources = context.getResources();
         boolean enableOverlay = resources.getBoolean(enableOverlayResId);
         if (!enableOverlay) {
-            return resources.getString(nonOverlayPackageResId);
+            if (Flags.fixServiceWatcher()) {
+                // we don't use getText() or similar because it won't return null values
+                TypedValue out = new TypedValue();
+                resources.getValue(nonOverlayPackageResId, out, true);
+                CharSequence explicitPackage = out.coerceToString();
+                if (explicitPackage == null) {
+                    return NO_MATCH_PACKAGE;
+                } else {
+                    return explicitPackage.toString();
+                }
+            } else {
+                return resources.getString(nonOverlayPackageResId);
+            }
         } else {
             return null;
         }
@@ -233,6 +251,10 @@
 
     @Override
     public boolean hasMatchingService() {
+        if (Flags.fixServiceWatcher() && NO_MATCH_PACKAGE.equals(mIntent.getPackage())) {
+            return false;
+        }
+
         int intentQueryFlags = MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE;
         if (mMatchSystemAppsOnly) {
             intentQueryFlags |= MATCH_SYSTEM_ONLY;
@@ -268,6 +290,10 @@
 
     @Override
     public BoundServiceInfo getServiceInfo() {
+        if (Flags.fixServiceWatcher() && NO_MATCH_PACKAGE.equals(mIntent.getPackage())) {
+            return null;
+        }
+
         BoundServiceInfo bestServiceInfo = null;
 
         // only allow services in the correct direct boot state to match
diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
index 285bcc3..0ffd002 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -2058,7 +2058,8 @@
     }
 
     private void registerCpuCyclesPerThreadGroupCluster() {
-        if (KernelCpuBpfTracking.isSupported()) {
+        if (KernelCpuBpfTracking.isSupported()
+                && !com.android.server.power.optimization.Flags.disableSystemServicePowerAttr()) {
             int tagId = FrameworkStatsLog.CPU_CYCLES_PER_THREAD_GROUP_CLUSTER;
             PullAtomMetadata metadata = new PullAtomMetadata.Builder()
                     .setAdditiveFields(new int[]{3, 4})
@@ -2073,6 +2074,10 @@
     }
 
     int pullCpuCyclesPerThreadGroupCluster(int atomTag, List<StatsEvent> pulledData) {
+        if (com.android.server.power.optimization.Flags.disableSystemServicePowerAttr()) {
+            return StatsManager.PULL_SKIP;
+        }
+
         SystemServiceCpuThreadTimes times = LocalServices.getService(BatteryStatsInternal.class)
                 .getSystemServiceCpuThreadTimes();
         if (times == null) {
diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java
index e5a8a6d..dbf777f 100644
--- a/services/core/java/com/android/server/trust/TrustManagerService.java
+++ b/services/core/java/com/android/server/trust/TrustManagerService.java
@@ -83,7 +83,6 @@
 import com.android.internal.util.DumpUtils;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.server.SystemService;
-import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -160,7 +159,6 @@
     private final ActivityManager mActivityManager;
     private FingerprintManager mFingerprintManager;
     private FaceManager mFaceManager;
-    private VirtualDeviceManagerInternal mVirtualDeviceManager;
 
     private enum TrustState {
         // UNTRUSTED means that TrustManagerService is currently *not* giving permission for the
diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
index b6d0ca1..eacd3f8 100644
--- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
+++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
@@ -4152,6 +4152,25 @@
         }
 
         @Override
+        public void onRequestSigning2(String id, String algorithm, String host,
+                int port, byte[] data) {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slogf.d(TAG, "onRequestSigning");
+                }
+                if (mSessionState.mSession == null || mSessionState.mClient == null) {
+                    return;
+                }
+                try {
+                    mSessionState.mClient.onRequestSigning2(
+                            id, algorithm, host, port, data, mSessionState.mSeq);
+                } catch (RemoteException e) {
+                    Slogf.e(TAG, "error in onRequestSigning", e);
+                }
+            }
+        }
+
+        @Override
         public void onRequestCertificate(String host, int port) {
             synchronized (mLock) {
                 if (DEBUG) {
diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
index 0f9e5b0..7052982 100644
--- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
+++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
@@ -183,9 +183,6 @@
                         getCurrentDisplayChange(fromRotation, startBounds);
                 mDisplayContent.mTransitionController.requestStartTransition(transition,
                         /* startTask= */ null, /* remoteTransition= */ null, displayChange);
-                mDisplayContent.mTransitionController.setDisplaySyncMethod(displayChange,
-                        mDisplayContent);
-                transition.setAllReady();
             }
         });
     }
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 5dcd335..82dbf8d 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -1634,12 +1634,19 @@
         if (configChanged) {
             mWaitingForConfig = true;
             if (mTransitionController.isShellTransitionsEnabled()) {
-                final TransitionRequestInfo.DisplayChange change =
-                        mTransitionController.isCollecting()
+                final Rect startBounds = currentDisplayConfig.windowConfiguration.getBounds();
+                final Rect endBounds = mTmpConfiguration.windowConfiguration.getBounds();
+                final Transition transition = mTransitionController.getCollectingTransition();
+                final TransitionRequestInfo.DisplayChange change = transition != null
                                 ? null : new TransitionRequestInfo.DisplayChange(mDisplayId);
                 if (change != null) {
-                    change.setStartAbsBounds(currentDisplayConfig.windowConfiguration.getBounds());
-                    change.setEndAbsBounds(mTmpConfiguration.windowConfiguration.getBounds());
+                    change.setStartAbsBounds(startBounds);
+                    change.setEndAbsBounds(endBounds);
+                } else {
+                    transition.setKnownConfigChanges(this, changes);
+                    // A collecting transition is existed. The sync method must be set before
+                    // collecting this display, so WindowState#prepareSync can use the sync method.
+                    mTransitionController.setDisplaySyncMethod(startBounds, endBounds, this);
                 }
                 requestChangeTransitionIfNeeded(changes, change);
             } else if (mLastHasContent) {
diff --git a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java
index 5f488b7..bdb4588 100644
--- a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java
+++ b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java
@@ -97,7 +97,7 @@
             mRecordedWC = (WindowContainer) mWms.mRoot.getDefaultDisplay();
         } else {
             mRecordedWC = mWms.mRoot.getActivity(activity -> activity.mLaunchCookie
-                    == mediaProjectionInfo.getLaunchCookie()).getTask();
+                    == mediaProjectionInfo.getLaunchCookie().binder).getTask();
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java
index 4e7a9bd..f4e9957 100644
--- a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java
@@ -303,8 +303,15 @@
         } else {
             if (DEBUG) appendLog("non-freeform-task-display-area");
         }
+        final boolean isUpdatingExistingTaskWindowingMode = task != null
+                && task.getRequestedOverrideWindowingMode() != WINDOWING_MODE_UNDEFINED
+                && launchMode != task.getRequestedOverrideWindowingMode();
+        if (DEBUG && isUpdatingExistingTaskWindowingMode) {
+            appendLog("updating-existing-task-windowing-mode");
+        }
         // If launch mode matches display windowing mode, let it inherit from display.
         outParams.mWindowingMode = launchMode == suggestedDisplayArea.getWindowingMode()
+                && !isUpdatingExistingTaskWindowingMode
                 ? WINDOWING_MODE_UNDEFINED : launchMode;
 
         if (phase == PHASE_WINDOWING_MODE) {
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index d7b4a39..25b5630f 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -640,11 +640,16 @@
     }
 
     /** Sets the sync method for the display change. */
-    void setDisplaySyncMethod(@NonNull TransitionRequestInfo.DisplayChange displayChange,
+    private void setDisplaySyncMethod(@NonNull TransitionRequestInfo.DisplayChange displayChange,
             @NonNull DisplayContent displayContent) {
         final Rect startBounds = displayChange.getStartAbsBounds();
         final Rect endBounds = displayChange.getEndAbsBounds();
         if (startBounds == null || endBounds == null) return;
+        setDisplaySyncMethod(startBounds, endBounds, displayContent);
+    }
+
+    void setDisplaySyncMethod(@NonNull Rect startBounds, @NonNull Rect endBounds,
+            @NonNull DisplayContent displayContent) {
         final int startWidth = startBounds.width();
         final int startHeight = startBounds.height();
         final int endWidth = endBounds.width();
diff --git a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
index 7b08413..4403bce 100644
--- a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
+++ b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
@@ -571,8 +571,8 @@
 }
 
 static jboolean com_android_server_am_CachedAppOptimizer_isFreezerProfileValid(JNIEnv* env) {
-    int uid = getuid();
-    int pid = getpid();
+    uid_t uid = getuid();
+    pid_t pid = getpid();
 
     return isProfileValidForProcess("Frozen", uid, pid) &&
             isProfileValidForProcess("Unfrozen", uid, pid);
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
index 667e086..281fb1c 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
@@ -27,6 +27,7 @@
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.content.ComponentName;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
@@ -201,7 +202,7 @@
     @SuppressWarnings("GuardedBy") // ErrorProne requires service.mLock which is the same
     // this.mLock
     protected void handlePackageRemovedMultiModeLocked(String packageName, int userId) {
-        updateProvidersWhenPackageRemoved(mContext, packageName);
+        updateProvidersWhenPackageRemoved(new SettingsWrapper(mContext), packageName);
 
         List<CredentialManagerServiceImpl> services = peekServiceListForUserLocked(userId);
         if (services == null) {
@@ -1134,13 +1135,14 @@
     }
 
     /** Updates the list of providers when an app is uninstalled. */
-    public static void updateProvidersWhenPackageRemoved(Context context, String packageName) {
+    public static void updateProvidersWhenPackageRemoved(
+            SettingsWrapper settingsWrapper, String packageName) {
+        Slog.i(TAG, "updateProvidersWhenPackageRemoved");
+
         // Get the current providers.
         String rawProviders =
-                Settings.Secure.getStringForUser(
-                    context.getContentResolver(),
-                    Settings.Secure.CREDENTIAL_SERVICE_PRIMARY,
-                    UserHandle.myUserId());
+                settingsWrapper.getStringForUser(
+                        Settings.Secure.CREDENTIAL_SERVICE_PRIMARY, UserHandle.myUserId());
         if (rawProviders == null) {
             Slog.w(TAG, "settings key is null");
             return;
@@ -1148,44 +1150,44 @@
 
         // Remove any providers from the primary setting that contain the package name
         // being removed.
-        Set<String> primaryProviders =
-                getStoredProviders(rawProviders, packageName);
-        if (!Settings.Secure.putString(
-                context.getContentResolver(),
+        Set<String> primaryProviders = getStoredProviders(rawProviders, packageName);
+        if (!settingsWrapper.putStringForUser(
                 Settings.Secure.CREDENTIAL_SERVICE_PRIMARY,
-                String.join(":", primaryProviders))) {
-            Slog.w(TAG, "Failed to remove primary package: " + packageName);
+                String.join(":", primaryProviders),
+                UserHandle.myUserId(),
+                /* overrideableByRestore= */ true)) {
+            Slog.e(TAG, "Failed to remove primary package: " + packageName);
             return;
         }
 
         // Read the autofill provider so we don't accidentally erase it.
         String autofillProvider =
-                Settings.Secure.getStringForUser(
-                    context.getContentResolver(),
-                    Settings.Secure.AUTOFILL_SERVICE,
-                    UserHandle.myUserId());
+                settingsWrapper.getStringForUser(
+                        Settings.Secure.AUTOFILL_SERVICE, UserHandle.myUserId());
 
         // If there is an autofill provider and it is the placeholder indicating
         // that the currently selected primary provider does not support autofill
         // then we should wipe the setting to keep it in sync.
         if (autofillProvider != null && primaryProviders.isEmpty()) {
             if (autofillProvider.equals(AUTOFILL_PLACEHOLDER_VALUE)) {
-                if (!Settings.Secure.putString(
-                        context.getContentResolver(),
+                if (!settingsWrapper.putStringForUser(
                         Settings.Secure.AUTOFILL_SERVICE,
-                        "")) {
-                    Slog.w(TAG, "Failed to remove autofill package: " + packageName);
+                        "",
+                        UserHandle.myUserId(),
+                        /* overrideableByRestore= */ true)) {
+                    Slog.e(TAG, "Failed to remove autofill package: " + packageName);
                 }
             } else {
                 // If the existing autofill provider is from the app being removed
                 // then erase the autofill service setting.
                 ComponentName cn = ComponentName.unflattenFromString(autofillProvider);
                 if (cn != null && cn.getPackageName().equals(packageName)) {
-                   if (!Settings.Secure.putString(
-                            context.getContentResolver(),
+                    if (!settingsWrapper.putStringForUser(
                             Settings.Secure.AUTOFILL_SERVICE,
-                            "")) {
-                        Slog.w(TAG, "Failed to remove autofill package: " + packageName);
+                            "",
+                            UserHandle.myUserId(),
+                            /* overrideableByRestore= */ true)) {
+                        Slog.e(TAG, "Failed to remove autofill package: " + packageName);
                     }
                 }
             }
@@ -1193,19 +1195,17 @@
 
         // Read the credential providers to remove any reference of the removed app.
         String rawCredentialProviders =
-                Settings.Secure.getStringForUser(
-                    context.getContentResolver(),
-                    Settings.Secure.CREDENTIAL_SERVICE,
-                    UserHandle.myUserId());
+                settingsWrapper.getStringForUser(
+                        Settings.Secure.CREDENTIAL_SERVICE, UserHandle.myUserId());
 
         // Remove any providers that belong to the removed app.
-        Set<String> credentialProviders =
-                getStoredProviders(rawCredentialProviders, packageName);
-        if (!Settings.Secure.putString(
-                context.getContentResolver(),
+        Set<String> credentialProviders = getStoredProviders(rawCredentialProviders, packageName);
+        if (!settingsWrapper.putStringForUser(
                 Settings.Secure.CREDENTIAL_SERVICE,
-                String.join(":", credentialProviders))) {
-            Slog.w(TAG, "Failed to remove secondary package: " + packageName);
+                String.join(":", credentialProviders),
+                UserHandle.myUserId(),
+                /* overrideableByRestore= */ true)) {
+            Slog.e(TAG, "Failed to remove secondary package: " + packageName);
         }
     }
 
@@ -1232,4 +1232,38 @@
 
         return providers;
     }
+
+    /** A wrapper class that can be used by tests for intercepting reads/writes. */
+    public static class SettingsWrapper {
+        private final Context mContext;
+
+        public SettingsWrapper(@NonNull Context context) {
+            this.mContext = context;
+        }
+
+        ContentResolver getContentResolver() {
+            return mContext.getContentResolver();
+        }
+
+        /** Retrieves the string value of a system setting */
+        public String getStringForUser(String name, int userHandle) {
+            return Settings.Secure.getStringForUser(getContentResolver(), name, userHandle);
+        }
+
+        /** Updates the string value of a system setting */
+        public boolean putStringForUser(
+                String name,
+                String value,
+                int userHandle,
+                boolean overrideableByRestore) {
+            return Settings.Secure.putStringForUser(
+                    getContentResolver(),
+                    name,
+                    value,
+                    null,
+                    false,
+                    userHandle,
+                    overrideableByRestore);
+        }
+    }
 }
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
index 4c487a7..ba72977 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
@@ -367,8 +367,11 @@
         // TODO(b/312397262): consider virtual displays cases
         synchronized (mLock) {
             if (mIsDualDisplayBlockingEnabled
-                    && !mExternalDisplaysConnected.get(displayId, false)
-                    && mDisplayManager.getDisplay(displayId).getType() == TYPE_EXTERNAL) {
+                    && !mExternalDisplaysConnected.get(displayId, false)) {
+                var display = mDisplayManager.getDisplay(displayId);
+                if (display == null || display.getType() != TYPE_EXTERNAL) {
+                    return;
+                }
                 mExternalDisplaysConnected.put(displayId, true);
 
                 // Only update the supported state when going from 0 external display to 1
diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
index ddf4a08..04cebab 100644
--- a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
+++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
@@ -591,6 +591,20 @@
     }
 
     @Test
+    public void testOnDisplayAddedWithNullDisplayDoesNotThrowNPE() {
+        createProvider(
+                createConfig(
+                        /* identifier= */ 1, /* name= */ "ONE",
+                        /* flags= */0, (c) -> true,
+                        FoldableDeviceStateProvider::hasNoConnectedExternalDisplay)
+        );
+
+        when(mDisplayManager.getDisplay(1)).thenReturn(null);
+        // This call should not throw NPE.
+        mProvider.onDisplayAdded(1);
+    }
+
+    @Test
     public void hasNoConnectedDisplay_afterExternalDisplayAddedAndRemoved_returnsTrue() {
         createProvider(
                 createConfig(
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java
index b9f1ea0..dc9631a 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java
@@ -116,7 +116,7 @@
                     ANY_CALLER_PID);
 
             verify(invoker.asBinder()).linkToDeath(any(IBinder.DeathRecipient.class), eq(0));
-            assertThat(mController.mClients).containsEntry(invoker.asBinder(), added);
+            assertThat(mController.getClient(invoker.asBinder())).isSameInstanceAs(added);
         }
     }
 
@@ -133,7 +133,7 @@
             var invoker = IInputMethodClientInvoker.create(mClient, mHandler);
             added = mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID,
                     ANY_CALLER_PID);
-            assertThat(mController.mClients).containsEntry(invoker.asBinder(), added);
+            assertThat(mController.getClient(invoker.asBinder())).isSameInstanceAs(added);
             assertThat(mController.removeClient(mClient)).isTrue();
         }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index 02e3ef4..75febd9 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -61,6 +61,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.ActivityOptions.LaunchCookie;
 import android.app.PropertyInvalidatedCache;
 import android.companion.virtual.IVirtualDevice;
 import android.companion.virtual.IVirtualDeviceManager;
@@ -1557,7 +1558,7 @@
         when(mMockProjectionService
                 .setContentRecordingSession(any(ContentRecordingSession.class), eq(projection)))
                 .thenReturn(true);
-        doReturn(mock(IBinder.class)).when(projection).getLaunchCookie();
+        doReturn(new LaunchCookie()).when(projection).getLaunchCookie();
         doReturn(true).when(mMockProjectionService).isCurrentProjection(eq(projection));
 
         final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
index c556aca..64076e6 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
@@ -1727,6 +1727,7 @@
 
         var desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID_2);
         assertThat(desiredSpecs.primary.render.max).isEqualTo(expectedMaxRenderFrameRate);
+        assertThat(desiredSpecs.appRequest.render.max).isEqualTo(expectedMaxRenderFrameRate);
     }
 
     @Test
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
index bb70080..9251376 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
@@ -21,6 +21,9 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
@@ -96,18 +99,11 @@
         mClock.realtime = 123;
 
         mHistory = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 32, 1024,
-                mStepDetailsCalculator, mClock, mMonotonicClock, mTracer) {
-            @Override
-            public boolean readFileToParcel(Parcel out, AtomicFile file) {
-                mReadFiles.add(file.getBaseFile().getName());
-                return super.readFileToParcel(out, file);
-            }
-        };
+                mStepDetailsCalculator, mClock, mMonotonicClock, mTracer);
 
         when(mStepDetailsCalculator.getHistoryStepDetails())
                 .thenReturn(new BatteryStats.HistoryStepDetails());
 
-
         mHistoryPrinter = new BatteryStats.HistoryPrinter();
     }
 
@@ -276,6 +272,15 @@
 
         mReadFiles.clear();
 
+        // Make an immutable copy and spy on it
+        mHistory = spy(mHistory.copy());
+
+        doAnswer(invocation -> {
+            AtomicFile file = invocation.getArgument(1);
+            mReadFiles.add(file.getBaseFile().getName());
+            return invocation.callRealMethod();
+        }).when(mHistory).readFileToParcel(any(), any());
+
         // Prepare history for iteration
         mHistory.iterate(0, MonotonicClock.UNDEFINED);
 
@@ -309,6 +314,15 @@
 
         mReadFiles.clear();
 
+        // Make an immutable copy and spy on it
+        mHistory = spy(mHistory.copy());
+
+        doAnswer(invocation -> {
+            AtomicFile file = invocation.getArgument(1);
+            mReadFiles.add(file.getBaseFile().getName());
+            return invocation.callRealMethod();
+        }).when(mHistory).readFileToParcel(any(), any());
+
         // Prepare history for iteration
         mHistory.iterate(1000, 3000);
 
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/SystemServicePowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/SystemServicePowerCalculatorTest.java
index 4dae2d5..8e53d52 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/SystemServicePowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/SystemServicePowerCalculatorTest.java
@@ -28,6 +28,9 @@
 import android.os.BatteryConsumer;
 import android.os.Binder;
 import android.os.Process;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -38,6 +41,7 @@
 import com.android.internal.os.KernelSingleUidTimeReader;
 import com.android.internal.os.PowerProfile;
 import com.android.internal.power.EnergyConsumerStats;
+import com.android.server.power.optimization.Flags;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -54,6 +58,8 @@
 @RunWith(AndroidJUnit4.class)
 @SuppressWarnings("GuardedBy")
 public class SystemServicePowerCalculatorTest {
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
 
     private static final double PRECISION = 0.000001;
     private static final int APP_UID1 = 100;
@@ -108,6 +114,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_DISABLE_SYSTEM_SERVICE_POWER_ATTR)
     public void testPowerProfileBasedModel() {
         prepareBatteryStats(null);
 
@@ -135,6 +142,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_DISABLE_SYSTEM_SERVICE_POWER_ATTR)
     public void testMeasuredEnergyBasedModel() {
         final boolean[] supportedPowerBuckets =
                 new boolean[EnergyConsumerStats.NUMBER_STANDARD_POWER_BUCKETS];
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 8958fac..e22d99d 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -36,6 +36,7 @@
         "-Werror",
     ],
     static_libs: [
+        "cts-input-lib",
         "frameworks-base-testutils",
         "services.accessibility",
         "services.appwidget",
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterInputTest.kt b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterInputTest.kt
new file mode 100644
index 0000000..52c7d8d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterInputTest.kt
@@ -0,0 +1,280 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.accessibility
+
+import android.hardware.display.DisplayManagerGlobal
+import android.os.SystemClock
+import android.view.Display
+import android.view.Display.DEFAULT_DISPLAY
+import android.view.DisplayAdjustments
+import android.view.DisplayInfo
+import android.view.IInputFilterHost
+import android.view.InputDevice.SOURCE_TOUCHSCREEN
+import android.view.InputEvent
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.ACTION_UP
+import android.view.MotionEvent.ACTION_HOVER_ENTER
+import android.view.MotionEvent.ACTION_HOVER_EXIT
+import android.view.MotionEvent.ACTION_HOVER_MOVE
+import android.view.WindowManagerPolicyConstants.FLAG_PASS_TO_USER
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.cts.input.inputeventmatchers.withDeviceId
+import com.android.cts.input.inputeventmatchers.withMotionAction
+import com.android.server.LocalServices
+import com.android.server.accessibility.magnification.MagnificationProcessor
+import com.android.server.wm.WindowManagerInternal
+import java.util.concurrent.LinkedBlockingQueue
+import org.hamcrest.Matchers.allOf
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.stubbing.OngoingStubbing
+
+
+/**
+ * Create a MotionEvent with the provided action, eventTime, and source
+ */
+fun createMotionEvent(action: Int, downTime: Long, eventTime: Long, source: Int, deviceId: Int):
+        MotionEvent {
+    val x = 1f
+    val y = 2f
+    val pressure = 3f
+    val size = 1f
+    val metaState = 0
+    val xPrecision = 0f
+    val yPrecision = 0f
+    val edgeFlags = 0
+    val displayId = 0
+    return MotionEvent.obtain(downTime, eventTime, action, x, y, pressure, size, metaState,
+        xPrecision, yPrecision, deviceId, edgeFlags, source, displayId)
+}
+
+/**
+ * Tests for AccessibilityInputFilter, focusing on the input event processing as seen by the callers
+ * of the InputFilter interface.
+ * The main interaction with AccessibilityInputFilter in these tests is with the filterInputEvent
+ * and sendInputEvent APIs of InputFilter.
+ */
+@RunWith(AndroidJUnit4::class)
+class AccessibilityInputFilterInputTest {
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+
+    private companion object{
+        const val ALL_A11Y_FEATURES = (AccessibilityInputFilter.FLAG_FEATURE_AUTOCLICK
+                or AccessibilityInputFilter.FLAG_FEATURE_TOUCH_EXPLORATION
+                or AccessibilityInputFilter.FLAG_FEATURE_CONTROL_SCREEN_MAGNIFIER
+                or AccessibilityInputFilter.FLAG_FEATURE_TRIGGERED_SCREEN_MAGNIFIER
+                or AccessibilityInputFilter.FLAG_FEATURE_INJECT_MOTION_EVENTS
+                or AccessibilityInputFilter.FLAG_FEATURE_FILTER_KEY_EVENTS)
+    }
+
+    @Rule
+    @JvmField
+    val mocks: MockitoRule = MockitoJUnit.rule()
+
+    @Mock
+    private lateinit var mockA11yController: WindowManagerInternal.AccessibilityControllerInternal
+
+    @Mock
+    private lateinit var mockWindowManagerService: WindowManagerInternal
+
+    @Mock
+    private lateinit var mockMagnificationProcessor: MagnificationProcessor
+
+    private val inputEvents = LinkedBlockingQueue<InputEvent>()
+    private val verifier = BlockingQueueEventVerifier(inputEvents)
+
+    @Mock
+    private lateinit var host: IInputFilterHost
+    private lateinit var ams: AccessibilityManagerService
+    private lateinit var a11yInputFilter: AccessibilityInputFilter
+    private val touchDeviceId = 1
+
+    @Before
+    fun setUp() {
+        val context = instrumentation.context
+        LocalServices.removeServiceForTest(WindowManagerInternal::class.java)
+        LocalServices.addService(WindowManagerInternal::class.java, mockWindowManagerService)
+
+        whenever(mockA11yController.isAccessibilityTracingEnabled).thenReturn(false)
+        whenever(
+            mockWindowManagerService.accessibilityController).thenReturn(
+            mockA11yController)
+
+        ams = Mockito.spy(AccessibilityManagerService(context))
+        val displayList = arrayListOf(createStubDisplay(DEFAULT_DISPLAY, DisplayInfo()))
+        whenever(ams.validDisplayList).thenReturn(displayList)
+        whenever(ams.magnificationProcessor).thenReturn(mockMagnificationProcessor)
+
+        doAnswer {
+            val event = it.getArgument(0) as MotionEvent
+            inputEvents.add(MotionEvent.obtain(event))
+        }.`when`(host).sendInputEvent(any(), anyInt())
+
+        a11yInputFilter = AccessibilityInputFilter(context, ams)
+        a11yInputFilter.install(host)
+    }
+
+    @After
+    fun tearDown() {
+        if (this::a11yInputFilter.isInitialized) {
+            a11yInputFilter.uninstall()
+        }
+    }
+
+    /**
+     * When no features are enabled, the events pass through the filter without getting modified.
+     */
+    @Test
+    fun testSingleDeviceTouchEventsWithoutA11yFeatures() {
+        enableFeatures(0)
+
+        val downTime = SystemClock.uptimeMillis()
+        val downEvent = createMotionEvent(
+            ACTION_DOWN, downTime, downTime, SOURCE_TOUCHSCREEN, touchDeviceId)
+        send(downEvent)
+        verifier.assertReceivedMotion(
+            allOf(withMotionAction(ACTION_DOWN), withDeviceId(touchDeviceId)))
+
+        val moveEvent = createMotionEvent(
+            ACTION_MOVE, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId)
+        send(moveEvent)
+        verifier.assertReceivedMotion(
+            allOf(withMotionAction(ACTION_MOVE), withDeviceId(touchDeviceId)))
+
+        val upEvent = createMotionEvent(
+            ACTION_UP, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId)
+        send(upEvent)
+        verifier.assertReceivedMotion(
+            allOf(withMotionAction(ACTION_UP), withDeviceId(touchDeviceId)))
+
+        verifier.assertNoEvents()
+    }
+
+    /**
+     * Enable all a11y features and send a touchscreen stream of DOWN -> MOVE -> UP events.
+     * These get converted into HOVER_ENTER -> HOVER_MOVE -> HOVER_EXIT events by the input filter.
+     */
+    @Test
+    fun testSingleDeviceTouchEventsWithAllA11yFeatures() {
+        enableFeatures(ALL_A11Y_FEATURES)
+
+        val downTime = SystemClock.uptimeMillis()
+        val downEvent = createMotionEvent(
+            ACTION_DOWN, downTime, downTime, SOURCE_TOUCHSCREEN, touchDeviceId)
+        send(MotionEvent.obtain(downEvent))
+
+        // DOWN event gets transformed to HOVER_ENTER
+        verifier.assertReceivedMotion(
+            allOf(withMotionAction(ACTION_HOVER_ENTER), withDeviceId(touchDeviceId)))
+
+        // MOVE becomes HOVER_MOVE
+        val moveEvent = createMotionEvent(
+            ACTION_MOVE, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId)
+        send(moveEvent)
+        verifier.assertReceivedMotion(
+            allOf(withMotionAction(ACTION_HOVER_MOVE), withDeviceId(touchDeviceId)))
+
+        // UP becomes HOVER_EXIT
+        val upEvent = createMotionEvent(
+            ACTION_UP, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId)
+        send(upEvent)
+
+        verifier.assertReceivedMotion(
+            allOf(withMotionAction(ACTION_HOVER_EXIT), withDeviceId(touchDeviceId)))
+
+        verifier.assertNoEvents()
+    }
+
+    /**
+     * Enable all a11y features and send a touchscreen event stream. In the middle of the gesture,
+     * disable the a11y features.
+     * When the a11y features are disabled, the filter generates HOVER_EXIT without further input
+     * from the dispatcher.
+     */
+    @Test
+    fun testSingleDeviceTouchEventsDisableFeaturesMidGesture() {
+        enableFeatures(ALL_A11Y_FEATURES)
+
+        val downTime = SystemClock.uptimeMillis()
+        val downEvent = createMotionEvent(
+            ACTION_DOWN, downTime, downTime, SOURCE_TOUCHSCREEN, touchDeviceId)
+        send(MotionEvent.obtain(downEvent))
+
+        // DOWN event gets transformed to HOVER_ENTER
+        verifier.assertReceivedMotion(
+            allOf(withMotionAction(ACTION_HOVER_ENTER), withDeviceId(touchDeviceId)))
+        verifier.assertNoEvents()
+
+        enableFeatures(0)
+        verifier.assertReceivedMotion(
+            allOf(withMotionAction(ACTION_HOVER_EXIT), withDeviceId(touchDeviceId)))
+        verifier.assertNoEvents()
+
+        val moveEvent = createMotionEvent(
+            ACTION_MOVE, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId)
+        send(moveEvent)
+        val upEvent = createMotionEvent(
+            ACTION_UP, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId)
+        send(upEvent)
+        // As the original gesture continues, no additional events should be getting sent by the
+        // filter because the HOVER_EXIT above already effectively finished the current gesture and
+        // the DOWN event was never sent to the host.
+
+        // Bug: the down event was swallowed, so the remainder of the gesture should be swallowed
+        // too. However, the MOVE and UP events are currently passed back to the dispatcher.
+        // TODO(b/310014874) - ensure a11y sends consistent input streams to the dispatcher
+        verifier.assertReceivedMotion(
+            allOf(withMotionAction(ACTION_MOVE), withDeviceId(touchDeviceId)))
+        verifier.assertReceivedMotion(
+            allOf(withMotionAction(ACTION_UP), withDeviceId(touchDeviceId)))
+
+        verifier.assertNoEvents()
+    }
+
+    private fun createStubDisplay(displayId: Int, displayInfo: DisplayInfo): Display {
+        val display = Display(DisplayManagerGlobal.getInstance(), displayId,
+            displayInfo, DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS)
+        return display
+    }
+
+    private fun send(event: InputEvent) {
+        // We need to make a copy of the event before sending it to the filter, because the filter
+        // will recycle it, but the caller of this function might want to still be able to use
+        // this event for subsequent checks
+        val eventCopy = if (event is MotionEvent) MotionEvent.obtain(event) else event
+        a11yInputFilter.filterInputEvent(eventCopy, FLAG_PASS_TO_USER)
+    }
+
+    private fun enableFeatures(features: Int) {
+        instrumentation.runOnMainSync { a11yInputFilter.setUserAndEnabledFeatures(0, features) }
+    }
+}
+
+private fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall)
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/BlockingQueueEventVerifier.kt b/services/tests/servicestests/src/com/android/server/accessibility/BlockingQueueEventVerifier.kt
new file mode 100644
index 0000000..b12f537
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/BlockingQueueEventVerifier.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.accessibility
+
+import android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS
+import android.view.InputEvent
+import android.view.MotionEvent
+import java.time.Duration
+import java.util.concurrent.BlockingQueue
+import java.util.concurrent.TimeUnit
+import org.junit.Assert.fail
+
+import org.hamcrest.Matcher
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Assert.assertNull
+
+private fun <T> getEvent(queue: BlockingQueue<T>, timeout: Duration): T? {
+    return queue.poll(timeout.toMillis(), TimeUnit.MILLISECONDS)
+}
+
+class BlockingQueueEventVerifier(val queue: BlockingQueue<InputEvent>) {
+    fun assertReceivedMotion(matcher: Matcher<MotionEvent>) {
+        val event = getMotionEvent()
+        assertThat("MotionEvent checks", event, matcher)
+    }
+
+    fun assertNoEvents() {
+        val event = getEvent(queue, Duration.ofMillis(50))
+        assertNull(event)
+    }
+
+    private fun getMotionEvent(): MotionEvent {
+        val event = getEvent(queue, Duration.ofMillis(DEFAULT_DISPATCHING_TIMEOUT_MILLIS.toLong()))
+        if (event == null) {
+            fail("Did not get an event")
+        }
+        if (event is MotionEvent) {
+            return event
+        }
+        fail("Instead of motion, got $event")
+        throw RuntimeException("should not reach here")
+    }
+}
+
diff --git a/services/tests/servicestests/src/com/android/server/credentials/CredentialManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/credentials/CredentialManagerServiceTest.java
index d850c73..57f3cc0 100644
--- a/services/tests/servicestests/src/com/android/server/credentials/CredentialManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/credentials/CredentialManagerServiceTest.java
@@ -22,6 +22,7 @@
 import android.os.UserHandle;
 import android.provider.Settings;
 
+import androidx.annotation.NonNull;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -40,10 +41,12 @@
 public final class CredentialManagerServiceTest {
 
     Context mContext = null;
+    MockSettingsWrapper mSettingsWrapper = null;
 
     @Before
     public void setUp() throws CertificateException {
         mContext = ApplicationProvider.getApplicationContext();
+        mSettingsWrapper = new MockSettingsWrapper(mContext);
     }
 
     @Test
@@ -81,7 +84,8 @@
                 Settings.Secure.CREDENTIAL_SERVICE_PRIMARY,
                 "com.example.test/com.example.test.TestActivity");
 
-        CredentialManagerService.updateProvidersWhenPackageRemoved(mContext, "com.example.test");
+        CredentialManagerService.updateProvidersWhenPackageRemoved(
+                mSettingsWrapper, "com.example.test");
 
         assertThat(getSettingsKey(Settings.Secure.AUTOFILL_SERVICE)).isEqualTo("");
         assertThat(getSettingsKey(Settings.Secure.CREDENTIAL_SERVICE))
@@ -101,7 +105,8 @@
         setSettingsKey(Settings.Secure.CREDENTIAL_SERVICE, testCredentialValue);
         setSettingsKey(Settings.Secure.CREDENTIAL_SERVICE_PRIMARY, testCredentialPrimaryValue);
 
-        CredentialManagerService.updateProvidersWhenPackageRemoved(mContext, "com.example.test3");
+        CredentialManagerService.updateProvidersWhenPackageRemoved(
+                mSettingsWrapper, "com.example.test3");
 
         // Since the provider removed was not a primary provider then we should do nothing.
         assertThat(getSettingsKey(Settings.Secure.AUTOFILL_SERVICE))
@@ -125,7 +130,8 @@
                 Settings.Secure.CREDENTIAL_SERVICE_PRIMARY,
                 "com.example.test/com.example.test.TestActivity");
 
-        CredentialManagerService.updateProvidersWhenPackageRemoved(mContext, "com.example.test");
+        CredentialManagerService.updateProvidersWhenPackageRemoved(
+                mSettingsWrapper, "com.example.test");
 
         assertThat(getSettingsKey(Settings.Secure.AUTOFILL_SERVICE)).isEqualTo("");
         assertThat(getSettingsKey(Settings.Secure.CREDENTIAL_SERVICE))
@@ -144,7 +150,8 @@
         setSettingsKey(Settings.Secure.CREDENTIAL_SERVICE, testCredentialValue);
         setSettingsKey(Settings.Secure.CREDENTIAL_SERVICE_PRIMARY, testCredentialPrimaryValue);
 
-        CredentialManagerService.updateProvidersWhenPackageRemoved(mContext, "com.example.test3");
+        CredentialManagerService.updateProvidersWhenPackageRemoved(
+                mSettingsWrapper, "com.example.test3");
 
         // Since the provider removed was not a primary provider then we should do nothing.
         assertCredentialPropertyEquals(
@@ -176,12 +183,36 @@
         assertThat(actualValueSet).isEqualTo(newValueSet);
     }
 
-    private void setSettingsKey(String key, String value) {
-        assertThat(Settings.Secure.putString(mContext.getContentResolver(), key, value)).isTrue();
+    private void setSettingsKey(String name, String value) {
+        assertThat(
+                        mSettingsWrapper.putStringForUser(
+                                name, value, UserHandle.myUserId(), true))
+                .isTrue();
     }
 
-    private String getSettingsKey(String key) {
-        return Settings.Secure.getStringForUser(
-                mContext.getContentResolver(), key, UserHandle.myUserId());
+    private String getSettingsKey(String name) {
+        return mSettingsWrapper.getStringForUser(name, UserHandle.myUserId());
+    }
+
+    private static final class MockSettingsWrapper
+            extends CredentialManagerService.SettingsWrapper {
+
+        MockSettingsWrapper(@NonNull Context context) {
+            super(context);
+        }
+
+        /** Updates the string value of a system setting */
+        @Override
+        public boolean putStringForUser(
+                String name,
+                String value,
+                int userHandle,
+                boolean overrideableByRestore) {
+            // This will ensure that when the settings putStringForUser method is called by
+            // CredentialManagerService that the overrideableByRestore bit is true.
+            assertThat(overrideableByRestore).isTrue();
+
+            return Settings.Secure.putStringForUser(getContentResolver(), name, value, userHandle);
+        }
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
index 6207349..1bd6e29 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
@@ -659,6 +659,64 @@
         mTestLooper.dispatchAll();
         assertThat(mActiveMediaSessionsPaused).isFalse();
     }
+    @Test
+    public void handleRoutingInformation_physicalAddressOfSender_Tv_activeSourceChange() {
+        mHdmiCecLocalDevicePlayback.mService.getHdmiCecConfig().setStringValue(
+                HdmiControlManager.CEC_SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST,
+                HdmiControlManager.POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST_STANDBY_NOW);
+        mHdmiCecLocalDevicePlayback.setActiveSource(mPlaybackLogicalAddress,
+                mPlaybackPhysicalAddress, "HdmiCecLocalDevicePlaybackTest");
+        mPowerManager.setInteractive(true);
+        // Physical address reported in this message is the same as message sender's (TV) physical
+        // address.
+        HdmiCecMessage message =
+                HdmiCecMessageBuilder.buildRoutingInformation(Constants.ADDR_TV, 0x0000);
+
+        assertThat(mHdmiCecLocalDevicePlayback.handleRoutingInformation(message))
+                .isEqualTo(Constants.HANDLED);
+        assertThat(mHdmiCecLocalDevicePlayback.isActiveSource()).isFalse();
+        assertThat(mHdmiCecLocalDevicePlayback.getActiveSource().physicalAddress).isEqualTo(
+                0x0000);
+        // Active source's logical address is invalidated.
+        // See {@link HdmiCecLocalDevicePlayback#handleRoutingChangeAndInformation}.
+        assertThat(mHdmiCecLocalDevicePlayback.getActiveSource().logicalAddress).isEqualTo(
+                ADDR_INVALID);
+        assertThat(mPowerManager.isInteractive()).isFalse();
+    }
+
+    @Test
+    public void handleRoutingInformation_physicalAddressOfSender_notTv_noActiveSourceChange() {
+        mHdmiCecLocalDevicePlayback.mService.getHdmiCecConfig().setStringValue(
+                HdmiControlManager.CEC_SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST,
+                HdmiControlManager.POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST_STANDBY_NOW);
+        mHdmiCecLocalDevicePlayback.setActiveSource(mPlaybackLogicalAddress,
+                mPlaybackPhysicalAddress, "HdmiCecLocalDevicePlaybackTest");
+        // Add a device to the network and assert that this device is included in the list of
+        // devices.
+        HdmiDeviceInfo infoPlayback = HdmiDeviceInfo.cecDeviceBuilder()
+                .setLogicalAddress(Constants.ADDR_PLAYBACK_3)
+                .setPhysicalAddress(0x1000)
+                .setPortId(PORT_1)
+                .setDeviceType(HdmiDeviceInfo.DEVICE_PLAYBACK)
+                .setVendorId(0x1000)
+                .setDisplayName("Playback 3")
+                .build();
+        mHdmiControlService.getHdmiCecNetwork().addCecDevice(infoPlayback);
+        mPowerManager.setInteractive(true);
+        // Physical address reported in this message is the same as message sender's (Playback_3)
+        // physical address.
+        HdmiCecMessage message =
+                HdmiCecMessageBuilder.buildRoutingInformation(Constants.ADDR_PLAYBACK_3, 0x1000);
+
+        assertThat(mHdmiCecLocalDevicePlayback.handleRoutingInformation(message))
+                .isEqualTo(Constants.HANDLED);
+        assertThat(mHdmiCecLocalDevicePlayback.isActiveSource()).isTrue();
+        assertThat(mHdmiCecLocalDevicePlayback.getActiveSource().physicalAddress).isEqualTo(
+                mPlaybackPhysicalAddress);
+        assertThat(mHdmiCecLocalDevicePlayback.getActiveSource().logicalAddress).isEqualTo(
+                mPlaybackLogicalAddress);
+        assertThat(mPowerManager.isInteractive()).isTrue();
+    }
 
     @Test
     public void handleSetStreamPath() {
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index 5e38010..67ae998 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -170,6 +170,11 @@
                     }
 
                     @Override
+                    boolean isPowerOnOrTransient() {
+                        return true;
+                    }
+
+                    @Override
                     void invokeDeviceEventListeners(HdmiDeviceInfo device, int status) {
                         mDeviceEventListeners.add(new DeviceEventListener(device, status));
                     }
@@ -1839,4 +1844,38 @@
 
         assertThat(mNativeWrapper.getResultMessages()).doesNotContain(activeSourceFromTv);
     }
+
+    @Test
+    public void handleStandby_fromActiveSource_standby() {
+        mPowerManager.setInteractive(true);
+        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        mHdmiControlService.setActiveSource(ADDR_PLAYBACK_1, 0x1000,
+                "HdmiCecLocalDeviceTvTest");
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage standbyMessage = HdmiCecMessageBuilder.buildStandby(ADDR_PLAYBACK_1,
+                ADDR_TV);
+        assertThat(mHdmiCecLocalDeviceTv.dispatchMessage(standbyMessage))
+                .isEqualTo(Constants.HANDLED);
+        mTestLooper.dispatchAll();
+
+        assertThat(mPowerManager.isInteractive()).isFalse();
+    }
+
+    @Test
+    public void handleStandby_fromNonActiveSource_noStandby() {
+        mPowerManager.setInteractive(true);
+        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        mHdmiControlService.setActiveSource(ADDR_PLAYBACK_2, 0x2000,
+                "HdmiCecLocalDeviceTvTest");
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage standbyMessage = HdmiCecMessageBuilder.buildStandby(ADDR_PLAYBACK_1,
+                ADDR_TV);
+        assertThat(mHdmiCecLocalDeviceTv.dispatchMessage(standbyMessage))
+                .isEqualTo(Constants.HANDLED);
+        mTestLooper.dispatchAll();
+
+        assertThat(mPowerManager.isInteractive()).isTrue();
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
index eca19c8..2da2f50 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
@@ -506,6 +506,14 @@
     }
 
     @Test
+    public void testUnlockUserWithTokenWithBadHandleReturnsFalse() {
+        final long badTokenHandle = 123456789;
+        final byte[] token = "some-high-entropy-secure-token".getBytes();
+        mService.initializeSyntheticPassword(PRIMARY_USER_ID);
+        assertFalse(mLocalService.unlockUserWithToken(badTokenHandle, token, PRIMARY_USER_ID));
+    }
+
+    @Test
     public void testGetHashFactorPrimaryUser() throws RemoteException {
         LockscreenCredential password = newPassword("password");
         initSpAndSetCredential(PRIMARY_USER_ID, password);
diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
index 097cc51..abd3abe 100644
--- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
@@ -49,6 +49,7 @@
 import static org.testng.Assert.assertThrows;
 
 import android.app.ActivityManagerInternal;
+import android.app.ActivityOptions.LaunchCookie;
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.content.pm.ApplicationInfo;
@@ -784,7 +785,7 @@
             @RecordContent int recordedContent)
             throws NameNotFoundException {
         MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
-        projection.setLaunchCookie(mock(IBinder.class));
+        projection.setLaunchCookie(new LaunchCookie());
         projection.start(mIMediaProjectionCallback);
         projection.notifyVirtualDisplayCreated(10);
         // Waiting for user to review consent.
@@ -825,7 +826,7 @@
     public void testSetUserReviewGrantedConsentResult_displayMirroring_noPriorSession()
             throws NameNotFoundException {
         MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
-        projection.setLaunchCookie(mock(IBinder.class));
+        projection.setLaunchCookie(new LaunchCookie());
         projection.start(mIMediaProjectionCallback);
         // Skip setting the prior session details.
 
@@ -844,7 +845,7 @@
     public void testSetUserReviewGrantedConsentResult_displayMirroring_sessionNotWaiting()
             throws NameNotFoundException {
         MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
-        projection.setLaunchCookie(mock(IBinder.class));
+        projection.setLaunchCookie(new LaunchCookie());
         projection.start(mIMediaProjectionCallback);
         // Session is not waiting for user's consent.
         doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
diff --git a/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt b/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt
index 757abde..e3ee21a 100644
--- a/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt
+++ b/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt
@@ -436,6 +436,13 @@
         validateTagCount("action", 20000, tag)
         validateTagCount("category", 40000, tag)
         validateTagCount("data", 40000, tag)
+        validateTagCount("uri-relative-filter-group", 100, tag)
+    }
+
+    @Test
+    fun parseUriRelativeFilterGroupTag() {
+        val tag = "uri-relative-filter-group"
+        validateTagCount("data", 100, tag)
     }
 
     @Test
@@ -465,6 +472,54 @@
             R.styleable.AndroidManifestData_pathAdvancedPattern,
             4000
         )
+        validateTagAttr(tag, "query", R.styleable.AndroidManifestData_query, 4000)
+        validateTagAttr(
+            tag,
+            "queryPattern",
+            R.styleable.AndroidManifestData_queryPattern,
+            4000
+        )
+        validateTagAttr(
+            tag,
+            "queryPrefix",
+            R.styleable.AndroidManifestData_queryPrefix,
+            4000
+        )
+        validateTagAttr(tag,
+            "querySuffix",
+            R.styleable.AndroidManifestData_querySuffix,
+            4000
+        )
+        validateTagAttr(
+            tag,
+            "queryAdvancedPattern",
+            R.styleable.AndroidManifestData_queryAdvancedPattern,
+            4000
+        )
+        validateTagAttr(tag, "fragment", R.styleable.AndroidManifestData_query, 4000)
+        validateTagAttr(
+            tag,
+            "fragmentPattern",
+            R.styleable.AndroidManifestData_fragmentPattern,
+            4000
+        )
+        validateTagAttr(
+            tag,
+            "fragmentPrefix",
+            R.styleable.AndroidManifestData_fragmentPrefix,
+            4000
+        )
+        validateTagAttr(tag,
+            "fragmentSuffix",
+            R.styleable.AndroidManifestData_fragmentSuffix,
+            4000
+        )
+        validateTagAttr(
+            tag,
+            "fragmentAdvancedPattern",
+            R.styleable.AndroidManifestData_fragmentAdvancedPattern,
+            4000
+        )
         validateTagAttr(tag, "mimeType", R.styleable.AndroidManifestData_mimeType, 255)
         validateTagAttr(tag, "mimeGroup", R.styleable.AndroidManifestData_mimeGroup, 1024)
     }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 227265a..87e822c 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -4762,6 +4762,7 @@
                 .setShouldDimWallpaper(true)
                 .setShouldUseNightMode(true)
                 .build();
+        user1Rule.zenPolicy = new ZenPolicy();
         verifyNoMoreInteractions(mDeviceEffectsApplier);
 
         mZenModeHelper.onUserSwitched(1);
@@ -5042,6 +5043,109 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void removeAndAddAutomaticZenRule_wasActive_isRestoredAsInactive() {
+        // Start with a rule.
+        mZenModeHelper.mConfig.automaticRules.clear();
+        AutomaticZenRule rule = new AutomaticZenRule.Builder("Test", CONDITION_ID)
+                .setConditionId(CONDITION_ID)
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .build();
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+                UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID);
+
+        // User customizes it.
+        AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule)
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+                .build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, UPDATE_ORIGIN_USER, "userUpdate",
+                Process.SYSTEM_UID);
+        assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
+
+        // App activates it.
+        mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP,
+                CUSTOM_PKG_UID);
+        assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS);
+
+        // App deletes it.
+        mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_APP, "delete it",
+                CUSTOM_PKG_UID);
+        assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0);
+        assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(1);
+        assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
+
+        // App adds it again.
+        String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+                UPDATE_ORIGIN_APP, "add it again", CUSTOM_PKG_UID);
+
+        // The rule is restored...
+        assertThat(newRuleId).isEqualTo(ruleId);
+        AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId);
+        assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS);
+
+        // ... but it is NOT active
+        ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(newRuleId);
+        assertThat(storedRule.isAutomaticActive()).isFalse();
+        assertThat(storedRule.isTrueOrUnknown()).isFalse();
+        assertThat(storedRule.condition).isNull();
+        assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_API)
+    public void removeAndAddAutomaticZenRule_wasSnoozed_isRestoredAsInactive() {
+        // Start with a rule.
+        mZenModeHelper.mConfig.automaticRules.clear();
+        AutomaticZenRule rule = new AutomaticZenRule.Builder("Test", CONDITION_ID)
+                .setConditionId(CONDITION_ID)
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .build();
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+                UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID);
+
+        // User customizes it.
+        AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule)
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+                .build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, UPDATE_ORIGIN_USER, "userUpdate",
+                Process.SYSTEM_UID);
+        assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
+
+        // App activates it.
+        mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP,
+                CUSTOM_PKG_UID);
+        assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS);
+
+        // User snoozes it.
+        mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI,
+                "snoozing", "systemui", Process.SYSTEM_UID);
+        assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
+
+        // App deletes it.
+        mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_APP, "delete it",
+                CUSTOM_PKG_UID);
+        assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0);
+        assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(1);
+
+        // App adds it again.
+        String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+                UPDATE_ORIGIN_APP, "add it again", CUSTOM_PKG_UID);
+
+        // The rule is restored...
+        assertThat(newRuleId).isEqualTo(ruleId);
+        AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId);
+        assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS);
+
+        // ... but it is NEITHER active NOR snoozed.
+        ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(newRuleId);
+        assertThat(storedRule.isAutomaticActive()).isFalse();
+        assertThat(storedRule.isTrueOrUnknown()).isFalse();
+        assertThat(storedRule.condition).isNull();
+        assertThat(storedRule.snoozing).isFalse();
+        assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
+    }
+
+    @Test
     public void testRuleCleanup() throws Exception {
         mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
         Instant now = Instant.ofEpochMilli(1701796461000L);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java
index 07cfbf0..16f963f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java
@@ -628,6 +628,27 @@
     }
 
     @Test
+    public void testLaunchWindowingModeUpdatesExistingTask() {
+        final TestDisplayContent freeformDisplay = createNewDisplayContent(
+                WINDOWING_MODE_FREEFORM);
+
+        mCurrent.mPreferredTaskDisplayArea = freeformDisplay.getDefaultTaskDisplayArea();
+        ActivityRecord activity = createSourceActivity(freeformDisplay);
+        final Task task = activity.getTask();
+        task.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+        final ActivityOptions options = ActivityOptions.makeBasic();
+        options.setLaunchWindowingMode(WINDOWING_MODE_FREEFORM);
+
+        assertEquals(RESULT_CONTINUE,
+                new CalculateRequestBuilder()
+                        .setTask(task)
+                        .setOptions(options)
+                        .calculate());
+
+        assertEquals(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode);
+    }
+
+    @Test
     public void testBoundsInOptionsInfersFreeformWithResizeableActivity() {
         final ActivityOptions options = ActivityOptions.makeBasic();
         options.setLaunchBounds(new Rect(0, 0, 100, 100));
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index f3f1838..642a561 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -1988,6 +1988,8 @@
     context_->SetNameManglerPolicy(NameManglerPolicy{context_->GetCompilationPackage()});
     context_->SetSplitNameDependencies(app_info_.split_name_dependencies);
 
+    std::unique_ptr<xml::XmlResource> pre_flags_filter_manifest_xml = manifest_xml->Clone();
+
     FeatureFlagsFilterOptions flags_filter_options;
     if (context_->GetMinSdkVersion() > SDK_UPSIDE_DOWN_CAKE) {
       // For API version > U, PackageManager will dynamically read the flag values and disable
@@ -2297,7 +2299,12 @@
         }
 
         if (options_.generate_java_class_path) {
-          if (!WriteManifestJavaFile(manifest_xml.get())) {
+          // The FeatureFlagsFilter may remove <permission> and <permission-group> elements that
+          // generate constants in the Manifest Java file. While we want those permissions and
+          // permission groups removed in the SDK (i.e., if a feature flag is disabled), the
+          // constants should still remain so that code referencing it (e.g., within a feature
+          // flag check) will still compile. Therefore we use the manifest XML before the filter.
+          if (!WriteManifestJavaFile(pre_flags_filter_manifest_xml.get())) {
             error = true;
           }
         }
diff --git a/tools/aapt2/cmd/Link_test.cpp b/tools/aapt2/cmd/Link_test.cpp
index 9323f3b..6cc42f1 100644
--- a/tools/aapt2/cmd/Link_test.cpp
+++ b/tools/aapt2/cmd/Link_test.cpp
@@ -1021,9 +1021,11 @@
                           .AddContents(manifest_contents)
                           .Build();
 
+  const std::string app_java = GetTestPath("app-java");
   auto app_link_args = LinkCommandBuilder(this)
                            .SetManifestFile(app_manifest)
                            .AddParameter("-I", android_apk)
+                           .AddParameter("--java", app_java)
                            .AddParameter("--feature-flags", "flag=false");
 
   const std::string app_apk = GetTestPath("app.apk");
@@ -1038,6 +1040,12 @@
   ASSERT_THAT(root, NotNull());
   auto maybe_removed = root->FindChild({}, "permission");
   ASSERT_THAT(maybe_removed, IsNull());
+
+  // Code for the permission should be generated even if the element is removed
+  const std::string manifest_java = app_java + "/com/example/app/Manifest.java";
+  std::string manifest_java_contents;
+  ASSERT_TRUE(android::base::ReadFileToString(manifest_java, &manifest_java_contents));
+  EXPECT_THAT(manifest_java_contents, HasSubstr(" public static final String FOO=\"FOO\";"));
 }
 
 TEST_F(LinkTest, FeatureFlagEnabled_SdkAtMostUDC) {
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/MessageQueue_host.java b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/MessageQueue_host.java
index 2e47d48..65da4a1 100644
--- a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/MessageQueue_host.java
+++ b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/MessageQueue_host.java
@@ -28,6 +28,7 @@
 
     private final Object mPoller = new Object();
     private volatile boolean mPolling;
+    private volatile boolean mPendingWake;
 
     private void validate() {
         if (mDeleted) {
@@ -62,7 +63,9 @@
         synchronized (q.mPoller) {
             q.mPolling = true;
             try {
-                if (timeoutMillis == 0) {
+                if (q.mPendingWake) {
+                    // Calling with pending wake returns immediately
+                } else if (timeoutMillis == 0) {
                     // Calling epoll_wait() with 0 returns immediately
                 } else if (timeoutMillis == -1) {
                     q.mPoller.wait();
@@ -72,6 +75,8 @@
             } catch (InterruptedException e) {
                 Thread.currentThread().interrupt();
             }
+            // Any reason for returning counts as a "wake", so clear pending
+            q.mPendingWake = false;
             q.mPolling = false;
         }
     }
@@ -79,6 +84,7 @@
     public static void nativeWake(long ptr) {
         var q = getInstance(ptr);
         synchronized (q.mPoller) {
+            q.mPendingWake = true;
             q.mPoller.notifyAll();
         }
     }